diff --git a/.eslintrc b/.eslintrc
index 12fefa0968..f3d4fd5091 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -2,65 +2,32 @@
"env": {
"browser": true,
"node": true,
- "es6": true
+ "es2022": true
},
"parserOptions": {
- "ecmaVersion": 11,
"sourceType": "module"
},
"extends": "eslint:recommended",
"rules": {
- "indent": [
- "error",
- "tab",
- { "SwitchCase": 1 }
- ],
- "brace-style": [
- "error",
- "1tbs"
- ],
- "space-unary-ops": [
- "error",
- { "words": true }
- ],
- "linebreak-style": [
- "error",
- "unix"
- ],
- "quotes": [
- "off"
- ],
- "semi": [
- "warn",
- "always"
- ],
- "camelcase": [
- "off"
- ],
- "no-unused-vars": [
- "warn"
- ],
- "no-redeclare": [
- "warn"
- ],
- "no-console": [
- "warn"
- ],
- "no-extra-boolean-cast": [
- "off"
- ],
- "no-control-regex": [
- "off"
- ],
- "space-before-blocks": "warn",
- "keyword-spacing": "warn",
- "comma-spacing": "warn",
- "key-spacing": "warn"
+ "indent": "off",
+ "brace-style": "off",
+ "no-mixed-spaces-and-tabs": "off",
+ "no-useless-escape": "off",
+ "space-unary-ops": ["error", { "words": true }],
+ "linebreak-style": "off",
+ "quotes": ["off"],
+ "semi": "off",
+ "camelcase": "off",
+ "no-unused-vars": "off",
+ "no-console": ["warn"],
+ "no-extra-boolean-cast": ["off"],
+ "no-control-regex": ["off"]
},
"root": true,
"globals": {
"frappe": true,
"Vue": true,
+ "SetVueGlobals": true,
"erpnext": true,
"hub": true,
"$": true,
@@ -97,8 +64,10 @@
"is_null": true,
"in_list": true,
"has_common": true,
+ "posthog": true,
"has_words": true,
"validate_email": true,
+ "open_web_template_values_editor": true,
"get_number_format": true,
"format_number": true,
"format_currency": true,
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index af6d8f26a7..94b76b12ce 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -9,21 +9,22 @@ jobs:
name: linters
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python 3.10
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: '3.10'
+ cache: pip
- name: Install and Run Pre-commit
- uses: pre-commit/action@v2.0.3
+ uses: pre-commit/action@v3.0.0
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- name: Download semgrep
- run: pip install semgrep==0.97.0
+ run: pip install semgrep
- name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 9b4db49d08..2ce1125456 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -7,11 +7,9 @@ on:
- '**.css'
- '**.md'
- '**.html'
- push:
- branches: [ develop ]
- paths-ignore:
- - '**.js'
- - '**.md'
+ schedule:
+ # Run everday at midnight UTC / 5:30 IST
+ - cron: "0 0 * * *"
workflow_dispatch:
inputs:
user:
diff --git a/.mergify.yml b/.mergify.yml
index c5f3d83ce6..804b27d435 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -15,6 +15,8 @@ pull_request_rules:
- or:
- base=version-13
- base=version-12
+ - base=version-14
+ - base=version-15
actions:
close:
comment:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d70977c07e..2c9a60c7c4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,8 +16,26 @@ repos:
- id: check-merge-conflict
- id: check-ast
+ - repo: https://github.com/pre-commit/mirrors-eslint
+ rev: v8.44.0
+ hooks:
+ - id: eslint
+ types_or: [javascript]
+ args: ['--quiet']
+ # Ignore any files that might contain jinja / bundles
+ exclude: |
+ (?x)^(
+ erpnext/public/dist/.*|
+ cypress/.*|
+ .*node_modules.*|
+ .*boilerplate.*|
+ erpnext/public/js/controllers/.*|
+ erpnext/templates/pages/order.js|
+ erpnext/templates/includes/.*
+ )$
+
- repo: https://github.com/PyCQA/flake8
- rev: 5.0.4
+ rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [
diff --git a/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json b/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json
index 8631d3dc2a..4883106227 100644
--- a/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json
+++ b/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json
@@ -4,18 +4,19 @@
"creation": "2020-07-17 11:25:34.593061",
"docstatus": 0,
"doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}",
"idx": 0,
"is_public": 1,
"is_standard": 1,
- "modified": "2020-07-22 12:24:49.144210",
+ "modified": "2023-07-19 13:13:13.307073",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget Variance",
"number_of_groups": 0,
"owner": "Administrator",
"report_name": "Budget Variance Report",
+ "roles": [],
"timeseries": 0,
"type": "Bar",
"use_report_chart": 1,
diff --git a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json
index 3fa995bbe1..25caa44769 100644
--- a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json
+++ b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json
@@ -4,18 +4,19 @@
"creation": "2020-07-17 11:25:34.448572",
"docstatus": 0,
"doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}",
"idx": 0,
"is_public": 1,
"is_standard": 1,
- "modified": "2020-07-22 12:33:48.888943",
+ "modified": "2023-07-19 13:08:56.470390",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Profit and Loss",
"number_of_groups": 0,
"owner": "Administrator",
"report_name": "Profit and Loss Statement",
+ "roles": [],
"timeseries": 0,
"type": "Bar",
"use_report_chart": 1,
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index fb49ef3a42..d0940c7df2 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -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.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
+ accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
def _book_deferred_revenue_or_expense(
item,
diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js
index 320e1cab7c..3c0eb85701 100644
--- a/erpnext/accounts/doctype/account/account.js
+++ b/erpnext/accounts/doctype/account/account.js
@@ -1,67 +1,83 @@
// 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(['account_type', 'tax_rate'], cint(frm.doc.is_group) == 0);
+ frm.toggle_display(["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 () {
@@ -79,38 +95,42 @@ frappe.ui.form.on('Account', {
frm.add_custom_button(__('General Ledger'), function () {
frappe.route_options = {
"account": frm.doc.name,
- "from_date": frappe.sys_defaults.year_start_date,
- "to_date": frappe.sys_defaults.year_end_date,
+ "from_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
+ "to_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"company": frm.doc.company
};
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",
@@ -119,44 +139,47 @@ 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;
}
@@ -166,23 +189,29 @@ 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();
- }
+ },
});
diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json
index e79fb66062..78f73efff1 100644
--- a/erpnext/accounts/doctype/account/account.json
+++ b/erpnext/accounts/doctype/account/account.json
@@ -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\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
+ "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\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"
},
{
"description": "Rate at which this tax is applied",
@@ -192,7 +192,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2023-04-11 16:08:46.983677",
+ "modified": "2023-07-20 18:18:44.405723",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -243,7 +243,6 @@
"read": 1,
"report": 1,
"role": "Accounts Manager",
- "set_user_permissions": 1,
"share": 1,
"write": 1
}
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index e94b7cf4c2..c1eca721b6 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -45,6 +45,7 @@ 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()
@@ -55,6 +56,20 @@ 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:
diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index d537adfcbf..f240aa6e95 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -194,8 +194,8 @@ frappe.treeview_settings["Account"] = {
click: function(node, btn) {
frappe.route_options = {
"account": node.label,
- "from_date": frappe.sys_defaults.year_start_date,
- "to_date": frappe.sys_defaults.year_end_date,
+ "from_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
+ "to_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"company": frappe.treeview_settings['Account'].treeview.page.fields_dict.company.get_value()
};
frappe.set_route("query-report", "General Ledger");
diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py
index 9540084e09..e75af7047f 100644
--- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py
+++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py
@@ -14,10 +14,8 @@ class AccountClosingBalance(Document):
pass
-def make_closing_entries(closing_entries, voucher_name):
+def make_closing_entries(closing_entries, voucher_name, company, closing_date):
accounting_dimensions = get_accounting_dimensions()
- company = closing_entries[0].get("company")
- closing_date = closing_entries[0].get("closing_date")
previous_closing_entries = get_previous_closing_entries(
company, closing_date, accounting_dimensions
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
index 2fa1d53c60..2f53f7b640 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
@@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', {
};
});
+ frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ return {
+ filters: {
+ company: d.company,
+ root_type: ["in", ["Asset", "Liability"]],
+ is_group: 0
+ }
+ }
+ });
+
if (!frm.is_new()) {
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
frappe.set_route("List", frm.doc.document_type);
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 15c84d462f..cfe5e6e800 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -39,6 +39,8 @@ class AccountingDimension(Document):
if not self.is_new():
self.validate_document_type_change()
+ self.validate_dimension_defaults()
+
def validate_document_type_change(self):
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
if doctype_before_save != self.document_type:
@@ -46,6 +48,14 @@ class AccountingDimension(Document):
message += _("Please create a new Accounting Dimension if required.")
frappe.throw(message)
+ def validate_dimension_defaults(self):
+ companies = []
+ for default in self.get("dimension_defaults"):
+ if default.company not in companies:
+ companies.append(default.company)
+ else:
+ frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
+
def after_insert(self):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
diff --git a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
index e9e1f43f99..7b6120a583 100644
--- a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
+++ b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
@@ -8,7 +8,10 @@
"reference_document",
"default_dimension",
"mandatory_for_bs",
- "mandatory_for_pl"
+ "mandatory_for_pl",
+ "column_break_lqns",
+ "automatically_post_balancing_accounting_entry",
+ "offsetting_account"
],
"fields": [
{
@@ -50,6 +53,23 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Mandatory For Profit and Loss Account"
+ },
+ {
+ "default": "0",
+ "fieldname": "automatically_post_balancing_accounting_entry",
+ "fieldtype": "Check",
+ "label": "Automatically post balancing accounting entry"
+ },
+ {
+ "fieldname": "offsetting_account",
+ "fieldtype": "Link",
+ "label": "Offsetting Account",
+ "mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry",
+ "options": "Account"
+ },
+ {
+ "fieldname": "column_break_lqns",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.js b/erpnext/accounts/doctype/accounting_period/accounting_period.js
index e3d805a168..f17b6f9c69 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.js
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.js
@@ -20,5 +20,11 @@ frappe.ui.form.on('Accounting Period', {
}
});
}
+
+ frm.set_query("document_type", "closed_documents", () => {
+ return {
+ query: "erpnext.controllers.queries.get_doctypes_for_closing",
+ }
+ });
}
});
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py
index 80c9715e8e..d5f37a6806 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py
@@ -11,6 +11,10 @@ class OverlapError(frappe.ValidationError):
pass
+class ClosedAccountingPeriod(frappe.ValidationError):
+ pass
+
+
class AccountingPeriod(Document):
def validate(self):
self.validate_overlap()
@@ -65,3 +69,42 @@ class AccountingPeriod(Document):
"closed_documents",
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
)
+
+
+def validate_accounting_period_on_doc_save(doc, method=None):
+ if doc.doctype == "Bank Clearance":
+ return
+ elif doc.doctype == "Asset":
+ if doc.is_existing_asset:
+ return
+ else:
+ date = doc.available_for_use_date
+ elif doc.doctype == "Asset Repair":
+ date = doc.completion_date
+ else:
+ date = doc.posting_date
+
+ ap = frappe.qb.DocType("Accounting Period")
+ cd = frappe.qb.DocType("Closed Document")
+
+ accounting_period = (
+ frappe.qb.from_(ap)
+ .from_(cd)
+ .select(ap.name)
+ .where(
+ (ap.name == cd.parent)
+ & (ap.company == doc.company)
+ & (cd.closed == 1)
+ & (cd.document_type == doc.doctype)
+ & (date >= ap.start_date)
+ & (date <= ap.end_date)
+ )
+ ).run(as_dict=1)
+
+ if accounting_period:
+ frappe.throw(
+ _("You cannot create a {0} within the closed Accounting Period {1}").format(
+ doc.doctype, frappe.bold(accounting_period[0]["name"])
+ ),
+ ClosedAccountingPeriod,
+ )
diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
index 85025d190f..41d94797ad 100644
--- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
@@ -6,9 +6,11 @@ import unittest
import frappe
from frappe.utils import add_months, nowdate
-from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
+from erpnext.accounts.doctype.accounting_period.accounting_period import (
+ ClosedAccountingPeriod,
+ OverlapError,
+)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-from erpnext.accounts.general_ledger import ClosedAccountingPeriod
test_dependencies = ["Item"]
@@ -33,9 +35,9 @@ class TestAccountingPeriod(unittest.TestCase):
ap1.save()
doc = create_sales_invoice(
- do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
+ do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
)
- self.assertRaises(ClosedAccountingPeriod, doc.submit)
+ self.assertRaises(ClosedAccountingPeriod, doc.save)
def tearDown(self):
for d in frappe.get_all("Accounting Period"):
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 7cd498da6b..6857ba343e 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -58,6 +58,7 @@
"closing_settings_tab",
"period_closing_settings_section",
"acc_frozen_upto",
+ "ignore_account_closing_balance",
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
@@ -406,6 +407,13 @@
"fieldname": "enable_fuzzy_matching",
"fieldtype": "Check",
"label": "Enable Fuzzy Matching"
+ },
+ {
+ "default": "0",
+ "description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ",
+ "fieldname": "ignore_account_closing_balance",
+ "fieldtype": "Check",
+ "label": "Ignore Account Closing Balance"
}
],
"icon": "icon-cog",
@@ -413,7 +421,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-06-15 16:35:45.123456",
+ "modified": "2023-07-27 15:05:34.000264",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 3b125a2986..ac3d44bb5e 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -14,21 +14,32 @@ from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
- def on_update(self):
- frappe.clear_cache()
-
def validate(self):
- frappe.db.set_default(
- "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
- )
+ old_doc = self.get_doc_before_save()
+ clear_cache = False
- frappe.db.set_default(
- "enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
- )
+ if old_doc.add_taxes_from_item_tax_template != self.add_taxes_from_item_tax_template:
+ frappe.db.set_default(
+ "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
+ )
+ clear_cache = True
+
+ if old_doc.enable_common_party_accounting != self.enable_common_party_accounting:
+ frappe.db.set_default(
+ "enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
+ )
+ clear_cache = True
self.validate_stale_days()
- self.enable_payment_schedule_in_print()
- self.validate_pending_reposts()
+
+ if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
+ self.enable_payment_schedule_in_print()
+
+ if old_doc.acc_frozen_upto != self.acc_frozen_upto:
+ self.validate_pending_reposts()
+
+ if clear_cache:
+ frappe.clear_cache()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index 6667193a54..83bd7fe862 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -102,7 +102,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
}
onScriptLoaded(me) {
- me.linkHandler = Plaid.create({
+ me.linkHandler = Plaid.create({ // eslint-disable-line no-undef
env: me.plaid_env,
token: me.token,
onSuccess: me.plaid_success
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js
index 632fab0197..c427cc8b87 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.js
+++ b/erpnext/accounts/doctype/cost_center/cost_center.js
@@ -70,7 +70,7 @@ frappe.ui.form.on('Cost Center', {
}
],
primary_action: function() {
- var data = d.get_values();
+ let data = d.get_values();
if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) {
d.hide();
return;
@@ -91,8 +91,8 @@ frappe.ui.form.on('Cost Center', {
if(r.message) {
frappe.set_route("Form", "Cost Center", r.message);
} else {
- me.frm.set_value("cost_center_name", data.cost_center_name);
- me.frm.set_value("cost_center_number", data.cost_center_number);
+ frm.set_value("cost_center_name", data.cost_center_name);
+ frm.set_value("cost_center_number", data.cost_center_number);
}
d.hide();
}
diff --git a/erpnext/accounts/doctype/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js
index 9909c6c2ab..1ac909e745 100644
--- a/erpnext/accounts/doctype/dunning/dunning.js
+++ b/erpnext/accounts/doctype/dunning/dunning.js
@@ -1,13 +1,14 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Dunning", {
setup: function (frm) {
- frm.set_query("sales_invoice", () => {
+ frm.set_query("sales_invoice", "overdue_payments", () => {
return {
filters: {
docstatus: 1,
company: frm.doc.company,
+ customer: frm.doc.customer,
outstanding_amount: [">", 0],
status: "Overdue"
},
@@ -22,14 +23,24 @@ frappe.ui.form.on("Dunning", {
}
};
});
+ frm.set_query("cost_center", () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query("contact_person", erpnext.queries.contact_query);
+ frm.set_query("customer_address", erpnext.queries.address_query);
+ frm.set_query("company_address", erpnext.queries.company_address_query);
+
+ // cannot add rows manually, only via button "Fetch Overdue Payments"
+ frm.set_df_property("overdue_payments", "cannot_add_rows", true);
},
refresh: function (frm) {
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
- frm.set_df_property(
- "sales_invoice",
- "read_only",
- frm.doc.__islocal ? 0 : 1
- );
if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
frm.add_custom_button(__("Resolve"), () => {
frm.set_value("status", "Resolved");
@@ -40,42 +51,111 @@ frappe.ui.form.on("Dunning", {
__("Payment"),
function () {
frm.events.make_payment_entry(frm);
- },__("Create")
+ }, __("Create")
);
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
- if(frm.doc.docstatus > 0) {
- frm.add_custom_button(__('Ledger'), function() {
- frappe.route_options = {
- "voucher_no": frm.doc.name,
- "from_date": frm.doc.posting_date,
- "to_date": frm.doc.posting_date,
- "company": frm.doc.company,
- "show_cancelled_entries": frm.doc.docstatus === 2
- };
- frappe.set_route("query-report", "General Ledger");
- }, __('View'));
+ if (frm.doc.docstatus === 0) {
+ frm.add_custom_button(__("Fetch Overdue Payments"), () => {
+ erpnext.utils.map_current_doc({
+ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
+ source_doctype: "Sales Invoice",
+ date_field: "due_date",
+ target: frm,
+ setters: {
+ customer: frm.doc.customer || undefined,
+ },
+ get_query_filters: {
+ docstatus: 1,
+ status: "Overdue",
+ company: frm.doc.company
+ },
+ allow_child_item_selection: true,
+ child_fieldname: "payment_schedule",
+ child_columns: ["due_date", "outstanding"],
+ });
+ });
}
+
+ frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' };
+
+ frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer));
},
- overdue_days: function (frm) {
- frappe.db.get_value(
- "Dunning Type",
- {
- start_day: ["<", frm.doc.overdue_days],
- end_day: [">=", frm.doc.overdue_days],
- },
- "dunning_type",
- (r) => {
- if (r) {
- frm.set_value("dunning_type", r.dunning_type);
- } else {
- frm.set_value("dunning_type", "");
- frm.set_value("rate_of_interest", "");
- frm.set_value("dunning_fee", "");
+ // When multiple companies are set up. in case company name is changed set default company address
+ company: function (frm) {
+ if (frm.doc.company) {
+ frappe.call({
+ method: "erpnext.setup.doctype.company.company.get_default_company_address",
+ args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
+ debounce: 2000,
+ callback: function (r) {
+ frm.set_value("company_address", r && r.message || "");
+ }
+ });
+
+ if (frm.fields_dict.currency) {
+ const company_currency = erpnext.get_currency(frm.doc.company);
+
+ if (!frm.doc.currency) {
+ frm.set_value("currency", company_currency);
+ }
+
+ if (frm.doc.currency == company_currency) {
+ frm.set_value("conversion_rate", 1.0);
}
}
- );
+
+ const company_doc = frappe.get_doc(":Company", frm.doc.company);
+ if (company_doc.default_letter_head) {
+ if (frm.fields_dict.letter_head) {
+ frm.set_value("letter_head", company_doc.default_letter_head);
+ }
+ }
+ }
+ },
+ currency: function (frm) {
+ // this.set_dynamic_labels();
+ const company_currency = erpnext.get_currency(frm.doc.company);
+ // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
+ if (frm.doc.currency && frm.doc.currency !== company_currency) {
+ frappe.call({
+ method: "erpnext.setup.utils.get_exchange_rate",
+ args: {
+ transaction_date: frm.doc.posting_date,
+ from_currency: frm.doc.currency,
+ to_currency: company_currency,
+ args: "for_selling"
+ },
+ freeze: true,
+ freeze_message: __("Fetching exchange rates ..."),
+ callback: function(r) {
+ const exchange_rate = flt(r.message);
+ if (exchange_rate != frm.doc.conversion_rate) {
+ frm.set_value("conversion_rate", exchange_rate);
+ }
+ }
+ });
+ } else {
+ frm.trigger("conversion_rate");
+ }
+ },
+ customer: (frm) => {
+ erpnext.utils.get_party_details(frm);
+ },
+ conversion_rate: function (frm) {
+ if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) {
+ frm.set_value("conversion_rate", 1.0);
+ }
+
+ // Make read only if Accounts Settings doesn't allow stale rates
+ frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
+ },
+ customer_address: function (frm) {
+ erpnext.utils.get_address_display(frm, "customer_address");
+ },
+ company_address: function (frm) {
+ erpnext.utils.get_address_display(frm, "company_address");
},
dunning_type: function (frm) {
frm.trigger("get_dunning_letter_text");
@@ -87,7 +167,7 @@ frappe.ui.form.on("Dunning", {
if (frm.doc.dunning_type) {
frappe.call({
method:
- "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
+ "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
args: {
dunning_type: frm.doc.dunning_type,
language: frm.doc.language,
@@ -106,49 +186,62 @@ frappe.ui.form.on("Dunning", {
});
}
},
- due_date: function (frm) {
- frm.trigger("calculate_overdue_days");
- },
posting_date: function (frm) {
frm.trigger("calculate_overdue_days");
},
rate_of_interest: function (frm) {
- frm.trigger("calculate_interest_and_amount");
- },
- outstanding_amount: function (frm) {
- frm.trigger("calculate_interest_and_amount");
- },
- interest_amount: function (frm) {
- frm.trigger("calculate_interest_and_amount");
+ frm.trigger("calculate_interest");
},
dunning_fee: function (frm) {
- frm.trigger("calculate_interest_and_amount");
+ frm.trigger("calculate_totals");
},
- sales_invoice: function (frm) {
- frm.trigger("calculate_overdue_days");
+ overdue_payments_add: function (frm) {
+ frm.trigger("calculate_totals");
+ },
+ overdue_payments_remove: function (frm) {
+ frm.trigger("calculate_totals");
},
calculate_overdue_days: function (frm) {
- if (frm.doc.posting_date && frm.doc.due_date) {
- const overdue_days = moment(frm.doc.posting_date).diff(
- frm.doc.due_date,
- "days"
- );
- frm.set_value("overdue_days", overdue_days);
- }
+ frm.doc.overdue_payments.forEach((row) => {
+ if (frm.doc.posting_date && row.due_date) {
+ const overdue_days = moment(frm.doc.posting_date).diff(
+ row.due_date,
+ "days"
+ );
+ frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days);
+ }
+ });
},
- calculate_interest_and_amount: function (frm) {
- const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100;
- const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount'));
- const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount'));
- const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total'));
- frm.set_value("interest_amount", interest_amount);
- frm.set_value("dunning_amount", dunning_amount);
- frm.set_value("grand_total", grand_total);
+ calculate_interest: function (frm) {
+ frm.doc.overdue_payments.forEach((row) => {
+ const interest_per_day = frm.doc.rate_of_interest / 100 / 365;
+ const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row));
+ frappe.model.set_value(row.doctype, row.name, "interest", interest);
+ });
+ },
+ calculate_totals: function (frm) {
+ const total_interest = frm.doc.overdue_payments
+ .reduce((prev, cur) => prev + cur.interest, 0);
+ const total_outstanding = frm.doc.overdue_payments
+ .reduce((prev, cur) => prev + cur.outstanding, 0);
+ const dunning_amount = total_interest + frm.doc.dunning_fee;
+ const base_dunning_amount = dunning_amount * frm.doc.conversion_rate;
+ const grand_total = total_outstanding + dunning_amount;
+
+ function setWithPrecison(field, value) {
+ frm.set_value(field, flt(value, precision(field)));
+ }
+
+ setWithPrecison("total_outstanding", total_outstanding);
+ setWithPrecison("total_interest", total_interest);
+ setWithPrecison("dunning_amount", dunning_amount);
+ setWithPrecison("base_dunning_amount", base_dunning_amount);
+ setWithPrecison("grand_total", grand_total);
},
make_payment_entry: function (frm) {
return frappe.call({
method:
- "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
+ "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
args: {
dt: frm.doc.doctype,
dn: frm.doc.name,
@@ -160,3 +253,9 @@ frappe.ui.form.on("Dunning", {
});
},
});
+
+frappe.ui.form.on("Overdue Payment", {
+ interest: function (frm) {
+ frm.trigger("calculate_totals");
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json
index 2a32b99f42..b7e8aeaaaf 100644
--- a/erpnext/accounts/doctype/dunning/dunning.json
+++ b/erpnext/accounts/doctype/dunning/dunning.json
@@ -2,49 +2,60 @@
"actions": [],
"allow_events_in_timeline": 1,
"autoname": "naming_series:",
+ "beta": 1,
"creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
- "title",
"naming_series",
- "sales_invoice",
"customer",
"customer_name",
- "outstanding_amount",
- "currency",
- "conversion_rate",
"column_break_3",
"company",
"posting_date",
"posting_time",
- "due_date",
- "overdue_days",
+ "status",
+ "section_break_9",
+ "currency",
+ "column_break_11",
+ "conversion_rate",
"address_and_contact_section",
+ "customer_address",
"address_display",
+ "contact_person",
"contact_display",
+ "column_break_16",
+ "company_address",
+ "company_address_display",
"contact_mobile",
"contact_email",
- "column_break_18",
- "company_address_display",
"section_break_6",
"dunning_type",
- "dunning_fee",
"column_break_8",
"rate_of_interest",
- "interest_amount",
"section_break_12",
- "dunning_amount",
- "grand_total",
- "income_account",
+ "overdue_payments",
+ "section_break_28",
+ "total_interest",
+ "dunning_fee",
"column_break_17",
- "status",
- "printing_setting_section",
+ "dunning_amount",
+ "base_dunning_amount",
+ "section_break_32",
+ "spacer",
+ "column_break_33",
+ "total_outstanding",
+ "grand_total",
+ "printing_settings_section",
"language",
"body_text",
"column_break_22",
"letter_head",
"closing_text",
+ "accounting_details_section",
+ "income_account",
+ "column_break_48",
+ "cost_center",
"amended_from"
],
"fields": [
@@ -60,32 +71,17 @@
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
- "options": "DUNN-.MM.-.YY.-"
+ "options": "DUNN-.MM.-.YY.-",
+ "print_hide": 1
},
{
- "fieldname": "sales_invoice",
- "fieldtype": "Link",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Sales Invoice",
- "options": "Sales Invoice",
- "reqd": 1
- },
- {
- "fetch_from": "sales_invoice.customer_name",
+ "fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Customer Name",
"read_only": 1
},
- {
- "fetch_from": "sales_invoice.outstanding_amount",
- "fieldname": "outstanding_amount",
- "fieldtype": "Currency",
- "label": "Outstanding Amount",
- "read_only": 1
- },
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
@@ -94,13 +90,8 @@
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
- "label": "Date"
- },
- {
- "fieldname": "overdue_days",
- "fieldtype": "Int",
- "label": "Overdue Days",
- "read_only": 1
+ "label": "Date",
+ "reqd": 1
},
{
"fieldname": "section_break_6",
@@ -112,16 +103,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Dunning Type",
- "options": "Dunning Type",
- "reqd": 1
- },
- {
- "default": "0",
- "fieldname": "interest_amount",
- "fieldtype": "Currency",
- "label": "Interest Amount",
- "precision": "2",
- "read_only": 1
+ "options": "Dunning Type"
},
{
"fieldname": "column_break_8",
@@ -134,6 +116,7 @@
"fieldname": "dunning_fee",
"fieldtype": "Currency",
"label": "Dunning Fee",
+ "options": "currency",
"precision": "2"
},
{
@@ -144,36 +127,24 @@
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
- {
- "fieldname": "printing_setting_section",
- "fieldtype": "Section Break",
- "label": "Printing Setting"
- },
{
"fieldname": "language",
"fieldtype": "Link",
"label": "Print Language",
- "options": "Language"
+ "options": "Language",
+ "print_hide": 1
},
{
"fieldname": "letter_head",
"fieldtype": "Link",
"label": "Letter Head",
- "options": "Letter Head"
+ "options": "Letter Head",
+ "print_hide": 1
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
- {
- "fetch_from": "sales_invoice.currency",
- "fieldname": "currency",
- "fieldtype": "Link",
- "hidden": 1,
- "label": "Currency",
- "options": "Currency",
- "read_only": 1
- },
{
"fieldname": "amended_from",
"fieldtype": "Link",
@@ -183,14 +154,6 @@
"print_hide": 1,
"read_only": 1
},
- {
- "allow_on_submit": 1,
- "default": "{customer_name}",
- "fieldname": "title",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Title"
- },
{
"fieldname": "body_text",
"fieldtype": "Text Editor",
@@ -201,13 +164,6 @@
"fieldtype": "Text Editor",
"label": "Closing Text"
},
- {
- "fetch_from": "sales_invoice.due_date",
- "fieldname": "due_date",
- "fieldtype": "Date",
- "label": "Due Date",
- "read_only": 1
- },
{
"fieldname": "posting_time",
"fieldtype": "Time",
@@ -222,26 +178,24 @@
"label": "Rate of Interest (%) Yearly"
},
{
+ "collapsible": 1,
"fieldname": "address_and_contact_section",
"fieldtype": "Section Break",
"label": "Address and Contact"
},
{
- "fetch_from": "sales_invoice.address_display",
"fieldname": "address_display",
"fieldtype": "Small Text",
"label": "Address",
"read_only": 1
},
{
- "fetch_from": "sales_invoice.contact_display",
"fieldname": "contact_display",
"fieldtype": "Small Text",
"label": "Contact",
"read_only": 1
},
{
- "fetch_from": "sales_invoice.contact_mobile",
"fieldname": "contact_mobile",
"fieldtype": "Small Text",
"label": "Mobile No",
@@ -249,18 +203,12 @@
"read_only": 1
},
{
- "fieldname": "column_break_18",
- "fieldtype": "Column Break"
- },
- {
- "fetch_from": "sales_invoice.company_address_display",
"fieldname": "company_address_display",
"fieldtype": "Small Text",
- "label": "Company Address",
+ "label": "Company Address Display",
"read_only": 1
},
{
- "fetch_from": "sales_invoice.contact_email",
"fieldname": "contact_email",
"fieldtype": "Data",
"label": "Contact Email",
@@ -268,18 +216,18 @@
"read_only": 1
},
{
- "fetch_from": "sales_invoice.customer",
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer",
- "read_only": 1
+ "reqd": 1
},
{
"default": "0",
"fieldname": "grand_total",
"fieldtype": "Currency",
"label": "Grand Total",
+ "options": "currency",
"precision": "2",
"read_only": 1
},
@@ -290,33 +238,150 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
- "options": "Draft\nResolved\nUnresolved\nCancelled"
- },
- {
- "fieldname": "dunning_amount",
- "fieldtype": "Currency",
- "hidden": 1,
- "label": "Dunning Amount",
+ "options": "Draft\nResolved\nUnresolved\nCancelled",
"read_only": 1
},
{
+ "description": "For dunning fee and interest",
+ "fetch_from": "dunning_type.income_account",
"fieldname": "income_account",
"fieldtype": "Link",
"label": "Income Account",
- "options": "Account"
+ "options": "Account",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "overdue_payments",
+ "fieldtype": "Table",
+ "label": "Overdue Payments",
+ "options": "Overdue Payment"
+ },
+ {
+ "fieldname": "section_break_28",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "total_interest",
+ "fieldtype": "Currency",
+ "label": "Total Interest",
+ "options": "currency",
+ "precision": "2",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_outstanding",
+ "fieldtype": "Currency",
+ "label": "Total Outstanding",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "customer_address",
+ "fieldtype": "Link",
+ "label": "Customer Address",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "contact_person",
+ "fieldtype": "Link",
+ "label": "Contact Person",
+ "options": "Contact",
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "dunning_amount",
+ "fieldtype": "Currency",
+ "label": "Dunning Amount",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fetch_from": "dunning_type.cost_center",
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "printing_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Printing Settings"
+ },
+ {
+ "fieldname": "section_break_32",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_33",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "spacer",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Spacer",
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company_address",
+ "fieldtype": "Link",
+ "label": "Company Address",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break",
+ "label": "Currency"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
},
{
- "fetch_from": "sales_invoice.conversion_rate",
"fieldname": "conversion_rate",
"fieldtype": "Float",
- "hidden": 1,
- "label": "Conversion Rate",
+ "label": "Conversion Rate"
+ },
+ {
+ "default": "0",
+ "fieldname": "base_dunning_amount",
+ "fieldtype": "Currency",
+ "label": "Dunning Amount (Company Currency)",
+ "options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_48",
+ "fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2023-06-03 16:24:01.677026",
+ "modified": "2023-06-15 15:46:53.865712",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning",
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index b4df0a5270..9d0d36b970 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -1,131 +1,150 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+"""
+# Accounting
+1. Payment of outstanding invoices with dunning amount
+ - Debit full amount to bank
+ - Credit invoiced amount to receivables
+ - Credit dunning amount to interest and similar revenue
+
+ -> Resolves dunning automatically
+"""
import json
import frappe
-from frappe.utils import cint, flt, getdate
+from frappe import _
+from frappe.contacts.doctype.address.address import get_address_display
+from frappe.utils import getdate
-from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
- get_accounting_dimensions,
-)
-from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
class Dunning(AccountsController):
def validate(self):
- self.validate_overdue_days()
- self.validate_amount()
- if not self.income_account:
- self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account")
+ self.validate_same_currency()
+ self.validate_overdue_payments()
+ self.validate_totals()
+ self.set_party_details()
+ self.set_dunning_level()
- def validate_overdue_days(self):
- self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
+ def validate_same_currency(self):
+ """
+ Throw an error if invoice currency differs from dunning currency.
+ """
+ for row in self.overdue_payments:
+ invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency")
+ if invoice_currency != self.currency:
+ frappe.throw(
+ _(
+ "The currency of invoice {} ({}) is different from the currency of this dunning ({})."
+ ).format(row.sales_invoice, invoice_currency, self.currency)
+ )
- def validate_amount(self):
- amounts = calculate_interest_and_amount(
- self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
+ def validate_overdue_payments(self):
+ daily_interest = self.rate_of_interest / 100 / 365
+
+ for row in self.overdue_payments:
+ row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0
+ row.interest = row.outstanding * daily_interest * row.overdue_days
+
+ def validate_totals(self):
+ self.total_outstanding = sum(row.outstanding for row in self.overdue_payments)
+ self.total_interest = sum(row.interest for row in self.overdue_payments)
+ self.dunning_amount = self.total_interest + self.dunning_fee
+ self.base_dunning_amount = self.dunning_amount * self.conversion_rate
+ self.grand_total = self.total_outstanding + self.dunning_amount
+
+ def set_party_details(self):
+ from erpnext.accounts.party import _get_party_details
+
+ party_details = _get_party_details(
+ self.customer,
+ ignore_permissions=self.flags.ignore_permissions,
+ doctype=self.doctype,
+ company=self.company,
+ posting_date=self.get("posting_date"),
+ fetch_payment_terms_template=False,
+ party_address=self.customer_address,
+ company_address=self.get("company_address"),
)
- if self.interest_amount != amounts.get("interest_amount"):
- self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
- if self.dunning_amount != amounts.get("dunning_amount"):
- self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
- if self.grand_total != amounts.get("grand_total"):
- self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
+ for field in [
+ "customer_address",
+ "address_display",
+ "company_address",
+ "contact_person",
+ "contact_display",
+ "contact_mobile",
+ ]:
+ self.set(field, party_details.get(field))
- def on_submit(self):
- self.make_gl_entries()
+ self.set("company_address_display", get_address_display(self.company_address))
- def on_cancel(self):
- if self.dunning_amount:
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
- make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
-
- def make_gl_entries(self):
- if not self.dunning_amount:
- return
- gl_entries = []
- invoice_fields = [
- "project",
- "cost_center",
- "debit_to",
- "party_account_currency",
- "conversion_rate",
- "cost_center",
- ]
- inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
-
- accounting_dimensions = get_accounting_dimensions()
- invoice_fields.extend(accounting_dimensions)
-
- dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
- default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
-
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": inv.debit_to,
- "party_type": "Customer",
- "party": self.customer,
- "due_date": self.due_date,
- "against": self.income_account,
- "debit": dunning_in_company_currency,
- "debit_in_account_currency": self.dunning_amount,
- "against_voucher": self.name,
- "against_voucher_type": "Dunning",
- "cost_center": inv.cost_center or default_cost_center,
- "project": inv.project,
+ def set_dunning_level(self):
+ for row in self.overdue_payments:
+ past_dunnings = frappe.get_all(
+ "Overdue Payment",
+ filters={
+ "payment_schedule": row.payment_schedule,
+ "parent": ("!=", row.parent),
+ "docstatus": 1,
},
- inv.party_account_currency,
- item=inv,
)
- )
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": self.income_account,
- "against": self.customer,
- "credit": dunning_in_company_currency,
- "cost_center": inv.cost_center or default_cost_center,
- "credit_in_account_currency": self.dunning_amount,
- "project": inv.project,
- },
- item=inv,
- )
- )
- make_gl_entries(
- gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
- )
+ row.dunning_level = len(past_dunnings) + 1
def resolve_dunning(doc, state):
+ """
+ Check if all payments have been made and resolve dunning, if yes. Called
+ when a Payment Entry is submitted.
+ """
for reference in doc.references:
- if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
- dunnings = frappe.get_list(
- "Dunning",
- filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
- ignore_permissions=True,
- )
+ # Consider partial and full payments:
+ # Submitting full payment: outstanding_amount will be 0
+ # Submitting 1st partial payment: outstanding_amount will be the pending installment
+ # Cancelling full payment: outstanding_amount will revert to total amount
+ # Cancelling last partial payment: outstanding_amount will revert to pending amount
+ submit_condition = reference.outstanding_amount < reference.total_amount
+ cancel_condition = reference.outstanding_amount <= reference.total_amount
+
+ if reference.reference_doctype == "Sales Invoice" and (
+ submit_condition if doc.docstatus == 1 else cancel_condition
+ ):
+ state = "Resolved" if doc.docstatus == 2 else "Unresolved"
+ dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
for dunning in dunnings:
- frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
+ resolve = True
+ dunning = frappe.get_doc("Dunning", dunning.get("name"))
+ for overdue_payment in dunning.overdue_payments:
+ outstanding_inv = frappe.get_value(
+ "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
+ )
+ outstanding_ps = frappe.get_value(
+ "Payment Schedule", overdue_payment.payment_schedule, "outstanding"
+ )
+ resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
+
+ dunning.status = "Resolved" if resolve else "Unresolved"
+ dunning.save()
-def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
- interest_amount = 0
- grand_total = flt(outstanding_amount) + flt(dunning_fee)
- if rate_of_interest:
- interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
- interest_amount = (interest_per_year * cint(overdue_days)) / 365
- grand_total += flt(interest_amount)
- dunning_amount = flt(interest_amount) + flt(dunning_fee)
- return {
- "interest_amount": interest_amount,
- "grand_total": grand_total,
- "dunning_amount": dunning_amount,
- }
+def get_linked_dunnings_as_per_state(sales_invoice, state):
+ dunning = frappe.qb.DocType("Dunning")
+ overdue_payment = frappe.qb.DocType("Overdue Payment")
+
+ return (
+ frappe.qb.from_(dunning)
+ .join(overdue_payment)
+ .on(overdue_payment.parent == dunning.name)
+ .select(dunning.name)
+ .where(
+ (dunning.status == state)
+ & (dunning.docstatus != 2)
+ & (overdue_payment.sales_invoice == sales_invoice)
+ )
+ ).run(as_dict=True)
@frappe.whitelist()
diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py
deleted file mode 100644
index d1d4031410..0000000000
--- a/erpnext/accounts/doctype/dunning/dunning_dashboard.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from frappe import _
-
-
-def get_data():
- return {
- "fieldname": "dunning",
- "non_standard_fieldnames": {
- "Journal Entry": "reference_name",
- "Payment Entry": "reference_name",
- },
- "transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
- }
diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py
index e1fd1e984f..b29ace275f 100644
--- a/erpnext/accounts/doctype/dunning/test_dunning.py
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -1,162 +1,197 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate, today
-from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount
+from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
unlink_payment_on_cancel_of_invoice,
)
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
+ create_dunning as create_dunning_from_sales_invoice,
+)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice_against_cost_center,
)
+test_dependencies = ["Company", "Cost Center"]
-class TestDunning(unittest.TestCase):
+
+class TestDunning(FrappeTestCase):
@classmethod
- def setUpClass(self):
- create_dunning_type()
- create_dunning_type_with_zero_interest_rate()
+ def setUpClass(cls):
+ super().setUpClass()
+ create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1)
+ create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0)
unlink_payment_on_cancel_of_invoice()
@classmethod
- def tearDownClass(self):
+ def tearDownClass(cls):
unlink_payment_on_cancel_of_invoice(0)
+ super().tearDownClass()
- def test_dunning(self):
- dunning = create_dunning()
- amounts = calculate_interest_and_amount(
- dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
- )
- self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
- self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
- self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
+ def test_dunning_without_fees(self):
+ dunning = create_dunning(overdue_days=20)
- def test_dunning_with_zero_interest_rate(self):
- dunning = create_dunning_with_zero_interest_rate()
- amounts = calculate_interest_and_amount(
- dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
- )
- self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
- self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
- self.assertEqual(round(amounts.get("grand_total"), 2), 120)
+ self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
+ self.assertEqual(round(dunning.total_interest, 2), 0.00)
+ self.assertEqual(round(dunning.dunning_fee, 2), 0.00)
+ self.assertEqual(round(dunning.dunning_amount, 2), 0.00)
+ self.assertEqual(round(dunning.grand_total, 2), 100.00)
- def test_gl_entries(self):
- dunning = create_dunning()
- dunning.submit()
- gl_entries = frappe.db.sql(
- """select account, debit, credit
- from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
- order by account asc""",
- dunning.name,
- as_dict=1,
- )
- self.assertTrue(gl_entries)
- expected_values = dict(
- (d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
- )
- 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_dunning_with_fees_and_interest(self):
+ dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
- def test_payment_entry(self):
- dunning = create_dunning()
+ self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
+ self.assertEqual(round(dunning.total_interest, 2), 0.41)
+ self.assertEqual(round(dunning.dunning_fee, 2), 10.00)
+ self.assertEqual(round(dunning.dunning_amount, 2), 10.41)
+ self.assertEqual(round(dunning.grand_total, 2), 110.41)
+
+ def test_dunning_with_payment_entry(self):
+ dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
dunning.submit()
pe = get_payment_entry("Dunning", dunning.name)
pe.reference_no = "1"
pe.reference_date = nowdate()
- pe.paid_from_account_currency = dunning.currency
- pe.paid_to_account_currency = dunning.currency
- pe.source_exchange_rate = 1
- pe.target_exchange_rate = 1
pe.insert()
pe.submit()
- si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
- self.assertEqual(si_doc.outstanding_amount, 0)
+
+ for overdue_payment in dunning.overdue_payments:
+ outstanding_amount = frappe.get_value(
+ "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
+ )
+ self.assertEqual(outstanding_amount, 0)
+
+ dunning.reload()
+ self.assertEqual(dunning.status, "Resolved")
+
+ def test_dunning_and_payment_against_partially_due_invoice(self):
+ """
+ Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
+ """
+ create_payment_terms_template_for_dunning()
+ sales_invoice = create_sales_invoice_against_cost_center(
+ posting_date=add_days(today(), -1 * 6),
+ qty=1,
+ rate=100,
+ do_not_submit=True,
+ )
+ sales_invoice.payment_terms_template = "_Test 50-50 for Dunning"
+ sales_invoice.submit()
+ dunning = create_dunning_from_sales_invoice(sales_invoice.name)
+
+ self.assertEqual(len(dunning.overdue_payments), 1)
+ self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning")
+
+ dunning.submit()
+ pe = get_payment_entry("Dunning", dunning.name)
+ pe.reference_no, pe.reference_date = "2", nowdate()
+ pe.insert()
+ pe.submit()
+ sales_invoice.load_from_db()
+ dunning.load_from_db()
+
+ self.assertEqual(sales_invoice.status, "Partly Paid")
+ self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0)
+ self.assertEqual(dunning.status, "Resolved")
+
+ # Test impact on cancellation of PE
+ pe.cancel()
+ sales_invoice.reload()
+ dunning.reload()
+
+ self.assertEqual(sales_invoice.status, "Overdue")
+ self.assertEqual(dunning.status, "Unresolved")
-def create_dunning():
- posting_date = add_days(today(), -20)
- due_date = add_days(today(), -15)
+def create_dunning(overdue_days, dunning_type_name=None):
+ posting_date = add_days(today(), -1 * overdue_days)
sales_invoice = create_sales_invoice_against_cost_center(
- posting_date=posting_date, due_date=due_date, status="Overdue"
+ posting_date=posting_date, qty=1, rate=100
)
- dunning_type = frappe.get_doc("Dunning Type", "First Notice")
- dunning = frappe.new_doc("Dunning")
- dunning.sales_invoice = sales_invoice.name
- dunning.customer_name = sales_invoice.customer_name
- dunning.outstanding_amount = sales_invoice.outstanding_amount
- dunning.debit_to = sales_invoice.debit_to
- dunning.currency = sales_invoice.currency
- dunning.company = sales_invoice.company
- dunning.posting_date = nowdate()
- dunning.due_date = sales_invoice.due_date
- dunning.dunning_type = "First Notice"
- dunning.rate_of_interest = dunning_type.rate_of_interest
- dunning.dunning_fee = dunning_type.dunning_fee
- dunning.save()
- return dunning
+ dunning = create_dunning_from_sales_invoice(sales_invoice.name)
+
+ if dunning_type_name:
+ dunning_type = frappe.get_doc("Dunning Type", dunning_type_name)
+ dunning.dunning_type = dunning_type.name
+ dunning.rate_of_interest = dunning_type.rate_of_interest
+ dunning.dunning_fee = dunning_type.dunning_fee
+ dunning.income_account = dunning_type.income_account
+ dunning.cost_center = dunning_type.cost_center
+
+ return dunning.save()
-def create_dunning_with_zero_interest_rate():
- posting_date = add_days(today(), -20)
- due_date = add_days(today(), -15)
- sales_invoice = create_sales_invoice_against_cost_center(
- posting_date=posting_date, due_date=due_date, status="Overdue"
- )
- dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
- dunning = frappe.new_doc("Dunning")
- dunning.sales_invoice = sales_invoice.name
- dunning.customer_name = sales_invoice.customer_name
- dunning.outstanding_amount = sales_invoice.outstanding_amount
- dunning.debit_to = sales_invoice.debit_to
- dunning.currency = sales_invoice.currency
- dunning.company = sales_invoice.company
- dunning.posting_date = nowdate()
- dunning.due_date = sales_invoice.due_date
- dunning.dunning_type = "First Notice with 0% Rate of Interest"
- dunning.rate_of_interest = dunning_type.rate_of_interest
- dunning.dunning_fee = dunning_type.dunning_fee
- dunning.save()
- return dunning
+def create_dunning_type(title, fee, interest, is_default):
+ company = "_Test Company"
+ if frappe.db.exists("Dunning Type", f"{title} - _TC"):
+ return
-
-def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type")
- dunning_type.dunning_type = "First Notice"
- dunning_type.start_day = 10
- dunning_type.end_day = 20
- dunning_type.dunning_fee = 20
- dunning_type.rate_of_interest = 8
+ dunning_type.dunning_type = title
+ dunning_type.company = company
+ dunning_type.is_default = is_default
+ dunning_type.dunning_fee = fee
+ dunning_type.rate_of_interest = interest
+ dunning_type.income_account = get_income_account(company)
+ dunning_type.cost_center = get_default_cost_center(company)
dunning_type.append(
"dunning_letter_text",
{
"language": "en",
- "body_text": "We have still not received payment for our invoice ",
+ "body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
},
)
- dunning_type.save()
+ dunning_type.insert()
-def create_dunning_type_with_zero_interest_rate():
- dunning_type = frappe.new_doc("Dunning Type")
- dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
- dunning_type.start_day = 10
- dunning_type.end_day = 20
- dunning_type.dunning_fee = 20
- dunning_type.rate_of_interest = 0
- dunning_type.append(
- "dunning_letter_text",
- {
- "language": "en",
- "body_text": "We have still not received payment for our invoice ",
- "closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
- },
+def get_income_account(company):
+ return (
+ frappe.get_value("Company", company, "default_income_account")
+ or frappe.get_all(
+ "Account",
+ filters={"is_group": 0, "company": company},
+ or_filters={
+ "report_type": "Profit and Loss",
+ "account_type": ("in", ("Income Account", "Temporary")),
+ },
+ limit=1,
+ pluck="name",
+ )[0]
)
- dunning_type.save()
+
+
+def create_payment_terms_template_for_dunning():
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
+
+ create_payment_term("_Test Payment Term 1 for Dunning")
+ create_payment_term("_Test Payment Term 2 for Dunning")
+
+ if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"):
+ frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "_Test 50-50 for Dunning",
+ "allocate_payment_based_on_payment_terms": 1,
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "payment_term": "_Test Payment Term 1 for Dunning",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 5,
+ },
+ {
+ "doctype": "Payment Terms Template Detail",
+ "payment_term": "_Test Payment Term 2 for Dunning",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 10,
+ },
+ ],
+ }
+ ).insert()
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.js b/erpnext/accounts/doctype/dunning_type/dunning_type.js
index 54156b488d..b2c08c1c7f 100644
--- a/erpnext/accounts/doctype/dunning_type/dunning_type.js
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.js
@@ -1,8 +1,24 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Dunning Type', {
- // refresh: function(frm) {
-
- // }
+frappe.ui.form.on("Dunning Type", {
+ setup: function (frm) {
+ frm.set_query("income_account", () => {
+ return {
+ filters: {
+ root_type: "Income",
+ is_group: 0,
+ company: frm.doc.company,
+ },
+ };
+ });
+ frm.set_query("cost_center", () => {
+ return {
+ filters: {
+ is_group: 0,
+ company: frm.doc.company,
+ },
+ };
+ });
+ },
});
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.json b/erpnext/accounts/doctype/dunning_type/dunning_type.json
index da43664472..5e39769735 100644
--- a/erpnext/accounts/doctype/dunning_type/dunning_type.json
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.json
@@ -1,23 +1,26 @@
{
"actions": [],
"allow_rename": 1,
- "autoname": "field:dunning_type",
+ "beta": 1,
"creation": "2019-12-04 04:59:08.003664",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"dunning_type",
- "overdue_interval_section",
- "start_day",
- "column_break_4",
- "end_day",
+ "is_default",
+ "column_break_3",
+ "company",
"section_break_6",
"dunning_fee",
"column_break_8",
"rate_of_interest",
"text_block_section",
- "dunning_letter_text"
+ "dunning_letter_text",
+ "section_break_9",
+ "income_account",
+ "column_break_13",
+ "cost_center"
],
"fields": [
{
@@ -45,10 +48,6 @@
"fieldtype": "Table",
"options": "Dunning Letter Text"
},
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
@@ -57,33 +56,62 @@
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
- {
- "fieldname": "overdue_interval_section",
- "fieldtype": "Section Break",
- "label": "Overdue Interval"
- },
- {
- "fieldname": "start_day",
- "fieldtype": "Int",
- "label": "Start Day"
- },
- {
- "fieldname": "end_day",
- "fieldtype": "Int",
- "label": "End Day"
- },
{
"fieldname": "rate_of_interest",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Rate of Interest (%) Yearly"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_default",
+ "fieldtype": "Check",
+ "label": "Is Default"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "income_account",
+ "fieldtype": "Link",
+ "label": "Income Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
}
],
- "links": [],
- "modified": "2020-07-15 17:14:17.835074",
+ "links": [
+ {
+ "link_doctype": "Dunning",
+ "link_fieldname": "dunning_type"
+ }
+ ],
+ "modified": "2021-11-13 00:25:35.659283",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning Type",
+ "naming_rule": "By script",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/accounts/doctype/dunning_type/dunning_type.py b/erpnext/accounts/doctype/dunning_type/dunning_type.py
index 1b9bb9c032..226e159a3b 100644
--- a/erpnext/accounts/doctype/dunning_type/dunning_type.py
+++ b/erpnext/accounts/doctype/dunning_type/dunning_type.py
@@ -2,9 +2,11 @@
# For license information, please see license.txt
-# import frappe
+import frappe
from frappe.model.document import Document
class DunningType(Document):
- pass
+ def autoname(self):
+ company_abbr = frappe.get_value("Company", self.company, "abbr")
+ self.name = f"{self.dunning_type} - {company_abbr}"
diff --git a/erpnext/accounts/doctype/dunning_type/test_records.json b/erpnext/accounts/doctype/dunning_type/test_records.json
new file mode 100644
index 0000000000..7f28aab873
--- /dev/null
+++ b/erpnext/accounts/doctype/dunning_type/test_records.json
@@ -0,0 +1,36 @@
+[
+ {
+ "doctype": "Dunning Type",
+ "dunning_type": "_Test First Notice",
+ "company": "_Test Company",
+ "is_default": 1,
+ "dunning_fee": 0.0,
+ "rate_of_interest": 0.0,
+ "dunning_letter_text": [
+ {
+ "language": "en",
+ "body_text": "We have still not received payment for our invoice",
+ "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
+ }
+ ],
+ "income_account": "Sales - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ },
+ {
+ "doctype": "Dunning Type",
+ "dunning_type": "_Test Second Notice",
+ "company": "_Test Company",
+ "is_default": 0,
+ "dunning_fee": 10.0,
+ "rate_of_interest": 10.0,
+ "dunning_letter_text": [
+ {
+ "language": "en",
+ "body_text": "We have still not received payment for our invoice",
+ "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
+ }
+ ],
+ "income_account": "Sales - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ }
+]
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json
index e6d97a1fb2..5063ec6076 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.json
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json
@@ -32,7 +32,11 @@
"finance_book",
"to_rename",
"due_date",
- "is_cancelled"
+ "is_cancelled",
+ "transaction_currency",
+ "debit_in_transaction_currency",
+ "credit_in_transaction_currency",
+ "transaction_exchange_rate"
],
"fields": [
{
@@ -253,15 +257,40 @@
"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,
- "modified": "2020-04-07 16:22:33.766994",
+ "links": [],
+ "modified": "2023-08-16 21:38:44.072267",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -290,5 +319,6 @@
"quick_entry": 1,
"search_fields": "voucher_no,account,posting_date,against_voucher",
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index f07a4fa3bc..7af40c46cb 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -58,6 +58,13 @@ 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",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index a51e38eefe..35a378856b 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -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'];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"];
},
refresh: function(frm) {
@@ -264,11 +264,11 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
}
if(jvd.party_type && jvd.party) {
- var party_field = "";
+ let party_field = "";
if(jvd.reference_type.indexOf("Sales")===0) {
- var party_field = "customer";
+ party_field = "customer";
} else if (jvd.reference_type.indexOf("Purchase")===0) {
- var party_field = "supplier";
+ party_field = "supplier";
}
if (party_field) {
@@ -368,7 +368,7 @@ cur_frm.cscript.update_totals = function(doc) {
td += flt(accounts[i].debit, precision("debit", accounts[i]));
tc += flt(accounts[i].credit, precision("credit", accounts[i]));
}
- var doc = locals[doc.doctype][doc.name];
+ doc = locals[doc.doctype][doc.name];
doc.total_debit = td;
doc.total_credit = tc;
doc.difference = flt((td - tc), precision("difference"));
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 80e72226d3..2eb54a54d5 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -9,6 +9,7 @@
"engine": "InnoDB",
"field_order": [
"entry_type_and_date",
+ "is_system_generated",
"title",
"voucher_type",
"naming_series",
@@ -533,13 +534,22 @@
"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-03-01 14:58:59.286591",
+ "modified": "2023-08-10 14:32:22.366895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index ea4a2d4b19..85ef6f76d2 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -18,6 +18,7 @@ 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,
@@ -87,15 +88,16 @@ class JournalEntry(AccountsController):
self.update_invoice_discounting()
def on_cancel(self):
- from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
-
- unlink_ref_doc_from_payment_entries(self)
+ # References for this Journal are removed on the `on_cancel` event in accounts_controller
+ super(JournalEntry, self).on_cancel()
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()
@@ -499,11 +501,12 @@ class JournalEntry(AccountsController):
)
if not against_entries:
- frappe.throw(
- _(
- "Journal Entry {0} does not have account {1} or already matched against other voucher"
- ).format(d.reference_name, d.account)
- )
+ 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)
+ )
else:
dr_or_cr = "debit" if d.credit > 0 else "credit"
valid = False
@@ -586,7 +589,9 @@ class JournalEntry(AccountsController):
else:
party_account = against_voucher[1]
- if against_voucher[0] != cstr(d.party) or party_account != d.account:
+ if (
+ against_voucher[0] != cstr(d.party) or party_account != d.account
+ ) and self.voucher_type != "Exchange Gain Or Loss":
frappe.throw(
_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
d.idx,
@@ -768,18 +773,23 @@ class JournalEntry(AccountsController):
)
):
- # 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,
- )
+ 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,
+ )
if not d.exchange_rate:
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
@@ -787,6 +797,9 @@ 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))
@@ -935,6 +948,8 @@ 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):
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index e7aca79d08..a6e920b7ef 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -5,6 +5,7 @@
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
@@ -13,6 +14,7 @@ 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])
diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
index 47ad19e0f9..3ba8cea94b 100644
--- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
+++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
@@ -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"
+ "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"
},
{
"fieldname": "reference_name",
@@ -284,7 +284,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-10-26 20:03:10.906259",
+ "modified": "2023-06-16 14:11:13.507807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
index 48a25ad6b8..4f58579a52 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
@@ -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 enought Loyalty Points to redeem"))
+ frappe.throw(_("You don't have enough Loyalty Points to redeem"))
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
- if loyalty_amount > ref_doc.grand_total:
- frappe.throw(_("You can't redeem Loyalty Points having more value than the Grand Total."))
+ if loyalty_amount > ref_doc.rounded_total:
+ frappe.throw(_("You can't redeem Loyalty Points having more value than the Rounded Total."))
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
ref_doc.loyalty_amount = loyalty_amount
diff --git a/erpnext/accounts/report/tds_payable_monthly/__init__.py b/erpnext/accounts/doctype/overdue_payment/__init__.py
similarity index 100%
rename from erpnext/accounts/report/tds_payable_monthly/__init__.py
rename to erpnext/accounts/doctype/overdue_payment/__init__.py
diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.json b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json
new file mode 100644
index 0000000000..99e16469d0
--- /dev/null
+++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.json
@@ -0,0 +1,170 @@
+{
+ "actions": [],
+ "creation": "2021-09-15 18:34:27.172906",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "sales_invoice",
+ "payment_schedule",
+ "dunning_level",
+ "payment_term",
+ "section_break_15",
+ "description",
+ "section_break_4",
+ "due_date",
+ "overdue_days",
+ "mode_of_payment",
+ "column_break_5",
+ "invoice_portion",
+ "section_break_16",
+ "payment_amount",
+ "outstanding",
+ "paid_amount",
+ "discounted_amount",
+ "interest"
+ ],
+ "fields": [
+ {
+ "columns": 2,
+ "fieldname": "payment_term",
+ "fieldtype": "Link",
+ "label": "Payment Term",
+ "options": "Payment Term",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_15",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "columns": 2,
+ "fetch_from": "payment_term.description",
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "due_date",
+ "fieldtype": "Date",
+ "label": "Due Date",
+ "read_only": 1
+ },
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "invoice_portion",
+ "fieldtype": "Percent",
+ "label": "Invoice Portion",
+ "read_only": 1
+ },
+ {
+ "columns": 2,
+ "fieldname": "payment_amount",
+ "fieldtype": "Currency",
+ "label": "Payment Amount",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "payment_amount",
+ "fieldname": "outstanding",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Outstanding",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "depends_on": "paid_amount",
+ "fieldname": "paid_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Amount",
+ "options": "currency"
+ },
+ {
+ "default": "0",
+ "depends_on": "discounted_amount",
+ "fieldname": "discounted_amount",
+ "fieldtype": "Currency",
+ "label": "Discounted Amount",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "sales_invoice",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Sales Invoice",
+ "options": "Sales Invoice",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "payment_schedule",
+ "fieldtype": "Data",
+ "label": "Payment Schedule",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "overdue_days",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Overdue Days",
+ "read_only": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "dunning_level",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Dunning Level",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "interest",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Interest",
+ "options": "currency",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-23 13:48:27.898830",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Overdue Payment",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/overdue_payment/overdue_payment.py b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py
new file mode 100644
index 0000000000..6a543ad467
--- /dev/null
+++ b/erpnext/accounts/doctype/overdue_payment/overdue_payment.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class OverduePayment(Document):
+ pass
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 0701435dfc..f131be2dfe 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -1,13 +1,15 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-{% include "erpnext/public/js/controllers/accounts.js" %}
frappe.provide("erpnext.accounts.dimensions");
cur_frm.cscript.tax_table = "Advance Taxes and Charges";
+erpnext.accounts.taxes.setup_tax_validations("Payment Entry");
+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', "Repost Payment Ledger"];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -106,12 +108,11 @@ frappe.ui.form.on('Payment Entry', {
});
frm.set_query("reference_doctype", "references", function() {
+ let doctypes = ["Journal Entry"];
if (frm.doc.party_type == "Customer") {
- var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
+ doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
} else if (frm.doc.party_type == "Supplier") {
- var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
- } else {
- var doctypes = ["Journal Entry"];
+ doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
}
return {
@@ -122,13 +123,10 @@ frappe.ui.form.on('Payment Entry', {
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
const child = locals[cdt][cdn];
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
- let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name});
-
- payment_term_list = payment_term_list.map(pt => pt.payment_term);
-
return {
+ query: "erpnext.controllers.queries.get_payment_terms_for_references",
filters: {
- 'name': ['in', payment_term_list]
+ 'reference': child.reference_name
}
}
}
@@ -165,6 +163,7 @@ frappe.ui.form.on('Payment Entry', {
},
company: function(frm) {
+ frm.trigger('party');
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
@@ -287,6 +286,13 @@ frappe.ui.form.on('Payment Entry', {
}
},
+ mode_of_payment: function(frm) {
+ erpnext.accounts.pos.get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account){
+ let payment_account_field = frm.doc.payment_type == "Receive" ? "paid_to" : "paid_from";
+ frm.set_value(payment_account_field, account);
+ })
+ },
+
party_type: function(frm) {
let party_types = Object.keys(frappe.boot.party_account_types);
@@ -319,10 +325,6 @@ frappe.ui.form.on('Payment Entry', {
}
},
- company: function(frm){
- frm.trigger('party');
- },
-
party: function(frm) {
if (frm.doc.contact_email || frm.doc.contact_person) {
frm.set_value("contact_email", "");
@@ -901,12 +903,12 @@ frappe.ui.form.on('Payment Entry', {
if(frm.doc.payment_type == "Receive"
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
- unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
+ unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges)
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
} else if (frm.doc.payment_type == "Pay"
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {
- unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions
+ unallocated_amount = (frm.doc.base_paid_amount + flt(frm.doc.base_total_taxes_and_charges) - (total_deductions
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
}
}
@@ -1106,7 +1108,7 @@ frappe.ui.form.on('Payment Entry', {
if (tax.charge_type === 'On Net Total') {
tax.charge_type = 'On Paid Amount';
}
- me.frm.add_child("taxes", tax);
+ frm.add_child("taxes", tax);
}
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
@@ -1222,7 +1224,7 @@ frappe.ui.form.on('Payment Entry', {
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item;
} else {
tax.grand_total_fraction_for_current_item =
- me.frm.doc["taxes"][i-1].grand_total_fraction_for_current_item +
+ frm.doc["taxes"][i-1].grand_total_fraction_for_current_item +
tax.tax_fraction_for_current_item;
}
@@ -1269,7 +1271,7 @@ frappe.ui.form.on('Payment Entry', {
}
});
- $.each(me.frm.doc["taxes"] || [], function(i, tax) {
+ $.each(frm.doc["taxes"] || [], function(i, tax) {
let current_tax_amount = frm.events.get_current_tax_amount(frm, tax);
// Adjust divisional loss to the last item
@@ -1463,4 +1465,4 @@ frappe.ui.form.on('Payment Entry', {
});
}
},
-})
\ No newline at end of file
+})
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index dcd7295bae..ac31e8a1db 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -28,7 +28,12 @@ from erpnext.accounts.general_ledger import (
process_gl_map,
)
from erpnext.accounts.party import get_party_account
-from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
+from erpnext.accounts.utils import (
+ cancel_exchange_gain_loss_journal,
+ get_account_currency,
+ get_balance_on,
+ get_outstanding_invoices,
+)
from erpnext.controllers.accounts_controller import (
AccountsController,
get_supplier_block_status,
@@ -66,7 +71,7 @@ class PaymentEntry(AccountsController):
self.setup_party_account_field()
self.set_missing_values()
self.set_liability_account()
- self.set_missing_ref_details()
+ self.set_missing_ref_details(force=True)
self.validate_payment_type()
self.validate_party_details()
self.set_exchange_rate()
@@ -142,7 +147,10 @@ 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()
@@ -207,53 +215,103 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
+ def term_based_allocation_enabled_for_reference(
+ self, reference_doctype: str, reference_name: str
+ ) -> bool:
+ if (
+ reference_doctype
+ and reference_doctype in ["Sales Invoice", "Sales Order", "Purchase Order", "Purchase Invoice"]
+ and reference_name
+ ):
+ if template := frappe.db.get_value(reference_doctype, reference_name, "payment_terms_template"):
+ return frappe.db.get_value(
+ "Payment Terms Template", template, "allocate_payment_based_on_payment_terms"
+ )
+ return False
+
def validate_allocated_amount_with_latest_data(self):
- 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,
- )
+ 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,
+ )
- # Group latest_references by (voucher_type, voucher_no)
- latest_lookup = {}
- for d in latest_references:
- d = frappe._dict(d)
- latest_lookup.update({(d.voucher_type, d.voucher_no): 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 d in self.get("references"):
- latest = latest_lookup.get((d.reference_doctype, d.reference_name))
+ for idx, d in enumerate(self.get("references"), start=1):
+ latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
- # 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)
- )
+ # 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))
+ )
- fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
+ # 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)
- if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
- frappe.throw(fail_message.format(d.idx))
+ # 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)
+ )
- # 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))
+ 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))
def delink_advance_entry_references(self):
for reference in self.references:
@@ -358,7 +416,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")
+ self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
if not self.source_exchange_rate:
self.source_exchange_rate = get_exchange_rate(
@@ -371,7 +429,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")
+ self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
if not self.target_exchange_rate:
self.target_exchange_rate = get_exchange_rate(
@@ -473,7 +531,7 @@ class PaymentEntry(AccountsController):
_(
"References {0} of type {1} had no outstanding amount left before submitting the Payment Entry. Now they have a negative outstanding amount."
).format(
- frappe.bold(comma_and((d.reference_name for d in references))),
+ frappe.bold(comma_and([d.reference_name for d in references])),
_(reference_doctype),
)
+ "
"
@@ -636,7 +694,9 @@ class PaymentEntry(AccountsController):
if not self.apply_tax_withholding_amount:
return
- net_total = self.paid_amount
+ order_amount = self.get_order_net_total()
+
+ net_total = flt(order_amount) + flt(self.unallocated_amount)
# Adding args as purchase invoice to get TDS amount
args = frappe._dict(
@@ -681,6 +741,20 @@ 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()
@@ -767,10 +841,25 @@ 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(d.exchange_rate), self.precision("base_paid_amount")
+ 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):
@@ -961,6 +1050,10 @@ 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:
@@ -1498,9 +1591,12 @@ 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(outstanding_invoices)
+ outstanding_invoices = split_invoices_based_on_payment_terms(
+ outstanding_invoices, args.get("company")
+ )
for d in outstanding_invoices:
d["exchange_rate"] = 1
@@ -1560,8 +1656,27 @@ def get_outstanding_reference_documents(args, validate=False):
return data
-def split_invoices_based_on_payment_terms(outstanding_invoices):
+def split_invoices_based_on_payment_terms(outstanding_invoices, company):
invoice_ref_based_on_payment_terms = {}
+
+ company_currency = (
+ frappe.db.get_value("Company", company, "default_currency") if company else None
+ )
+ exc_rates = frappe._dict()
+ for doctype in ["Sales Invoice", "Purchase Invoice"]:
+ invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
+ for x in frappe.db.get_all(
+ doctype,
+ filters={"name": ["in", invoices]},
+ fields=["name", "currency", "conversion_rate", "party_account_currency"],
+ ):
+ exc_rates[x.name] = frappe._dict(
+ conversion_rate=x.conversion_rate,
+ currency=x.currency,
+ party_account_currency=x.party_account_currency,
+ company_currency=company_currency,
+ )
+
for idx, d in enumerate(outstanding_invoices):
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
payment_term_template = frappe.db.get_value(
@@ -1578,6 +1693,14 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
for payment_term in payment_schedule:
if payment_term.outstanding > 0.1:
+ doc_details = exc_rates.get(payment_term.parent, None)
+ is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
+ doc_details.party_account_currency != doc_details.company_currency
+ )
+ payment_term_outstanding = flt(payment_term.outstanding)
+ if not is_multi_currency_acc:
+ payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
+
invoice_ref_based_on_payment_terms.setdefault(idx, [])
invoice_ref_based_on_payment_terms[idx].append(
frappe._dict(
@@ -1589,6 +1712,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
"posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount),
"outstanding_amount": flt(d.outstanding_amount),
+ "payment_term_outstanding": payment_term_outstanding,
+ "allocated_amount": payment_term_outstanding
+ if payment_term_outstanding
+ else d.outstanding_amount,
"payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term,
"account": d.account,
@@ -1664,7 +1791,7 @@ def get_orders_to_be_billed(
{party_type} = %s
and docstatus = 1
and company = %s
- and ifnull(status, "") != "Closed"
+ and status != "Closed"
and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid
and abs(100 - per_billed) > 0.01
{condition}
@@ -1914,7 +2041,6 @@ 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) >= (
@@ -2010,28 +2136,27 @@ def get_payment_entry(
pe.append("references", reference)
else:
if dt == "Dunning":
+ for overdue_payment in doc.overdue_payments:
+ pe.append(
+ "references",
+ {
+ "reference_doctype": "Sales Invoice",
+ "reference_name": overdue_payment.sales_invoice,
+ "payment_term": overdue_payment.payment_term,
+ "due_date": overdue_payment.due_date,
+ "total_amount": overdue_payment.outstanding,
+ "outstanding_amount": overdue_payment.outstanding,
+ "allocated_amount": overdue_payment.outstanding,
+ },
+ )
+
pe.append(
- "references",
+ "deductions",
{
- "reference_doctype": "Sales Invoice",
- "reference_name": doc.get("sales_invoice"),
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- "total_amount": doc.get("outstanding_amount"),
- "outstanding_amount": doc.get("outstanding_amount"),
- "allocated_amount": doc.get("outstanding_amount"),
- },
- )
- pe.append(
- "references",
- {
- "reference_doctype": dt,
- "reference_name": dn,
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- "total_amount": doc.get("dunning_amount"),
- "outstanding_amount": doc.get("dunning_amount"),
- "allocated_amount": doc.get("dunning_amount"),
+ "account": doc.income_account,
+ "cost_center": doc.cost_center,
+ "amount": -1 * doc.dunning_amount,
+ "description": _("Interest and/or dunning fee"),
},
)
else:
@@ -2055,7 +2180,7 @@ def get_payment_entry(
update_accounting_dimensions(pe, doc)
if party_account and bank:
- pe.set_exchange_rate(ref_doc=reference_doc)
+ pe.set_exchange_rate(ref_doc=doc)
pe.set_amounts()
if discount_amount:
@@ -2125,8 +2250,10 @@ def set_party_account_currency(dt, party_account, doc):
def set_payment_type(dt, doc):
if (
- dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0)
- ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0):
+ (dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0))
+ or (dt == "Purchase Invoice" and doc.outstanding_amount < 0)
+ or dt == "Dunning"
+ ):
payment_type = "Receive"
else:
payment_type = "Pay"
@@ -2371,6 +2498,7 @@ def get_reference_as_per_payment_terms(
"due_date": doc.get("due_date"),
"total_amount": grand_total,
"outstanding_amount": outstanding_amount,
+ "payment_term_outstanding": payment_term_outstanding,
"payment_term": payment_term.payment_term,
"allocated_amount": payment_term_outstanding,
}
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 70cc4b3d34..8f9f7ce3be 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -31,6 +31,16 @@ 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")
@@ -591,21 +601,15 @@ 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,
@@ -792,33 +796,28 @@ 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, 5000, si.name],
+ ["_Test Receivable USD - _TC", 0, 5500, 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)
@@ -1061,6 +1060,147 @@ class TestPaymentEntry(FrappeTestCase):
}
self.assertDictEqual(ref_details, expected_response)
+ @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_on_payment_terms(self):
+ """
+ Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown.
+
+ """
+ customer = create_customer()
+ create_payment_terms_template()
+
+ # Validate allocation on base/company currency
+ si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200)
+ si1.payment_terms_template = "Test Receivable Template"
+ si1.save().submit()
+
+ si1.reload()
+ pe = get_payment_entry(si1.doctype, si1.name).save()
+ # Allocated amount should be according to the payment schedule
+ for idx, schedule in enumerate(si1.payment_schedule):
+ with self.subTest(idx=idx):
+ self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
+ pe.save()
+
+ # Overallocation validation should trigger
+ pe.paid_amount = 400
+ pe.references[0].allocated_amount = 200
+ pe.references[1].allocated_amount = 200
+ self.assertRaises(frappe.ValidationError, pe.save)
+ pe.delete()
+ si1.cancel()
+ si1.delete()
+
+ # Validate allocation on foreign currency
+ si2 = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=80,
+ do_not_save=1,
+ )
+ si2.payment_terms_template = "Test Receivable Template"
+ si2.save().submit()
+
+ si2.reload()
+ pe = get_payment_entry(si2.doctype, si2.name).save()
+ # Allocated amount should be according to the payment schedule
+ for idx, schedule in enumerate(si2.payment_schedule):
+ with self.subTest(idx=idx):
+ self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
+ pe.save()
+
+ # Overallocation validation should trigger
+ pe.paid_amount = 200
+ pe.references[0].allocated_amount = 100
+ pe.references[1].allocated_amount = 100
+ self.assertRaises(frappe.ValidationError, pe.save)
+ pe.delete()
+ si2.cancel()
+ si2.delete()
+
+ # Validate allocation in base/company currency on a foreign currency document
+ # when invoice is made is foreign currency, but posted to base/company currency debtors account
+ si3 = create_sales_invoice(
+ customer=customer,
+ currency="USD",
+ conversion_rate=80,
+ do_not_save=1,
+ )
+ si3.payment_terms_template = "Test Receivable Template"
+ si3.save().submit()
+
+ si3.reload()
+ pe = get_payment_entry(si3.doctype, si3.name).save()
+ # Allocated amount should be equal to payment term outstanding
+ self.assertEqual(len(pe.references), 2)
+ for idx, ref in enumerate(pe.references):
+ with self.subTest(idx=idx):
+ self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount)
+ pe.save()
+
+ # Overallocation validation should trigger
+ pe.paid_amount = 16000
+ pe.references[0].allocated_amount = 8000
+ pe.references[1].allocated_amount = 8000
+ self.assertRaises(frappe.ValidationError, pe.save)
+ pe.delete()
+ 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")
@@ -1150,3 +1290,17 @@ def create_payment_terms_template_with_discount(
def create_payment_term(name):
if not frappe.db.exists("Payment Term", name):
frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
+
+
+def create_customer(name="_Test Customer 2 USD", currency="USD"):
+ customer = None
+ if frappe.db.exists("Customer", name):
+ customer = name
+ else:
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = name
+ customer.default_currency = currency
+ customer.type = "Individual"
+ customer.save()
+ customer = customer.name
+ return customer
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js
index 7d85d89c45..6630e7122c 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.js
+++ b/erpnext/accounts/doctype/payment_order/payment_order.js
@@ -124,7 +124,7 @@ frappe.ui.form.on('Payment Order', {
return frappe.call({
method: "erpnext.accounts.doctype.payment_order.payment_order.make_payment_records",
args: {
- "name": me.frm.doc.name,
+ "name": frm.doc.name,
"supplier": args.supplier,
"mode_of_payment": args.mode_of_payment
},
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 25d94c55d3..3a9e80a9d9 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -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, get_link_to_form, getdate, nowdate, today
+from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
@@ -14,6 +14,7 @@ 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,
)
@@ -276,6 +277,11 @@ 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
)
@@ -363,12 +369,6 @@ 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,6 +656,8 @@ 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,
@@ -669,13 +671,42 @@ 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,
+ )
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 2ac7df0e39..1d843abde1 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -686,14 +686,24 @@ class TestPaymentReconciliation(FrappeTestCase):
# Check if difference journal entry gets generated for difference amount after reconciliation
pr.reconcile()
- total_debit_amount = frappe.db.get_all(
+ total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
- "sum(debit) as amount",
+ "sum(credit) as amount",
group_by="reference_name",
)[0].amount
- self.assertEqual(flt(total_debit_amount, 2), -500)
+ # 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"
+ )
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index e17a846dd8..feb2fdffc9 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -144,8 +144,7 @@ class TestPaymentRequest(unittest.TestCase):
(d[0], d)
for d in [
["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
- [pr.payment_account, 6290.0, 0, None],
- ["_Test Exchange Gain/Loss - _TC", 0, 1290, None],
+ [pr.payment_account, 5000.0, 0, None],
]
)
diff --git a/erpnext/accounts/doctype/payment_term/payment_term.js b/erpnext/accounts/doctype/payment_term/payment_term.js
index feecf93484..0898a09a1c 100644
--- a/erpnext/accounts/doctype/payment_term/payment_term.js
+++ b/erpnext/accounts/doctype/payment_term/payment_term.js
@@ -14,7 +14,7 @@ frappe.ui.form.on('Payment Term', {
if (frm.doc.discount) {
let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]);
if (frm.doc.discount_type == 'Amount') {
- description = __("{0} will be given as discount.", [fmt_money(frm.doc.discount)]);
+ description = __("{0} will be given as discount.", [frm.doc.discount]);
}
frm.set_df_property("discount", "description", description);
}
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index 641f4528c5..49472484ef 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -126,21 +126,22 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self, get_opening_entries=False):
gl_entries = self.get_gl_entries()
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
- if gl_entries:
- if len(gl_entries) > 5000:
- frappe.enqueue(
- process_gl_entries,
- gl_entries=gl_entries,
- closing_entries=closing_entries,
- voucher_name=self.name,
- queue="long",
- )
- frappe.msgprint(
- _("The GL Entries will be processed in the background, it can take a few minutes."),
- alert=True,
- )
- else:
- process_gl_entries(gl_entries, closing_entries, voucher_name=self.name)
+ if len(gl_entries) > 5000:
+ frappe.enqueue(
+ process_gl_entries,
+ gl_entries=gl_entries,
+ closing_entries=closing_entries,
+ voucher_name=self.name,
+ company=self.company,
+ closing_date=self.posting_date,
+ queue="long",
+ )
+ frappe.msgprint(
+ _("The GL Entries will be processed in the background, it can take a few minutes."),
+ alert=True,
+ )
+ else:
+ process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = []
@@ -321,24 +322,22 @@ class PeriodClosingVoucher(AccountsController):
return query.run(as_dict=1)
-def process_gl_entries(gl_entries, closing_entries, voucher_name=None):
+def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
from erpnext.accounts.general_ledger import make_gl_entries
try:
- make_gl_entries(gl_entries, merge_entries=False)
- make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name)
- frappe.db.set_value(
- "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
- )
+ if gl_entries:
+ make_gl_entries(gl_entries, merge_entries=False)
+
+ make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
+ frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
- frappe.db.set_value(
- "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
- )
+ frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
def make_reverse_gl_entries(voucher_type, voucher_no):
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index a6c0102a7f..faceaf35d5 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -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.expected_amount - row.closing_amount));
+ frappe.model.set_value(cdt, cdn, "difference", flt(row.closing_amount - row.expected_amount));
}
})
@@ -185,6 +185,7 @@ 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", {
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
index 9d15e6cf35..a98a24c463 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
@@ -221,6 +221,7 @@
"read_only": 1
},
{
+ "default": "Now",
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
@@ -235,7 +236,7 @@
"link_fieldname": "pos_closing_entry"
}
],
- "modified": "2022-08-01 11:37:14.991228",
+ "modified": "2023-08-10 16:25:49.322697",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",
diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
index 8eb28dfaa2..93ba90ad9f 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
@@ -8,9 +8,11 @@ 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
@@ -49,6 +51,54 @@ class TestPOSClosingEntry(unittest.TestCase):
self.assertEqual(pcv_doc.total_quantity, 2)
self.assertEqual(pcv_doc.net_total, 6700)
+ def test_pos_closing_without_item_code(self):
+ """
+ Test if POS Closing Entry is created without item code
+ """
+ test_user, pos_profile = init_user_and_profile()
+ opening_entry = create_opening_entry(pos_profile, test_user.name)
+
+ pos_inv = create_pos_invoice(
+ rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1
+ )
+ pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
+ pos_inv.submit()
+
+ pcv_doc = make_closing_entry_from_opening(opening_entry)
+ pcv_doc.submit()
+
+ 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)
@@ -105,3 +155,19 @@ 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
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 32e267f33c..6f0b8019b8 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -1,9 +1,10 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-{% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts");
+erpnext.sales_common.setup_selling_controller();
+erpnext.accounts.pos.setup("POS Invoice");
erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnext.selling.SellingController {
settings = {};
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 4b2fcec757..89a96118ec 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -542,6 +542,7 @@ 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
@@ -595,7 +596,6 @@ 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)
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 0fce61f1e7..00c402f97b 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -986,19 +986,34 @@ def create_pos_invoice(**args):
msg = f"Serial No {args.serial_no} not available for Item {args.item}"
frappe.throw(_(msg))
- pos_inv.append(
- "items",
- {
- "item_code": args.item or args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 1,
- "rate": args.rate if args.get("rate") is not None else 100,
- "income_account": args.income_account or "Sales - _TC",
- "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
- "cost_center": args.cost_center or "_Test Cost Center - _TC",
- "serial_and_batch_bundle": bundle_id,
- },
- )
+ pos_invoice_item = {
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 1,
+ "rate": args.rate if args.get("rate") is not None else 100,
+ "income_account": args.income_account or "Sales - _TC",
+ "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ "serial_and_batch_bundle": bundle_id,
+ }
+ # append in pos invoice items without item_code by checking flag without_item_code
+ if args.without_item_code:
+ pos_inv.append(
+ "items",
+ {
+ **pos_invoice_item,
+ "item_name": args.item_name or "_Test Item",
+ "description": args.item_name or "_Test Item",
+ },
+ )
+
+ else:
+ pos_inv.append(
+ "items",
+ {
+ **pos_invoice_item,
+ "item_code": args.item or args.item_code or "_Test Item",
+ },
+ )
if not args.do_not_save:
pos_inv.insert()
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index d8cbcc141b..b587ce603f 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -95,7 +95,6 @@ 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):
@@ -108,7 +107,6 @@ 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
@@ -165,8 +163,7 @@ class POSInvoiceMergeLog(Document):
for i in items:
if (
i.item_code == item.item_code
- and not i.serial_no
- and not i.batch_no
+ and not i.serial_and_batch_bundle
and i.uom == item.uom
and i.net_rate == item.net_rate
and i.warehouse == item.warehouse
@@ -385,6 +382,7 @@ 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:
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index 813d20dbf9..0a89aee8e9 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -1,8 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-{% include "erpnext/public/js/controllers/accounts.js" %}
-
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
frm.set_query("selling_price_list", function() {
@@ -148,4 +146,4 @@ frappe.ui.form.on('POS Profile', {
frm.toggle_display('expense_account',
erpnext.is_perpetual_inventory_enabled(frm.doc.company));
}
-});
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
index 8bb7092dc5..1a1ab4d800 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
@@ -146,7 +146,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-04-21 17:19:30.912953",
+ "modified": "2023-08-11 10:56:51.699137",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",
@@ -154,15 +154,25 @@
"owner": "Administrator",
"permissions": [
{
+ "amend": 1,
+ "cancel": 1,
"create": 1,
"delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
- "role": "System Manager",
+ "role": "Accounts 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
}
],
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 08f4cf45d6..6193c849b5 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -140,7 +140,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
def get_ar_filters(doc, entry):
return {
"report_date": doc.posting_date if doc.posting_date else None,
- "customer_name": entry.customer,
+ "customer": entry.customer,
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
"sales_partner": doc.sales_partner if doc.sales_partner else None,
"sales_person": doc.sales_person if doc.sales_person else None,
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
index 07e1896292..259526f8c4 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
@@ -10,16 +10,12 @@
{{ _(report.report_name) }}
- {% if (filters.customer_name) %}
- {{ filters.customer_name }}
- {% else %}
- {{ filters.customer ~ filters.supplier }}
- {% endif %}
+ {{ filters.customer }}
- {% if (filters.tax_id) %}
- {{ _("Tax Id: ") }}{{ filters.tax_id }}
- {% endif %}
+ {% if (filters.tax_id) %}
+ {{ _("Tax Id: ") }}{{ filters.tax_id }}
+ {% endif %}
{{ _(filters.ageing_based_on) }}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 6a558ca606..66438a7efa 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -2,7 +2,11 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.accounts");
-{% include 'erpnext/public/js/controllers/buying.js' %};
+
+erpnext.accounts.payment_triggers.setup("Purchase Invoice");
+erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
+erpnext.accounts.taxes.setup_tax_validations("Purchase Invoice");
+erpnext.buying.setup_buying_controller();
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
setup(doc) {
@@ -31,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"];
+ this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
@@ -97,12 +101,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
cur_frm.add_custom_button(__('Return / Debit Note'),
this.make_debit_note, __('Create'));
}
-
- if(!doc.auto_repeat) {
- cur_frm.add_custom_button(__('Subscription'), function() {
- erpnext.utils.make_subscription(doc.doctype, doc.name)
- }, __('Create'))
- }
}
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
@@ -506,7 +504,8 @@ frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) {
frm.custom_make_buttons = {
'Purchase Invoice': 'Return / Debit Note',
- 'Payment Entry': 'Payment'
+ 'Payment Entry': 'Payment',
+ 'Landed Cost Voucher': function () { frm.trigger('create_landed_cost_voucher') },
}
frm.set_query("additional_discount_account", function() {
@@ -544,6 +543,26 @@ frappe.ui.form.on("Purchase Invoice", {
frm.events.add_custom_buttons(frm);
},
+ mode_of_payment: function(frm) {
+ erpnext.accounts.pos.get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account) {
+ frm.set_value("cash_bank_account", account);
+ })
+ },
+
+ create_landed_cost_voucher: function (frm) {
+ let lcv = frappe.model.get_new_doc('Landed Cost Voucher');
+ lcv.company = frm.doc.company;
+
+ let lcv_receipt = frappe.model.get_new_doc('Landed Cost Purchase Invoice');
+ lcv_receipt.receipt_document_type = 'Purchase Invoice';
+ lcv_receipt.receipt_document = frm.doc.name;
+ lcv_receipt.supplier = frm.doc.supplier;
+ lcv_receipt.grand_total = frm.doc.grand_total;
+ lcv.purchase_receipts = [lcv_receipt];
+
+ frappe.set_route("Form", lcv.doctype, lcv.name);
+ },
+
add_custom_buttons: function(frm) {
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
frm.add_custom_button(__('Purchase Receipt'), () => {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index d8759e95b8..0599e19d9b 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -167,6 +167,7 @@
"column_break_63",
"unrealized_profit_loss_account",
"subscription_section",
+ "subscription",
"auto_repeat",
"update_auto_repeat_reference",
"column_break_114",
@@ -1423,6 +1424,12 @@
"options": "Advance Tax",
"read_only": 1
},
+ {
+ "fieldname": "subscription",
+ "fieldtype": "Link",
+ "label": "Subscription",
+ "options": "Subscription"
+ },
{
"default": "0",
"fieldname": "is_old_subcontracting_flow",
@@ -1577,7 +1584,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2023-07-04 17:22:59.145031",
+ "modified": "2023-07-25 17:22:59.145031",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 230a8b3c58..f33439989a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -229,7 +229,7 @@ class PurchaseInvoice(BuyingController):
)
if (
- cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
+ cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
and not self.is_return
and not self.is_internal_supplier
):
@@ -536,6 +536,7 @@ 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)
@@ -580,7 +581,6 @@ 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,30 +969,6 @@ 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")
@@ -1439,6 +1415,8 @@ 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",
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 8c96480478..ce7ada3b09 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1273,10 +1273,11 @@ 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", -35000.0],
- ["Exchange Gain/Loss - _TC", -2500.0],
+ ["_Test Payable USD - _TC", -37500.0],
]
gl_entries = frappe.db.sql(
@@ -1293,6 +1294,31 @@ 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",
@@ -1317,10 +1343,12 @@ 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", -35000.0],
- ["Exchange Gain/Loss - _TC", -1500.0],
+ ["_Test Payable USD - _TC", -36500.0],
]
gl_entries = frappe.db.sql(
@@ -1351,12 +1379,39 @@ 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()
@@ -1736,6 +1791,107 @@ 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 (
+ clear_dimension_defaults,
+ create_accounting_dimension,
+ disable_dimension,
+ )
+
+ create_account(
+ account_name="Offsetting",
+ company="_Test Company",
+ parent_account="Temporary Accounts - _TC",
+ )
+
+ create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC")
+
+ branch1 = frappe.new_doc("Branch")
+ branch1.branch = "Location 1"
+ branch1.insert(ignore_if_duplicate=True)
+ branch2 = frappe.new_doc("Branch")
+ branch2.branch = "Location 2"
+ branch2.insert(ignore_if_duplicate=True)
+
+ pi = make_purchase_invoice(
+ company="_Test Company",
+ customer="_Test Supplier",
+ do_not_save=True,
+ do_not_submit=True,
+ rate=1000,
+ price_list_rate=1000,
+ qty=1,
+ )
+ pi.branch = branch1.branch
+ pi.items[0].branch = branch2.branch
+ pi.save()
+ pi.submit()
+
+ expected_gle = [
+ ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), branch2.branch],
+ ["Creditors - _TC", 0.0, 1000, nowdate(), branch1.branch],
+ ["Offsetting - _TC", 1000, 0.0, nowdate(), branch1.branch],
+ ["Offsetting - _TC", 0.0, 1000, nowdate(), branch2.branch],
+ ]
+
+ check_gl_entries(
+ self,
+ pi.name,
+ expected_gle,
+ nowdate(),
+ voucher_type="Purchase Invoice",
+ additional_columns=["branch"],
+ )
+ clear_dimension_defaults("Branch")
+ disable_dimension()
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
@@ -1748,9 +1904,16 @@ def set_advance_flag(company, flag, default_account):
)
-def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="Purchase Invoice"):
+def check_gl_entries(
+ doc,
+ voucher_no,
+ expected_gle,
+ posting_date,
+ voucher_type="Purchase Invoice",
+ additional_columns=None,
+):
gl = frappe.qb.DocType("GL Entry")
- q = (
+ query = (
frappe.qb.from_(gl)
.select(gl.account, gl.debit, gl.credit, gl.posting_date)
.where(
@@ -1761,7 +1924,12 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
)
.orderby(gl.posting_date, gl.account, gl.creation)
)
- gl_entries = q.run(as_dict=True)
+
+ if additional_columns:
+ for col in additional_columns:
+ query = query.select(gl[col])
+
+ gl_entries = query.run(as_dict=True)
for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account)
@@ -1769,6 +1937,12 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
+ if additional_columns:
+ j = 4
+ for col in additional_columns:
+ doc.assertEqual(expected_gle[i][j], gle[col])
+ j += 1
+
def create_tax_witholding_category(category_name, company, account):
from erpnext.accounts.utils import get_fiscal_year
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 4afc4512ff..81c7577467 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -443,7 +443,8 @@
"hidden": 1,
"label": "Batch No",
"options": "Batch",
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "col_br_wh",
@@ -890,7 +891,8 @@
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
- "print_hide": 1
+ "print_hide": 1,
+ "search_index": 1
},
{
"depends_on": "eval:parent.update_stock == 1",
@@ -905,7 +907,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-07-04 17:22:21.501152",
+ "modified": "2023-07-26 12:54:53.178156",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.js b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.js
index eb0ea7fef8..78dc4bee1a 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.js
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.js
@@ -1,30 +1,31 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
+erpnext.accounts.taxes.setup_tax_validations("Purchase Taxes and Charges Template");
+erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
-{% include "erpnext/public/js/controllers/accounts.js" %}
+frappe.ui.form.on("Purchase Taxes and Charges", {
+ add_deduct_tax(doc, cdt, cdn) {
+ let d = locals[cdt][cdn];
-frappe.ui.form.on("Purchase Taxes and Charges", "add_deduct_tax", function(doc, cdt, cdn) {
- var d = locals[cdt][cdn];
+ if(!d.category && d.add_deduct_tax) {
+ frappe.msgprint(__("Please select Category first"));
+ d.add_deduct_tax = '';
+ }
+ else if(d.category != 'Total' && d.add_deduct_tax == 'Deduct') {
+ frappe.msgprint(__("Cannot deduct when category is for 'Valuation' or 'Valuation and Total'"));
+ d.add_deduct_tax = '';
+ }
+ refresh_field('add_deduct_tax', d.name, 'taxes');
+ },
- if(!d.category && d.add_deduct_tax) {
- frappe.msgprint(__("Please select Category first"));
- d.add_deduct_tax = '';
+ category(doc, cdt, cdn) {
+ let d = locals[cdt][cdn];
+
+ if(d.category != 'Total' && d.add_deduct_tax == 'Deduct') {
+ frappe.msgprint(__("Cannot deduct when category is for 'Valuation' or 'Valuation and Total'"));
+ d.add_deduct_tax = '';
+ }
+ refresh_field('add_deduct_tax', d.name, 'taxes');
}
- else if(d.category != 'Total' && d.add_deduct_tax == 'Deduct') {
- frappe.msgprint(__("Cannot deduct when category is for 'Valuation' or 'Valuation and Total'"));
- d.add_deduct_tax = '';
- }
- refresh_field('add_deduct_tax', d.name, 'taxes');
-});
-
-frappe.ui.form.on("Purchase Taxes and Charges", "category", function(doc, cdt, cdn) {
- var d = locals[cdt][cdn];
-
- if (d.category != 'Total' && d.add_deduct_tax == 'Deduct') {
- frappe.msgprint(__("Cannot deduct when category is for 'Valuation' or 'Vaulation and Total'"));
- d.add_deduct_tax = '';
- }
- refresh_field('add_deduct_tax', d.name, 'taxes');
});
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html
new file mode 100644
index 0000000000..2dec8f753f
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+ {% for col in gl_columns%}
+
+ {% endfor %}
+
+
+
+ {% for col in gl_columns%}
+ {{ col.label }} |
+ {% endfor %}
+
+
+{% for gl in gl_data%}
+{% if gl["old"]%}
+
+{% else %}
+
+{% endif %}
+ {% for col in gl_columns %}
+
+ {{ gl[col.fieldname] }}
+ |
+ {% endfor %}
+
+{% endfor %}
+
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
new file mode 100644
index 0000000000..3a87a380d1
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
@@ -0,0 +1,50 @@
+// 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);
+ }
+ }
+ });
+ });
+ }
+});
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
new file mode 100644
index 0000000000..8d56c9bb11
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
@@ -0,0 +1,81 @@
+{
+ "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": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
new file mode 100644
index 0000000000..4cf2ed2f46
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
@@ -0,0 +1,183 @@
+# 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()
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
new file mode 100644
index 0000000000..0e75dd2e3e
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
@@ -0,0 +1,202 @@
+# 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}))
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
new file mode 100644
index 0000000000..4a2041f88c
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
@@ -0,0 +1,40 @@
+{
+ "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": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py
new file mode 100644
index 0000000000..9221f44735
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py
@@ -0,0 +1,9 @@
+# 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
diff --git a/erpnext/accounts/doctype/sales_invoice/regional/italy.js b/erpnext/accounts/doctype/sales_invoice/regional/italy.js
index 21eb8ce661..2f305b914e 100644
--- a/erpnext/accounts/doctype/sales_invoice/regional/italy.js
+++ b/erpnext/accounts/doctype/sales_invoice/regional/italy.js
@@ -1,3 +1,23 @@
-{% include "erpnext/regional/italy/sales_invoice.js" %}
-
-erpnext.setup_e_invoice_button('Sales Invoice')
+frappe.ui.form.on("Sales Invoice", {
+ refresh: (frm) => {
+ if(frm.doc.docstatus == 1) {
+ frm.add_custom_button(__('Generate E-Invoice'), () => {
+ frm.call({
+ method: "erpnext.regional.italy.utils.generate_single_invoice",
+ args: {
+ docname: frm.doc.name
+ },
+ callback: function(r) {
+ frm.reload_doc();
+ if(r.message) {
+ open_url_post(frappe.request.url, {
+ cmd: 'frappe.core.doctype.file.file.download_file',
+ file_url: r.message
+ });
+ }
+ }
+ });
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 8753ebc3ba..a4bcdb41db 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -1,10 +1,13 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-{% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts");
-
+erpnext.accounts.taxes.setup_tax_validations("Sales Invoice");
+erpnext.accounts.payment_triggers.setup("Sales Invoice");
+erpnext.accounts.pos.setup("Sales Invoice");
+erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
+erpnext.sales_common.setup_selling_controller();
erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends erpnext.selling.SellingController {
setup(doc) {
this.setup_posting_date_time_check();
@@ -34,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"];
+ 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
@@ -142,9 +145,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
cur_frm.events.create_invoice_discounting(cur_frm);
}, __('Create'));
- if (doc.due_date < frappe.datetime.get_today()) {
- cur_frm.add_custom_button(__('Dunning'), function() {
- cur_frm.events.create_dunning(cur_frm);
+ const payment_is_overdue = doc.payment_schedule.map(
+ row => Date.parse(row.due_date) < Date.now()
+ ).reduce(
+ (prev, current) => prev || current
+ );
+
+ if (payment_is_overdue) {
+ this.frm.add_custom_button(__('Dunning'), () => {
+ this.frm.events.create_dunning(this.frm);
}, __('Create'));
}
}
@@ -154,12 +163,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
cur_frm.cscript.make_maintenance_schedule();
}, __('Create'));
}
-
- if(!doc.auto_repeat) {
- cur_frm.add_custom_button(__('Subscription'), function() {
- erpnext.utils.make_subscription(doc.doctype, doc.name)
- }, __('Create'))
- }
}
// Show buttons only when pos view is active
@@ -711,7 +714,7 @@ frappe.ui.form.on('Sales Invoice', {
frm.set_query('pos_profile', function(doc) {
if(!doc.company) {
- frappe.throw(_('Please set Company'));
+ frappe.throw(__('Please set Company'));
}
return {
@@ -767,7 +770,6 @@ frappe.ui.form.on('Sales Invoice', {
update_stock: function(frm, dt, dn) {
frm.events.hide_fields(frm);
- frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock);
frm.trigger('reset_posting_time');
},
@@ -858,7 +860,7 @@ frappe.ui.form.on('Sales Invoice', {
kwargs = Object();
}
- if (!kwargs.hasOwnProperty("project") && frm.doc.project) {
+ if (!Object.prototype.hasOwnProperty.call(kwargs, "project") && frm.doc.project) {
kwargs.project = frm.doc.project;
}
@@ -891,6 +893,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.events.append_time_log(frm, timesheet, 1.0);
}
});
+ frm.refresh_field("timesheets");
+ frm.trigger("calculate_timesheet_totals");
},
async get_exchange_rate(frm, from_currency, to_currency) {
@@ -930,9 +934,6 @@ frappe.ui.form.on('Sales Invoice', {
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
row.timesheet_detail = time_log.name;
row.project_name = time_log.project_name;
-
- frm.refresh_field("timesheets");
- frm.trigger("calculate_timesheet_totals");
},
calculate_timesheet_totals: function(frm) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index f0d3f72094..7581366bc0 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -194,6 +194,7 @@
"select_print_heading",
"language",
"subscription_section",
+ "subscription",
"from_date",
"auto_repeat",
"column_break_140",
@@ -2017,6 +2018,12 @@
"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\"",
@@ -2157,7 +2164,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2023-06-21 16:02:18.988799",
+ "modified": "2023-07-25 16:02:18.988799",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 7ab1c89397..0bc5aa2ed2 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -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 get_account_currency
+from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center,
@@ -32,6 +32,7 @@ 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
@@ -113,7 +114,6 @@ class SalesInvoice(SellingController):
if cint(self.update_stock):
self.validate_dropship_item()
- self.validate_item_code()
self.validate_warehouse()
self.update_current_stock()
self.validate_delivery_note()
@@ -386,6 +386,8 @@ 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",
)
@@ -854,11 +856,6 @@ class SalesInvoice(SellingController):
):
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
- def validate_item_code(self):
- for d in self.get("items"):
- if not d.item_code and self.is_opening == "No":
- msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True)
-
def validate_warehouse(self):
super(SalesInvoice, self).validate_warehouse()
@@ -1035,7 +1032,10 @@ 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 +1060,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,12 +1182,13 @@ 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 after being sold through Sales Invoice {1}."
+ "This schedule was created when Asset {0} was returned through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
@@ -1215,6 +1216,7 @@ 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
@@ -1652,15 +1654,13 @@ 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 Coalesce, Sum
+ from frappe.query_builder.functions import 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) & (Coalesce(doc.return_against, "") == self.name)
- )
+ .where((doc.docstatus == 1) & (doc.is_return == 1) & (doc.return_against == self.name))
).run()
return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
@@ -1839,7 +1839,7 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
doc = frappe.get_doc(ref_doc, inter_company_reference)
ref_party = doc.supplier if doctype in ["Sales Invoice", "Sales Order"] else doc.customer
if not frappe.db.get_value(partytype, {"represents_company": doc.company}, "name") == party:
- frappe.throw(_("Invalid {0} for Inter Company Transaction.").format(partytype))
+ frappe.throw(_("Invalid {0} for Inter Company Transaction.").format(_(partytype)))
if not frappe.get_cached_value(ref_partytype, ref_party, "represents_company") == company:
frappe.throw(_("Invalid Company for Inter Company Transaction."))
@@ -1853,7 +1853,7 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
if not company in companies:
frappe.throw(
_("{0} not allowed to transact with {1}. Please change the Company.").format(
- partytype, company
+ _(partytype), company
)
)
@@ -2516,55 +2516,49 @@ def get_mode_of_payment_info(mode_of_payment, company):
@frappe.whitelist()
-def create_dunning(source_name, target_doc=None):
+def create_dunning(source_name, target_doc=None, ignore_permissions=False):
from frappe.model.mapper import get_mapped_doc
- from erpnext.accounts.doctype.dunning.dunning import (
- calculate_interest_and_amount,
- get_dunning_letter_text,
- )
+ def postprocess_dunning(source, target):
+ from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text
- def set_missing_values(source, target):
- target.sales_invoice = source_name
- target.outstanding_amount = source.outstanding_amount
- overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days
- target.overdue_days = overdue_days
- if frappe.db.exists(
- "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
- ):
- dunning_type = frappe.get_doc(
- "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
- )
+ dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company})
+ if dunning_type:
+ dunning_type = frappe.get_doc("Dunning Type", dunning_type)
target.dunning_type = dunning_type.name
target.rate_of_interest = dunning_type.rate_of_interest
target.dunning_fee = dunning_type.dunning_fee
- letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict())
+ target.income_account = dunning_type.income_account
+ target.cost_center = dunning_type.cost_center
+ letter_text = get_dunning_letter_text(
+ dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
+ )
+
if letter_text:
target.body_text = letter_text.get("body_text")
target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language")
- amounts = calculate_interest_and_amount(
- target.outstanding_amount,
- target.rate_of_interest,
- target.dunning_fee,
- target.overdue_days,
- )
- target.interest_amount = amounts.get("interest_amount")
- target.dunning_amount = amounts.get("dunning_amount")
- target.grand_total = amounts.get("grand_total")
- doclist = get_mapped_doc(
- "Sales Invoice",
- source_name,
- {
+ target.validate()
+
+ return get_mapped_doc(
+ from_doctype="Sales Invoice",
+ from_docname=source_name,
+ target_doc=target_doc,
+ table_maps={
"Sales Invoice": {
"doctype": "Dunning",
- }
+ "field_map": {"customer_address": "customer_address", "parent": "sales_invoice"},
+ },
+ "Payment Schedule": {
+ "doctype": "Overdue Payment",
+ "field_map": {"name": "payment_schedule", "parent": "sales_invoice"},
+ "condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(),
+ },
},
- target_doc,
- set_missing_values,
+ postprocess=postprocess_dunning,
+ ignore_permissions=ignore_permissions,
)
- return doclist
def check_if_return_invoice_linked_with_payment_entry(self):
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
index 0a765f3f46..6fdcf263a5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
@@ -15,6 +15,7 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "sales_order"],
+ "Delivery Note": ["items", "delivery_note"],
"Timesheet": ["timesheets", "time_sheet"],
},
"transactions": [
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 0280c3590c..f9cfe5a920 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1900,16 +1900,22 @@ class TestSalesInvoice(unittest.TestCase):
si = self.create_si_to_test_tax_breakup()
- itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
+ itemised_tax_data = get_itemised_tax_breakup_data(si)
- expected_itemised_tax = {
- "_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}},
- "_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}},
- }
- expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0}
+ expected_itemised_tax = [
+ {
+ "item": "_Test Item",
+ "taxable_amount": 10000.0,
+ "Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0},
+ },
+ {
+ "item": "_Test Item 2",
+ "taxable_amount": 5000.0,
+ "Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0},
+ },
+ ]
- self.assertEqual(itemised_tax, expected_itemised_tax)
- self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount)
+ self.assertEqual(itemised_tax_data, expected_itemised_tax)
frappe.flags.country = None
@@ -2043,28 +2049,27 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 228.82)
self.assertEqual(si.rounding_adjustment, -0.01)
- 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],
- ]
- )
+ 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],
+ ]
gl_entries = frappe.db.sql(
- """select account, debit, credit
+ """select account, sum(debit) as debit, sum(credit) as 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 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)
+ 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)
def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
@@ -2119,13 +2124,14 @@ 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.01, 0],
+ ["Round Off - _TC", 0.02, 0.01],
]
)
gl_entries = frappe.db.sql(
- """select account, debit, credit
+ """select account, sum(debit) as debit, sum(credit) as 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,
@@ -3207,15 +3213,10 @@ 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
@@ -3248,18 +3249,28 @@ 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())
- frappe.db.set_single_value(
- "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
+ 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,
)
def test_batch_expiry_for_sales_invoice_return(self):
@@ -3365,6 +3376,14 @@ 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(
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index f3e21858c4..abeaab1d25 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -604,7 +604,8 @@
"hidden": 1,
"label": "Batch No",
"options": "Batch",
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "col_break5",
@@ -894,13 +895,14 @@
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
- "print_hide": 1
+ "print_hide": 1,
+ "search_index": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-03-12 13:42:24.303113",
+ "modified": "2023-07-26 12:53:22.404057",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js
index 066c4eae43..00e8b621c0 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js
@@ -1,6 +1,5 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-cur_frm.cscript.tax_table = "Sales Taxes and Charges";
-
-{% include "erpnext/public/js/controllers/accounts.js" %}
+erpnext.accounts.taxes.setup_tax_validations("Sales Taxes and Charges Template");
+erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js
index 1a9066470a..ae789b5424 100644
--- a/erpnext/accounts/doctype/subscription/subscription.js
+++ b/erpnext/accounts/doctype/subscription/subscription.js
@@ -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,76 +20,60 @@ frappe.ui.form.on('Subscription', {
});
},
- 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)
- );
- }
+ 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')
+ );
}
},
- cancel_this_subscription: function(frm) {
- const doc = frm.doc;
+ cancel_this_subscription: function (frm) {
frappe.confirm(
__('This action will stop future billing. Are you sure you want to cancel this subscription?'),
- function() {
- frappe.call({
- method:
- "erpnext.accounts.doctype.subscription.subscription.cancel_subscription",
- args: {name: doc.name},
- callback: function(data){
- if(!data.exc){
- frm.reload_doc();
- }
+ () => {
+ frm.call('cancel_subscription').then(r => {
+ if (!r.exec) {
+ frm.reload_doc();
}
});
}
);
},
- renew_this_subscription: function(frm) {
- const doc = frm.doc;
+ renew_this_subscription: function (frm) {
frappe.confirm(
- __('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();
- }
+ __('Are you sure you want to restart this subscription?'),
+ () => {
+ frm.call('restart_subscription').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();
- }
+ get_subscription_updates: function (frm) {
+ frm.call('process').then(r => {
+ if (!r.exec) {
+ frm.reload_doc();
}
});
}
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index c4e4be7f78..c15aa1e05a 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -19,6 +19,7 @@
"trial_period_end",
"follow_calendar_months",
"generate_new_invoices_past_due_date",
+ "submit_invoice",
"column_break_11",
"current_invoice_start",
"current_invoice_end",
@@ -35,12 +36,8 @@
"cb_2",
"additional_discount_percentage",
"additional_discount_amount",
- "sb_3",
- "submit_invoice",
- "invoices",
"accounting_dimensions_section",
- "cost_center",
- "dimension_col_break"
+ "cost_center"
],
"fields": [
{
@@ -162,29 +159,12 @@
"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",
@@ -259,15 +239,27 @@
"default": "1",
"fieldname": "submit_invoice",
"fieldtype": "Check",
- "label": "Submit Invoice Automatically"
+ "label": "Submit Generated Invoices"
}
],
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-04-19 15:24:27.550797",
+ "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",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -309,5 +301,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 8708342b11..bbcade1758 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -2,14 +2,17 @@
# 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,
@@ -17,8 +20,7 @@ from frappe.utils.data import (
nowdate,
)
-import erpnext
-from erpnext import get_default_company
+from erpnext import get_default_company, get_default_cost_center
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
@@ -26,33 +28,39 @@ 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=None, return_date=False):
+ def update_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
"""
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`.
-
- 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
"""
+ 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):
_current_invoice_start = self.get_current_invoice_start(date)
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
- if return_date:
- return _current_invoice_start, _current_invoice_end
+ return _current_invoice_start, _current_invoice_end
- self.current_invoice_start = _current_invoice_start
- self.current_invoice_end = _current_invoice_end
-
- def get_current_invoice_start(self, date=None):
+ def get_current_invoice_start(
+ self, date: Optional[Union[datetime.date, str]] = None
+ ) -> Union[datetime.date, str]:
"""
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
@@ -75,13 +83,13 @@ class Subscription(Document):
return _current_invoice_start
- def get_current_invoice_end(self, date=None):
+ def get_current_invoice_end(
+ self, date: Optional[Union[datetime.date, str]] = None
+ ) -> Union[datetime.date, str]:
"""
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`.
@@ -105,24 +113,13 @@ 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"]
- 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"
- )
+ _end = add_months(getdate(date), billing_interval_count - 1)
+ _current_invoice_end = get_last_day(_end)
if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
_current_invoice_end = self.end_date
@@ -130,7 +127,7 @@ class Subscription(Document):
return _current_invoice_end
@staticmethod
- def validate_plans_billing_cycle(billing_cycle_data):
+ def validate_plans_billing_cycle(billing_cycle_data: List[Dict[str, str]]) -> None:
"""
Makes sure that all `Subscription Plan` in the `Subscription` have the
same billing interval
@@ -138,10 +135,9 @@ 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):
+ def get_billing_cycle_and_interval(self) -> List[Dict[str, str]]:
"""
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]
@@ -156,72 +152,65 @@ class Subscription(Document):
return billing_info
- def get_billing_cycle_data(self):
+ def get_billing_cycle_data(self) -> Dict[str, int]:
"""
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
- self.validate_plans_billing_cycle(billing_info)
+ data = dict()
+ interval = billing_info[0]["billing_interval"]
+ interval_count = billing_info[0]["billing_interval_count"]
- 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 not in ["Day", "Week"]:
+ data["days"] = -1
- return data
+ 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
- def set_status_grace_period(self):
- """
- Sets the `Subscription` `status` based on the preference set in `Subscription Settings`.
+ return data
- 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):
+ def set_subscription_status(self) -> None:
"""
Sets the status of the `Subscription`
"""
if self.is_trialling():
self.status = "Trialling"
- elif self.status == "Active" and self.end_date and getdate() > getdate(self.end_date):
+ elif (
+ self.status == "Active"
+ and self.end_date
+ and getdate(frappe.flags.current_date) > getdate(self.end_date)
+ ):
self.status = "Completed"
elif self.is_past_grace_period():
- subscription_settings = frappe.get_single("Subscription Settings")
- self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
+ self.status = self.get_status_for_past_grace_period()
+ self.cancelation_date = (
+ getdate(frappe.flags.current_date) if self.status == "Cancelled" else None
+ )
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():
- self.status = "Active"
- elif self.is_new_subscription():
+ elif not self.has_outstanding_invoice() or self.is_new_subscription():
self.status = "Active"
+
self.save()
- def is_trialling(self):
+ def is_trialling(self) -> bool:
"""
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):
+ def period_has_passed(end_date: Union[str, datetime.date]) -> bool:
"""
Returns true if the given `end_date` has passed
"""
@@ -229,61 +218,59 @@ class Subscription(Document):
if not end_date:
return True
- end_date = getdate(end_date)
- return getdate() > getdate(end_date)
+ return getdate(frappe.flags.current_date) > getdate(end_date)
- def is_past_grace_period(self):
+ 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:
"""
Returns `True` if the grace period for the `Subscription` has passed
"""
- 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)
+ if not self.current_invoice_is_past_due():
+ return
- return getdate() > add_days(current_invoice.due_date, 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)
+ )
- def current_invoice_is_past_due(self, current_invoice=None):
+ def current_invoice_is_past_due(self) -> bool:
"""
Returns `True` if the current generated invoice is overdue
"""
- if not current_invoice:
- current_invoice = self.get_current_invoice()
-
- if not current_invoice or self.is_paid(current_invoice):
+ if not self.current_invoice or self.is_paid(self.current_invoice):
return False
- else:
- return getdate() > getdate(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"
+ return getdate(frappe.flags.current_date) >= getdate(self.current_invoice.due_date)
- 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")))
+ @property
+ def invoice_document_type(self) -> str:
+ return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
- def is_new_subscription(self):
+ def is_new_subscription(self) -> bool:
"""
Returns `True` if `Subscription` has never generated an invoice
"""
- return len(self.invoices) == 0
+ return self.is_new() or not frappe.db.exists(
+ {"doctype": self.invoice_document_type, "subscription": self.name}
+ )
- def validate(self):
+ def validate(self) -> None:
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 = erpnext.get_default_cost_center(self.get("company"))
+ self.cost_center = get_default_cost_center(self.get("company"))
- def validate_trial_period(self):
+ def validate_trial_period(self) -> None:
"""
Runs sanity checks on trial period dates for the `Subscription`
"""
@@ -297,7 +284,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):
+ def validate_end_date(self) -> None:
billing_cycle_info = self.get_billing_cycle_data()
end_date = add_to_date(self.start_date, **billing_cycle_info)
@@ -306,53 +293,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):
- if self.follow_calendar_months:
- billing_info = self.get_billing_cycle_and_interval()
+ def validate_to_follow_calendar_months(self) -> None:
+ if not self.follow_calendar_months:
+ return
- if not self.end_date:
- frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
+ billing_info = self.get_billing_cycle_and_interval()
- if billing_info[0]["billing_interval"] != "Month":
- frappe.throw(
- _("Billing Interval in Subscription Plan must be Month to follow calendar months")
- )
+ if not self.end_date:
+ frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
- def after_insert(self):
+ 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:
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
self.set_subscription_status()
- def generate_invoice(self, prorate=0):
+ def generate_invoice(
+ self,
+ from_date: Optional[Union[str, datetime.date]] = None,
+ to_date: Optional[Union[str, datetime.date]] = None,
+ ) -> Document:
"""
Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
saves the `Subscription`.
+ Backwards compatibility
"""
+ return self.create_invoice(from_date=from_date, to_date=to_date)
- 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):
+ def create_invoice(
+ self,
+ from_date: Optional[Union[str, datetime.date]] = None,
+ to_date: Optional[Union[str, datetime.date]] = None,
+ ) -> Document:
"""
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 = (
@@ -363,17 +350,17 @@ class Subscription(Document):
invoice.cost_center = self.cost_center
- if doctype == "Sales Invoice":
+ if self.invoice_document_type == "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:
@@ -382,7 +369,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, prorate)
+ items_list = self.get_items_from_plans(self.plans, is_prorate())
for item in items_list:
item["cost_center"] = self.cost_center
invoice.append("items", item)
@@ -390,9 +377,9 @@ class Subscription(Document):
# Taxes
tax_template = ""
- if doctype == "Sales Invoice" and self.sales_tax_template:
+ if self.invoice_document_type == "Sales Invoice" and self.sales_tax_template:
tax_template = self.sales_tax_template
- if doctype == "Purchase Invoice" and self.purchase_tax_template:
+ if self.invoice_document_type == "Purchase Invoice" and self.purchase_tax_template:
tax_template = self.purchase_tax_template
if tax_template:
@@ -424,8 +411,9 @@ class Subscription(Document):
invoice.apply_discount_on = discount_on if discount_on else "Grand Total"
# Subscription period
- invoice.from_date = self.current_invoice_start
- invoice.to_date = self.current_invoice_end
+ 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.flags.ignore_mandatory = True
@@ -437,13 +425,20 @@ class Subscription(Document):
return invoice
- def get_items_from_plans(self, plans, prorate=0):
+ def get_items_from_plans(
+ self, plans: List[Dict[str, str]], prorate: Optional[bool] = None
+ ) -> List[Dict]:
"""
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, self.generate_invoice_at_period_start
+ self.current_invoice_end,
+ self.current_invoice_start,
+ cint(self.generate_invoice_at_period_start),
)
items = []
@@ -465,7 +460,11 @@ 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,
}
@@ -503,254 +502,184 @@ class Subscription(Document):
return items
- def process(self):
+ @frappe.whitelist()
+ def process(self) -> bool:
"""
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 self.status == "Active":
- self.process_for_active()
- elif self.status in ["Past Due Date", "Unpaid"]:
- self.process_for_past_due_date()
+ 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()
self.set_subscription_status()
self.save()
- 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)
- )
+ 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_prepaid_to_invoice(self):
- if not self.generate_invoice_at_period_start:
+ return True
+ else:
return False
- 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()
-
+ def is_current_invoice_generated(
+ self,
+ _current_start_date: Union[datetime.date, str] = None,
+ _current_end_date: Union[datetime.date, str] = None,
+ ) -> bool:
if not (_current_start_date and _current_end_date):
- _current_start_date, _current_end_date = self.update_subscription_period(
- date=add_days(self.current_invoice_end, 1), return_date=True
+ _current_start_date, _current_end_date = self._get_subscription_period(
+ date=add_days(self.current_invoice_end, 1)
)
- if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(
- _current_end_date
- ):
+ if self.current_invoice and getdate(_current_start_date) <= getdate(
+ self.current_invoice.posting_date
+ ) <= getdate(_current_end_date):
return True
return False
- def process_for_active(self):
+ @property
+ def current_invoice(self) -> Union[Document, None]:
"""
- Called by `process` if the status of the `Subscription` is 'Active'.
-
- 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'
+ Adds property for accessing the current_invoice
"""
+ return self.get_current_invoice()
- 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 get_current_invoice(self) -> Union[Document, None]:
+ """
+ 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",
+ )
- prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
- self.generate_invoice(prorate)
+ if invoice:
+ return frappe.get_doc(self.invoice_document_type, invoice[0])
- 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):
+ def cancel_subscription_at_period_end(self) -> None:
"""
Called when `Subscription.cancel_at_period_end` is truthy
"""
- if self.end_date and getdate() < getdate(self.end_date):
- return
-
self.status = "Cancelled"
- if not self.cancelation_date:
- self.cancelation_date = nowdate()
+ 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)
+ @property
+ def invoices(self) -> List[Dict]:
+ return frappe.get_all(
+ self.invoice_document_type,
+ filters={"subscription": self.name},
+ order_by="from_date asc",
+ )
@staticmethod
- def is_paid(invoice):
+ def is_paid(invoice: Document) -> bool:
"""
Return `True` if the given invoice is paid
"""
return invoice.status == "Paid"
- def has_outstanding_invoice(self):
+ def has_outstanding_invoice(self) -> int:
"""
Returns `True` if the most recent invoice for the `Subscription` is not 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)}
+ return frappe.db.count(
+ self.invoice_document_type,
+ {
+ "subscription": self.name,
+ "status": ["!=", "Paid"],
+ },
)
- if outstanding_invoices:
- return True
- else:
- False
-
- def cancel_subscription(self):
+ @frappe.whitelist()
+ def cancel_subscription(self) -> None:
"""
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":
- 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()
+ if self.status == "Cancelled":
+ frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
- def restart_subscription(self):
+ 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:
"""
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 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."))
+ if not self.status == "Cancelled":
+ frappe.throw(_("You cannot restart a Subscription that is not cancelled."), InvoiceNotCancelled)
- def get_precision(self):
- invoice = self.get_current_invoice()
- if invoice:
- return invoice.precision("grand_total")
+ self.status = "Active"
+ self.cancelation_date = None
+ self.update_subscription_period(frappe.flags.current_date or nowdate())
+ self.save()
-def get_calendar_months(billing_interval):
- calendar_months = []
- start = 0
- while start < 12:
- start += billing_interval
- calendar_months.append(start)
-
- return calendar_months
+def is_prorate() -> int:
+ return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))
-def get_prorata_factor(period_end, period_start, is_prepaid):
+def get_prorata_factor(
+ period_end: Union[datetime.date, str],
+ period_start: Union[datetime.date, str],
+ is_prepaid: Optional[int] = None,
+) -> Union[int, float]:
if is_prepaid:
- 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
+ return 1
- return prorate_factor
+ diff = flt(date_diff(nowdate(), period_start) + 1)
+ plan_days = flt(date_diff(period_end, period_start) + 1)
+ return diff / plan_days
-def process_all():
+def process_all() -> None:
"""
Task to updates the status of all `Subscription` apart from those that are cancelled
"""
- 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:
+ for subscription in frappe.get_all("Subscription", {"status": ("!=", "Cancelled")}, pluck="name"):
try:
- subscription = frappe.get_doc("Subscription", data["name"])
+ subscription = frappe.get_doc("Subscription", subscription)
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()
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index eb17daa282..0bb171f464 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -11,6 +11,7 @@ from frappe.utils.data import (
date_diff,
flt,
get_date_str,
+ getdate,
nowdate,
)
@@ -90,10 +91,18 @@ 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")
@@ -116,8 +125,6 @@ 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"
@@ -133,8 +140,6 @@ 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"
@@ -144,7 +149,6 @@ 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")
@@ -156,7 +160,6 @@ 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")
@@ -169,13 +172,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-01-01")
- subscription.process()
+ self.assertEqual(subscription.current_invoice_start, "2018-02-01")
+ self.assertEqual(subscription.current_invoice_end, "2018-02-28")
self.assertEqual(subscription.status, "Unpaid")
- subscription.delete()
def test_status_goes_back_to_active_after_invoice_is_paid(self):
subscription = frappe.new_doc("Subscription")
@@ -183,7 +186,9 @@ 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)
@@ -203,11 +208,8 @@ 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()
@@ -215,20 +217,18 @@ 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,21 +248,26 @@ 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.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.days_until_due = 10
- subscription.start_date = add_months(nowdate(), -1)
+ subscription.start_date = _date
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 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")
- subscription.delete()
+ frappe.flags.current_date = add_days(subscription.current_invoice_end, 3)
+ self.assertEqual(len(subscription.invoices), 1)
+ self.assertEqual(subscription.status, "Active")
def test_subscription_is_past_due_doesnt_change_within_grace_period(self):
settings = frappe.get_single("Subscription Settings")
@@ -276,6 +281,8 @@ 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")
@@ -292,7 +299,6 @@ 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")
@@ -319,8 +325,6 @@ 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"
@@ -331,8 +335,6 @@ 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
@@ -372,7 +374,6 @@ 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()
@@ -395,8 +396,6 @@ 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
@@ -422,8 +421,6 @@ 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
@@ -437,23 +434,22 @@ 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), invoices)
+ self.assertEqual(len(subscription.invoices), 1)
subscription.process()
self.assertEqual(subscription.status, "Cancelled")
- self.assertEqual(len(subscription.invoices), invoices)
+ self.assertEqual(len(subscription.invoices), 1)
subscription.process()
self.assertEqual(subscription.status, "Cancelled")
- self.assertEqual(len(subscription.invoices), invoices)
+ self.assertEqual(len(subscription.invoices), 1)
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")
@@ -468,6 +464,7 @@ 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
@@ -478,19 +475,18 @@ class TestSubscription(unittest.TestCase):
subscription.restart_subscription()
self.assertEqual(subscription.status, "Active")
- self.assertEqual(len(subscription.invoices), 0)
+ self.assertEqual(len(subscription.invoices), 1)
subscription.process()
- self.assertEqual(subscription.status, "Active")
- self.assertEqual(len(subscription.invoices), 0)
+ self.assertEqual(subscription.status, "Unpaid")
+ self.assertEqual(len(subscription.invoices), 1)
subscription.process()
- self.assertEqual(subscription.status, "Active")
- self.assertEqual(len(subscription.invoices), 0)
+ self.assertEqual(subscription.status, "Unpaid")
+ self.assertEqual(len(subscription.invoices), 1)
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")
@@ -503,8 +499,11 @@ 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")
@@ -517,12 +516,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")
@@ -533,8 +532,6 @@ 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"
@@ -549,8 +546,6 @@ 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"
@@ -565,8 +560,6 @@ 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.
@@ -614,8 +607,6 @@ 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"
@@ -623,14 +614,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):
@@ -639,11 +630,12 @@ 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()
@@ -652,8 +644,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
-
+ # subscription and the interval between the subscriptions is 3 months
+ frappe.flags.current_date = "2018-04-01"
subscription.process()
self.assertEqual(len(subscription.invoices), 2)
@@ -662,7 +654,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()
@@ -682,7 +674,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()
@@ -692,5 +684,47 @@ 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].invoice, "currency")
+ currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "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"),
+ )
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 58792d1d8a..954b4e7957 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -100,11 +100,14 @@ 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.throw(
- _("Please set associated account in Tax Withholding Category {0} against Company {1}").format(
- tax_withholding_category, inv.company
- )
+ 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)
)
+ 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
@@ -262,14 +265,20 @@ 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:
- tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total)
+ 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
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, tax_deducted, vouchers)
+ tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
elif party_type == "Customer":
if tax_deducted:
@@ -416,7 +425,7 @@ def get_deducted_tax(taxable_vouchers, tax_details):
return sum(entries)
-def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
+def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
tds_amount = 0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
@@ -476,7 +485,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
- if (threshold and inv.tax_withholding_net_total >= threshold) or (
+ 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 (
cumulative_threshold and supp_credit_amt >= cumulative_threshold
):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
@@ -491,15 +505,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
net_total += inv.tax_withholding_net_total
supp_credit_amt = net_total - cumulative_threshold
- 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)
+ 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
+ )
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
@@ -577,8 +586,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
return inv.grand_total - tcs_tax_row_amount
-def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
- tds_amount = 0
+def get_limit_consumed(ldc, parties):
limit_consumed = frappe.db.get_value(
"Purchase Invoice",
{
@@ -592,37 +600,29 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
"sum(tax_withholding_net_total)",
)
- 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
+ return limit_consumed
-def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
- if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
+def get_lower_deduction_amount(
+ current_amount, limit_consumed, certificate_limit, rate, tax_details
+):
+ if certificate_limit - flt(limit_consumed) - flt(current_amount) >= 0:
return current_amount * rate / 100
else:
- ltds_amount = certificate_limit - flt(deducted_amount)
+ ltds_amount = certificate_limit - flt(limit_consumed)
tds_amount = current_amount - ltds_amount
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
-def is_valid_certificate(
- valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit
-):
- valid = False
+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
- 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
+ return False
def normal_round(number):
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index 4580b13613..0fbaf23c3c 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -4,6 +4,7 @@
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
@@ -17,6 +18,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
# create relevant supplier, etc
create_records()
create_tax_withholding_category_records()
+ make_pan_no_field()
def tearDown(self):
cancel_invoices()
@@ -316,6 +318,42 @@ 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"
@@ -415,6 +453,40 @@ 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(
@@ -573,6 +645,8 @@ 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
@@ -769,3 +843,39 @@ 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)
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index f1dad875fa..3803836ef7 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -13,14 +13,11 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
+from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry
-class ClosedAccountingPeriod(frappe.ValidationError):
- pass
-
-
def make_gl_entries(
gl_map,
cancel=False,
@@ -31,6 +28,7 @@ def make_gl_entries(
):
if gl_map:
if not cancel:
+ make_acc_dimensions_offsetting_entry(gl_map)
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
@@ -54,6 +52,63 @@ def make_gl_entries(
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
+def make_acc_dimensions_offsetting_entry(gl_map):
+ accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(
+ gl_map, gl_map[0].company
+ )
+ no_of_dimensions = len(accounting_dimensions_to_offset)
+ if no_of_dimensions == 0:
+ return
+
+ offsetting_entries = []
+
+ for gle in gl_map:
+ for dimension in accounting_dimensions_to_offset:
+ offsetting_entry = gle.copy()
+ debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0
+ credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0
+ offsetting_entry.update(
+ {
+ "account": dimension.offsetting_account,
+ "debit": debit,
+ "credit": credit,
+ "debit_in_account_currency": debit,
+ "credit_in_account_currency": credit,
+ "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name),
+ "against_voucher": None,
+ }
+ )
+ offsetting_entry["against_voucher_type"] = None
+ offsetting_entries.append(offsetting_entry)
+
+ gl_map += offsetting_entries
+
+
+def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
+ acc_dimension = frappe.qb.DocType("Accounting Dimension")
+ dimension_detail = frappe.qb.DocType("Accounting Dimension Detail")
+
+ acc_dimensions = (
+ frappe.qb.from_(acc_dimension)
+ .inner_join(dimension_detail)
+ .on(acc_dimension.name == dimension_detail.parent)
+ .select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account)
+ .where(
+ (acc_dimension.disabled == 0)
+ & (dimension_detail.company == company)
+ & (dimension_detail.automatically_post_balancing_accounting_entry == 1)
+ )
+ ).run(as_dict=True)
+
+ accounting_dimensions_to_offset = []
+ for acc_dimension in acc_dimensions:
+ values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
+ if len(values) > 1:
+ accounting_dimensions_to_offset.append(acc_dimension)
+
+ return accounting_dimensions_to_offset
+
+
def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account]
@@ -108,7 +163,8 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
if not gl_map:
return []
- gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
+ if gl_map[0].voucher_type != "Period Closing Voucher":
+ gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
if merge_entries:
gl_map = merge_similar_entries(gl_map, precision)
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 03cf82a2b0..0d67752ba7 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -14,6 +14,7 @@ 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,
@@ -33,6 +34,7 @@ import erpnext
from erpnext import get_company_currency
from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
+from erpnext.utilities.regional import temporary_flag
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
SALES_TRANSACTION_TYPES = {
@@ -261,9 +263,8 @@ def set_address_details(
)
if doctype in TRANSACTION_TYPES:
- # required to set correct region
- frappe.flags.company = company
- get_regional_address_details(party_details, doctype, company)
+ with temporary_flag("company", company):
+ get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address
@@ -706,6 +707,7 @@ 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
@@ -920,32 +922,35 @@ 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
+ party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
):
- cond = "1=1"
+ 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"))
+
if posting_date:
if future_payment:
- cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
+ query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
else:
- cond = "posting_date <= '{0}'".format(posting_date)
+ query = query.where(gle.posting_date <= posting_date)
if company:
- cond += "and company = {0}".format(frappe.db.escape(company))
+ query = query.where(gle.company == company)
if party:
- cond += "and party = {0}".format(frappe.db.escape(party))
+ query = query.where(gle.party == party)
- 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,
- )
+ data = query.run(as_dict=True)
if data:
return frappe._dict(data)
diff --git a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json
index a7eac70b65..c48e1cf35b 100644
--- a/erpnext/accounts/print_format/dunning_letter/dunning_letter.json
+++ b/erpnext/accounts/print_format/dunning_letter/dunning_letter.json
@@ -1,4 +1,5 @@
{
+ "absolute_value": 0,
"align_labels_right": 0,
"creation": "2019-12-11 04:37:14.012805",
"css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n",
@@ -9,10 +10,10 @@
"docstatus": 0,
"doctype": "Print Format",
"font": "Arial",
- "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"{{doc.customer_name}}
\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n
{{_(doc.dunning_type)}}
\\n
{{ doc.name }}
\\n
\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n \\n \\n {{_(\\\"Description\\\")}} | \\n\\t {{_(\\\"Amount\\\")}} | \\n
\\n \\n \\n {{_(\\\"Outstanding Amount\\\")}}\\n | \\n \\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n | \\n
\\n {%if doc.rate_of_interest > 0%}\\n \\n \\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n | \\n \\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n | \\n
\\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n \\n \\n {{_(\\\"Dunning Fee\\\")}}\\n | \\n \\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n | \\n
\\n {% endif %}\\n \\n
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n\\n\\t\\t
\\n\\t\\t\\t{{_(\\\"Grand Total\\\")}}
\\n\\t\\t
\\n\\t\\t\\t{{doc.get_formatted(\\\"grand_total\\\")}}\\n\\t\\t
\\n
\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
+ "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"{{doc.customer_name}}
\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n
{{_(doc.dunning_type)}}
\\n
{{ doc.name }}
\\n
\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"overdue_payments\", \"print_hide\": 0, \"label\": \"Overdue Payments\", \"visible_columns\": [{\"fieldname\": \"sales_invoice\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"dunning_level\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"due_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"overdue_days\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"invoice_portion\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"outstanding\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"interest\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_outstanding\", \"print_hide\": 0, \"label\": \"Total Outstanding\"}, {\"fieldname\": \"dunning_fee\", \"print_hide\": 0, \"label\": \"Dunning Fee\"}, {\"fieldname\": \"total_interest\", \"print_hide\": 0, \"label\": \"Total Interest\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
"idx": 0,
"line_breaks": 0,
- "modified": "2020-07-14 18:25:44.348207",
+ "modified": "2021-09-30 10:22:02.603871",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning Letter",
diff --git a/erpnext/accounts/report/account_balance/account_balance.js b/erpnext/accounts/report/account_balance/account_balance.js
index bb66951cdc..5681be9211 100644
--- a/erpnext/accounts/report/account_balance/account_balance.js
+++ b/erpnext/accounts/report/account_balance/account_balance.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Account Balance"] = {
"filters": [
diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.py b/erpnext/accounts/report/accounts_payable/accounts_payable.py
index 7b19994911..8279afbc2b 100644
--- a/erpnext/accounts/report/accounts_payable/accounts_payable.py
+++ b/erpnext/accounts/report/accounts_payable/accounts_payable.py
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
def execute(filters=None):
args = {
- "party_type": "Supplier",
+ "account_type": "Payable",
"naming_by": ["Buying Settings", "supp_master_name"],
}
return ReceivablePayableReport(filters).run(args)
diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py
index 65fe1de568..834c83c38e 100644
--- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py
+++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py
@@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
def execute(filters=None):
args = {
- "party_type": "Supplier",
+ "account_type": "Payable",
"naming_by": ["Buying Settings", "supp_master_name"],
}
return AccountsReceivableSummary(filters).run(args)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 30f7fb38c5..f78a84086a 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -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
+from frappe.query_builder.functions import Date, Sum
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 = {
- "party_type": "Customer",
+ "account_type": "Receivable",
"naming_by": ["Selling Settings", "cust_master_name"],
}
return ReceivablePayableReport(filters).run(args)
@@ -70,8 +70,11 @@ 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.party_type == "Customer" else "credit"
- self.party_type = self.filters.party_type
+ 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.party_details = {}
self.invoices = set()
self.skip_total_row = 0
@@ -197,6 +200,7 @@ 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):
@@ -207,8 +211,9 @@ class ReceivablePayableReport(object):
return
# amount in "Party Currency", if its supplied. If not, amount in company currency
- if self.filters.get(scrub(self.party_type)):
- amount = ple.amount_in_account_currency
+ for party_type in self.party_type:
+ if self.filters.get(scrub(party_type)):
+ amount = ple.amount_in_account_currency
else:
amount = ple.amount
amount_in_account_currency = ple.amount_in_account_currency
@@ -362,7 +367,7 @@ class ReceivablePayableReport(object):
def get_invoice_details(self):
self.invoice_details = frappe._dict()
- if self.party_type == "Customer":
+ if self.account_type == "Receivable":
si_list = frappe.db.sql(
"""
select name, due_date, po_no
@@ -390,7 +395,7 @@ class ReceivablePayableReport(object):
d.sales_person
)
- if self.party_type == "Supplier":
+ if self.account_type == "Payable":
for pi in frappe.db.sql(
"""
select name, due_date, bill_no, bill_date
@@ -421,20 +426,21 @@ class ReceivablePayableReport(object):
# customer / supplier name
party_details = self.get_party_details(row.party) or {}
row.update(party_details)
- if self.filters.get(scrub(self.filters.party_type)):
- row.currency = row.account_currency
+ for party_type in self.party_type:
+ if self.filters.get(scrub(party_type)):
+ row.currency = row.account_currency
+ break
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 "oustanding" for this term
+ # update "paid" and "outstanding" for this term
if not term.paid:
self.allocate_closing_to_term(row, term, "paid")
- # update "credit_note" and "oustanding" for this term
+ # update "credit_note" and "outstanding" for this term
if term.outstanding:
self.allocate_closing_to_term(row, term, "credit_note")
@@ -446,7 +452,8 @@ class ReceivablePayableReport(object):
"""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
- ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
+ si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
+ ps.description, ps.paid_amount, ps.discounted_amount
from `tab{0}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
@@ -462,6 +469,10 @@ 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
@@ -476,7 +487,7 @@ class ReceivablePayableReport(object):
) and d.currency == d.party_account_currency:
invoiced = d.payment_amount
else:
- invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision)
+ invoiced = d.base_payment_amount
row.payment_terms.append(
term.update(
@@ -532,65 +543,67 @@ class ReceivablePayableReport(object):
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
def get_future_payments_from_payment_entry(self):
- 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,
- )
+ 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):
- if self.filters.get("party"):
- 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"
-
- return frappe.db.sql(
- """
- select
- jea.reference_name as invoice_no,
+ 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,
- 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,
+ 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 != "")
+ )
)
+ 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")
+ )
+
+ query = query.having(qb.Field("future_amount") > 0)
+ return query.run(as_dict=True)
+
def allocate_future_payments(self, row):
# future payments are captured in additional columns
# this method allocates pending future payments against a voucher to
@@ -619,13 +632,17 @@ class ReceivablePayableReport(object):
row.future_ref = ", ".join(row.future_ref)
def get_return_entries(self):
- doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
+ doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
- party_field = scrub(self.filters.party_type)
- if self.filters.get(party_field):
- filters.update({party_field: self.filters.get(party_field)})
+ 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)})
self.return_entries = frappe._dict(
- frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1)
+ frappe.get_all(
+ doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1
+ )
)
def set_ageing(self, row):
@@ -716,6 +733,7 @@ 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"):
@@ -746,16 +764,18 @@ class ReceivablePayableReport(object):
def prepare_conditions(self):
self.qb_selection_filter = []
- party_type_field = scrub(self.party_type)
- self.qb_selection_filter.append(self.ple.party_type == self.party_type)
+ 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)
- 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()
@@ -784,11 +804,10 @@ 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": account_type, "company": self.filters.company}
+ "Account", filters={"account_type": self.account_type, "company": self.filters.company}
)
]
@@ -878,7 +897,7 @@ class ReceivablePayableReport(object):
def get_party_details(self, party):
if not party in self.party_details:
- if self.party_type == "Customer":
+ if self.account_type == "Receivable":
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
if self.filters.get("sales_partner"):
@@ -901,14 +920,20 @@ class ReceivablePayableReport(object):
self.columns = []
self.add_column("Posting Date", fieldtype="Date")
self.add_column(
- label=_(self.party_type),
+ label="Party Type",
+ fieldname="party_type",
+ fieldtype="Data",
+ width=100,
+ )
+ self.add_column(
+ label="Party",
fieldname="party",
- fieldtype="Link",
- options=self.party_type,
+ fieldtype="Dynamic Link",
+ options="party_type",
width=180,
)
self.add_column(
- label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
+ label=self.account_type + " Account",
fieldname="party_account",
fieldtype="Link",
options="Account",
@@ -916,13 +941,19 @@ 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(
- _("{0} Name").format(self.party_type),
- fieldname=scrub(self.party_type) + "_name",
+ label=label,
+ fieldname=fieldname,
fieldtype="Data",
)
- if self.party_type == "Customer":
+ if self.account_type == "Receivable":
self.add_column(
_("Customer Contact"),
fieldname="customer_primary_contact",
@@ -942,7 +973,7 @@ class ReceivablePayableReport(object):
self.add_column(label="Due Date", fieldtype="Date")
- if self.party_type == "Supplier":
+ if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
@@ -952,7 +983,7 @@ class ReceivablePayableReport(object):
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
self.add_column(_("Paid Amount"), fieldname="paid")
- if self.party_type == "Customer":
+ if self.account_type == "Receivable":
self.add_column(_("Credit Note"), fieldname="credit_note")
else:
# note: fieldname is still `credit_note`
@@ -970,7 +1001,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.party_type == "Customer":
+ if self.filters.account_type == "Receivable":
self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
# comma separated list of linked delivery notes
@@ -991,7 +1022,7 @@ class ReceivablePayableReport(object):
if self.filters.sales_partner:
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
- if self.filters.party_type == "Supplier":
+ if self.filters.account_type == "Payable":
self.add_column(
label=_("Supplier Group"),
fieldname="supplier_group",
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index 9c01b1a498..da4c9dabbf 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
def execute(filters=None):
args = {
- "party_type": "Customer",
+ "account_type": "Receivable",
"naming_by": ["Selling Settings", "cust_master_name"],
}
@@ -21,7 +21,10 @@ def execute(filters=None):
class AccountsReceivableSummary(ReceivablePayableReport):
def run(self, args):
- self.party_type = args.get("party_type")
+ 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_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1]
)
@@ -35,13 +38,19 @@ 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=self.filters.get(scrub(self.party_type)),
+ party=party,
+ account_type=self.account_type,
)
or {}
)
@@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
row.party = party
if self.party_naming_by == "Naming Series":
- row.party_name = frappe.get_cached_value(
- self.party_type, party, scrub(self.party_type) + "_name"
- )
+ 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.update(party_dict)
@@ -93,6 +106,7 @@ 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(
@@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport):
def get_columns(self):
self.columns = []
self.add_column(
- label=_(self.party_type),
+ label=_("Party Type"),
+ fieldname="party_type",
+ fieldtype="Data",
+ width=100,
+ )
+ self.add_column(
+ label=_("Party"),
fieldname="party",
- fieldtype="Link",
- options=self.party_type,
+ fieldtype="Dynamic Link",
+ options="party_type",
width=180,
)
if self.party_naming_by == "Naming Series":
- self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data")
+ self.add_column(
+ label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"),
+ fieldname="party_name",
+ fieldtype="Data",
+ )
- credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note"
+ credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note"
self.add_column(_("Advance Amount"), fieldname="advance")
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
@@ -159,7 +183,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.party_type == "Customer":
+ if self.account_type == "Receivable":
self.add_column(
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
)
diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json
index bee2829c87..0ef9d858dd 100644
--- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json
+++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json
@@ -1,20 +1,23 @@
{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2016-04-08 14:49:58.133098",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 2,
- "is_standard": "Yes",
- "modified": "2017-02-24 20:08:26.084484",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Asset Depreciation Ledger",
- "owner": "Administrator",
- "ref_doctype": "Asset",
- "report_name": "Asset Depreciation Ledger",
- "report_type": "Script Report",
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2016-04-08 14:49:58.133098",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 2,
+ "is_standard": "Yes",
+ "letterhead": null,
+ "modified": "2023-07-26 21:05:33.554778",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Asset Depreciation Ledger",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Asset",
+ "report_name": "Asset Depreciation Ledger",
+ "report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js
index 1da35cd95b..5f78b77934 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js
@@ -15,14 +15,14 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
"reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"reqd": 1
},
{
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.json b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.json
index eab95fc73b..2ea9af223e 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.json
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.json
@@ -1,20 +1,23 @@
{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2016-04-08 14:56:37.235981",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 2,
- "is_standard": "Yes",
- "modified": "2017-02-24 20:08:18.660476",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Asset Depreciations and Balances",
- "owner": "Administrator",
- "ref_doctype": "Asset",
- "report_name": "Asset Depreciations and Balances",
- "report_type": "Script Report",
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2016-04-08 14:56:37.235981",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 2,
+ "is_standard": "Yes",
+ "letterhead": null,
+ "modified": "2023-07-26 21:04:54.751077",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Asset Depreciations and Balances",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Asset",
+ "report_name": "Asset Depreciations and Balances",
+ "report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js
index 4a4ad4d71c..c65b9e8ccc 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.js
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js
@@ -1,22 +1,26 @@
// 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,
});
});
diff --git a/erpnext/accounts/report/balance_sheet/test_balance_sheet.py b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py
new file mode 100644
index 0000000000..3cb6efebee
--- /dev/null
+++ b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py
@@ -0,0 +1,51 @@
+# 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)
diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js
index f0b6c6b20a..8a7d071a47 100644
--- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js
+++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.js
@@ -7,7 +7,7 @@ frappe.query_reports["Bank Clearance Summary"] = {
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
"width": "80"
},
{
diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.js b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.js
index e1fccb6e72..7617ed1e22 100644
--- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.js
+++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports['Billed Items To Be Received'] = {
'filters': [
@@ -17,7 +17,7 @@ frappe.query_reports['Billed Items To Be Received'] = {
'fieldname': 'posting_date',
'fieldtype': 'Date',
'reqd': 1,
- 'default': get_today()
+ 'default': frappe.datetime.get_today()
},
{
'label': __('Purchase Invoice'),
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
index 5955c2e0fc..15aa265b56 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
@@ -8,7 +8,7 @@ frappe.query_reports["Budget Variance Report"] = {
label: __("From Fiscal Year"),
fieldtype: "Link",
options: "Fiscal Year",
- default: frappe.sys_defaults.fiscal_year,
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
reqd: 1
},
{
@@ -16,7 +16,7 @@ frappe.query_reports["Budget Variance Report"] = {
label: __("To Fiscal Year"),
fieldtype: "Link",
options: "Fiscal Year",
- default: frappe.sys_defaults.fiscal_year,
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
reqd: 1
},
{
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
index d58fd95a84..1afa8d5625 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Consolidated Financial Statement"] = {
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 6e39ee9944..080e45a798 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -6,6 +6,7 @@ from collections import defaultdict
import frappe
from frappe import _
+from frappe.query_builder import Criterion
from frappe.utils import flt, getdate
import erpnext
@@ -359,6 +360,7 @@ 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)
@@ -603,6 +605,7 @@ def set_gl_entries_by_account(
accounts_by_name,
accounts,
ignore_closing_entries=False,
+ root_type=None,
):
"""Returns a dict like { "account": [gl entries], ... }"""
@@ -610,7 +613,6 @@ 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""",
@@ -626,27 +628,43 @@ def set_gl_entries_by_account(
)
for d in companies:
- 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,
+ 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)
)
+ 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
@@ -716,23 +734,25 @@ def validate_entries(key, entry, accounts_by_name, accounts):
accounts.insert(idx + 1, args)
-def get_additional_conditions(from_date, ignore_closing_entries, filters):
+def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
+ gle = frappe.qb.DocType("GL Entry")
additional_conditions = []
if ignore_closing_entries:
- additional_conditions.append("ifnull(gl.voucher_type, '')!='Period Closing Voucher'")
+ additional_conditions.append((gle.voucher_type != "Period Closing Voucher"))
if from_date:
- additional_conditions.append("gl.posting_date >= %(from_date)s")
+ 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")
if filters.get("include_default_book_entries"):
- additional_conditions.append(
- "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
- )
+ additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
else:
- additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
+ additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
- return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
+ return additional_conditions
def add_total_row(out, root_type, balance_must_be, companies, company_currency):
diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js
index a123631663..74d52de2d2 100644
--- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js
+++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Customer Ledger Summary"] = {
"filters": [
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js
index 96e0c844ca..eec904ec85 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
function get_filters() {
let filters = [
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
index c84b843f1f..28d0c20a91 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
@@ -2,6 +2,7 @@ import unittest
import frappe
from frappe import qb
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate
from erpnext.accounts.doctype.account.test_account import create_account
@@ -10,16 +11,15 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
Deferred_Revenue_and_Expense_Report,
)
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.stock.doctype.item.test_item import create_item
-class TestDeferredRevenueAndExpense(unittest.TestCase):
+class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
@classmethod
def setUpClass(self):
- clear_accounts_and_items()
- create_company()
self.maxDiff = None
def clear_old_entries(self):
@@ -51,55 +51,58 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
if deferred_invoices:
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
- def test_deferred_revenue(self):
- self.clear_old_entries()
+ def setup_deferred_accounts_and_items(self):
+ # created deferred expense accounts, if not found
+ self.deferred_revenue_account = create_account(
+ account_name="Deferred Revenue",
+ parent_account="Current Liabilities - " + self.company_abbr,
+ company=self.company,
+ )
# created deferred expense accounts, if not found
- deferred_revenue_account = create_account(
- account_name="Deferred Revenue",
- parent_account="Current Liabilities - _CD",
- company="_Test Company DR",
+ self.deferred_expense_account = create_account(
+ account_name="Deferred Expense",
+ parent_account="Current Assets - " + self.company_abbr,
+ company=self.company,
)
- acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
- acc_settings.book_deferred_entries_based_on = "Months"
- acc_settings.save()
+ def setUp(self):
+ self.create_company()
+ self.create_customer("_Test Customer")
+ self.create_supplier("_Test Furniture Supplier")
+ self.setup_deferred_accounts_and_items()
+ self.clear_old_entries()
- customer = frappe.new_doc("Customer")
- customer.customer_name = "_Test Customer DR"
- customer.type = "Individual"
- customer.insert()
+ def tearDown(self):
+ frappe.db.rollback()
- item = create_item(
- "_Test Internet Subscription",
- is_stock_item=0,
- warehouse="All Warehouses - _CD",
- company="_Test Company DR",
- )
+ @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
+ def test_deferred_revenue(self):
+ self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
+ item = frappe.get_doc("Item", self.item)
item.enable_deferred_revenue = 1
- item.deferred_revenue_account = deferred_revenue_account
+ item.deferred_revenue_account = self.deferred_revenue_account
item.no_of_months = 3
item.save()
si = create_sales_invoice(
- item=item.name,
- company="_Test Company DR",
- customer="_Test Customer DR",
- debit_to="Debtors - _CD",
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
posting_date="2021-05-01",
- parent_cost_center="Main - _CD",
- cost_center="Main - _CD",
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
do_not_save=True,
rate=300,
price_list_rate=300,
)
- si.items[0].income_account = "Sales - _CD"
+ si.items[0].income_account = self.income_account
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2021-05-01"
si.items[0].service_end_date = "2021-08-01"
- si.items[0].deferred_revenue_account = deferred_revenue_account
- si.items[0].income_account = "Sales - _CD"
+ si.items[0].deferred_revenue_account = self.deferred_revenue_account
si.save()
si.submit()
@@ -110,7 +113,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
start_date="2021-05-01",
end_date="2021-08-01",
type="Income",
- company="_Test Company DR",
+ company=self.company,
)
)
pda.insert()
@@ -120,7 +123,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict(
{
- "company": frappe.defaults.get_user_default("Company"),
+ "company": self.company,
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
@@ -142,57 +145,36 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
]
self.assertEqual(report.period_total, expected)
+ @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
def test_deferred_expense(self):
- self.clear_old_entries()
-
- # created deferred expense accounts, if not found
- deferred_expense_account = create_account(
- account_name="Deferred Expense",
- parent_account="Current Assets - _CD",
- company="_Test Company DR",
- )
-
- acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
- acc_settings.book_deferred_entries_based_on = "Months"
- acc_settings.save()
-
- supplier = create_supplier(
- supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
- )
- supplier.save()
-
- item = create_item(
- "_Test Office Desk",
- is_stock_item=0,
- warehouse="All Warehouses - _CD",
- company="_Test Company DR",
- )
+ self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
+ item = frappe.get_doc("Item", self.item)
item.enable_deferred_expense = 1
- item.deferred_expense_account = deferred_expense_account
+ item.deferred_expense_account = self.deferred_expense_account
item.no_of_months_exp = 3
item.save()
pi = make_purchase_invoice(
- item=item.name,
- company="_Test Company DR",
- supplier="_Test Furniture Supplier",
+ item=self.item,
+ company=self.company,
+ supplier=self.supplier,
is_return=False,
update_stock=False,
posting_date=frappe.utils.datetime.date(2021, 5, 1),
- parent_cost_center="Main - _CD",
- cost_center="Main - _CD",
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
do_not_save=True,
rate=300,
price_list_rate=300,
- warehouse="All Warehouses - _CD",
+ warehouse=self.warehouse,
qty=1,
)
pi.set_posting_time = True
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2021-05-01"
pi.items[0].service_end_date = "2021-08-01"
- pi.items[0].deferred_expense_account = deferred_expense_account
- pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
+ pi.items[0].deferred_expense_account = self.deferred_expense_account
+ pi.items[0].expense_account = self.expense_account
pi.save()
pi.submit()
@@ -203,7 +185,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
start_date="2021-05-01",
end_date="2021-08-01",
type="Expense",
- company="_Test Company DR",
+ company=self.company,
)
)
pda.insert()
@@ -213,7 +195,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict(
{
- "company": frappe.defaults.get_user_default("Company"),
+ "company": self.company,
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
@@ -235,52 +217,31 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
]
self.assertEqual(report.period_total, expected)
+ @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
def test_zero_months(self):
- self.clear_old_entries()
- # created deferred expense accounts, if not found
- deferred_revenue_account = create_account(
- account_name="Deferred Revenue",
- parent_account="Current Liabilities - _CD",
- company="_Test Company DR",
- )
-
- acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
- acc_settings.book_deferred_entries_based_on = "Months"
- acc_settings.save()
-
- customer = frappe.new_doc("Customer")
- customer.customer_name = "_Test Customer DR"
- customer.type = "Individual"
- customer.insert()
-
- item = create_item(
- "_Test Internet Subscription",
- is_stock_item=0,
- warehouse="All Warehouses - _CD",
- company="_Test Company DR",
- )
+ self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
+ item = frappe.get_doc("Item", self.item)
item.enable_deferred_revenue = 1
- item.deferred_revenue_account = deferred_revenue_account
+ item.deferred_revenue_account = self.deferred_revenue_account
item.no_of_months = 0
item.save()
si = create_sales_invoice(
item=item.name,
- company="_Test Company DR",
- customer="_Test Customer DR",
- debit_to="Debtors - _CD",
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
posting_date="2021-05-01",
- parent_cost_center="Main - _CD",
- cost_center="Main - _CD",
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
do_not_save=True,
rate=300,
price_list_rate=300,
)
si.items[0].enable_deferred_revenue = 1
- si.items[0].income_account = "Sales - _CD"
- si.items[0].deferred_revenue_account = deferred_revenue_account
- si.items[0].income_account = "Sales - _CD"
+ si.items[0].income_account = self.income_account
+ si.items[0].deferred_revenue_account = self.deferred_revenue_account
si.save()
si.submit()
@@ -291,7 +252,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
start_date="2021-05-01",
end_date="2021-08-01",
type="Income",
- company="_Test Company DR",
+ company=self.company,
)
)
pda.insert()
@@ -301,7 +262,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict(
{
- "company": frappe.defaults.get_user_default("Company"),
+ "company": self.company,
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
@@ -322,30 +283,3 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
-
-
-def create_company():
- company = frappe.db.exists("Company", "_Test Company DR")
- if not company:
- company = frappe.new_doc("Company")
- company.company_name = "_Test Company DR"
- company.default_currency = "INR"
- company.chart_of_accounts = "Standard"
- company.insert()
-
-
-def clear_accounts_and_items():
- item = qb.DocType("Item")
- account = qb.DocType("Account")
- customer = qb.DocType("Customer")
- supplier = qb.DocType("Supplier")
-
- qb.from_(account).delete().where(
- (account.account_name == "Deferred Revenue")
- | (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
- ).run()
- qb.from_(item).delete().where(
- (item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
- ).run()
- qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
- qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
index 9d416db4fd..79e5a0997f 100644
--- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Dimension-wise Accounts Balance Report"] = {
@@ -38,14 +38,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
"reqd": 1
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"reqd": 1
},
{
diff --git a/erpnext/accounts/report/financial_ratios/__init__.py b/erpnext/accounts/report/financial_ratios/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.js b/erpnext/accounts/report/financial_ratios/financial_ratios.js
new file mode 100644
index 0000000000..643423d865
--- /dev/null
+++ b/erpnext/accounts/report/financial_ratios/financial_ratios.js
@@ -0,0 +1,72 @@
+// 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 = $(`${value}`);
+ let $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("").parent().html();
+ }
+
+ if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
+ column.fieldtype = "Data";
+ }
+
+ value = default_formatter(value, row, column, data);
+
+ return value;
+ },
+};
diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.json b/erpnext/accounts/report/financial_ratios/financial_ratios.json
new file mode 100644
index 0000000000..1a2e56bad1
--- /dev/null
+++ b/erpnext/accounts/report/financial_ratios/financial_ratios.json
@@ -0,0 +1,37 @@
+{
+ "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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py
new file mode 100644
index 0000000000..57421ebcb0
--- /dev/null
+++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py
@@ -0,0 +1,296 @@
+# 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
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index db9609debe..693725d8f5 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -188,6 +188,7 @@ def get_data(
filters,
gl_entries_by_account,
ignore_closing_entries=ignore_closing_entries,
+ root_type=root_type,
)
calculate_values(
@@ -334,12 +335,10 @@ 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)
@@ -417,23 +416,44 @@ 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={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
+ filters=account_filters,
pluck="name",
)
if accounts_list:
# For balance sheet
- if not from_date:
- from_date = filters["period_start_date"]
+ ignore_closing_balances = frappe.db.get_single_value(
+ "Accounts Settings", "ignore_account_closing_balance"
+ )
+ if not from_date and not ignore_closing_balances:
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
- filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", from_date)},
+ filters={
+ "docstatus": 1,
+ "company": filters.company,
+ "posting_date": ("<", filters["period_start_date"]),
+ },
fields=["posting_date", "name"],
order_by="posting_date desc",
limit=1,
@@ -617,7 +637,13 @@ 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}
+ {
+ "fieldname": "total",
+ "label": _("Total"),
+ "fieldtype": "Currency",
+ "width": 150,
+ "options": "currency",
+ }
)
return columns
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js
new file mode 100644
index 0000000000..7e6b0537e8
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js
@@ -0,0 +1,52 @@
+// 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()
+};
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json
new file mode 100644
index 0000000000..1d0d9d134d
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json
@@ -0,0 +1,32 @@
+{
+ "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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py
new file mode 100644
index 0000000000..553c137f02
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py
@@ -0,0 +1,221 @@
+# 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
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py
new file mode 100644
index 0000000000..4b0e99d712
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py
@@ -0,0 +1,100 @@
+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)
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 57a9091cf9..37d0659acf 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -188,6 +188,11 @@ 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"
}
]
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index d7af167e38..e05a4e79e8 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -182,12 +182,18 @@ 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,
+ cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
remarks, against, is_opening, creation {select_fields}
from `tabGL Entry`
@@ -195,6 +201,7 @@ 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,
@@ -562,6 +569,34 @@ 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"),
diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js
index 92cf36ebc5..f6b0b8c3f7 100644
--- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js
+++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Gross and Net Profit Report"] = {
"filters": [
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 53921dc66e..752054834b 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -15,14 +15,14 @@ frappe.query_reports["Gross Profit"] = {
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
"reqd": 1
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"reqd": 1
},
{
diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js
index 7908c07a0a..bd9b54398b 100644
--- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js
+++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Inactive Sales Items"] = {
"filters": [
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index 050e6bc5d2..8b929bf472 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -309,7 +309,8 @@ def get_conditions(filters):
def get_items(filters, additional_query_columns):
conditions = get_conditions(filters)
-
+ if additional_query_columns:
+ additional_query_columns = "," + ",".join(additional_query_columns)
return frappe.db.sql(
"""
select
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index 4d24dd9076..1e7ac33c32 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -381,7 +381,8 @@ def get_group_by_conditions(filters, doctype):
def get_items(filters, additional_query_columns, additional_conditions=None):
conditions = get_conditions(filters, additional_conditions)
-
+ if additional_query_columns:
+ additional_query_columns = "," + ",".join(additional_query_columns)
return frappe.db.sql(
"""
select
diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.js b/erpnext/accounts/report/payment_ledger/payment_ledger.js
index a5a4108f1d..65380ccdda 100644
--- a/erpnext/accounts/report/payment_ledger/payment_ledger.js
+++ b/erpnext/accounts/report/payment_ledger/payment_ledger.js
@@ -1,6 +1,6 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
function get_filters() {
let filters = [
diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js
index 2343eaa846..5ec2c9880c 100644
--- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js
+++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.js
@@ -15,7 +15,7 @@ frappe.query_reports["Payment Period Based On Invoice Date"] = {
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_start_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
fieldname:"to_date",
diff --git a/erpnext/accounts/report/pos_register/pos_register.js b/erpnext/accounts/report/pos_register/pos_register.js
index b8d48d92de..6e5491a0f8 100644
--- a/erpnext/accounts/report/pos_register/pos_register.js
+++ b/erpnext/accounts/report/pos_register/pos_register.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["POS Register"] = {
"filters": [
diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py
index 9c0aba332e..488bb9957c 100644
--- a/erpnext/accounts/report/pos_register/pos_register.py
+++ b/erpnext/accounts/report/pos_register/pos_register.py
@@ -50,20 +50,20 @@ def get_pos_entries(filters, group_by_field):
order_by = "p.posting_date"
select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", ""
if group_by_field == "mode_of_payment":
- select_mop_field = ", sip.mode_of_payment"
+ select_mop_field = ", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount"
from_sales_invoice_payment = ", `tabSales Invoice Payment` sip"
- group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount, 0) != 0 AND"
+ group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount - IF(sip.type='Cash', p.change_amount, 0), 0) != 0 AND"
order_by += ", sip.mode_of_payment"
elif group_by_field:
order_by += ", p.{}".format(group_by_field)
+ select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
return frappe.db.sql(
"""
SELECT
p.posting_date, p.name as pos_invoice, p.pos_profile,
- p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount,
- p.customer, p.is_return {select_mop_field}
+ p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
FROM
`tabPOS Invoice` p {from_sales_invoice_payment}
WHERE
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
index e794f270c2..9fe93b9772 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
@@ -1,19 +1,18 @@
// 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);
-
- 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
- }
+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: "accumulated_values",
+ label: __("Accumulated Values"),
+ fieldtype: "Check",
+ default: 1,
+ });
});
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
index 6caebd34a2..c9accef7a6 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
@@ -16,9 +16,30 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldname": "based_on",
"label": __("Based On"),
"fieldtype": "Select",
- "options": ["Cost Center", "Project"],
+ "options": ["Cost Center", "Project", "Accounting Dimension"],
"default": "Cost Center",
- "reqd": 1
+ "reqd": 1,
+ "on_change": function(query_report){
+ let based_on = query_report.get_values().based_on;
+ if(based_on!='Accounting Dimension'){
+ frappe.query_report.set_filter_value({
+ accounting_dimension: ''
+ });
+ }
+ }
+ },
+ {
+ "fieldname": "accounting_dimension",
+ "label": __("Accounting Dimension"),
+ "fieldtype": "Link",
+ "options": "Accounting Dimension",
+ "get_query": () =>{
+ return {
+ filters: {
+ "disabled": 0
+ }
+ }
+ }
},
{
"fieldname": "fiscal_year",
@@ -45,13 +66,13 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
},
{
"fieldname": "show_zero_values",
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
index 183e279fe5..dfb941d912 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.utils import cstr, flt
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.report.financial_statements import (
filter_accounts,
filter_out_zero_value_rows,
@@ -16,10 +17,12 @@ value_fields = ("income", "expense", "gross_profit_loss")
def execute(filters=None):
- if not filters.get("based_on"):
- filters["based_on"] = "Cost Center"
+ if filters.get("based_on") == "Accounting Dimension" and not filters.get("accounting_dimension"):
+ frappe.throw(_("Select Accounting Dimension."))
- based_on = filters.based_on.replace(" ", "_").lower()
+ based_on = (
+ filters.based_on if filters.based_on != "Accounting Dimension" else filters.accounting_dimension
+ )
validate_filters(filters)
accounts = get_accounts_data(based_on, filters.get("company"))
data = get_data(accounts, filters, based_on)
@@ -28,14 +31,14 @@ def execute(filters=None):
def get_accounts_data(based_on, company):
- if based_on == "cost_center":
+ if based_on == "Cost Center":
return frappe.db.sql(
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
from `tabCost Center` where company=%s order by name""",
company,
as_dict=True,
)
- elif based_on == "project":
+ elif based_on == "Project":
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
else:
filters = {}
@@ -56,11 +59,17 @@ def get_data(accounts, filters, based_on):
gl_entries_by_account = {}
+ accounting_dimensions = get_dimensions(with_cost_center_and_project=True)[0]
+ fieldname = ""
+ for dimension in accounting_dimensions:
+ if dimension["document_type"] == based_on:
+ fieldname = dimension["fieldname"]
+
set_gl_entries_by_account(
filters.get("company"),
filters.get("from_date"),
filters.get("to_date"),
- based_on,
+ fieldname,
gl_entries_by_account,
ignore_closing_entries=not flt(filters.get("with_period_closing_entry")),
)
@@ -199,7 +208,7 @@ def set_gl_entries_by_account(
additional_conditions = []
if ignore_closing_entries:
- additional_conditions.append("and ifnull(voucher_type, '')!='Period Closing Voucher'")
+ additional_conditions.append("and voucher_type !='Period Closing Voucher'")
if from_date:
additional_conditions.append("and posting_date >= %(from_date)s")
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.js b/erpnext/accounts/report/purchase_register/purchase_register.js
index aaf76c4299..57cb703bae 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.js
+++ b/erpnext/accounts/report/purchase_register/purchase_register.js
@@ -52,6 +52,12 @@ frappe.query_reports["Purchase Register"] = {
"label": __("Item Group"),
"fieldtype": "Link",
"options": "Item Group"
+ },
+ {
+ "fieldname": "include_payments",
+ "label": __("Show Ledger View"),
+ "fieldtype": "Check",
+ "default": 0
}
]
}
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py
index 69827aca69..c7b7e2f7c1 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -4,13 +4,22 @@
import frappe
from frappe import _, msgprint
-from frappe.utils import flt
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import flt, getdate
+from pypika import Order
-from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
- get_accounting_dimensions,
- get_dimension_with_children,
+from erpnext.accounts.party import get_party_account
+from erpnext.accounts.report.utils import (
+ get_advance_taxes_and_charges,
+ get_conditions,
+ get_journal_entries,
+ get_opening_row,
+ get_party_details,
+ get_payment_entries,
+ get_query_columns,
+ get_taxes_query,
+ get_values_for_columns,
)
-from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
def execute(filters=None):
@@ -21,9 +30,15 @@ def _execute(filters=None, additional_table_columns=None):
if not filters:
filters = {}
+ include_payments = filters.get("include_payments")
+ if filters.get("include_payments") and not filters.get("supplier"):
+ frappe.throw(_("Please select a supplier for fetching payments."))
invoice_list = get_invoices(filters, get_query_columns(additional_table_columns))
+ if filters.get("include_payments"):
+ invoice_list += get_payments(filters)
+
columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(
- invoice_list, additional_table_columns
+ invoice_list, additional_table_columns, include_payments
)
if not invoice_list:
@@ -33,14 +48,28 @@ def _execute(filters=None, additional_table_columns=None):
invoice_expense_map = get_invoice_expense_map(invoice_list)
internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(
- invoice_list, invoice_expense_map, expense_accounts
+ invoice_list, invoice_expense_map, expense_accounts, include_payments
)
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
suppliers = list(set(d.supplier for d in invoice_list))
- supplier_details = get_supplier_details(suppliers)
+ supplier_details = get_party_details("Supplier", suppliers)
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
+ res = []
+ if include_payments:
+ opening_row = get_opening_row(
+ "Supplier", filters.supplier, getdate(filters.from_date), filters.company
+ )[0]
+ res.append(
+ {
+ "payable_account": opening_row.account,
+ "debit": flt(opening_row.debit),
+ "credit": flt(opening_row.credit),
+ "balance": flt(opening_row.balance),
+ }
+ )
+
data = []
for inv in invoice_list:
# invoice details
@@ -48,24 +77,23 @@ def _execute(filters=None, additional_table_columns=None):
purchase_receipt = list(set(invoice_po_pr_map.get(inv.name, {}).get("purchase_receipt", [])))
project = list(set(invoice_po_pr_map.get(inv.name, {}).get("project", [])))
- row = [
- inv.name,
- inv.posting_date,
- inv.supplier,
- inv.supplier_name,
- *get_values_for_columns(additional_table_columns, inv).values(),
- supplier_details.get(inv.supplier), # supplier_group
- inv.tax_id,
- inv.credit_to,
- inv.mode_of_payment,
- ", ".join(project),
- inv.bill_no,
- inv.bill_date,
- inv.remarks,
- ", ".join(purchase_order),
- ", ".join(purchase_receipt),
- company_currency,
- ]
+ row = {
+ "voucher_type": inv.doctype,
+ "voucher_no": inv.name,
+ "posting_date": inv.posting_date,
+ "supplier_id": inv.supplier,
+ "supplier_name": inv.supplier_name,
+ **get_values_for_columns(additional_table_columns, inv),
+ "supplier_group": supplier_details.get(inv.supplier).get("supplier_group"),
+ "tax_id": supplier_details.get(inv.supplier).get("tax_id"),
+ "payable_account": inv.credit_to,
+ "mode_of_payment": inv.mode_of_payment,
+ "project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
+ "remarks": inv.remarks,
+ "purchase_order": ", ".join(purchase_order),
+ "purchase_receipt": ", ".join(purchase_receipt),
+ "currency": company_currency,
+ }
# map expense values
base_net_total = 0
@@ -75,14 +103,16 @@ def _execute(filters=None, additional_table_columns=None):
else:
expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc))
base_net_total += expense_amount
- row.append(expense_amount)
+ row.update({frappe.scrub(expense_acc): expense_amount})
# Add amount in unrealized account
for account in unrealized_profit_loss_accounts:
- row.append(flt(internal_invoice_map.get((inv.name, account))))
+ row.update(
+ {frappe.scrub(account + "_unrealized"): flt(internal_invoice_map.get((inv.name, account)))}
+ )
# net total
- row.append(base_net_total or inv.base_net_total)
+ row.update({"net_total": base_net_total or inv.base_net_total})
# tax account
total_tax = 0
@@ -90,45 +120,190 @@ def _execute(filters=None, additional_table_columns=None):
if tax_acc not in expense_accounts:
tax_amount = flt(invoice_tax_map.get(inv.name, {}).get(tax_acc))
total_tax += tax_amount
- row.append(tax_amount)
+ row.update({frappe.scrub(tax_acc): tax_amount})
# total tax, grand total, rounded total & outstanding amount
- row += [total_tax, inv.base_grand_total, flt(inv.base_grand_total, 0), inv.outstanding_amount]
+ row.update(
+ {
+ "total_tax": total_tax,
+ "grand_total": inv.base_grand_total,
+ "rounded_total": inv.base_rounded_total,
+ "outstanding_amount": inv.outstanding_amount,
+ }
+ )
+
+ if inv.doctype == "Purchase Invoice":
+ row.update({"debit": inv.base_grand_total, "credit": 0.0})
+ else:
+ row.update({"debit": 0.0, "credit": inv.base_grand_total})
data.append(row)
- return columns, data
+ res += sorted(data, key=lambda x: x["posting_date"])
+
+ if include_payments:
+ running_balance = flt(opening_row.balance)
+ for row in range(1, len(res)):
+ running_balance += res[row]["debit"] - res[row]["credit"]
+ res[row].update({"balance": running_balance})
+
+ return columns, res, None, None, None, include_payments
-def get_columns(invoice_list, additional_table_columns):
+def get_columns(invoice_list, additional_table_columns, include_payments=False):
"""return columns based on filters"""
columns = [
- _("Invoice") + ":Link/Purchase Invoice:120",
- _("Posting Date") + ":Date:80",
- _("Supplier Id") + "::120",
- _("Supplier Name") + "::120",
+ {
+ "label": _("Voucher Type"),
+ "fieldname": "voucher_type",
+ "width": 120,
+ },
+ {
+ "label": _("Voucher"),
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "options": "voucher_type",
+ "width": 120,
+ },
+ {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 80},
+ {
+ "label": _("Supplier"),
+ "fieldname": "supplier_id",
+ "fieldtype": "Link",
+ "options": "Supplier",
+ "width": 120,
+ },
+ {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 120},
]
- if additional_table_columns:
+ if additional_table_columns and not include_payments:
columns += additional_table_columns
- columns += [
- _("Supplier Group") + ":Link/Supplier Group:120",
- _("Tax Id") + "::80",
- _("Payable Account") + ":Link/Account:120",
- _("Mode of Payment") + ":Link/Mode of Payment:80",
- _("Project") + ":Link/Project:80",
- _("Bill No") + "::120",
- _("Bill Date") + ":Date:80",
- _("Remarks") + "::150",
- _("Purchase Order") + ":Link/Purchase Order:100",
- _("Purchase Receipt") + ":Link/Purchase Receipt:100",
- {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
- ]
+ if not include_payments:
+ columns += [
+ {
+ "label": _("Supplier Group"),
+ "fieldname": "supplier_group",
+ "fieldtype": "Link",
+ "options": "Supplier Group",
+ "width": 120,
+ },
+ {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 80},
+ {
+ "label": _("Payable Account"),
+ "fieldname": "payable_account",
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 100,
+ },
+ {
+ "label": _("Mode Of Payment"),
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Data",
+ "width": 120,
+ },
+ {
+ "label": _("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "width": 80,
+ },
+ {"label": _("Bill No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 120},
+ {"label": _("Bill Date"), "fieldname": "bill_date", "fieldtype": "Date", "width": 80},
+ {
+ "label": _("Purchase Order"),
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "options": "Purchase Order",
+ "width": 100,
+ },
+ {
+ "label": _("Purchase Receipt"),
+ "fieldname": "purchase_receipt",
+ "fieldtype": "Link",
+ "options": "Purchase Receipt",
+ "width": 100,
+ },
+ {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
+ ]
+ else:
+ columns += [
+ {
+ "fieldname": "payable_account",
+ "label": _("Payable Account"),
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 120,
+ },
+ {"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 120},
+ {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 120},
+ {"fieldname": "balance", "label": _("Balance"), "fieldtype": "Currency", "width": 120},
+ ]
+ account_columns, accounts = get_account_columns(invoice_list, include_payments)
+
+ columns = (
+ columns
+ + account_columns[0]
+ + account_columns[1]
+ + [
+ {
+ "label": _("Net Total"),
+ "fieldname": "net_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ ]
+ + account_columns[2]
+ + [
+ {
+ "label": _("Total Tax"),
+ "fieldname": "total_tax",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ ]
+ )
+
+ if not include_payments:
+ columns += [
+ {
+ "label": _("Grand Total"),
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Rounded Total"),
+ "fieldname": "rounded_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Outstanding Amount"),
+ "fieldname": "outstanding_amount",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ ]
+ columns += [{"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 120}]
+ return columns, accounts[0], accounts[2], accounts[1]
+
+
+def get_account_columns(invoice_list, include_payments):
expense_accounts = []
tax_accounts = []
unrealized_profit_loss_accounts = []
+ expense_columns = []
+ tax_columns = []
+ unrealized_profit_loss_account_columns = []
+
if invoice_list:
expense_accounts = frappe.db.sql_list(
"""select distinct expense_account
@@ -139,15 +314,18 @@ def get_columns(invoice_list, additional_table_columns):
tuple([inv.name for inv in invoice_list]),
)
- tax_accounts = frappe.db.sql_list(
- """select distinct account_head
- from `tabPurchase Taxes and Charges` where parenttype = 'Purchase Invoice'
- and docstatus = 1 and (account_head is not null and account_head != '')
- and category in ('Total', 'Valuation and Total')
- and parent in (%s) order by account_head"""
- % ", ".join(["%s"] * len(invoice_list)),
- tuple(inv.name for inv in invoice_list),
+ purchase_taxes_query = get_taxes_query(
+ invoice_list, "Purchase Taxes and Charges", "Purchase Invoice"
)
+ purchase_tax_accounts = purchase_taxes_query.run(as_dict=True, pluck="account_head")
+ tax_accounts = purchase_tax_accounts
+
+ if include_payments:
+ advance_taxes_query = get_taxes_query(
+ invoice_list, "Advance Taxes and Charges", "Payment Entry"
+ )
+ advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
+ tax_accounts = set(tax_accounts + advance_tax_accounts)
unrealized_profit_loss_accounts = frappe.db.sql_list(
"""SELECT distinct unrealized_profit_loss_account
@@ -158,107 +336,102 @@ def get_columns(invoice_list, additional_table_columns):
tuple(inv.name for inv in invoice_list),
)
- expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
- unrealized_profit_loss_account_columns = [
- (account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts
- ]
- tax_columns = [
- (account + ":Currency/currency:120")
- for account in tax_accounts
- if account not in expense_accounts
- ]
+ for account in expense_accounts:
+ expense_columns.append(
+ {
+ "label": account,
+ "fieldname": frappe.scrub(account),
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ )
- columns = (
- columns
- + expense_columns
- + unrealized_profit_loss_account_columns
- + [_("Net Total") + ":Currency/currency:120"]
- + tax_columns
- + [
- _("Total Tax") + ":Currency/currency:120",
- _("Grand Total") + ":Currency/currency:120",
- _("Rounded Total") + ":Currency/currency:120",
- _("Outstanding Amount") + ":Currency/currency:120",
- ]
- )
+ for account in tax_accounts:
+ if account not in expense_accounts:
+ tax_columns.append(
+ {
+ "label": account,
+ "fieldname": frappe.scrub(account),
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ )
- return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts
+ for account in unrealized_profit_loss_accounts:
+ unrealized_profit_loss_account_columns.append(
+ {
+ "label": account,
+ "fieldname": frappe.scrub(account),
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ )
+ columns = [expense_columns, unrealized_profit_loss_account_columns, tax_columns]
+ accounts = [expense_accounts, unrealized_profit_loss_accounts, tax_accounts]
-def get_conditions(filters):
- conditions = ""
-
- if filters.get("company"):
- conditions += " and company=%(company)s"
- if filters.get("supplier"):
- conditions += " and supplier = %(supplier)s"
-
- if filters.get("from_date"):
- conditions += " and posting_date>=%(from_date)s"
- if filters.get("to_date"):
- conditions += " and posting_date<=%(to_date)s"
-
- if filters.get("mode_of_payment"):
- conditions += " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"
-
- if filters.get("cost_center"):
- conditions += """ and exists(select name from `tabPurchase Invoice Item`
- where parent=`tabPurchase Invoice`.name
- and ifnull(`tabPurchase Invoice Item`.cost_center, '') = %(cost_center)s)"""
-
- if filters.get("warehouse"):
- conditions += """ and exists(select name from `tabPurchase Invoice Item`
- where parent=`tabPurchase Invoice`.name
- and ifnull(`tabPurchase Invoice Item`.warehouse, '') = %(warehouse)s)"""
-
- if filters.get("item_group"):
- conditions += """ and exists(select name from `tabPurchase Invoice Item`
- where parent=`tabPurchase Invoice`.name
- and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s)"""
-
- accounting_dimensions = get_accounting_dimensions(as_list=False)
-
- if accounting_dimensions:
- common_condition = """
- and exists(select name from `tabPurchase Invoice Item`
- where parent=`tabPurchase Invoice`.name
- """
- for dimension in accounting_dimensions:
- if filters.get(dimension.fieldname):
- if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
- filters[dimension.fieldname] = get_dimension_with_children(
- dimension.document_type, filters.get(dimension.fieldname)
- )
-
- conditions += (
- common_condition
- + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
- )
- else:
- conditions += (
- common_condition
- + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
- )
-
- return conditions
+ return columns, accounts
def get_invoices(filters, additional_query_columns):
- conditions = get_conditions(filters)
- return frappe.db.sql(
- """
- select
- name, posting_date, credit_to, supplier, supplier_name, tax_id, bill_no, bill_date,
- remarks, base_net_total, base_grand_total, outstanding_amount,
- mode_of_payment {0}
- from `tabPurchase Invoice`
- where docstatus = 1 {1}
- order by posting_date desc, name desc""".format(
- additional_query_columns, conditions
- ),
- filters,
- as_dict=1,
+ pi = frappe.qb.DocType("Purchase Invoice")
+ invoice_item = frappe.qb.DocType("Purchase Invoice Item")
+ query = (
+ frappe.qb.from_(pi)
+ .inner_join(invoice_item)
+ .on(pi.name == invoice_item.parent)
+ .select(
+ ConstantColumn("Purchase Invoice").as_("doctype"),
+ pi.name,
+ pi.posting_date,
+ pi.credit_to,
+ pi.supplier,
+ pi.supplier_name,
+ pi.tax_id,
+ pi.bill_no,
+ pi.bill_date,
+ pi.remarks,
+ pi.base_net_total,
+ pi.base_grand_total,
+ pi.outstanding_amount,
+ pi.mode_of_payment,
+ )
+ .where((pi.docstatus == 1))
+ .orderby(pi.posting_date, pi.name, order=Order.desc)
)
+ if additional_query_columns:
+ for col in additional_query_columns:
+ query = query.select(col)
+ if filters.get("supplier"):
+ query = query.where(pi.supplier == filters.supplier)
+ query = get_conditions(
+ filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item"
+ )
+ if filters.get("include_payments"):
+ party_account = get_party_account(
+ "Supplier", filters.get("supplier"), filters.get("company"), include_advance=True
+ )
+ query = query.where(pi.credit_to.isin(party_account))
+ invoices = query.run(as_dict=True)
+ return invoices
+
+
+def get_payments(filters):
+ args = frappe._dict(
+ account="credit_to",
+ account_fieldname="paid_to",
+ party="supplier",
+ party_name="supplier_name",
+ party_account=get_party_account(
+ "Supplier", filters.supplier, filters.company, include_advance=True
+ ),
+ )
+ payment_entries = get_payment_entries(filters, args)
+ journal_entries = get_journal_entries(filters, args)
+ return payment_entries + journal_entries
def get_invoice_expense_map(invoice_list):
@@ -300,7 +473,9 @@ def get_internal_invoice_map(invoice_list):
return internal_invoice_map
-def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
+def get_invoice_tax_map(
+ invoice_list, invoice_expense_map, expense_accounts, include_payments=False
+):
tax_details = frappe.db.sql(
"""
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)
@@ -315,6 +490,9 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
as_dict=1,
)
+ if include_payments:
+ tax_details += get_advance_taxes_and_charges(invoice_list)
+
invoice_tax_map = {}
for d in tax_details:
if d.account_head in expense_accounts:
@@ -382,17 +560,3 @@ def get_account_details(invoice_list):
account_map[acc.name] = acc.parent_account
return account_map
-
-
-def get_supplier_details(suppliers):
- supplier_details = {}
- for supp in frappe.db.sql(
- """select name, supplier_group from `tabSupplier`
- where name in (%s)"""
- % ", ".join(["%s"] * len(suppliers)),
- tuple(suppliers),
- as_dict=1,
- ):
- supplier_details.setdefault(supp.name, supp.supplier_group)
-
- return supplier_details
diff --git a/erpnext/accounts/report/purchase_register/test_purchase_register.py b/erpnext/accounts/report/purchase_register/test_purchase_register.py
new file mode 100644
index 0000000000..6903662e68
--- /dev/null
+++ b/erpnext/accounts/report/purchase_register/test_purchase_register.py
@@ -0,0 +1,128 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_months, getdate, today
+
+from erpnext.accounts.report.purchase_register.purchase_register import execute
+
+
+class TestPurchaseRegister(FrappeTestCase):
+ def test_purchase_register(self):
+ frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
+ frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
+
+ filters = frappe._dict(
+ company="_Test Company 6", from_date=add_months(today(), -1), to_date=today()
+ )
+
+ pi = make_purchase_invoice()
+
+ report_results = execute(filters)
+ first_row = frappe._dict(report_results[1][0])
+ self.assertEqual(first_row.voucher_type, "Purchase Invoice")
+ self.assertEqual(first_row.voucher_no, pi.name)
+ self.assertEqual(first_row.payable_account, "Creditors - _TC6")
+ self.assertEqual(first_row.net_total, 1000)
+ self.assertEqual(first_row.total_tax, 100)
+ self.assertEqual(first_row.grand_total, 1100)
+
+ def test_purchase_register_ledger_view(self):
+ frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
+ frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
+
+ filters = frappe._dict(
+ company="_Test Company 6",
+ from_date=add_months(today(), -1),
+ to_date=today(),
+ include_payments=True,
+ supplier="_Test Supplier",
+ )
+
+ pi = make_purchase_invoice()
+ pe = make_payment_entry()
+
+ report_results = execute(filters)
+ first_row = frappe._dict(report_results[1][2])
+ self.assertEqual(first_row.voucher_type, "Payment Entry")
+ self.assertEqual(first_row.voucher_no, pe.name)
+ self.assertEqual(first_row.payable_account, "Creditors - _TC6")
+ self.assertEqual(first_row.debit, 0)
+ self.assertEqual(first_row.credit, 600)
+ self.assertEqual(first_row.balance, 500)
+
+
+def make_purchase_invoice():
+ from erpnext.accounts.doctype.account.test_account import create_account
+ from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ gst_acc = create_account(
+ account_name="GST",
+ account_type="Tax",
+ parent_account="Duties and Taxes - _TC6",
+ company="_Test Company 6",
+ account_currency="INR",
+ )
+ create_warehouse(warehouse_name="_Test Warehouse - _TC6", company="_Test Company 6")
+ create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company 6")
+ pi = create_purchase_invoice_with_taxes()
+ pi.submit()
+ return pi
+
+
+def create_purchase_invoice_with_taxes():
+ return frappe.get_doc(
+ {
+ "doctype": "Purchase Invoice",
+ "posting_date": today(),
+ "supplier": "_Test Supplier",
+ "company": "_Test Company 6",
+ "cost_center": "_Test Cost Center - _TC6",
+ "taxes_and_charges": "",
+ "currency": "INR",
+ "credit_to": "Creditors - _TC6",
+ "items": [
+ {
+ "doctype": "Purchase Invoice Item",
+ "cost_center": "_Test Cost Center - _TC6",
+ "item_code": "_Test Item",
+ "qty": 1,
+ "rate": 1000,
+ "expense_account": "Stock Received But Not Billed - _TC6",
+ }
+ ],
+ "taxes": [
+ {
+ "account_head": "GST - _TC6",
+ "cost_center": "_Test Cost Center - _TC6",
+ "add_deduct_tax": "Add",
+ "category": "Valuation and Total",
+ "charge_type": "Actual",
+ "description": "Shipping Charges",
+ "doctype": "Purchase Taxes and Charges",
+ "parentfield": "taxes",
+ "rate": 100,
+ "tax_amount": 100.0,
+ }
+ ],
+ }
+ )
+
+
+def make_payment_entry():
+ frappe.set_user("Administrator")
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+
+ return create_payment_entry(
+ company="_Test Company 6",
+ party_type="Supplier",
+ party="_Test Supplier",
+ payment_type="Pay",
+ paid_from="Cash - _TC6",
+ paid_to="Creditors - _TC6",
+ paid_amount=600,
+ save=1,
+ submit=1,
+ )
diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js
index 44e20e83c5..92e6f7f514 100644
--- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js
+++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js
@@ -29,7 +29,6 @@ frappe.query_reports["Sales Payment Summary"] = {
"label": __("Owner"),
"fieldtype": "Link",
"options": "User",
- "defaults": user
},
{
"fieldname":"is_pos",
diff --git a/erpnext/accounts/report/sales_register/sales_register.js b/erpnext/accounts/report/sales_register/sales_register.js
index 2c9b01bbaa..1a41172a97 100644
--- a/erpnext/accounts/report/sales_register/sales_register.js
+++ b/erpnext/accounts/report/sales_register/sales_register.js
@@ -64,6 +64,12 @@ frappe.query_reports["Sales Register"] = {
"label": __("Item Group"),
"fieldtype": "Link",
"options": "Item Group"
+ },
+ {
+ "fieldname": "include_payments",
+ "label": __("Show Ledger View"),
+ "fieldtype": "Check",
+ "default": 0
}
]
}
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 291c7d976e..35d8d16479 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -5,13 +5,22 @@
import frappe
from frappe import _, msgprint
from frappe.model.meta import get_field_precision
-from frappe.utils import flt
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import flt, getdate
+from pypika import Order
-from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
- get_accounting_dimensions,
- get_dimension_with_children,
+from erpnext.accounts.party import get_party_account
+from erpnext.accounts.report.utils import (
+ get_advance_taxes_and_charges,
+ get_conditions,
+ get_journal_entries,
+ get_opening_row,
+ get_party_details,
+ get_payment_entries,
+ get_query_columns,
+ get_taxes_query,
+ get_values_for_columns,
)
-from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
def execute(filters=None):
@@ -22,9 +31,15 @@ def _execute(filters, additional_table_columns=None):
if not filters:
filters = frappe._dict({})
+ include_payments = filters.get("include_payments")
+ if filters.get("include_payments") and not filters.get("customer"):
+ frappe.throw(_("Please select a customer for fetching payments."))
invoice_list = get_invoices(filters, get_query_columns(additional_table_columns))
+ if filters.get("include_payments"):
+ invoice_list += get_payments(filters)
+
columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(
- invoice_list, additional_table_columns
+ invoice_list, additional_table_columns, include_payments
)
if not invoice_list:
@@ -34,13 +49,29 @@ def _execute(filters, additional_table_columns=None):
invoice_income_map = get_invoice_income_map(invoice_list)
internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_income_map, invoice_tax_map = get_invoice_tax_map(
- invoice_list, invoice_income_map, income_accounts
+ invoice_list, invoice_income_map, income_accounts, include_payments
)
# Cost Center & Warehouse Map
invoice_cc_wh_map = get_invoice_cc_wh_map(invoice_list)
invoice_so_dn_map = get_invoice_so_dn_map(invoice_list)
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
mode_of_payments = get_mode_of_payments([inv.name for inv in invoice_list])
+ customers = list(set(d.customer for d in invoice_list))
+ customer_details = get_party_details("Customer", customers)
+
+ res = []
+ if include_payments:
+ opening_row = get_opening_row(
+ "Customer", filters.customer, getdate(filters.from_date), filters.company
+ )[0]
+ res.append(
+ {
+ "receivable_account": opening_row.account,
+ "debit": flt(opening_row.debit),
+ "credit": flt(opening_row.credit),
+ "balance": flt(opening_row.balance),
+ }
+ )
data = []
for inv in invoice_list:
@@ -51,14 +82,15 @@ def _execute(filters, additional_table_columns=None):
warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", [])))
row = {
- "invoice": inv.name,
+ "voucher_type": inv.doctype,
+ "voucher_no": inv.name,
"posting_date": inv.posting_date,
"customer": inv.customer,
"customer_name": inv.customer_name,
**get_values_for_columns(additional_table_columns, inv),
- "customer_group": inv.get("customer_group"),
- "territory": inv.get("territory"),
- "tax_id": inv.get("tax_id"),
+ "customer_group": customer_details.get(inv.customer).get("customer_group"),
+ "territory": customer_details.get(inv.customer).get("territory"),
+ "tax_id": customer_details.get(inv.customer).get("tax_id"),
"receivable_account": inv.debit_to,
"mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])),
"project": inv.project,
@@ -116,19 +148,36 @@ def _execute(filters, additional_table_columns=None):
}
)
+ if inv.doctype == "Sales Invoice":
+ row.update({"debit": inv.base_grand_total, "credit": 0.0})
+ else:
+ row.update({"debit": 0.0, "credit": inv.base_grand_total})
data.append(row)
- return columns, data
+ res += sorted(data, key=lambda x: x["posting_date"])
+
+ if include_payments:
+ running_balance = flt(opening_row.balance)
+ for row in range(1, len(res)):
+ running_balance += res[row]["debit"] - res[row]["credit"]
+ res[row].update({"balance": running_balance})
+
+ return columns, res, None, None, None, include_payments
-def get_columns(invoice_list, additional_table_columns):
+def get_columns(invoice_list, additional_table_columns, include_payments=False):
"""return columns based on filters"""
columns = [
{
- "label": _("Invoice"),
- "fieldname": "invoice",
- "fieldtype": "Link",
- "options": "Sales Invoice",
+ "label": _("Voucher Type"),
+ "fieldname": "voucher_type",
+ "width": 120,
+ },
+ {
+ "label": _("Voucher"),
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "options": "voucher_type",
"width": 120,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 80},
@@ -142,83 +191,156 @@ def get_columns(invoice_list, additional_table_columns):
{"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120},
]
- if additional_table_columns:
+ if additional_table_columns and not include_payments:
columns += additional_table_columns
- columns += [
+ if not include_payments:
+ columns += [
+ {
+ "label": _("Customer Group"),
+ "fieldname": "customer_group",
+ "fieldtype": "Link",
+ "options": "Customer Group",
+ "width": 120,
+ },
+ {
+ "label": _("Territory"),
+ "fieldname": "territory",
+ "fieldtype": "Link",
+ "options": "Territory",
+ "width": 80,
+ },
+ {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 80},
+ {
+ "label": _("Receivable Account"),
+ "fieldname": "receivable_account",
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 100,
+ },
+ {
+ "label": _("Mode Of Payment"),
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Data",
+ "width": 120,
+ },
+ {
+ "label": _("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "width": 80,
+ },
+ {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 100},
+ {
+ "label": _("Sales Order"),
+ "fieldname": "sales_order",
+ "fieldtype": "Link",
+ "options": "Sales Order",
+ "width": 100,
+ },
+ {
+ "label": _("Delivery Note"),
+ "fieldname": "delivery_note",
+ "fieldtype": "Link",
+ "options": "Delivery Note",
+ "width": 100,
+ },
+ {
+ "label": _("Cost Center"),
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "options": "Cost Center",
+ "width": 100,
+ },
+ {
+ "label": _("Warehouse"),
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "width": 100,
+ },
+ {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
+ ]
+ else:
+ columns += [
+ {
+ "fieldname": "receivable_account",
+ "label": _("Receivable Account"),
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 120,
+ },
+ {"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 120},
+ {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 120},
+ {"fieldname": "balance", "label": _("Balance"), "fieldtype": "Currency", "width": 120},
+ ]
+
+ account_columns, accounts = get_account_columns(invoice_list, include_payments)
+
+ net_total_column = [
{
- "label": _("Customer Group"),
- "fieldname": "customer_group",
- "fieldtype": "Link",
- "options": "Customer Group",
+ "label": _("Net Total"),
+ "fieldname": "net_total",
+ "fieldtype": "Currency",
+ "options": "currency",
"width": 120,
- },
- {
- "label": _("Territory"),
- "fieldname": "territory",
- "fieldtype": "Link",
- "options": "Territory",
- "width": 80,
- },
- {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 120},
- {
- "label": _("Receivable Account"),
- "fieldname": "receivable_account",
- "fieldtype": "Link",
- "options": "Account",
- "width": 80,
- },
- {
- "label": _("Mode Of Payment"),
- "fieldname": "mode_of_payment",
- "fieldtype": "Data",
- "width": 120,
- },
- {
- "label": _("Project"),
- "fieldname": "project",
- "fieldtype": "Link",
- "options": "Project",
- "width": 80,
- },
- {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 150},
- {"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 150},
- {
- "label": _("Sales Order"),
- "fieldname": "sales_order",
- "fieldtype": "Link",
- "options": "Sales Order",
- "width": 100,
- },
- {
- "label": _("Delivery Note"),
- "fieldname": "delivery_note",
- "fieldtype": "Link",
- "options": "Delivery Note",
- "width": 100,
- },
- {
- "label": _("Cost Center"),
- "fieldname": "cost_center",
- "fieldtype": "Link",
- "options": "Cost Center",
- "width": 100,
- },
- {
- "label": _("Warehouse"),
- "fieldname": "warehouse",
- "fieldtype": "Link",
- "options": "Warehouse",
- "width": 100,
- },
- {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
+ }
]
+ total_columns = [
+ {
+ "label": _("Tax Total"),
+ "fieldname": "tax_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ }
+ ]
+ if not include_payments:
+ total_columns += [
+ {
+ "label": _("Grand Total"),
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Rounded Total"),
+ "fieldname": "rounded_total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Outstanding Amount"),
+ "fieldname": "outstanding_amount",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ ]
+
+ columns = (
+ columns
+ + account_columns[0]
+ + account_columns[2]
+ + net_total_column
+ + account_columns[1]
+ + total_columns
+ )
+ columns += [{"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 150}]
+ return columns, accounts[0], accounts[1], accounts[2]
+
+
+def get_account_columns(invoice_list, include_payments):
income_accounts = []
tax_accounts = []
+ unrealized_profit_loss_accounts = []
+
income_columns = []
tax_columns = []
- unrealized_profit_loss_accounts = []
unrealized_profit_loss_account_columns = []
if invoice_list:
@@ -230,14 +352,16 @@ def get_columns(invoice_list, additional_table_columns):
tuple(inv.name for inv in invoice_list),
)
- tax_accounts = frappe.db.sql_list(
- """select distinct account_head
- from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice'
- and docstatus = 1 and base_tax_amount_after_discount_amount != 0
- and parent in (%s) order by account_head"""
- % ", ".join(["%s"] * len(invoice_list)),
- tuple(inv.name for inv in invoice_list),
- )
+ sales_taxes_query = get_taxes_query(invoice_list, "Sales Taxes and Charges", "Sales Invoice")
+ sales_tax_accounts = sales_taxes_query.run(as_dict=True, pluck="account_head")
+ tax_accounts = sales_tax_accounts
+
+ if include_payments:
+ advance_taxes_query = get_taxes_query(
+ invoice_list, "Advance Taxes and Charges", "Payment Entry"
+ )
+ advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
+ tax_accounts = set(tax_accounts + advance_tax_accounts)
unrealized_profit_loss_accounts = frappe.db.sql_list(
"""SELECT distinct unrealized_profit_loss_account
@@ -283,133 +407,71 @@ def get_columns(invoice_list, additional_table_columns):
}
)
- net_total_column = [
- {
- "label": _("Net Total"),
- "fieldname": "net_total",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- }
- ]
+ columns = [income_columns, unrealized_profit_loss_account_columns, tax_columns]
+ accounts = [income_accounts, unrealized_profit_loss_accounts, tax_accounts]
- total_columns = [
- {
- "label": _("Tax Total"),
- "fieldname": "tax_total",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- },
- {
- "label": _("Grand Total"),
- "fieldname": "grand_total",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- },
- {
- "label": _("Rounded Total"),
- "fieldname": "rounded_total",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- },
- {
- "label": _("Outstanding Amount"),
- "fieldname": "outstanding_amount",
- "fieldtype": "Currency",
- "options": "currency",
- "width": 120,
- },
- ]
-
- columns = (
- columns
- + income_columns
- + unrealized_profit_loss_account_columns
- + net_total_column
- + tax_columns
- + total_columns
- )
-
- return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts
-
-
-def get_conditions(filters):
- conditions = ""
-
- accounting_dimensions = get_accounting_dimensions(as_list=False) or []
- accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
-
- if filters.get("company"):
- conditions += " and company=%(company)s"
-
- if filters.get("customer") and "customer" not in accounting_dimensions_list:
- conditions += " and customer = %(customer)s"
-
- if filters.get("from_date"):
- conditions += " and posting_date >= %(from_date)s"
- if filters.get("to_date"):
- conditions += " and posting_date <= %(to_date)s"
-
- if filters.get("owner"):
- conditions += " and owner = %(owner)s"
-
- def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
- if not filters.get(field) or field in accounting_dimensions_list:
- return ""
- return f""" and exists(select name from `tab{table}`
- where parent=`tabSales Invoice`.name
- and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
-
- conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment")
- conditions += get_sales_invoice_item_field_condition("cost_center")
- conditions += get_sales_invoice_item_field_condition("warehouse")
- conditions += get_sales_invoice_item_field_condition("brand")
- conditions += get_sales_invoice_item_field_condition("item_group")
-
- if accounting_dimensions:
- common_condition = """
- and exists(select name from `tabSales Invoice Item`
- where parent=`tabSales Invoice`.name
- """
- for dimension in accounting_dimensions:
- if filters.get(dimension.fieldname):
- if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
- filters[dimension.fieldname] = get_dimension_with_children(
- dimension.document_type, filters.get(dimension.fieldname)
- )
-
- conditions += (
- common_condition
- + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
- )
- else:
- conditions += (
- common_condition
- + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
- )
-
- return conditions
+ return columns, accounts
def get_invoices(filters, additional_query_columns):
- conditions = get_conditions(filters)
- return frappe.db.sql(
- """
- select name, posting_date, debit_to, project, customer,
- customer_name, owner, remarks, territory, tax_id, customer_group,
- base_net_total, base_grand_total, base_rounded_total, outstanding_amount,
- is_internal_customer, represents_company, company {0}
- from `tabSales Invoice`
- where docstatus = 1 {1}
- order by posting_date desc, name desc""".format(
- additional_query_columns, conditions
- ),
- filters,
- as_dict=1,
+ si = frappe.qb.DocType("Sales Invoice")
+ invoice_item = frappe.qb.DocType("Sales Invoice Item")
+ invoice_payment = frappe.qb.DocType("Sales Invoice Payment")
+ query = (
+ frappe.qb.from_(si)
+ .inner_join(invoice_item)
+ .on(si.name == invoice_item.parent)
+ .left_join(invoice_payment)
+ .on(si.name == invoice_payment.parent)
+ .select(
+ ConstantColumn("Sales Invoice").as_("doctype"),
+ si.name,
+ si.posting_date,
+ si.debit_to,
+ si.project,
+ si.customer,
+ si.customer_name,
+ si.owner,
+ si.remarks,
+ si.territory,
+ si.tax_id,
+ si.customer_group,
+ si.base_net_total,
+ si.base_grand_total,
+ si.base_rounded_total,
+ si.outstanding_amount,
+ si.is_internal_customer,
+ si.represents_company,
+ si.company,
+ )
+ .where((si.docstatus == 1))
+ .orderby(si.posting_date, si.name, order=Order.desc)
)
+ if additional_query_columns:
+ for col in additional_query_columns:
+ query = query.select(col)
+ if filters.get("customer"):
+ query = query.where(si.customer == filters.customer)
+ query = get_conditions(
+ filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
+ )
+ invoices = query.run(as_dict=True)
+ return invoices
+
+
+def get_payments(filters):
+ args = frappe._dict(
+ account="debit_to",
+ account_fieldname="paid_from",
+ party="customer",
+ party_name="customer_name",
+ party_account=get_party_account(
+ "Customer", filters.customer, filters.company, include_advance=True
+ ),
+ )
+ payment_entries = get_payment_entries(filters, args)
+ journal_entries = get_journal_entries(filters, args)
+ return payment_entries + journal_entries
def get_invoice_income_map(invoice_list):
@@ -447,7 +509,7 @@ def get_internal_invoice_map(invoice_list):
return internal_invoice_map
-def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
+def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, include_payments=False):
tax_details = frappe.db.sql(
"""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount
@@ -457,6 +519,9 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
as_dict=1,
)
+ if include_payments:
+ tax_details += get_advance_taxes_and_charges(invoice_list)
+
invoice_tax_map = {}
for d in tax_details:
if d.account_head in income_accounts:
@@ -475,7 +540,7 @@ def get_invoice_so_dn_map(invoice_list):
si_items = frappe.db.sql(
"""select parent, sales_order, delivery_note, so_detail
from `tabSales Invoice Item` where parent in (%s)
- and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')"""
+ and (sales_order != '' or delivery_note != '')"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
@@ -510,7 +575,7 @@ def get_invoice_cc_wh_map(invoice_list):
si_items = frappe.db.sql(
"""select parent, cost_center, warehouse
from `tabSales Invoice Item` where parent in (%s)
- and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')"""
+ and (cost_center != '' or warehouse != '')"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
diff --git a/erpnext/accounts/report/share_balance/share_balance.js b/erpnext/accounts/report/share_balance/share_balance.js
index 6db5bdd299..ac64a0bfb9 100644
--- a/erpnext/accounts/report/share_balance/share_balance.js
+++ b/erpnext/accounts/report/share_balance/share_balance.js
@@ -1,7 +1,7 @@
// -*- coding: utf-8 -*-
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Share Balance"] = {
"filters": [
diff --git a/erpnext/accounts/report/share_ledger/share_ledger.js b/erpnext/accounts/report/share_ledger/share_ledger.js
index 6d1c44a6d0..4f2d2cc78f 100644
--- a/erpnext/accounts/report/share_ledger/share_ledger.js
+++ b/erpnext/accounts/report/share_ledger/share_ledger.js
@@ -1,7 +1,7 @@
// -*- coding: utf-8 -*-
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Share Ledger"] = {
"filters": [
diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
index 5dc4c3d1c1..8e3c8ac630 100644
--- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
+++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Supplier Ledger Summary"] = {
"filters": [
diff --git a/erpnext/accounts/report/tax_withholding_details/__init__.py b/erpnext/accounts/report/tax_withholding_details/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.js b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.js
new file mode 100644
index 0000000000..8808165919
--- /dev/null
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.js
@@ -0,0 +1,62 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+
+frappe.query_reports["Tax Withholding Details"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "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;
+ },
+ "get_query": function() {
+ return {
+ "filters": {
+ "tax_withholding_category": ["!=",""],
+ }
+ }
+ },
+ },
+ {
+ "fieldname":"from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ "reqd": 1,
+ "width": "60px"
+ },
+ {
+ "fieldname":"to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "default": frappe.datetime.get_today(),
+ "reqd": 1,
+ "width": "60px"
+ }
+ ]
+}
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.json b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.json
similarity index 88%
rename from erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.json
rename to erpnext/accounts/report/tax_withholding_details/tax_withholding_details.json
index 4d555bd8ba..fb204b319d 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.json
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.json
@@ -12,11 +12,11 @@
"modified": "2021-09-20 12:05:50.387572",
"modified_by": "Administrator",
"module": "Accounts",
- "name": "TDS Payable Monthly",
+ "name": "Tax Withholding Details",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Invoice",
- "report_name": "TDS Payable Monthly",
+ "report_name": "Tax Withholding Details",
"report_type": "Script Report",
"roles": [
{
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
similarity index 56%
rename from erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
rename to erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index 98838907be..7d16661472 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -7,19 +7,26 @@ 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,
- invoice_net_total_map,
+ 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, invoice_net_total_map
+ filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
)
return columns, res
@@ -31,79 +38,96 @@ def validate_filters(filters):
def get_result(
- filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
+ filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
):
- supplier_map = get_supplier_pan_map()
+ party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
out = []
for name, details in gle_map.items():
- tds_deducted, total_amount_credited = 0, 0
+ tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
for entry in details:
- supplier = entry.party or entry.against
+ party = entry.party or entry.against
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if voucher_type == "Journal Entry":
- suppliers = journal_entry_party_map.get(name)
- if suppliers:
- supplier = suppliers[0]
+ party_list = journal_entry_party_map.get(name)
+ if party_list:
+ party = party_list[0]
if not tax_withholding_category:
- tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
+ tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category)
if entry.account in tds_accounts:
- tds_deducted += entry.credit - entry.debit
+ tax_amount += entry.credit - entry.debit
- if invoice_net_total_map.get(name):
- total_amount_credited = invoice_net_total_map.get(name)
+ if net_total_map.get(name):
+ total_amount, grand_total, base_total = net_total_map.get(name)
else:
- total_amount_credited += entry.credit
+ total_amount += entry.credit
+
+ if tax_amount:
+ if party_map.get(party, {}).get("party_type") == "Supplier":
+ party_name = "supplier_name"
+ party_type = "supplier_type"
+ else:
+ party_name = "customer_name"
+ party_type = "customer_type"
- if tds_deducted:
row = {
"pan"
- if frappe.db.has_column("Supplier", "pan")
- else "tax_id": supplier_map.get(supplier, {}).get("pan"),
- "supplier": supplier_map.get(supplier, {}).get("name"),
+ if frappe.db.has_column(filters.party_type, "pan")
+ else "tax_id": party_map.get(party, {}).get("pan"),
+ "party": party_map.get(party, {}).get("name"),
}
if filters.naming_series == "Naming Series":
- row.update({"supplier_name": supplier_map.get(supplier, {}).get("supplier_name")})
+ row.update({"party_name": party_map.get(party, {}).get(party_name)})
row.update(
{
"section_code": tax_withholding_category,
- "entity_type": supplier_map.get(supplier, {}).get("supplier_type"),
- "tds_rate": rate,
- "total_amount_credited": total_amount_credited,
- "tds_deducted": tds_deducted,
+ "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,
"ref_no": name,
}
)
-
out.append(row)
return out
-def get_supplier_pan_map():
- supplier_map = frappe._dict()
- suppliers = frappe.db.get_all(
- "Supplier", fields=["name", "pan", "supplier_type", "supplier_name", "tax_withholding_category"]
- )
+def get_party_pan_map(party_type):
+ party_map = frappe._dict()
- for d in suppliers:
- supplier_map[d.name] = d
+ fields = ["name", "tax_withholding_category"]
+ if party_type == "Supplier":
+ fields += ["supplier_type", "supplier_name"]
+ else:
+ fields += ["customer_type", "customer_name"]
- return supplier_map
+ if frappe.db.has_column(party_type, "pan"):
+ fields.append("pan")
+
+ party_details = frappe.db.get_all(party_type, fields=fields)
+
+ for party in party_details:
+ party.party_type = party_type
+ party_map[party.name] = party
+
+ return party_map
def get_gle_map(documents):
@@ -127,59 +151,81 @@ def get_gle_map(documents):
def get_columns(filters):
- pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
+ 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": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
{
- "label": _("Supplier"),
- "options": "Supplier",
- "fieldname": "supplier",
- "fieldtype": "Link",
+ "label": _(filters.get("party_type")),
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "options": "party_type",
"width": 180,
},
]
if filters.naming_series == "Naming Series":
columns.append(
- {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180}
+ {
+ "label": _(filters.party_type + " 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": 180,
+ "width": 90,
},
- {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
- {"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90},
+ {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
{
- "label": _("Total Amount Credited"),
- "fieldname": "total_amount_credited",
+ "label": _("Total Amount"),
+ "fieldname": "total_amount",
"fieldtype": "Float",
"width": 90,
},
{
- "label": _("Amount of TDS Deducted"),
- "fieldname": "tds_deducted",
+ "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
+ "fieldname": "rate",
+ "fieldtype": "Percent",
+ "width": 90,
+ },
+ {
+ "label": _("Tax Amount"),
+ "fieldname": "tax_amount",
"fieldtype": "Float",
"width": 90,
},
{
- "label": _("Date of Transaction"),
- "fieldname": "transaction_date",
- "fieldtype": "Date",
+ "label": _("Grand Total"),
+ "fieldname": "grand_total",
+ "fieldtype": "Float",
"width": 90,
},
- {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 90},
+ {
+ "label": _("Base Total"),
+ "fieldname": "base_total",
+ "fieldtype": "Float",
+ "width": 90,
+ },
+ {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
{
"label": _("Reference No."),
"fieldname": "ref_no",
"fieldtype": "Dynamic Link",
"options": "transaction_type",
- "width": 90,
+ "width": 180,
},
]
)
@@ -190,10 +236,11 @@ def get_columns(filters):
def get_tds_docs(filters):
tds_documents = []
purchase_invoices = []
+ sales_invoices = []
payment_entries = []
journal_entries = []
tax_category_map = frappe._dict()
- invoice_net_total_map = frappe._dict()
+ 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")
@@ -209,10 +256,13 @@ def get_tds_docs(filters):
"against": ("not in", bank_accounts),
}
- if filters.get("supplier"):
+ party = frappe.get_all(filters.get("party_type"), pluck="name")
+ query_filters.update({"against": ("in", party)})
+
+ if filters.get("party"):
del query_filters["account"]
del query_filters["against"]
- or_filters = {"against": filters.get("supplier"), "party": filters.get("supplier")}
+ or_filters = {"against": filters.get("party"), "party": filters.get("party")}
tds_docs = frappe.get_all(
"GL Entry",
@@ -224,6 +274,8 @@ def get_tds_docs(filters):
for d in tds_docs:
if d.voucher_type == "Purchase Invoice":
purchase_invoices.append(d.voucher_no)
+ if d.voucher_type == "Sales Invoice":
+ sales_invoices.append(d.voucher_no)
elif d.voucher_type == "Payment Entry":
payment_entries.append(d.voucher_no)
elif d.voucher_type == "Journal Entry":
@@ -232,10 +284,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, invoice_net_total_map)
+ get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map)
+
+ if sales_invoices:
+ get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map)
if payment_entries:
- get_doc_info(payment_entries, "Payment Entry", tax_category_map)
+ get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map)
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
@@ -246,7 +301,7 @@ def get_tds_docs(filters):
tds_accounts,
tax_category_map,
journal_entry_party_map,
- invoice_net_total_map,
+ net_total_map,
)
@@ -264,9 +319,25 @@ def get_journal_entry_party_map(journal_entries):
return journal_entry_party_map
-def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None):
+def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
if doctype == "Purchase Invoice":
- fields = ["name", "tax_withholding_category", "base_tax_withholding_net_total"]
+ 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",
+ ]
else:
fields = ["name", "tax_withholding_category"]
@@ -275,7 +346,15 @@ def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None
for entry in entries:
tax_category_map.update({entry.name: entry.tax_withholding_category})
if doctype == "Purchase Invoice":
- invoice_net_total_map.update({entry.name: entry.base_tax_withholding_net_total})
+ 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]}
+ )
def get_tax_rate_map(filters):
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js
index d3d45b353a..a0be1b5abd 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["TDS Computation Summary"] = {
"filters": [
@@ -12,17 +12,35 @@ frappe.query_reports["TDS Computation Summary"] = {
"default": frappe.defaults.get_default('company')
},
{
- "fieldname":"supplier",
- "label": __("Supplier"),
- "fieldtype": "Link",
- "options": "Supplier",
+ "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;
+ },
"get_query": function() {
return {
"filters": {
"tax_withholding_category": ["!=",""],
}
}
- }
+ },
},
{
"fieldname":"from_date",
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index c6aa21cc86..82f97f1894 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -9,9 +9,14 @@ from erpnext.accounts.utils import get_fiscal_year
def execute(filters=None):
- validate_filters(filters)
+ 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.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name")
+ filters.update({"naming_series": party_naming_by})
+
+ validate_filters(filters)
columns = get_columns(filters)
(
@@ -25,7 +30,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_supplier_and_category(res)
+ final_result = group_by_party_and_category(res, filters)
return columns, final_result
@@ -43,60 +48,67 @@ def validate_filters(filters):
filters["fiscal_year"] = from_year
-def group_by_supplier_and_category(data):
- supplier_category_wise_map = {}
+def group_by_party_and_category(data, filters):
+ party_category_wise_map = {}
for row in data:
- supplier_category_wise_map.setdefault(
- (row.get("supplier"), row.get("section_code")),
+ party_category_wise_map.setdefault(
+ (row.get("party"), row.get("section_code")),
{
"pan": row.get("pan"),
- "supplier": row.get("supplier"),
- "supplier_name": row.get("supplier_name"),
+ "tax_id": row.get("tax_id"),
+ "party": row.get("party"),
+ "party_name": row.get("party_name"),
"section_code": row.get("section_code"),
"entity_type": row.get("entity_type"),
- "tds_rate": row.get("tds_rate"),
- "total_amount_credited": 0.0,
- "tds_deducted": 0.0,
+ "rate": row.get("rate"),
+ "total_amount": 0.0,
+ "tax_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")))[
+ "total_amount"
+ ] += row.get("total_amount", 0.0)
- supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[
- "tds_deducted"
- ] += row.get("tds_deducted", 0.0)
+ party_category_wise_map.get((row.get("party"), row.get("section_code")))[
+ "tax_amount"
+ ] += row.get("tax_amount", 0.0)
- final_result = get_final_result(supplier_category_wise_map)
+ final_result = get_final_result(party_category_wise_map)
return final_result
-def get_final_result(supplier_category_wise_map):
+def get_final_result(party_category_wise_map):
out = []
- for key, value in supplier_category_wise_map.items():
+ for key, value in party_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": _("PAN"), "fieldname": "pan", "fieldtype": "Data", "width": 90},
+ {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
{
- "label": _("Supplier"),
- "options": "Supplier",
- "fieldname": "supplier",
- "fieldtype": "Link",
+ "label": _(filters.get("party_type")),
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "options": "party_type",
"width": 180,
},
]
if filters.naming_series == "Naming Series":
columns.append(
- {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180}
+ {
+ "label": _(filters.party_type + " Name"),
+ "fieldname": "party_name",
+ "fieldtype": "Data",
+ "width": 180,
+ }
)
columns.extend(
@@ -109,18 +121,23 @@ 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": _("Total Amount Credited"),
- "fieldname": "total_amount_credited",
- "fieldtype": "Float",
- "width": 90,
+ "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
+ "fieldname": "rate",
+ "fieldtype": "Percent",
+ "width": 120,
},
{
- "label": _("Amount of TDS Deducted"),
- "fieldname": "tds_deducted",
+ "label": _("Total Amount"),
+ "fieldname": "total_amount",
"fieldtype": "Float",
- "width": 90,
+ "width": 120,
+ },
+ {
+ "label": _("Tax Amount"),
+ "fieldname": "tax_amount",
+ "fieldtype": "Float",
+ "width": 120,
},
]
)
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
deleted file mode 100644
index ff2aa30601..0000000000
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-/* eslint-disable */
-
-frappe.query_reports["TDS Payable Monthly"] = {
- "filters": [
- {
- "fieldname":"company",
- "label": __("Company"),
- "fieldtype": "Link",
- "options": "Company",
- "default": frappe.defaults.get_default('company')
- },
- {
- "fieldname":"supplier",
- "label": __("Supplier"),
- "fieldtype": "Link",
- "options": "Supplier",
- },
- {
- "fieldname":"from_date",
- "label": __("From Date"),
- "fieldtype": "Date",
- "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
- "reqd": 1,
- "width": "60px"
- },
- {
- "fieldname":"to_date",
- "label": __("To Date"),
- "fieldtype": "Date",
- "default": frappe.datetime.get_today(),
- "reqd": 1,
- "width": "60px"
- }
- ]
-}
diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py
new file mode 100644
index 0000000000..4682ac4500
--- /dev/null
+++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py
@@ -0,0 +1,118 @@
+# Copyright (c) 2022, 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.trial_balance.trial_balance import execute
+
+
+class TestTrialBalance(FrappeTestCase):
+ def setUp(self):
+ from erpnext.accounts.doctype.account.test_account import create_account
+ from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+ from erpnext.accounts.utils import get_fiscal_year
+
+ self.company = create_company()
+ create_cost_center(
+ cost_center_name="Test Cost Center",
+ company="Trial Balance Company",
+ parent_cost_center="Trial Balance Company - TBC",
+ )
+ create_account(
+ account_name="Offsetting",
+ company="Trial Balance Company",
+ parent_account="Temporary Accounts - TBC",
+ )
+ self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0]
+ create_accounting_dimension()
+
+ def test_offsetting_entries_for_accounting_dimensions(self):
+ """
+ Checks if Trial Balance Report is balanced when filtered using a particular Accounting Dimension
+ """
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'")
+ frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'")
+
+ branch1 = frappe.new_doc("Branch")
+ branch1.branch = "Location 1"
+ branch1.insert(ignore_if_duplicate=True)
+ branch2 = frappe.new_doc("Branch")
+ branch2.branch = "Location 2"
+ branch2.insert(ignore_if_duplicate=True)
+
+ si = create_sales_invoice(
+ company=self.company,
+ debit_to="Debtors - TBC",
+ cost_center="Test Cost Center - TBC",
+ income_account="Sales - TBC",
+ do_not_submit=1,
+ )
+ si.branch = "Location 1"
+ si.items[0].branch = "Location 2"
+ si.save()
+ si.submit()
+
+ filters = frappe._dict(
+ {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]}
+ )
+ total_row = execute(filters)[1][-1]
+ self.assertEqual(total_row["debit"], total_row["credit"])
+
+ def tearDown(self):
+ clear_dimension_defaults("Branch")
+ disable_dimension()
+
+
+def create_company(**args):
+ args = frappe._dict(args)
+ company = frappe.get_doc(
+ {
+ "doctype": "Company",
+ "company_name": args.company_name or "Trial Balance Company",
+ "country": args.country or "India",
+ "default_currency": args.currency or "INR",
+ }
+ )
+ company.insert(ignore_if_duplicate=True)
+ return company.name
+
+
+def create_accounting_dimension(**args):
+ args = frappe._dict(args)
+ document_type = args.document_type or "Branch"
+ if frappe.db.exists("Accounting Dimension", document_type):
+ accounting_dimension = frappe.get_doc("Accounting Dimension", document_type)
+ accounting_dimension.disabled = 0
+ else:
+ accounting_dimension = frappe.new_doc("Accounting Dimension")
+ accounting_dimension.document_type = document_type
+ accounting_dimension.insert()
+
+ accounting_dimension.set("dimension_defaults", [])
+ accounting_dimension.append(
+ "dimension_defaults",
+ {
+ "company": args.company or "Trial Balance Company",
+ "automatically_post_balancing_accounting_entry": 1,
+ "offsetting_account": args.offsetting_account or "Offsetting - TBC",
+ },
+ )
+ accounting_dimension.save()
+
+
+def disable_dimension(**args):
+ args = frappe._dict(args)
+ document_type = args.document_type or "Branch"
+ dimension = frappe.get_doc("Accounting Dimension", document_type)
+ dimension.disabled = 1
+ dimension.save()
+
+
+def clear_dimension_defaults(dimension_name):
+ accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name)
+ accounting_dimension.dimension_defaults = []
+ accounting_dimension.save()
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js
index e45c3adcb6..ee6b4fef21 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.js
+++ b/erpnext/accounts/report/trial_balance/trial_balance.js
@@ -37,13 +37,13 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
},
{
"fieldname": "cost_center",
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 39917f90c9..376571f034 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -142,14 +142,20 @@ def get_opening_balances(filters):
def get_rootwise_opening_balances(filters, report_type):
gle = []
- last_period_closing_voucher = frappe.db.get_all(
- "Period Closing Voucher",
- filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
- fields=["posting_date", "name"],
- order_by="posting_date desc",
- limit=1,
+ last_period_closing_voucher = ""
+ ignore_closing_balances = frappe.db.get_single_value(
+ "Accounts Settings", "ignore_account_closing_balance"
)
+ if not ignore_closing_balances:
+ last_period_closing_voucher = frappe.db.get_all(
+ "Period Closing Voucher",
+ filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
+ fields=["posting_date", "name"],
+ order_by="posting_date desc",
+ limit=1,
+ )
+
accounting_dimensions = get_accounting_dimensions(as_list=False)
if last_period_closing_voucher:
@@ -231,6 +237,9 @@ def get_opening_balance(
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
)
+ if doctype == "GL Entry":
+ opening_balance = opening_balance.where(closing_balance.is_cancelled == 0)
+
if (
not filters.show_unclosed_fy_pl_balances
and report_type == "Profit and Loss"
@@ -250,7 +259,7 @@ def get_opening_balance(
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
cost_center = frappe.qb.DocType("Cost Center")
opening_balance = opening_balance.where(
- closing_balance.cost_center.in_(
+ closing_balance.cost_center.isin(
frappe.qb.from_(cost_center)
.select("name")
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js
index 0f7578cdc1..33c644adcb 100644
--- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js
+++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js
@@ -36,13 +36,13 @@ frappe.query_reports["Trial Balance for Party"] = {
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
},
{
"fieldname":"party_type",
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index 7ea1fac105..0753fff834 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -1,8 +1,16 @@
import frappe
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Sum
from frappe.utils import flt, formatdate, get_datetime_str, get_table_name
+from pypika import Order
from erpnext import get_company_currency, get_default_company
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+ get_accounting_dimensions,
+ get_dimension_with_children,
+)
from erpnext.accounts.doctype.fiscal_year.fiscal_year import get_from_and_to_date
+from erpnext.accounts.party import get_party_account
from erpnext.setup.utils import get_exchange_rate
__exchange_rates = {}
@@ -165,7 +173,7 @@ def get_query_columns(report_columns):
else:
columns.append(fieldname)
- return ", " + ", ".join(columns)
+ return columns
def get_values_for_columns(report_columns, report_row):
@@ -179,3 +187,200 @@ def get_values_for_columns(report_columns, report_row):
values[fieldname] = report_row.get(fieldname)
return values
+
+
+def get_party_details(party_type, party_list):
+ party_details = {}
+ party = frappe.qb.DocType(party_type)
+ query = frappe.qb.from_(party).select(party.name, party.tax_id).where(party.name.isin(party_list))
+ if party_type == "Supplier":
+ query = query.select(party.supplier_group)
+ else:
+ query = query.select(party.customer_group, party.territory)
+
+ party_detail_list = query.run(as_dict=True)
+ for party_dict in party_detail_list:
+ party_details[party_dict.name] = party_dict
+ return party_details
+
+
+def get_taxes_query(invoice_list, doctype, parenttype):
+ taxes = frappe.qb.DocType(doctype)
+
+ query = (
+ frappe.qb.from_(taxes)
+ .select(taxes.account_head)
+ .distinct()
+ .where(
+ (taxes.parenttype == parenttype)
+ & (taxes.docstatus == 1)
+ & (taxes.account_head.isnotnull())
+ & (taxes.parent.isin([inv.name for inv in invoice_list]))
+ )
+ .orderby(taxes.account_head)
+ )
+
+ if doctype == "Purchase Taxes and Charges":
+ return query.where(taxes.category.isin(["Total", "Valuation and Total"]))
+ elif doctype == "Sales Taxes and Charges":
+ return query
+ return query.where(taxes.charge_type.isin(["On Paid Amount", "Actual"]))
+
+
+def get_journal_entries(filters, args):
+ je = frappe.qb.DocType("Journal Entry")
+ journal_account = frappe.qb.DocType("Journal Entry Account")
+ query = (
+ frappe.qb.from_(je)
+ .inner_join(journal_account)
+ .on(je.name == journal_account.parent)
+ .select(
+ je.voucher_type.as_("doctype"),
+ je.name,
+ je.posting_date,
+ journal_account.account.as_(args.account),
+ journal_account.party.as_(args.party),
+ journal_account.party.as_(args.party_name),
+ je.bill_no,
+ je.bill_date,
+ je.remark.as_("remarks"),
+ je.total_amount.as_("base_net_total"),
+ je.total_amount.as_("base_grand_total"),
+ je.mode_of_payment,
+ journal_account.project,
+ )
+ .where(
+ (je.voucher_type == "Journal Entry")
+ & (journal_account.party == filters.get(args.party))
+ & (journal_account.account.isin(args.party_account))
+ )
+ .orderby(je.posting_date, je.name, order=Order.desc)
+ )
+ query = get_conditions(filters, query, doctype="Journal Entry", payments=True)
+ journal_entries = query.run(as_dict=True)
+ return journal_entries
+
+
+def get_payment_entries(filters, args):
+ pe = frappe.qb.DocType("Payment Entry")
+ query = (
+ frappe.qb.from_(pe)
+ .select(
+ ConstantColumn("Payment Entry").as_("doctype"),
+ pe.name,
+ pe.posting_date,
+ pe[args.account_fieldname].as_(args.account),
+ pe.party.as_(args.party),
+ pe.party_name.as_(args.party_name),
+ pe.remarks,
+ pe.paid_amount.as_("base_net_total"),
+ pe.paid_amount_after_tax.as_("base_grand_total"),
+ pe.mode_of_payment,
+ pe.project,
+ pe.cost_center,
+ )
+ .where(
+ (pe.party == filters.get(args.party)) & (pe[args.account_fieldname].isin(args.party_account))
+ )
+ .orderby(pe.posting_date, pe.name, order=Order.desc)
+ )
+ query = get_conditions(filters, query, doctype="Payment Entry", payments=True)
+ payment_entries = query.run(as_dict=True)
+ return payment_entries
+
+
+def get_conditions(filters, query, doctype, child_doctype=None, payments=False):
+ parent_doc = frappe.qb.DocType(doctype)
+ if child_doctype:
+ child_doc = frappe.qb.DocType(child_doctype)
+
+ if parent_doc.get_table_name() == "tabSales Invoice":
+ if filters.get("owner"):
+ query = query.where(parent_doc.owner == filters.owner)
+ if filters.get("mode_of_payment"):
+ payment_doc = frappe.qb.DocType("Sales Invoice Payment")
+ query = query.where(payment_doc.mode_of_payment == filters.mode_of_payment)
+ if not payments:
+ if filters.get("brand"):
+ query = query.where(child_doc.brand == filters.brand)
+ else:
+ if filters.get("mode_of_payment"):
+ query = query.where(parent_doc.mode_of_payment == filters.mode_of_payment)
+
+ if filters.get("company"):
+ query = query.where(parent_doc.company == filters.company)
+ if filters.get("from_date"):
+ query = query.where(parent_doc.posting_date >= filters.from_date)
+ if filters.get("to_date"):
+ query = query.where(parent_doc.posting_date <= filters.to_date)
+
+ if payments:
+ if filters.get("cost_center"):
+ query = query.where(parent_doc.cost_center == filters.cost_center)
+ else:
+ if filters.get("cost_center"):
+ query = query.where(child_doc.cost_center == filters.cost_center)
+ if filters.get("warehouse"):
+ query = query.where(child_doc.warehouse == filters.warehouse)
+ if filters.get("item_group"):
+ query = query.where(child_doc.item_group == filters.item_group)
+
+ if parent_doc.get_table_name() != "tabJournal Entry":
+ query = filter_invoices_based_on_dimensions(filters, query, parent_doc)
+ return query
+
+
+def get_advance_taxes_and_charges(invoice_list):
+ adv_taxes = frappe.qb.DocType("Advance Taxes and Charges")
+ return (
+ frappe.qb.from_(adv_taxes)
+ .select(
+ adv_taxes.parent,
+ adv_taxes.account_head,
+ (
+ frappe.qb.terms.Case()
+ .when(adv_taxes.add_deduct_tax == "Add", Sum(adv_taxes.base_tax_amount))
+ .else_(Sum(adv_taxes.base_tax_amount) * -1)
+ ).as_("tax_amount"),
+ )
+ .where(
+ (adv_taxes.parent.isin([inv.name for inv in invoice_list]))
+ & (adv_taxes.charge_type.isin(["On Paid Amount", "Actual"]))
+ & (adv_taxes.base_tax_amount != 0)
+ )
+ .groupby(adv_taxes.parent, adv_taxes.account_head, adv_taxes.add_deduct_tax)
+ ).run(as_dict=True)
+
+
+def filter_invoices_based_on_dimensions(filters, query, parent_doc):
+ accounting_dimensions = get_accounting_dimensions(as_list=False)
+ if accounting_dimensions:
+ for dimension in accounting_dimensions:
+ if filters.get(dimension.fieldname):
+ if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
+ filters[dimension.fieldname] = get_dimension_with_children(
+ dimension.document_type, filters.get(dimension.fieldname)
+ )
+ fieldname = dimension.fieldname
+ query = query.where(parent_doc[fieldname] == filters.fieldname)
+ return query
+
+
+def get_opening_row(party_type, party, from_date, company):
+ party_account = get_party_account(party_type, party, company, include_advance=True)
+ gle = frappe.qb.DocType("GL Entry")
+ return (
+ frappe.qb.from_(gle)
+ .select(
+ ConstantColumn("Opening").as_("account"),
+ Sum(gle.debit).as_("debit"),
+ Sum(gle.credit).as_("credit"),
+ (Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
+ )
+ .where(
+ (gle.account.isin(party_account))
+ & (gle.party == party)
+ & (gle.posting_date < from_date)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)
diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js
index 0c148f85fb..f7ab029f19 100644
--- a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js
+++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js
@@ -1,6 +1,6 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Voucher-wise Balance"] = {
"filters": [
diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py
index 5ab3611b9a..bd9e9fccad 100644
--- a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py
+++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py
@@ -46,6 +46,7 @@ def get_data(filters):
.select(
gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")
)
+ .where(gle.is_cancelled == 0)
.groupby(gle.voucher_no)
)
query = apply_filters(query, filters, gle)
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
new file mode 100644
index 0000000000..70bbf7e694
--- /dev/null
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -0,0 +1,105 @@
+import frappe
+
+from erpnext.stock.doctype.item.test_item import create_item
+
+
+class AccountsTestMixin:
+ def create_customer(self, customer_name="_Test Customer", currency=None):
+ if not frappe.db.exists("Customer", customer_name):
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = customer_name
+ customer.type = "Individual"
+
+ if currency:
+ customer.default_currency = currency
+ customer.save()
+ self.customer = customer.name
+ else:
+ self.customer = customer_name
+
+ def create_supplier(self, supplier_name="_Test Supplier", currency=None):
+ if not frappe.db.exists("Supplier", supplier_name):
+ supplier = frappe.new_doc("Supplier")
+ supplier.supplier_name = supplier_name
+ supplier.supplier_type = "Individual"
+ supplier.supplier_group = "Local"
+
+ if currency:
+ supplier.default_currency = currency
+ supplier.save()
+ self.supplier = supplier.name
+ else:
+ self.supplier = supplier_name
+
+ def create_item(self, item_name="_Test Item", 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
+
+ def create_company(self, company_name="_Test Company", abbr="_TC"):
+ self.company_abbr = abbr
+ if frappe.db.exists("Company", company_name):
+ company = frappe.get_doc("Company", company_name)
+ else:
+ company = frappe.get_doc(
+ {
+ "doctype": "Company",
+ "company_name": company_name,
+ "country": "India",
+ "default_currency": "INR",
+ "create_chart_of_accounts_based_on": "Standard Template",
+ "chart_of_accounts": "Standard",
+ }
+ )
+ company = company.save()
+
+ self.company = company.name
+ self.cost_center = company.cost_center
+ self.warehouse = "Stores - " + abbr
+ self.finished_warehouse = "Finished Goods - " + abbr
+ self.income_account = "Sales - " + abbr
+ self.expense_account = "Cost of Goods Sold - " + abbr
+ self.debit_to = "Debtors - " + abbr
+ 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(
+ {
+ "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",
+ "account_name": "HDFC",
+ "parent_account": "Bank Accounts - " + abbr,
+ }
+ ),
+ ]
+ 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)
diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py
index 3aca60eae5..3cb5e42e7a 100644
--- a/erpnext/accounts/test/test_utils.py
+++ b/erpnext/accounts/test/test_utils.py
@@ -80,18 +80,27 @@ 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
+ item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32, do_not_submit=1
)
+ 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.insert()
+ 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)
- 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")
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 4b54483bc0..bccf6f10b6 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -179,6 +179,7 @@ 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")
@@ -254,6 +255,21 @@ 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 """
@@ -263,7 +279,8 @@ def get_balance_on(
if company:
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
- if account or (party_type and party):
+ if account or (party_type and party) or account_type:
+
if in_account_currency:
select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
else:
@@ -276,7 +293,6 @@ def get_balance_on(
select_field, " and ".join(cond)
)
)[0][0]
-
# if bal is None, return 0
return flt(bal)
@@ -459,6 +475,9 @@ 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
@@ -576,7 +595,11 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
# new row with references
new_row = journal_entry.append("accounts")
- new_row.update((frappe.copy_doc(jv_detail)).as_dict())
+ # Copy field values into new row
+ [
+ new_row.set(field, jv_detail.get(field))
+ for field in frappe.get_meta("Journal Entry Account").get_fieldnames_with_value()
+ ]
new_row.set(d["dr_or_cr"], d["allocated_amount"])
new_row.set(
@@ -614,9 +637,7 @@ 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 not d.exchange_gain_loss
- else payment_entry.get_exchange_rate(),
+ "exchange_rate": d.exchange_rate if 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,
}
@@ -638,28 +659,48 @@ 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)
@@ -867,6 +908,7 @@ def get_outstanding_invoices(
min_outstanding=None,
max_outstanding=None,
accounting_dimensions=None,
+ vouchers=None,
):
ple = qb.DocType("Payment Ledger Entry")
@@ -892,6 +934,7 @@ 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,
@@ -1112,7 +1155,8 @@ def get_autoname_with_number(number_value, doc_title, company):
def parse_naming_series_variable(doc, variable):
if variable == "FY":
- return get_fiscal_year(date=doc.get("posting_date"), company=doc.get("company"))[0]
+ date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
+ return get_fiscal_year(date=date, company=doc.get("company"))[0]
@frappe.whitelist()
@@ -1815,3 +1859,74 @@ 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
diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index c27ede29d1..dfdae1dec6 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -5,7 +5,7 @@
"label": "Profit and Loss"
}
],
- "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\":\"Your Shortcuts\",\"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\":\"Reports & Masters\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Payment\",\"col\":3}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"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\":\"Reports & Masters\",\"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,11 +1061,28 @@
"type": "Link"
}
],
- "modified": "2023-07-04 14:32:15.842044",
+ "modified": "2023-08-10 17:41:14.059005",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
- "number_cards": [],
+ "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"
+ }
+ ],
"owner": "Administrator",
"parent_page": "",
"public": 1,
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 43920adca3..0a2f61d23b 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -141,7 +141,7 @@ frappe.ui.form.on('Asset', {
frm.trigger("set_depr_posting_failure_alert");
}
- frm.trigger("setup_chart");
+ frm.trigger("setup_chart_and_depr_schedule_view");
}
frm.trigger("toggle_reference_doc");
@@ -206,7 +206,43 @@ frappe.ui.form.on('Asset', {
})
},
- setup_chart: async function(frm) {
+ render_depreciation_schedule_view: function(frm, depr_schedule) {
+ let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
+
+ let data = [];
+
+ 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);
+ });
+
+ 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 => `${value}`, width: 312}
+ ],
+ data: data,
+ serialNoColumn: false,
+ checkboxColumn: true,
+ cellHeight: 35
+ });
+
+ 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) {
if(frm.doc.finance_books.length > 1) {
return
}
@@ -228,7 +264,7 @@ frappe.ui.form.on('Asset', {
"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
{
asset_name: frm.doc.name,
- status: frm.doc.docstatus ? "Active" : "Draft",
+ status: "Active",
finance_book: frm.doc.finance_books[0].finance_book || null
}
)).message;
@@ -246,6 +282,9 @@ frappe.ui.form.on('Asset', {
}
}
});
+
+ frm.toggle_display(["depreciation_schedule_view"], 1);
+ frm.events.render_depreciation_schedule_view(frm, depr_schedule);
} else {
if(frm.doc.opening_accumulated_depreciation) {
x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' }));
@@ -473,7 +512,7 @@ frappe.ui.form.on('Asset', {
}
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) {
- doctype_field = frappe.scrub(doctype)
+ let doctype_field = frappe.scrub(doctype)
frm.set_value(doctype_field, '');
frappe.msgprint({
title: __('Invalid {0}', [__(doctype)]),
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index a1e8f331cd..befb5248d5 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -43,6 +43,7 @@
"column_break_33",
"opening_accumulated_depreciation",
"number_of_depreciations_booked",
+ "is_fully_depreciated",
"section_break_36",
"finance_books",
"section_break_33",
@@ -52,6 +53,8 @@
"column_break_24",
"frequency_of_depreciation",
"next_depreciation_date",
+ "depreciation_schedule_sb",
+ "depreciation_schedule_view",
"insurance_details",
"policy_number",
"insurer",
@@ -203,6 +206,7 @@
"fieldname": "disposal_date",
"fieldtype": "Date",
"label": "Disposal Date",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -242,19 +246,17 @@
"label": "Is Existing Asset"
},
{
- "depends_on": "is_existing_asset",
+ "depends_on": "eval:(doc.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 && doc.opening_accumulated_depreciation)",
+ "depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "number_of_depreciations_booked",
"fieldtype": "Int",
- "label": "Number of Depreciations Booked",
- "no_copy": 1
+ "label": "Number of Depreciations Booked"
},
{
"collapsible": 1,
@@ -487,6 +489,24 @@
"options": "\nSuccessful\nFailed",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "depreciation_schedule_sb",
+ "fieldtype": "Section Break",
+ "label": "Depreciation Schedule"
+ },
+ {
+ "fieldname": "depreciation_schedule_view",
+ "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,
@@ -513,6 +533,11 @@
"link_doctype": "Asset Depreciation Schedule",
"link_fieldname": "asset"
},
+ {
+ "group": "Activity",
+ "link_doctype": "Asset Activity",
+ "link_fieldname": "asset"
+ },
{
"group": "Journal Entry",
"link_doctype": "Journal Entry",
@@ -520,7 +545,7 @@
"table_fieldname": "accounts"
}
],
- "modified": "2023-03-30 15:07:41.542374",
+ "modified": "2023-07-28 20:12:44.819616",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -564,4 +589,4 @@
"states": [],
"title_field": "asset_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 42f531189a..2060c6ca83 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -21,9 +21,11 @@ from frappe.utils import (
import erpnext
from erpnext.accounts.general_ledger import make_reverse_gl_entries
from erpnext.assets.doctype.asset.depreciation import (
+ get_comma_separated_links,
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,
@@ -58,9 +60,19 @@ class Asset(AccountsController):
self.make_asset_movement()
if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries()
- if not self.split_from:
- make_draft_asset_depr_schedules_if_not_present(self)
+ if self.calculate_depreciation and 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:
+ asset_depr_schedules_links = get_comma_separated_links(
+ asset_depr_schedules_names, "Asset Depreciation Schedule"
+ )
+ frappe.msgprint(
+ _(
+ "Asset Depreciation Schedules created:
{0}
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()
@@ -71,10 +83,29 @@ 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 not self.split_from:
- make_draft_asset_depr_schedules(self)
+ if self.calculate_depreciation and 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"
+ )
+ frappe.msgprint(
+ _(
+ "Asset Depreciation Schedules created:
{0}
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:
@@ -117,17 +148,33 @@ class Asset(AccountsController):
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
def validate_cost_center(self):
- if not self.cost_center:
- return
-
- 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"),
+ 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"),
+ )
+
+ 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"),
+ )
def validate_in_use_date(self):
if not self.available_for_use_date:
@@ -176,8 +223,11 @@ class Asset(AccountsController):
if not self.calculate_depreciation:
return
- elif not self.finance_books:
- frappe.throw(_("Enter depreciation details"))
+ 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"))
if self.is_existing_asset:
return
@@ -258,7 +308,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 equal to {0}").format(
+ _("Opening Accumulated Depreciation must be less than or equal to {0}").format(
depreciable_amount
)
)
@@ -394,7 +444,9 @@ 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:
+ if (
+ flt(value_after_depreciation) <= expected_value_after_useful_life or self.is_fully_depreciated
+ ):
status = "Fully Depreciated"
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
status = "Partially Depreciated"
@@ -426,7 +478,9 @@ class Asset(AccountsController):
@frappe.whitelist()
def get_manual_depreciation_entries(self):
- (_, _, depreciation_expense_account) = get_depreciation_accounts(self)
+ (_, _, depreciation_expense_account) = get_depreciation_accounts(
+ self.asset_category, self.company
+ )
gle = frappe.qb.DocType("GL Entry")
@@ -769,10 +823,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)
+ ) = get_depreciation_accounts(asset.asset_category, asset.company)
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
@@ -880,6 +934,13 @@ 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
@@ -933,6 +994,8 @@ def create_new_asset_after_split(asset, split_qty):
)
new_asset.gross_purchase_amount = new_gross_purchase_amount
+ if asset.purchase_receipt_amount:
+ new_asset.purchase_receipt_amount = new_gross_purchase_amount
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name
@@ -945,6 +1008,15 @@ 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()
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index e1431eae17..e2a4b2909a 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -4,6 +4,8 @@
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,
@@ -21,6 +23,7 @@ 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,
@@ -42,11 +45,48 @@ def post_depreciation_entries(date=None):
failed_asset_names = []
error_log_names = []
- for asset_name in get_depreciable_assets(date):
- asset_doc = frappe.get_doc("Asset", asset_name)
+ 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
+ ),
+ }
+ )
try:
- make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
+ 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,
+ )
+
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
@@ -61,18 +101,36 @@ def post_depreciation_entries(date=None):
frappe.db.commit()
-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,
+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)
)
+ 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"):
@@ -82,8 +140,60 @@ 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):
+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,
+):
frappe.has_permission("Journal Entry", throw=True)
if not date:
@@ -91,100 +201,144 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None):
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
- asset_name = asset_depr_schedule_doc.asset
+ asset = frappe.get_doc("Asset", asset_depr_schedule_doc.asset)
- asset = frappe.get_doc("Asset", asset_name)
- (
- fixed_asset_account,
- accumulated_depreciation_account,
- depreciation_expense_account,
- ) = get_depreciation_accounts(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
+ )
- depreciation_cost_center, depreciation_series = frappe.get_cached_value(
- "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
- )
+ 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 = asset.cost_center or depreciation_cost_center
- accounting_dimensions = get_checks_for_pl_and_bs_accounts()
+ if not accounting_dimensions:
+ accounting_dimensions = get_checks_for_pl_and_bs_accounts()
- 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)
+ depreciation_posting_error = None
- credit_account, debit_account = get_credit_and_debit_accounts(
- accumulated_depreciation_account, depreciation_expense_account
+ 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_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")
+ frappe.db.commit()
+ except Exception as e:
+ frappe.db.rollback()
+ depreciation_posting_error = e
asset.set_status()
- return asset_depr_schedule_doc
+ if not depreciation_posting_error:
+ asset.db_set("depr_entry_posting_status", "Successful")
+ return asset_depr_schedule_doc
+
+ raise depreciation_posting_error
-def get_depreciation_accounts(asset):
+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):
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
accounts = frappe.db.get_value(
"Asset Category Account",
- filters={"parent": asset.asset_category, "company_name": asset.company},
+ filters={"parent": asset_category, "company_name": company},
fieldname=[
"fixed_asset_account",
"accumulated_depreciation_account",
@@ -200,7 +354,7 @@ def get_depreciation_accounts(asset):
if not accumulated_depreciation_account or not depreciation_expense_account:
accounts = frappe.get_cached_value(
- "Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"]
+ "Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
)
if not accumulated_depreciation_account:
@@ -215,7 +369,7 @@ def get_depreciation_accounts(asset):
):
frappe.throw(
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
- asset.asset_category, asset.company
+ asset_category, company
)
)
@@ -325,6 +479,8 @@ 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))
@@ -349,6 +505,8 @@ 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
@@ -398,6 +556,15 @@ 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()
@@ -551,8 +718,8 @@ def get_gl_entries_on_asset_disposal(
def get_asset_details(asset, finance_book=None):
- fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(
- asset
+ fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
+ asset.asset_category, asset.company
)
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 2a74f20e1b..cd66f1d136 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -754,6 +754,40 @@ 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,
@@ -1724,6 +1758,7 @@ 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,
},
)
diff --git a/erpnext/assets/doctype/asset_activity/__init__.py b/erpnext/assets/doctype/asset_activity/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.js b/erpnext/assets/doctype/asset_activity/asset_activity.js
new file mode 100644
index 0000000000..38d3434746
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Asset Activity", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.json b/erpnext/assets/doctype/asset_activity/asset_activity.json
new file mode 100644
index 0000000000..476fb2732e
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.json
@@ -0,0 +1,109 @@
+{
+ "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": []
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.py b/erpnext/assets/doctype/asset_activity/asset_activity.py
new file mode 100644
index 0000000000..28e1b3e32a
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.py
@@ -0,0 +1,20 @@
+# 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)
diff --git a/erpnext/assets/doctype/asset_activity/test_asset_activity.py b/erpnext/assets/doctype/asset_activity/test_asset_activity.py
new file mode 100644
index 0000000000..7a21559c52
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/test_asset_activity.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestAssetActivity(FrappeTestCase):
+ pass
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index a883bec71b..324b7392a8 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -18,6 +18,7 @@ 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
@@ -329,7 +330,7 @@ class AssetCapitalization(StockController):
gl_entries = self.get_gl_entries()
if gl_entries:
- make_gl_entries(gl_entries, from_repost=from_repost)
+ make_gl_entries(gl_entries, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -359,9 +360,6 @@ 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
@@ -519,6 +517,13 @@ 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."
@@ -542,9 +547,30 @@ class AssetCapitalization(StockController):
def set_consumed_asset_status(self, asset):
if self.docstatus == 1:
- asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized")
+ 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)
+ ),
+ )
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()
diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js
index c702687072..7dde14ea0e 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.js
+++ b/erpnext/assets/doctype/asset_category/asset_category.js
@@ -33,6 +33,7 @@ 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
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index 2e1def98fc..8d351412ca 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -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": {"root_type": ["Expense", "Income"]},
+ "depreciation_expense_account": {"account_type": ["Depreciation"]},
"capital_work_in_progress_account": {"account_type": ["Capital Work in Progress"]},
}
for d in self.accounts:
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
index d38508d0c4..3772ef4d68 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -19,6 +19,7 @@
"depreciation_method",
"total_number_of_depreciations",
"rate_of_depreciation",
+ "daily_depreciation",
"column_break_8",
"frequency_of_depreciation",
"expected_value_after_useful_life",
@@ -174,12 +175,21 @@
"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-02-26 16:37:23.734806",
+ "modified": "2023-08-10 22:22:09.722968",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index deae8c7891..39ebd4ec0e 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -153,6 +153,7 @@ 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(
@@ -499,29 +500,36 @@ def get_total_days(date, frequency):
return date_diff(date, period_start_date)
-@erpnext.allow_regional
def get_depreciation_amount(
asset,
depreciable_value,
- row,
+ fb_row,
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
):
- if row.depreciation_method in ("Straight Line", "Manual"):
- return get_straight_line_or_manual_depr_amount(asset, row)
+ if fb_row.depreciation_method in ("Straight Line", "Manual"):
+ return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx)
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,
- row.rate_of_depreciation,
- row.frequency_of_depreciation,
+ rate_of_depreciation,
+ fb_row.frequency_of_depreciation,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
)
-def get_straight_line_or_manual_depr_amount(asset, row):
+@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):
# 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)) / (
@@ -534,11 +542,30 @@ def get_straight_line_or_manual_depr_amount(asset, row):
)
# if the Depreciation Schedule is being prepared for the first time
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)
+ 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)
def get_wdv_or_dd_depr_amount(
@@ -571,6 +598,8 @@ def get_wdv_or_dd_depr_amount(
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
+ asset_depr_schedules_names = []
+
for row in asset_doc.get("finance_books"):
draft_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Draft", row.finance_book
@@ -581,12 +610,20 @@ def make_draft_asset_depr_schedules_if_not_present(asset_doc):
)
if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name:
- make_draft_asset_depr_schedule(asset_doc, row)
+ name = make_draft_asset_depr_schedule(asset_doc, row)
+ asset_depr_schedules_names.append(name)
+
+ return asset_depr_schedules_names
def make_draft_asset_depr_schedules(asset_doc):
+ asset_depr_schedules_names = []
+
for row in asset_doc.get("finance_books"):
- make_draft_asset_depr_schedule(asset_doc, row)
+ name = make_draft_asset_depr_schedule(asset_doc, row)
+ asset_depr_schedules_names.append(name)
+
+ return asset_depr_schedules_names
def make_draft_asset_depr_schedule(asset_doc, row):
@@ -596,6 +633,8 @@ def make_draft_asset_depr_schedule(asset_doc, row):
asset_depr_schedule_doc.insert()
+ return asset_depr_schedule_doc.name
+
def update_draft_asset_depr_schedules(asset_doc):
for row in asset_doc.get("finance_books"):
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index e5a5f194c1..4121302c1e 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -8,6 +8,7 @@
"finance_book",
"depreciation_method",
"total_number_of_depreciations",
+ "daily_depreciation",
"column_break_5",
"frequency_of_depreciation",
"depreciation_start_date",
@@ -17,6 +18,7 @@
],
"fields": [
{
+ "columns": 2,
"fieldname": "finance_book",
"fieldtype": "Link",
"in_list_view": 1,
@@ -32,6 +34,7 @@
"reqd": 1
},
{
+ "columns": 2,
"fieldname": "total_number_of_depreciations",
"fieldtype": "Int",
"in_list_view": 1,
@@ -43,6 +46,7 @@
"fieldtype": "Column Break"
},
{
+ "columns": 2,
"fieldname": "frequency_of_depreciation",
"fieldtype": "Int",
"in_list_view": 1,
@@ -57,6 +61,7 @@
"mandatory_depends_on": "eval:parent.doctype == 'Asset'"
},
{
+ "columns": 1,
"default": "0",
"depends_on": "eval:parent.doctype == 'Asset'",
"fieldname": "expected_value_after_useful_life",
@@ -79,12 +84,19 @@
"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": "2021-06-17 12:59:05.743683",
+ "modified": "2023-08-10 22:10:36.576199",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
@@ -93,5 +105,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py
index 22055dcb73..620aad80ed 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -5,6 +5,9 @@
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):
@@ -62,20 +65,21 @@ class AssetMovement(Document):
frappe.throw(_("Source and Target Location cannot be same"))
if self.purpose == "Receipt":
- if not (d.source_location or d.from_employee) and not (d.target_location or d.to_employee):
+ if not (d.source_location) and not (d.target_location or d.to_employee):
frappe.throw(
_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset)
)
- elif d.from_employee and not d.target_location:
- frappe.throw(
- _("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
- )
- elif d.to_employee and d.target_location:
- frappe.throw(
- _(
- "Asset {0} cannot be received at a location and given to an employee in a single movement"
- ).format(d.asset)
- )
+ elif d.source_location:
+ if d.from_employee and not d.target_location:
+ frappe.throw(
+ _("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
+ )
+ elif d.to_employee and d.target_location:
+ frappe.throw(
+ _(
+ "Asset {0} cannot be received at a location and given to an employee in a single movement"
+ ).format(d.asset)
+ )
def validate_employee(self):
for d in self.assets:
@@ -127,5 +131,24 @@ 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)
- frappe.db.set_value("Asset", d.asset, "custodian", current_employee)
+ 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)),
+ )
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index f649e510f9..7e95cb2a1b 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -8,6 +8,7 @@ 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,
@@ -25,8 +26,14 @@ class AssetRepair(AccountsController):
self.calculate_total_repair_cost()
def update_status(self):
- if self.repair_status == "Pending":
+ if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
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()
@@ -68,6 +75,13 @@ 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)
@@ -95,6 +109,13 @@ 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()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 8426ed43ff..823b6e9e21 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -12,6 +12,7 @@ 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,
@@ -27,9 +28,21 @@ 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")
@@ -51,10 +64,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)
+ ) = get_depreciation_accounts(asset.asset_category, asset.company)
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
@@ -65,21 +78,23 @@ 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()
diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
index abe295c680..884e0c6cb2 100644
--- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
+++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
@@ -53,7 +53,7 @@
},
{
"allow_on_submit": 1,
- "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())",
+ "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= frappe.datetime.now_date())",
"fieldname": "make_depreciation_entry",
"fieldtype": "Button",
"label": "Make Depreciation Entry"
@@ -61,7 +61,7 @@
],
"istable": 1,
"links": [],
- "modified": "2023-03-13 23:17:15.849950",
+ "modified": "2023-07-26 12:56:48.718736",
"modified_by": "Administrator",
"module": "Assets",
"name": "Depreciation Schedule",
diff --git a/erpnext/assets/report/asset_activity/__init__.py b/erpnext/assets/report/asset_activity/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/assets/report/asset_activity/asset_activity.json b/erpnext/assets/report/asset_activity/asset_activity.json
new file mode 100644
index 0000000000..cc46775197
--- /dev/null
+++ b/erpnext/assets/report/asset_activity/asset_activity.json
@@ -0,0 +1,33 @@
+{
+ "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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
index 48b17f58fb..48d33314ec 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Fixed Asset Register"] = {
"filters": [
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.json b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.json
index b40243cb75..9074ba1cc1 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.json
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.json
@@ -1,13 +1,15 @@
{
- "add_total_row": 0,
+ "add_total_row": 1,
+ "columns": [],
"creation": "2019-09-23 16:35:02.836134",
- "disable_prepared_report": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
+ "filters": [],
"idx": 0,
"is_standard": "Yes",
- "modified": "2019-10-22 13:00:31.539726",
+ "letterhead": null,
+ "modified": "2023-07-26 21:03:20.722628",
"modified_by": "Administrator",
"module": "Assets",
"name": "Fixed Asset Register",
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index 6911f94bbb..bf62a8fb39 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -7,13 +7,14 @@ from itertools import chain
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull, Sum
-from frappe.utils import cstr, flt, formatdate, getdate
+from frappe.utils import add_months, cstr, flt, formatdate, getdate, nowdate, today
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
@@ -37,15 +38,26 @@ 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]]
- if filters.filter_based_on == "Fiscal Year":
+ 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
+
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"):
@@ -54,12 +66,12 @@ def get_conditions(filters):
conditions["cost_center"] = filters.get("cost_center")
if status:
- # In Store assets are those that are not sold or scrapped
+ # In Store assets are those that are not sold or scrapped or capitalized or decapitalized
operand = "not in"
if status not in "In Location":
operand = "in"
- conditions["status"] = (operand, ["Sold", "Scrapped"])
+ conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized", "Decapitalized"])
return conditions
@@ -71,36 +83,6 @@ def get_data(filters):
pr_supplier_map = get_purchase_receipt_supplier_map()
pi_supplier_map = get_purchase_invoice_supplier_map()
- group_by = frappe.scrub(filters.get("group_by"))
-
- if group_by == "asset_category":
- fields = ["asset_category", "gross_purchase_amount", "opening_accumulated_depreciation"]
- assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by)
-
- elif group_by == "location":
- fields = ["location", "gross_purchase_amount", "opening_accumulated_depreciation"]
- assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by)
-
- else:
- fields = [
- "name as asset_id",
- "asset_name",
- "status",
- "department",
- "company",
- "cost_center",
- "calculate_depreciation",
- "purchase_receipt",
- "asset_category",
- "purchase_date",
- "gross_purchase_amount",
- "location",
- "available_for_use_date",
- "purchase_invoice",
- "opening_accumulated_depreciation",
- ]
- assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
-
assets_linked_to_fb = get_assets_linked_to_fb(filters)
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
@@ -114,6 +96,31 @@ def get_data(filters):
depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book)
+ group_by = frappe.scrub(filters.get("group_by"))
+
+ if group_by in ("asset_category", "location"):
+ data = get_group_by_data(group_by, conditions, assets_linked_to_fb, depreciation_amount_map)
+ return data
+
+ fields = [
+ "name as asset_id",
+ "asset_name",
+ "status",
+ "department",
+ "company",
+ "cost_center",
+ "calculate_depreciation",
+ "purchase_receipt",
+ "asset_category",
+ "purchase_date",
+ "gross_purchase_amount",
+ "location",
+ "available_for_use_date",
+ "purchase_invoice",
+ "opening_accumulated_depreciation",
+ ]
+ assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
+
for asset in assets_record:
if (
assets_linked_to_fb
@@ -136,7 +143,7 @@ def get_data(filters):
or pi_supplier_map.get(asset.purchase_invoice),
"gross_purchase_amount": asset.gross_purchase_amount,
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
- "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map),
+ "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
"available_for_use_date": asset.available_for_use_date,
"location": asset.location,
"asset_category": asset.asset_category,
@@ -230,12 +237,11 @@ def get_assets_linked_to_fb(filters):
return assets_linked_to_fb
-def get_depreciation_amount_of_asset(asset, depreciation_amount_map):
- return depreciation_amount_map.get(asset.asset_id) or 0.0
-
-
def get_asset_depreciation_amount_map(filters, finance_book):
- date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
+ start_date = (
+ filters.from_date if filters.filter_based_on == "Date Range" else filters.year_start_date
+ )
+ end_date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
asset = frappe.qb.DocType("Asset")
gle = frappe.qb.DocType("GL Entry")
@@ -256,25 +262,77 @@ def get_asset_depreciation_amount_map(filters, finance_book):
)
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
+ .where(company.name == filters.company)
.where(asset.docstatus == 1)
- .groupby(asset.name)
)
+ if filters.only_existing_assets:
+ query = query.where(asset.is_existing_asset == 1)
+ if filters.asset_category:
+ query = query.where(asset.asset_category == filters.asset_category)
+ if filters.cost_center:
+ query = query.where(asset.cost_center == filters.cost_center)
+ if filters.status:
+ if filters.status == "In Location":
+ query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
+ else:
+ query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
if finance_book:
query = query.where(
(gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull())
)
else:
query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull()))
-
if filters.filter_based_on in ("Date Range", "Fiscal Year"):
- query = query.where(gle.posting_date <= date)
+ query = query.where(gle.posting_date >= start_date)
+ query = query.where(gle.posting_date <= end_date)
+
+ query = query.groupby(asset.name)
asset_depr_amount_map = query.run()
return dict(asset_depr_amount_map)
+def get_group_by_data(group_by, conditions, assets_linked_to_fb, depreciation_amount_map):
+ fields = [
+ group_by,
+ "name",
+ "gross_purchase_amount",
+ "opening_accumulated_depreciation",
+ "calculate_depreciation",
+ ]
+ assets = frappe.db.get_all("Asset", filters=conditions, fields=fields)
+
+ data = []
+
+ for a in assets:
+ if assets_linked_to_fb and a.calculate_depreciation and a.name not in assets_linked_to_fb:
+ continue
+
+ a["depreciated_amount"] = depreciation_amount_map.get(a["name"], 0.0)
+ a["asset_value"] = (
+ a["gross_purchase_amount"] - a["opening_accumulated_depreciation"] - a["depreciated_amount"]
+ )
+
+ del a["name"]
+ del a["calculate_depreciation"]
+
+ idx = ([i for i, d in enumerate(data) if a[group_by] == d[group_by]] or [None])[0]
+ if idx is None:
+ data.append(a)
+ else:
+ for field in (
+ "gross_purchase_amount",
+ "opening_accumulated_depreciation",
+ "depreciated_amount",
+ "asset_value",
+ ):
+ data[idx][field] = data[idx][field] + a[field]
+
+ return data
+
+
def get_purchase_receipt_supplier_map():
return frappe._dict(
frappe.db.sql(
@@ -313,35 +371,35 @@ def get_columns(filters):
"fieldtype": "Link",
"fieldname": frappe.scrub(filters.get("group_by")),
"options": filters.get("group_by"),
- "width": 120,
+ "width": 216,
},
{
"label": _("Gross Purchase Amount"),
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"options": "company:currency",
- "width": 100,
+ "width": 250,
},
{
"label": _("Opening Accumulated Depreciation"),
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"options": "company:currency",
- "width": 90,
+ "width": 250,
},
{
"label": _("Depreciated Amount"),
"fieldname": "depreciated_amount",
"fieldtype": "Currency",
"options": "company:currency",
- "width": 100,
+ "width": 250,
},
{
"label": _("Asset Value"),
"fieldname": "asset_value",
"fieldtype": "Currency",
"options": "company:currency",
- "width": 100,
+ "width": 250,
},
]
diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json
index d810effda0..c6b321e9c1 100644
--- a/erpnext/assets/workspace/assets/assets.json
+++ b/erpnext/assets/workspace/assets/assets.json
@@ -183,6 +183,17 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "dependencies": "Asset Activity",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Activity",
+ "link_count": 0,
+ "link_to": "Asset Activity",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
}
],
"modified": "2023-05-24 14:47:20.243146",
diff --git a/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json b/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json
index 6452ed2139..751796bbbb 100644
--- a/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json
+++ b/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json
@@ -5,18 +5,19 @@
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0,
"doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Item\"}",
- "idx": 0,
+ "idx": 1,
"is_public": 1,
"is_standard": 1,
- "modified": "2020-07-21 16:13:25.092287",
+ "modified": "2023-07-19 13:06:42.937941",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Trends",
"number_of_groups": 0,
"owner": "Administrator",
"report_name": "Purchase Order Trends",
+ "roles": [],
"timeseries": 0,
"type": "Line",
"use_report_chart": 1,
diff --git a/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json b/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json
index 6f7da8ea87..f6b9717539 100644
--- a/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json
+++ b/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json
@@ -4,18 +4,19 @@
"creation": "2020-07-20 21:01:02.329519",
"docstatus": 0,
"doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Supplier\"}",
"idx": 0,
"is_public": 1,
"is_standard": 1,
- "modified": "2020-07-22 12:43:40.829652",
+ "modified": "2023-07-19 13:07:41.753556",
"modified_by": "Administrator",
"module": "Buying",
"name": "Top Suppliers",
"number_of_groups": 0,
"owner": "Administrator",
"report_name": "Purchase Receipt Trends",
+ "roles": [],
"timeseries": 0,
"type": "Bar",
"use_report_chart": 1,
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 8fa8f30554..7c33056a91 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -3,7 +3,10 @@
frappe.provide("erpnext.buying");
frappe.provide("erpnext.accounts.dimensions");
-{% include 'erpnext/public/js/controllers/buying.js' %};
+
+erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
+erpnext.accounts.taxes.setup_tax_validations("Purchase Order");
+erpnext.buying.setup_buying_controller();
frappe.ui.form.on("Purchase Order", {
setup: function(frm) {
@@ -62,7 +65,7 @@ frappe.ui.form.on("Purchase Order", {
get_materials_from_supplier: function(frm) {
let po_details = [];
- if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
+ if (frm.doc.supplied_items && (flt(frm.doc.per_received, 2) == 100 || frm.doc.status === 'Closed')) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name)
@@ -181,7 +184,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}
if(!in_list(["Closed", "Delivered"], doc.status)) {
- if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) {
+ if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received, 2) < 100 && flt(this.frm.doc.per_billed, 2) < 100) {
// Don't add Update Items button if the PO is following the new subcontracting flow.
if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
this.frm.add_custom_button(__('Update Items'), () => {
@@ -195,7 +198,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}
}
if (this.frm.has_perm("submit")) {
- if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) {
+ if(flt(doc.per_billed, 2) < 100 || flt(doc.per_received, 2) < 100) {
if (doc.status != "On Hold") {
this.frm.add_custom_button(__('Hold'), () => this.hold_purchase_order(), __("Status"));
} else{
@@ -218,7 +221,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}
if(doc.status != "Closed") {
if (doc.status != "On Hold") {
- if(flt(doc.per_received) < 100 && allow_receipt) {
+ if(flt(doc.per_received, 2) < 100 && allow_receipt) {
cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
if (doc.is_subcontracted) {
if (doc.is_old_subcontracting_flow) {
@@ -231,11 +234,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}
}
}
- if(flt(doc.per_billed) < 100)
+ if(flt(doc.per_billed, 2) < 100)
cur_frm.add_custom_button(__('Purchase Invoice'),
this.make_purchase_invoice, __('Create'));
- if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
+ if(flt(doc.per_billed, 2) < 100 && doc.status != "Delivered") {
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
@@ -243,17 +246,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
);
}
- if(flt(doc.per_billed) < 100) {
+ if(flt(doc.per_billed, 2) < 100) {
this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create'));
}
- if(!doc.auto_repeat) {
- cur_frm.add_custom_button(__('Subscription'), function() {
- erpnext.utils.make_subscription(doc.doctype, doc.name)
- }, __('Create'))
- }
-
if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
let me = this;
let internal = me.frm.doc.is_internal_supplier;
@@ -369,7 +366,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
},
allow_child_item_selection: true,
child_fieldname: "items",
- child_columns: ["item_code", "qty"]
+ child_columns: ["item_code", "qty", "ordered_qty"]
})
}, __("Get Items From"));
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 2f0b7862a8..31a06cf95e 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -1,11 +1,10 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-
-{% include 'erpnext/public/js/controllers/buying.js' %};
-
cur_frm.add_fetch('contact', 'email_id', 'email_id')
+erpnext.buying.setup_buying_controller();
+
frappe.ui.form.on("Request for Quotation",{
setup: function(frm) {
frm.custom_make_buttons = {
@@ -245,19 +244,21 @@ frappe.ui.form.on("Request for Quotation",{
]
});
- dialog.fields_dict['supplier'].df.onchange = () => {
- var supplier = dialog.get_value('supplier');
- frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => {
+ dialog.fields_dict["supplier"].df.onchange = () => {
+ frm.call("get_supplier_email_preview", {
+ supplier: dialog.get_value("supplier"),
+ }).then(({ message }) => {
dialog.fields_dict.email_preview.$wrapper.empty();
- dialog.fields_dict.email_preview.$wrapper.append(result.message);
+ dialog.fields_dict.email_preview.$wrapper.append(
+ message.message
+ );
+ dialog.set_value("subject", message.subject);
});
-
- }
+ };
dialog.fields_dict.note.$wrapper.append(`This is a preview of the email to be sent. A PDF of the document will
automatically be attached with the email.
`);
- dialog.set_value("subject", frm.doc.subject);
dialog.show();
}
})
@@ -436,7 +437,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
//Remove blanks
for (var j = 0; j < frm.doc.suppliers.length; j++) {
- if(!frm.doc.suppliers[j].hasOwnProperty("supplier")) {
+ if(!Object.prototype.hasOwnProperty.call(frm.doc.suppliers[j], "supplier")) {
frm.get_field("suppliers").grid.grid_rows[j].remove();
}
}
@@ -445,10 +446,11 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
if(r.message) {
for (var i = 0; i < r.message.length; i++) {
var exists = false;
+ let supplier = "";
if (r.message[i].constructor === Array){
- var supplier = r.message[i][0];
+ supplier = r.message[i][0];
} else {
- var supplier = r.message[i].name;
+ supplier = r.message[i].name;
}
for (var j = 0; j < doc.suppliers.length;j++) {
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index bd65b0c805..fbfc1ac169 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -20,11 +20,11 @@
"items_section",
"items",
"supplier_response_section",
- "salutation",
- "subject",
- "col_break_email_1",
"email_template",
"preview",
+ "col_break_email_1",
+ "html_llwp",
+ "send_attached_files",
"sec_break_email_2",
"message_for_supplier",
"terms_section_break",
@@ -236,23 +236,6 @@
"print_hide": 1,
"read_only": 1
},
- {
- "fetch_from": "email_template.subject",
- "fetch_if_empty": 1,
- "fieldname": "subject",
- "fieldtype": "Data",
- "label": "Subject",
- "print_hide": 1
- },
- {
- "description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.",
- "fieldname": "salutation",
- "fieldtype": "Link",
- "label": "Salutation",
- "no_copy": 1,
- "options": "Salutation",
- "print_hide": 1
- },
{
"fieldname": "col_break_email_1",
"fieldtype": "Column Break"
@@ -285,13 +268,28 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
+ },
+ {
+ "fieldname": "html_llwp",
+ "fieldtype": "HTML",
+ "options": "In your Email Template, you can use the following special variables:\n
\n\n - \n
{{ update_password_link }}
: A link where your supplier can set a new password to log into your portal.\n \n - \n
{{ portal_link }}
: A link to this RFQ in your supplier portal.\n \n - \n
{{ supplier_name }}
: The company name of your supplier.\n \n - \n
{{ contact.salutation }} {{ contact.last_name }}
: The contact person of your supplier.\n - \n
{{ user_fullname }}
: Your full name.\n \n
\n\nApart from these, you can access all values in this RFQ, like {{ message_for_supplier }}
or {{ terms }}
.
",
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1
+ },
+ {
+ "default": "1",
+ "description": "If enabled, all files attached to this document will be attached to each email",
+ "fieldname": "send_attached_files",
+ "fieldtype": "Check",
+ "label": "Send Attached Files"
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-01-31 23:22:06.684694",
+ "modified": "2023-08-08 16:30:10.870429",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 4590f8c3d9..56840c11a6 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -116,7 +116,10 @@ class RequestforQuotation(BuyingController):
route = frappe.db.get_value(
"Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
)
- return get_url("/app/{0}/".format(route) + self.name)
+ if not route:
+ frappe.throw(_("Please add Request for Quotation to the sidebar in Portal Settings."))
+
+ return get_url(f"{route}/{self.name}")
def update_supplier_part_no(self, supplier):
self.vendor = supplier
@@ -179,37 +182,32 @@ class RequestforQuotation(BuyingController):
if full_name == "Guest":
full_name = "Administrator"
- # send document dict and some important data from suppliers row
- # to render message_for_supplier from any template
doc_args = self.as_dict()
- doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
- # Get Contact Full Name
- supplier_name = None
if data.get("contact"):
- contact_name = frappe.db.get_value(
- "Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
- )
- supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
+ contact = frappe.get_doc("Contact", data.get("contact"))
+ doc_args["contact"] = contact.as_dict()
- args = {
- "update_password_link": update_password_link,
- "message": frappe.render_template(self.message_for_supplier, doc_args),
- "rfq_link": rfq_link,
- "user_fullname": full_name,
- "supplier_name": supplier_name or data.get("supplier_name"),
- "supplier_salutation": self.salutation or "Dear Mx.",
- }
-
- subject = self.subject or _("Request for Quotation")
- template = "templates/emails/request_for_quotation.html"
+ doc_args.update(
+ {
+ "supplier": data.get("supplier"),
+ "supplier_name": data.get("supplier_name"),
+ "update_password_link": f'{_("Set Password")}',
+ "portal_link": f' {_("Submit your Quotation")} ',
+ "user_fullname": full_name,
+ }
+ )
+ email_template = frappe.get_doc("Email Template", self.email_template)
+ message = frappe.render_template(email_template.response_, doc_args)
+ subject = frappe.render_template(email_template.subject, doc_args)
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
- message = frappe.get_template(template).render(args)
if preview:
- return message
+ return {"message": message, "subject": subject}
- attachments = self.get_attachments()
+ attachments = None
+ if self.send_attached_files:
+ attachments = self.get_attachments()
self.send_email(data, sender, subject, message, attachments)
diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
index d250e6f18a..42fa1d923e 100644
--- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
@@ -2,11 +2,14 @@
# See license.txt
+from urllib.parse import urlparse
+
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
+ RequestforQuotation,
create_supplier_quotation,
get_pdf,
make_supplier_quotation_from_rfq,
@@ -125,13 +128,18 @@ class TestRequestforQuotation(FrappeTestCase):
rfq.status = "Draft"
rfq.submit()
+ def test_get_link(self):
+ rfq = make_request_for_quotation()
+ parsed_link = urlparse(rfq.get_link())
+ self.assertEqual(parsed_link.path, f"/rfq/{rfq.name}")
+
def test_get_pdf(self):
rfq = make_request_for_quotation()
get_pdf(rfq.name, rfq.get("suppliers")[0].supplier)
self.assertEqual(frappe.local.response.type, "pdf")
-def make_request_for_quotation(**args):
+def make_request_for_quotation(**args) -> "RequestforQuotation":
"""
:param supplier_data: List containing supplier data
"""
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
index dc9c590dc5..addf5a5e77 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
@@ -1,9 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-// attach required files
-{% include 'erpnext/public/js/controllers/buying.js' %};
-
+erpnext.buying.setup_buying_controller();
erpnext.buying.SupplierQuotationController = class SupplierQuotationController extends erpnext.buying.BuyingController {
setup() {
this.frm.custom_make_buttons = {
diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js
index b4cd852c32..e9d56784ff 100644
--- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js
+++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.js
@@ -1,8 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* global frappe, refresh_field */
-
frappe.ui.form.on("Supplier Scorecard", {
setup: function(frm) {
if (frm.doc.indicator_color !== "") {
@@ -79,7 +77,7 @@ var loadAllStandings = function(frm) {
callback: function(r) {
for (var j = 0; j < frm.doc.standings.length; j++)
{
- if(!frm.doc.standings[j].hasOwnProperty("standing_name")) {
+ if(!Object.prototype.hasOwnProperty.call(frm.doc.standings[j], "standing_name")) {
frm.get_field("standings").grid.grid_rows[j].remove();
}
}
diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
index 58da851295..6e22acf01a 100644
--- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
@@ -339,29 +339,35 @@ def make_default_records():
{
"min_grade": 0.0,
"prevent_rfqs": 1,
+ "warn_rfqs": 0,
"notify_supplier": 0,
"max_grade": 30.0,
"prevent_pos": 1,
+ "warn_pos": 0,
"standing_color": "Red",
"notify_employee": 0,
"standing_name": "Very Poor",
},
{
"min_grade": 30.0,
- "prevent_rfqs": 1,
+ "prevent_rfqs": 0,
+ "warn_rfqs": 1,
"notify_supplier": 0,
"max_grade": 50.0,
"prevent_pos": 0,
- "standing_color": "Red",
+ "warn_pos": 1,
+ "standing_color": "Yellow",
"notify_employee": 0,
"standing_name": "Poor",
},
{
"min_grade": 50.0,
"prevent_rfqs": 0,
+ "warn_rfqs": 0,
"notify_supplier": 0,
"max_grade": 80.0,
"prevent_pos": 0,
+ "warn_pos": 0,
"standing_color": "Green",
"notify_employee": 0,
"standing_name": "Average",
@@ -369,9 +375,11 @@ def make_default_records():
{
"min_grade": 80.0,
"prevent_rfqs": 0,
+ "warn_rfqs": 0,
"notify_supplier": 0,
"max_grade": 100.0,
"prevent_pos": 0,
+ "warn_pos": 0,
"standing_color": "Blue",
"notify_employee": 0,
"standing_name": "Excellent",
diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js
index dc5474e3b4..edf0b04d72 100644
--- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js
+++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js
@@ -1,7 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* global frappe, __ */
frappe.listview_settings["Supplier Scorecard"] = {
add_fields: ["indicator_color", "status"],
@@ -14,4 +13,4 @@ frappe.listview_settings["Supplier Scorecard"] = {
}
},
-};
+}
diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js
index 9f8a2dee81..2186cd89eb 100644
--- a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js
+++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.js
@@ -1,8 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* global frappe */
-
frappe.ui.form.on("Supplier Scorecard Criteria", {
refresh: function() {}
});
diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js
index a4cdeb3195..62079cc3e0 100644
--- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js
+++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.js
@@ -1,9 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* global frappe */
-
-
frappe.ui.form.on("Supplier Scorecard Period", {
onload: function(frm) {
let criteria_grid = frm.get_field("criteria").grid;
diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js
index dccfcc34bb..22756e75cf 100644
--- a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js
+++ b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.js
@@ -1,7 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* global frappe */
frappe.ui.form.on("Supplier Scorecard Standing", {
refresh: function() {
diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js
index 2d74fdd190..b3b4321a88 100644
--- a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js
+++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.js
@@ -1,8 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* global frappe */
-
frappe.ui.form.on("Supplier Scorecard Variable", {
refresh: function() {
diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.js b/erpnext/buying/report/procurement_tracker/procurement_tracker.js
index 283d56c946..7e4822c31e 100644
--- a/erpnext/buying/report/procurement_tracker/procurement_tracker.js
+++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Procurement Tracker"] = {
"filters": [
@@ -27,13 +27,13 @@ frappe.query_reports["Procurement Tracker"] = {
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_start_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_end_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
},
]
}
diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js
index a884f06d2c..250b334d65 100644
--- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js
+++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Purchase Analytics"] = {
"filters": [
@@ -35,14 +35,14 @@ frappe.query_reports["Purchase Analytics"] = {
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_start_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_end_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
reqd: 1
},
{
@@ -81,8 +81,9 @@ frappe.query_reports["Purchase Analytics"] = {
const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return;
- row_name = data[2].content;
- length = data.length;
+ let row_name = data[2].content;
+ let length = data.length;
+ let row_values = '';
if (tree_type == "Supplier") {
row_values = data
@@ -104,7 +105,7 @@ frappe.query_reports["Purchase Analytics"] = {
});
}
- entry = {
+ let entry = {
name: row_name,
values: row_values,
};
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
index 721e54e46f..91506c0ab3 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Purchase Order Analysis"] = {
"filters": [
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js
index d727584d0a..cb05109d5b 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Requested Items to Order and Receive"] = {
"filters": [
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
index 075671f4ec..800b8ab7db 100644
--- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Subcontract Order Summary"] = {
"filters": [
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
index 9db769d59b..35be2a9cf8 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Subcontracted Item To Be Received"] = {
"filters": [
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
index 7e5338f353..33b26dcb5f 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
"filters": [
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 4193b5327d..340ec01bee 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -5,7 +5,7 @@
import json
import frappe
-from frappe import _, bold, throw
+from frappe import _, bold, qb, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, Sum
@@ -32,13 +32,19 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
apply_pricing_rule_on_transaction,
get_applied_pricing_rules,
)
+from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import (
get_party_account,
get_party_account_currency,
get_party_gle_currency,
validate_party_frozen_disabled,
)
-from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
+from erpnext.accounts.utils import (
+ create_gain_loss_journal,
+ get_account_currency,
+ get_fiscal_years,
+ validate_fiscal_year,
+)
from erpnext.buying.utils import update_last_purchase_rate
from erpnext.controllers.print_settings import (
set_print_templates_for_item_table,
@@ -56,6 +62,7 @@ from erpnext.stock.get_item_details import (
get_item_tax_map,
get_item_warehouse,
)
+from erpnext.utilities.regional import temporary_flag
from erpnext.utilities.transaction_base import TransactionBase
@@ -709,7 +716,9 @@ class AccountsController(TransactionBase):
def validate_enabled_taxes_and_charges(self):
taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges")
- if frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"):
+ if self.taxes_and_charges and frappe.get_cached_value(
+ taxes_and_charges_doctype, self.taxes_and_charges, "disabled"
+ ):
frappe.throw(
_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges)
)
@@ -760,7 +769,9 @@ class AccountsController(TransactionBase):
}
)
- update_gl_dict_with_regional_fields(self, gl_dict)
+ with temporary_flag("company", self.company):
+ update_gl_dict_with_regional_fields(self, gl_dict)
+
accounting_dimensions = get_accounting_dimensions()
dimension_dict = frappe._dict()
@@ -794,8 +805,28 @@ class AccountsController(TransactionBase):
gl_dict, account_currency, self.get("conversion_rate"), self.company_currency
)
+ # Update details in transaction currency
+ gl_dict.update(
+ {
+ "transaction_currency": self.get("currency") or self.company_currency,
+ "transaction_exchange_rate": self.get("conversion_rate", 1),
+ "debit_in_transaction_currency": self.get_value_in_transaction_currency(
+ account_currency, args, "debit"
+ ),
+ "credit_in_transaction_currency": self.get_value_in_transaction_currency(
+ account_currency, args, "credit"
+ ),
+ }
+ )
+
return gl_dict
+ def get_value_in_transaction_currency(self, account_currency, args, field):
+ if account_currency == self.get("currency"):
+ return args.get(field + "_in_account_currency")
+ else:
+ return flt(args.get(field, 0) / self.get("conversion_rate", 1))
+
def validate_qty_is_not_zero(self):
if self.doctype != "Purchase Receipt":
for item in self.items:
@@ -965,67 +996,160 @@ class AccountsController(TransactionBase):
d.exchange_gain_loss = difference
- def make_exchange_gain_loss_gl_entries(self, gl_entries):
- if self.get("doctype") in ["Purchase Invoice", "Sales Invoice"]:
- for d in self.get("advances"):
- if d.exchange_gain_loss:
- is_purchase_invoice = self.get("doctype") == "Purchase Invoice"
- party = self.supplier if is_purchase_invoice else self.customer
- party_account = self.credit_to if is_purchase_invoice else self.debit_to
- party_type = "Supplier" if is_purchase_invoice else "Customer"
+ 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
+ )
- gain_loss_account = frappe.get_cached_value(
- "Company", self.company, "exchange_gain_loss_account"
- )
- if not gain_loss_account:
- frappe.throw(
- _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company"))
- )
- account_currency = get_account_currency(gain_loss_account)
- if account_currency != self.company_currency:
- frappe.throw(
- _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency)
- )
+ precision_loss = self.get("base_net_total") - flt(
+ self.get("net_total") * self.conversion_rate, self.precision("net_total")
+ )
- # for purchase
- dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
- if not is_purchase_invoice:
- # just reverse for sales?
- dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+ credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
+ against = self.supplier if self.doctype == "Purchase Invoice" else self.customer
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": gain_loss_account,
- "account_currency": account_currency,
- "against": party,
- dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
- dr_or_cr: abs(d.exchange_gain_loss),
- "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
- "project": self.project,
- },
- item=d,
+ if precision_loss:
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": round_off_account,
+ "against": against,
+ credit_or_debit: 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 make_exchange_gain_loss_journal(self, args: dict = None) -> None:
+ """
+ Make Exchange Gain/Loss journal for Invoices and Payments
+ """
+ # Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
+ # see accounts/utils.py:cancel_exchange_gain_loss_journal()
+ if self.docstatus == 1:
+ if self.get("doctype") == "Journal Entry":
+ # 'args' is populated with exchange gain/loss account and the amount to be booked.
+ # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
+ # and below logic is only for such scenarios
+ if args:
+ for arg in args:
+ # Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount`
+ if (
+ arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0
+ ) and arg.get("difference_account"):
+
+ party_account = arg.get("account")
+ gain_loss_account = arg.get("difference_account")
+ difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss")
+ if difference_amount > 0:
+ dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit"
+ else:
+ dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit"
+
+ reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+
+ je = create_gain_loss_journal(
+ self.company,
+ arg.get("party_type"),
+ arg.get("party"),
+ party_account,
+ gain_loss_account,
+ difference_amount,
+ dr_or_cr,
+ reverse_dr_or_cr,
+ arg.get("against_voucher_type"),
+ arg.get("against_voucher"),
+ arg.get("idx"),
+ self.doctype,
+ self.name,
+ arg.get("idx"),
+ )
+ frappe.msgprint(
+ _("Exchange Gain/Loss amount has been booked through {0}").format(
+ get_link_to_form("Journal Entry", je)
+ )
+ )
+
+ if self.get("doctype") == "Payment Entry":
+ # For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation
+ gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0]
+ booked = []
+ if gain_loss_to_book:
+ vtypes = [x.reference_doctype for x in gain_loss_to_book]
+ vnames = [x.reference_name for x in gain_loss_to_book]
+ je = qb.DocType("Journal Entry")
+ jea = qb.DocType("Journal Entry Account")
+ parents = (
+ qb.from_(jea)
+ .select(jea.parent)
+ .where(
+ (jea.reference_type == "Payment Entry")
+ & (jea.reference_name == self.name)
+ & (jea.docstatus == 1)
)
+ .run()
)
- dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
-
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": party_account,
- "party_type": party_type,
- "party": party,
- "against": gain_loss_account,
- dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate),
- dr_or_cr: abs(d.exchange_gain_loss),
- "cost_center": self.cost_center,
- "project": self.project,
- },
- self.party_account_currency,
- item=self,
+ booked = []
+ if parents:
+ booked = (
+ qb.from_(je)
+ .inner_join(jea)
+ .on(je.name == jea.parent)
+ .select(jea.reference_type, jea.reference_name, jea.reference_detail_no)
+ .where(
+ (je.docstatus == 1)
+ & (je.name.isin(parents))
+ & (je.voucher_type == "Exchange Gain or Loss")
+ )
+ .run()
+ )
+
+ for d in gain_loss_to_book:
+ # Filter out References for which Gain/Loss is already booked
+ if d.exchange_gain_loss and (
+ (d.reference_doctype, d.reference_name, str(d.idx)) not in booked
+ ):
+ if self.payment_type == "Receive":
+ party_account = self.paid_from
+ elif self.payment_type == "Pay":
+ party_account = self.paid_to
+
+ dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
+
+ if d.reference_doctype == "Purchase Invoice":
+ dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+
+ reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+
+ gain_loss_account = frappe.get_cached_value(
+ "Company", self.company, "exchange_gain_loss_account"
+ )
+
+ je = create_gain_loss_journal(
+ self.company,
+ self.party_type,
+ self.party,
+ party_account,
+ gain_loss_account,
+ d.exchange_gain_loss,
+ dr_or_cr,
+ reverse_dr_or_cr,
+ d.reference_doctype,
+ d.reference_name,
+ d.idx,
+ self.doctype,
+ self.name,
+ d.idx,
+ )
+ frappe.msgprint(
+ _("Exchange Gain/Loss amount has been booked through {0}").format(
+ get_link_to_form("Journal Entry", je)
+ )
)
- )
def update_against_document_in_jv(self):
"""
@@ -1087,9 +1211,15 @@ class AccountsController(TransactionBase):
reconcile_against_document(lst)
def on_cancel(self):
- from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
+ from erpnext.accounts.utils import (
+ cancel_exchange_gain_loss_journal,
+ unlink_ref_doc_from_payment_entries,
+ )
+
+ if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
+ # Cancel Exchange Gain/Loss Journal before unlinking
+ cancel_exchange_gain_loss_journal(self)
- if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
unlink_ref_doc_from_payment_entries(self)
@@ -1676,8 +1806,13 @@ class AccountsController(TransactionBase):
)
self.append("payment_schedule", data)
+ allocate_payment_based_on_payment_terms = frappe.db.get_value(
+ "Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
+ )
+
if not (
automatically_fetch_payment_terms
+ and allocate_payment_based_on_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
for d in self.get("payment_schedule"):
diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py
index d86607d8db..59f13c6dfa 100644
--- a/erpnext/controllers/print_settings.py
+++ b/erpnext/controllers/print_settings.py
@@ -14,9 +14,6 @@ def set_print_templates_for_item_table(doc, settings):
}
}
- if doc.meta.get_field("items"):
- doc.meta.get_field("items").hide_in_print_layout = ["uom", "stock_uom"]
-
doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"]
if settings.compact_item_print:
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 3bb11282f1..5ec24743d9 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -822,6 +822,15 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters)
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_doctypes_for_closing(doctype, txt, searchfield, start, page_len, filters):
+ doctypes = frappe.get_hooks("period_closing_doctypes")
+ if txt:
+ doctypes = [d for d in doctypes if txt.lower() in d.lower()]
+ return [(d,) for d in set(doctypes)]
+
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
@@ -865,3 +874,18 @@ def get_fields(doctype, fields=None):
fields.insert(1, meta.title_field.strip())
return unique(fields)
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, filters) -> list:
+ terms = []
+ if filters:
+ terms = frappe.db.get_all(
+ "Payment Schedule",
+ filters={"parent": filters.get("reference")},
+ fields=["payment_term"],
+ limit=page_len,
+ as_list=1,
+ )
+ return terms
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 58cab147a4..73a248fb53 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import comma_or, flt, getdate, now, nowdate
+from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate
class OverAllowanceError(frappe.ValidationError):
@@ -233,6 +233,18 @@ class StatusUpdater(Document):
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
+ if not frappe.db.get_single_value("Selling Settings", "allow_negative_rates_for_items"):
+ if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0:
+ frappe.throw(
+ _(
+ "For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}"
+ ).format(
+ frappe.bold(d.item_code),
+ frappe.bold(_("`Allow Negative rates for Items`")),
+ get_link_to_form("Selling Settings", "Selling Settings"),
+ ),
+ )
+
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
args["name"] = d.get(args["join_field"])
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index caf4b6f18b..d669abe910 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -15,7 +15,7 @@ from erpnext.accounts.general_ledger import (
make_reverse_gl_entries,
process_gl_map,
)
-from erpnext.accounts.utils import get_fiscal_year
+from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
@@ -534,6 +534,7 @@ class StockController(AccountsController):
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
def make_gl_entries_on_cancel(self):
+ cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
if frappe.db.sql(
"""select name from `tabGL Entry` where voucher_type=%s
and voucher_no=%s""",
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 57339bf4ca..6633f4f6eb 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -550,7 +550,7 @@ class SubcontractingController(StockController):
if rm_obj.serial_and_batch_bundle:
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
- rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
+ rm_obj.rate = get_incoming_rate(args)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 4661c5ca7e..62d4c53868 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -18,6 +18,7 @@ from erpnext.controllers.accounts_controller import (
validate_taxes_and_charges,
)
from erpnext.stock.get_item_details import _get_item_tax_template
+from erpnext.utilities.regional import temporary_flag
class calculate_taxes_and_totals(object):
@@ -942,7 +943,6 @@ class calculate_taxes_and_totals(object):
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
return
- frappe.flags.company = doc.company
# get headers
tax_accounts = []
@@ -952,22 +952,17 @@ def get_itemised_tax_breakup_html(doc):
if tax.description not in tax_accounts:
tax_accounts.append(tax.description)
- headers = get_itemised_tax_breakup_header(doc.doctype + " Item", tax_accounts)
-
- # get tax breakup data
- itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(doc)
-
- get_rounded_tax_amount(itemised_tax, doc.precision("tax_amount", "taxes"))
-
- update_itemised_tax_data(doc)
- frappe.flags.company = None
+ with temporary_flag("company", doc.company):
+ headers = get_itemised_tax_breakup_header(doc.doctype + " Item", tax_accounts)
+ itemised_tax_data = get_itemised_tax_breakup_data(doc)
+ get_rounded_tax_amount(itemised_tax_data, doc.precision("tax_amount", "taxes"))
+ update_itemised_tax_data(doc)
return frappe.render_template(
"templates/includes/itemised_tax_breakup.html",
dict(
headers=headers,
- itemised_tax=itemised_tax,
- itemised_taxable_amount=itemised_taxable_amount,
+ itemised_tax_data=itemised_tax_data,
tax_accounts=tax_accounts,
doc=doc,
),
@@ -977,10 +972,8 @@ def get_itemised_tax_breakup_html(doc):
@frappe.whitelist()
def get_round_off_applicable_accounts(company, account_list):
# required to set correct region
- frappe.flags.company = company
- account_list = get_regional_round_off_accounts(company, account_list)
-
- return account_list
+ with temporary_flag("company", company):
+ return get_regional_round_off_accounts(company, account_list)
@erpnext.allow_regional
@@ -1005,7 +998,15 @@ def get_itemised_tax_breakup_data(doc):
itemised_taxable_amount = get_itemised_taxable_amount(doc.items)
- return itemised_tax, itemised_taxable_amount
+ itemised_tax_data = []
+ for item_code, taxes in itemised_tax.items():
+ itemised_tax_data.append(
+ frappe._dict(
+ {"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code), **taxes}
+ )
+ )
+
+ return itemised_tax_data
def get_itemised_tax(taxes, with_tax_account=False):
@@ -1050,9 +1051,10 @@ def get_itemised_taxable_amount(items):
def get_rounded_tax_amount(itemised_tax, precision):
# Rounding based on tax_amount precision
- for taxes in itemised_tax.values():
- for tax_account in taxes:
- taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision)
+ for taxes in itemised_tax:
+ for row in taxes.values():
+ if isinstance(row, dict) and isinstance(row["tax_amount"], float):
+ row["tax_amount"] = flt(row["tax_amount"], precision)
class init_landed_taxes_and_totals(object):
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
new file mode 100644
index 0000000000..0f8e133e0f
--- /dev/null
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -0,0 +1,999 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import unittest
+
+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, flt, nowdate
+
+from erpnext import get_default_cost_center
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.party import get_party_account
+from erpnext.stock.doctype.item.test_item import create_item
+
+
+def make_customer(customer_name, currency=None):
+ if not frappe.db.exists("Customer", customer_name):
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = customer_name
+ customer.customer_type = "Individual"
+
+ if currency:
+ customer.default_currency = currency
+ customer.save()
+ return customer.name
+ else:
+ return customer_name
+
+
+def make_supplier(supplier_name, currency=None):
+ if not frappe.db.exists("Supplier", supplier_name):
+ supplier = frappe.new_doc("Supplier")
+ supplier.supplier_name = supplier_name
+ supplier.supplier_type = "Individual"
+ supplier.supplier_group = "All Supplier Groups"
+
+ if currency:
+ supplier.default_currency = currency
+ supplier.save()
+ return supplier.name
+ else:
+ return supplier_name
+
+
+class TestAccountsController(FrappeTestCase):
+ """
+ Test Exchange Gain/Loss booking on various scenarios.
+ Test Cases are numbered for better organization
+
+ 10 series - Sales Invoice against Payment Entries
+ 20 series - Sales Invoice against Journals
+ 30 series - Sales Invoice against Credit Notes
+ """
+
+ def setUp(self):
+ self.create_company()
+ self.create_account()
+ self.create_item()
+ self.create_parties()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_company(self):
+ company_name = "_Test Company"
+ self.company_abbr = abbr = "_TC"
+ if frappe.db.exists("Company", company_name):
+ company = frappe.get_doc("Company", company_name)
+ else:
+ company = frappe.get_doc(
+ {
+ "doctype": "Company",
+ "company_name": company_name,
+ "country": "India",
+ "default_currency": "INR",
+ "create_chart_of_accounts_based_on": "Standard Template",
+ "chart_of_accounts": "Standard",
+ }
+ )
+ company = company.save()
+
+ self.company = company.name
+ self.cost_center = company.cost_center
+ self.warehouse = "Stores - " + abbr
+ self.finished_warehouse = "Finished Goods - " + abbr
+ self.income_account = "Sales - " + abbr
+ self.expense_account = "Cost of Goods Sold - " + abbr
+ self.debit_to = "Debtors - " + abbr
+ self.debit_usd = "Debtors USD - " + abbr
+ self.cash = "Cash - " + abbr
+ self.creditors = "Creditors - " + abbr
+
+ def create_item(self):
+ item = create_item(
+ item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse
+ )
+ self.item = item if isinstance(item, str) else item.item_code
+
+ def create_parties(self):
+ self.create_customer()
+ self.create_supplier()
+
+ def create_customer(self):
+ self.customer = make_customer("_Test MC Customer USD", "USD")
+
+ def create_supplier(self):
+ self.supplier = make_supplier("_Test MC Supplier USD", "USD")
+
+ def create_account(self):
+ account_name = "Debtors USD"
+ if not frappe.db.get_value(
+ "Account", filters={"account_name": account_name, "company": self.company}
+ ):
+ acc = frappe.new_doc("Account")
+ acc.account_name = account_name
+ acc.parent_account = "Accounts Receivable - " + self.company_abbr
+ acc.company = self.company
+ acc.account_currency = "USD"
+ acc.account_type = "Receivable"
+ acc.insert()
+ else:
+ name = frappe.db.get_value(
+ "Account",
+ filters={"account_name": account_name, "company": self.company},
+ fieldname="name",
+ pluck=True,
+ )
+ acc = frappe.get_doc("Account", name)
+ self.debtors_usd = acc.name
+
+ def create_sales_invoice(
+ self,
+ qty=1,
+ rate=1,
+ conversion_rate=80,
+ posting_date=nowdate(),
+ do_not_save=False,
+ do_not_submit=False,
+ ):
+ """
+ Helper function to populate default values in sales invoice
+ """
+ sinv = create_sales_invoice(
+ qty=qty,
+ rate=rate,
+ company=self.company,
+ customer=self.customer,
+ item_code=self.item,
+ item_name=self.item,
+ cost_center=self.cost_center,
+ warehouse=self.warehouse,
+ debit_to=self.debit_usd,
+ parent_cost_center=self.cost_center,
+ update_stock=0,
+ currency="USD",
+ conversion_rate=conversion_rate,
+ is_pos=0,
+ is_return=0,
+ return_against=None,
+ income_account=self.income_account,
+ expense_account=self.expense_account,
+ do_not_save=do_not_save,
+ do_not_submit=do_not_submit,
+ )
+ return sinv
+
+ def create_payment_entry(
+ self, amount=1, source_exc_rate=75, posting_date=nowdate(), customer=None
+ ):
+ """
+ Helper function to populate default values in payment entry
+ """
+ payment = create_payment_entry(
+ company=self.company,
+ payment_type="Receive",
+ party_type="Customer",
+ party=customer or self.customer,
+ paid_from=self.debit_usd,
+ paid_to=self.cash,
+ paid_amount=amount,
+ )
+ payment.source_exchange_rate = source_exc_rate
+ payment.received_amount = source_exc_rate * amount
+ payment.posting_date = posting_date
+ return payment
+
+ def clear_old_entries(self):
+ doctype_list = [
+ "GL Entry",
+ "Payment Ledger Entry",
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Payment Entry",
+ "Journal Entry",
+ ]
+ for doctype in doctype_list:
+ qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
+
+ def create_payment_reconciliation(self):
+ pr = frappe.new_doc("Payment Reconciliation")
+ pr.company = self.company
+ pr.party_type = "Customer"
+ pr.party = self.customer
+ pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
+ pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
+ return pr
+
+ def create_journal_entry(
+ self,
+ acc1=None,
+ acc1_exc_rate=None,
+ acc2_exc_rate=None,
+ acc2=None,
+ acc1_amount=0,
+ acc2_amount=0,
+ posting_date=None,
+ cost_center=None,
+ ):
+ je = frappe.new_doc("Journal Entry")
+ je.posting_date = posting_date or nowdate()
+ je.company = self.company
+ je.user_remark = "test"
+ je.multi_currency = True
+ if not cost_center:
+ cost_center = self.cost_center
+ je.set(
+ "accounts",
+ [
+ {
+ "account": acc1,
+ "exchange_rate": acc1_exc_rate or 1,
+ "cost_center": cost_center,
+ "debit_in_account_currency": acc1_amount if acc1_amount > 0 else 0,
+ "credit_in_account_currency": abs(acc1_amount) if acc1_amount < 0 else 0,
+ "debit": acc1_amount * acc1_exc_rate if acc1_amount > 0 else 0,
+ "credit": abs(acc1_amount * acc1_exc_rate) if acc1_amount < 0 else 0,
+ },
+ {
+ "account": acc2,
+ "exchange_rate": acc2_exc_rate or 1,
+ "cost_center": cost_center,
+ "credit_in_account_currency": acc2_amount if acc2_amount > 0 else 0,
+ "debit_in_account_currency": abs(acc2_amount) if acc2_amount < 0 else 0,
+ "credit": acc2_amount * acc2_exc_rate if acc2_amount > 0 else 0,
+ "debit": abs(acc2_amount * acc2_exc_rate) if acc2_amount < 0 else 0,
+ },
+ ],
+ )
+ return je
+
+ 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 assert_ledger_outstanding(
+ self,
+ voucher_type: str,
+ voucher_no: str,
+ outstanding: float,
+ outstanding_in_account_currency: float,
+ ) -> None:
+ """
+ Assert outstanding amount based on ledger on both company/base currency and account currency
+ """
+
+ ple = qb.DocType("Payment Ledger Entry")
+ current_outstanding = (
+ qb.from_(ple)
+ .select(
+ Sum(ple.amount).as_("outstanding"),
+ Sum(ple.amount_in_account_currency).as_("outstanding_in_account_currency"),
+ )
+ .where(
+ (ple.against_voucher_type == voucher_type)
+ & (ple.against_voucher_no == voucher_no)
+ & (ple.delinked == 0)
+ )
+ .run(as_dict=True)[0]
+ )
+ self.assertEqual(outstanding, current_outstanding.outstanding)
+ self.assertEqual(
+ outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency
+ )
+
+ def test_10_payment_against_sales_invoice(self):
+ # Sales Invoice in Foreign Currency
+ rate = 80
+ rate_in_account_currency = 1
+
+ si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency)
+
+ # Test payments with different exchange rates
+ for exc_rate in [75.9, 83.1, 80.01]:
+ with self.subTest(exc_rate=exc_rate):
+ pe = self.create_payment_entry(amount=1, source_exc_rate=exc_rate).save()
+ pe.append(
+ "references",
+ {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
+ )
+ pe = pe.save().submit()
+
+ # Outstanding in both currencies should be '0'
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Exchange Gain/Loss Journal should've been created.
+ 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.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_pe), 1)
+ self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
+
+ # Cancel Payment
+ pe.cancel()
+
+ # outstanding should be same as grand total
+ si.reload()
+ self.assertEqual(si.outstanding_amount, rate_in_account_currency)
+ self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency)
+
+ # Exchange Gain/Loss Journal should've been cancelled
+ 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, [])
+ self.assertEqual(exc_je_for_pe, [])
+
+ def test_11_advance_against_sales_invoice(self):
+ # Advance Payment
+ adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
+ adv.reload()
+
+ # Sales Invoices in different exchange rates
+ for exc_rate in [75.9, 83.1, 80.01]:
+ with self.subTest(exc_rate=exc_rate):
+ si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
+ advances = si.get_advance_entries()
+ self.assertEqual(len(advances), 1)
+ self.assertEqual(advances[0].reference_name, adv.name)
+ si.append(
+ "advances",
+ {
+ "doctype": "Sales Invoice Advance",
+ "reference_type": advances[0].reference_type,
+ "reference_name": advances[0].reference_name,
+ "reference_row": advances[0].reference_row,
+ "advance_amount": 1,
+ "allocated_amount": 1,
+ "ref_exchange_rate": advances[0].exchange_rate,
+ "remarks": advances[0].remarks,
+ },
+ )
+
+ si = si.save()
+ si = si.submit()
+
+ # Outstanding in both currencies should be '0'
+ adv.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Exchange Gain/Loss Journal should've been created.
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_adv), 1)
+ self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+ # Cancel Invoice
+ si.cancel()
+
+ # Exchange Gain/Loss Journal should've been cancelled
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ self.assertEqual(exc_je_for_si, [])
+ self.assertEqual(exc_je_for_adv, [])
+
+ def test_12_partial_advance_and_payment_for_sales_invoice(self):
+ """
+ Sales invoice with partial advance payment, and a normal payment reconciled
+ """
+ # Partial Advance
+ adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
+ adv.reload()
+
+ # sales invoice with advance(partial amount)
+ rate = 80
+ rate_in_account_currency = 1
+ si = self.create_sales_invoice(
+ qty=2, conversion_rate=80, rate=rate_in_account_currency, do_not_submit=True
+ )
+ advances = si.get_advance_entries()
+ self.assertEqual(len(advances), 1)
+ self.assertEqual(advances[0].reference_name, adv.name)
+ si.append(
+ "advances",
+ {
+ "doctype": "Sales Invoice Advance",
+ "reference_type": advances[0].reference_type,
+ "reference_name": advances[0].reference_name,
+ "advance_amount": 1,
+ "allocated_amount": 1,
+ "ref_exchange_rate": advances[0].exchange_rate,
+ "remarks": advances[0].remarks,
+ },
+ )
+ si = si.save()
+ si = si.submit()
+
+ # Outstanding should be there in both currencies
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1) # account currency
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+ # Exchange Gain/Loss Journal should've been created for the partial advance
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_adv), 1)
+ self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+ # Payment for remaining amount
+ pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
+ pe.append(
+ "references",
+ {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
+ )
+ pe = pe.save().submit()
+
+ # Outstanding in both currencies should be '0'
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Exchange Gain/Loss Journal should've been created for the payment
+ 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.assertNotEqual(exc_je_for_si, [])
+ # There should be 2 JE's now. One for the advance and one for the payment
+ self.assertEqual(len(exc_je_for_si), 2)
+ self.assertEqual(len(exc_je_for_pe), 1)
+ self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
+
+ # Cancel Invoice
+ si.reload()
+ si.cancel()
+
+ # Exchange Gain/Loss Journal should been cancelled
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ self.assertEqual(exc_je_for_si, [])
+ self.assertEqual(exc_je_for_pe, [])
+ self.assertEqual(exc_je_for_adv, [])
+
+ def test_13_partial_advance_and_payment_for_invoice_with_cancellation(self):
+ """
+ Invoice with partial advance payment, and a normal payment. Then cancel advance and payment.
+ """
+ # Partial Advance
+ adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
+ adv.reload()
+
+ # invoice with advance(partial amount)
+ si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1, do_not_submit=True)
+ advances = si.get_advance_entries()
+ self.assertEqual(len(advances), 1)
+ self.assertEqual(advances[0].reference_name, adv.name)
+ si.append(
+ "advances",
+ {
+ "doctype": "Sales Invoice Advance",
+ "reference_type": advances[0].reference_type,
+ "reference_name": advances[0].reference_name,
+ "advance_amount": 1,
+ "allocated_amount": 1,
+ "ref_exchange_rate": advances[0].exchange_rate,
+ "remarks": advances[0].remarks,
+ },
+ )
+ si = si.save()
+ si = si.submit()
+
+ # Outstanding should be there in both currencies
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1) # account currency
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+ # Exchange Gain/Loss Journal should've been created for the partial advance
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_adv), 1)
+ self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+ # Payment(remaining amount)
+ pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
+ pe.append(
+ "references",
+ {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
+ )
+ pe = pe.save().submit()
+
+ # Outstanding should be '0' in both currencies
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Exchange Gain/Loss Journal should've been created for the payment
+ 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.assertNotEqual(exc_je_for_si, [])
+ # There should be 2 JE's now. One for the advance and one for the payment
+ self.assertEqual(len(exc_je_for_si), 2)
+ self.assertEqual(len(exc_je_for_pe), 1)
+ self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
+
+ adv.reload()
+ adv.cancel()
+
+ # Outstanding should be there in both currencies, since advance is cancelled.
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1) # account currency
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ # Exchange Gain/Loss Journal for advance should been cancelled
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_pe), 1)
+ self.assertEqual(exc_je_for_adv, [])
+
+ def test_14_same_payment_split_against_invoice(self):
+ # Invoice in Foreign Currency
+ si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
+ # Payment
+ pe = self.create_payment_entry(amount=2, source_exc_rate=75).save()
+ pe.append(
+ "references",
+ {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
+ )
+ pe = pe.save().submit()
+
+ # There should be outstanding in both currencies
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1)
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+ # Exchange Gain/Loss Journal should've been created.
+ 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.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_pe), 1)
+ self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
+
+ # Reconcile the remaining amount
+ pr = frappe.get_doc("Payment Reconciliation")
+ pr.company = self.company
+ pr.party_type = "Customer"
+ pr.party = self.customer
+ pr.receivable_payable_account = self.debit_usd
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [x.as_dict() for x in pr.payments]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.reconcile()
+ self.assertEqual(len(pr.invoices), 0)
+ self.assertEqual(len(pr.payments), 0)
+
+ # Exc gain/loss journal should have been creaetd for the reconciled amount
+ 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(len(exc_je_for_si), 2)
+ self.assertEqual(len(exc_je_for_pe), 2)
+ self.assertEqual(exc_je_for_si, exc_je_for_pe)
+
+ # There should be no outstanding
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Cancel Payment
+ pe.reload()
+ pe.cancel()
+
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 2)
+ self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
+
+ # Exchange Gain/Loss Journal should've been cancelled
+ 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, [])
+ self.assertEqual(exc_je_for_pe, [])
+
+ def test_20_journal_against_sales_invoice(self):
+ # Invoice in Foreign Currency
+ si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
+ # Payment
+ je = self.create_journal_entry(
+ acc1=self.debit_usd,
+ acc1_exc_rate=75,
+ acc2=self.cash,
+ acc1_amount=-1,
+ acc2_amount=-75,
+ acc2_exc_rate=1,
+ )
+ je.accounts[0].party_type = "Customer"
+ je.accounts[0].party = self.customer
+ je = je.save().submit()
+
+ # Reconcile the remaining amount
+ pr = self.create_payment_reconciliation()
+ # pr.receivable_payable_account = self.debit_usd
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [x.as_dict() for x in pr.payments]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.reconcile()
+ self.assertEqual(len(pr.invoices), 0)
+ self.assertEqual(len(pr.payments), 0)
+
+ # There should be no outstanding in both currencies
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Exchange Gain/Loss Journal should've been created.
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(
+ len(exc_je_for_si), 2
+ ) # payment also has reference. so, there are 2 journals referencing invoice
+ self.assertEqual(len(exc_je_for_je), 1)
+ self.assertIn(exc_je_for_je[0], exc_je_for_si)
+
+ # Cancel Payment
+ je.reload()
+ je.cancel()
+
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1)
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+ # Exchange Gain/Loss Journal should've been cancelled
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+ self.assertEqual(exc_je_for_si, [])
+ self.assertEqual(exc_je_for_je, [])
+
+ def test_21_advance_journal_against_sales_invoice(self):
+ # Advance Payment
+ adv_exc_rate = 80
+ adv = self.create_journal_entry(
+ acc1=self.debit_usd,
+ acc1_exc_rate=adv_exc_rate,
+ acc2=self.cash,
+ acc1_amount=-1,
+ acc2_amount=adv_exc_rate * -1,
+ acc2_exc_rate=1,
+ )
+ adv.accounts[0].party_type = "Customer"
+ adv.accounts[0].party = self.customer
+ adv.accounts[0].is_advance = "Yes"
+ adv = adv.save().submit()
+ adv.reload()
+
+ # Sales Invoices in different exchange rates
+ for exc_rate in [75.9, 83.1]:
+ with self.subTest(exc_rate=exc_rate):
+ si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
+ advances = si.get_advance_entries()
+ self.assertEqual(len(advances), 1)
+ self.assertEqual(advances[0].reference_name, adv.name)
+ si.append(
+ "advances",
+ {
+ "doctype": "Sales Invoice Advance",
+ "reference_type": advances[0].reference_type,
+ "reference_name": advances[0].reference_name,
+ "reference_row": advances[0].reference_row,
+ "advance_amount": 1,
+ "allocated_amount": 1,
+ "ref_exchange_rate": advances[0].exchange_rate,
+ "remarks": advances[0].remarks,
+ },
+ )
+
+ si = si.save()
+ si = si.submit()
+
+ # Outstanding in both currencies should be '0'
+ adv.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Exchange Gain/Loss Journal should've been created.
+ exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name]
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_adv), 1)
+ self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+ # Cancel Invoice
+ si.cancel()
+
+ # Exchange Gain/Loss Journal should've been cancelled
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ self.assertEqual(exc_je_for_si, [])
+ self.assertEqual(exc_je_for_adv, [])
+
+ def test_22_partial_advance_and_payment_for_invoice_with_cancellation(self):
+ """
+ Invoice with partial advance payment as Journal, and a normal payment. Then cancel advance and payment.
+ """
+ # Partial Advance
+ adv_exc_rate = 75
+ adv = self.create_journal_entry(
+ acc1=self.debit_usd,
+ acc1_exc_rate=adv_exc_rate,
+ acc2=self.cash,
+ acc1_amount=-1,
+ acc2_amount=adv_exc_rate * -1,
+ acc2_exc_rate=1,
+ )
+ adv.accounts[0].party_type = "Customer"
+ adv.accounts[0].party = self.customer
+ adv.accounts[0].is_advance = "Yes"
+ adv = adv.save().submit()
+ adv.reload()
+
+ # invoice with advance(partial amount)
+ si = self.create_sales_invoice(qty=3, conversion_rate=80, rate=1, do_not_submit=True)
+ advances = si.get_advance_entries()
+ self.assertEqual(len(advances), 1)
+ self.assertEqual(advances[0].reference_name, adv.name)
+ si.append(
+ "advances",
+ {
+ "doctype": "Sales Invoice Advance",
+ "reference_type": advances[0].reference_type,
+ "reference_name": advances[0].reference_name,
+ "reference_row": advances[0].reference_row,
+ "advance_amount": 1,
+ "allocated_amount": 1,
+ "ref_exchange_rate": advances[0].exchange_rate,
+ "remarks": advances[0].remarks,
+ },
+ )
+
+ si = si.save()
+ si = si.submit()
+
+ # Outstanding should be there in both currencies
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 2) # account currency
+ self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
+
+ # Exchange Gain/Loss Journal should've been created for the partial advance
+ exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name]
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_adv), 1)
+ self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+ # Payment
+ adv2_exc_rate = 83
+ pay = self.create_journal_entry(
+ acc1=self.debit_usd,
+ acc1_exc_rate=adv2_exc_rate,
+ acc2=self.cash,
+ acc1_amount=-2,
+ acc2_amount=adv2_exc_rate * -2,
+ acc2_exc_rate=1,
+ )
+ pay.accounts[0].party_type = "Customer"
+ pay.accounts[0].party = self.customer
+ pay.accounts[0].is_advance = "Yes"
+ pay = pay.save().submit()
+ pay.reload()
+
+ # Reconcile the remaining amount
+ pr = self.create_payment_reconciliation()
+ # pr.receivable_payable_account = self.debit_usd
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [x.as_dict() for x in pr.payments]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.reconcile()
+ self.assertEqual(len(pr.invoices), 0)
+ self.assertEqual(len(pr.payments), 0)
+
+ # Outstanding should be '0' in both currencies
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Exchange Gain/Loss Journal should've been created for the payment
+ exc_je_for_si = [
+ x
+ for x in self.get_journals_for(si.doctype, si.name)
+ if x.parent != adv.name and x.parent != pay.name
+ ]
+ exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ # There should be 2 JE's now. One for the advance and one for the payment
+ self.assertEqual(len(exc_je_for_si), 2)
+ self.assertEqual(len(exc_je_for_pe), 1)
+ self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
+
+ adv.reload()
+ adv.cancel()
+
+ # Outstanding should be there in both currencies, since advance is cancelled.
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1) # account currency
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+ exc_je_for_si = [
+ x
+ for x in self.get_journals_for(si.doctype, si.name)
+ if x.parent != adv.name and x.parent != pay.name
+ ]
+ exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name)
+ exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+ # Exchange Gain/Loss Journal for advance should been cancelled
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_pe), 1)
+ self.assertEqual(exc_je_for_adv, [])
+
+ def test_23_same_journal_split_against_single_invoice(self):
+ # Invoice in Foreign Currency
+ si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
+ # Payment
+ je = self.create_journal_entry(
+ acc1=self.debit_usd,
+ acc1_exc_rate=75,
+ acc2=self.cash,
+ acc1_amount=-2,
+ acc2_amount=-150,
+ acc2_exc_rate=1,
+ )
+ je.accounts[0].party_type = "Customer"
+ je.accounts[0].party = self.customer
+ je = je.save().submit()
+
+ # Reconcile the first half
+ pr = self.create_payment_reconciliation()
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [x.as_dict() for x in pr.payments]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ difference_amount = pr.calculate_difference_on_allocation_change(
+ [x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1
+ )
+ pr.allocation[0].allocated_amount = 1
+ pr.allocation[0].difference_amount = difference_amount
+ pr.reconcile()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+
+ # There should be outstanding in both currencies
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1)
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+ # Exchange Gain/Loss Journal should've been created.
+ exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
+ exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_je), 1)
+ self.assertIn(exc_je_for_je[0], exc_je_for_si)
+
+ # reconcile remaining half
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [x.as_dict() for x in pr.payments]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.allocation[0].allocated_amount = 1
+ pr.allocation[0].difference_amount = difference_amount
+ pr.reconcile()
+ self.assertEqual(len(pr.invoices), 0)
+ self.assertEqual(len(pr.payments), 0)
+
+ # Exchange Gain/Loss Journal should've been created.
+ exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
+ exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 2)
+ self.assertEqual(len(exc_je_for_je), 2)
+ self.assertIn(exc_je_for_je[0], exc_je_for_si)
+
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+ # Cancel Payment
+ je.reload()
+ je.cancel()
+
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 2)
+ self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
+
+ # Exchange Gain/Loss Journal should've been cancelled
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+ self.assertEqual(exc_je_for_si, [])
+ self.assertEqual(exc_je_for_je, [])
+
+ def test_30_cr_note_against_sales_invoice(self):
+ """
+ Reconciling Cr Note against Sales Invoice, both having different exchange rates
+ """
+ # Invoice in Foreign currency
+ si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
+
+ # Cr Note in Foreign currency of different exchange rate
+ cr_note = self.create_sales_invoice(qty=-2, conversion_rate=75, rate=1, do_not_save=True)
+ cr_note.is_return = 1
+ cr_note.save().submit()
+
+ # Reconcile the first half
+ pr = self.create_payment_reconciliation()
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [x.as_dict() for x in pr.payments]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ difference_amount = pr.calculate_difference_on_allocation_change(
+ [x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1
+ )
+ pr.allocation[0].allocated_amount = 1
+ pr.allocation[0].difference_amount = difference_amount
+ pr.reconcile()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+
+ # Exchange Gain/Loss Journal should've been created.
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 2)
+ self.assertEqual(len(exc_je_for_cr), 2)
+ self.assertEqual(exc_je_for_cr, exc_je_for_si)
+
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1)
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+ cr_note.reload()
+ cr_note.cancel()
+
+ # Exchange Gain/Loss Journal should've been created.
+ exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+ exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(len(exc_je_for_cr), 0)
+
+ # The Credit Note JE is still active and is referencing the sales invoice
+ # So, outstanding stays the same
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 1)
+ self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py
index 642722ae6b..01b6f5ceba 100644
--- a/erpnext/controllers/website_list_for_contact.py
+++ b/erpnext/controllers/website_list_for_contact.py
@@ -206,9 +206,11 @@ def post_process(doctype, data):
)
if doc.get("per_delivered"):
- doc.status_percent += flt(doc.per_delivered)
+ doc.status_percent += flt(doc.per_delivered, 2)
doc.status_display.append(
- _("Delivered") if doc.per_delivered == 100 else _("{0}% Delivered").format(doc.per_delivered)
+ _("Delivered")
+ if flt(doc.per_delivered, 2) == 100
+ else _("{0}% Delivered").format(doc.per_delivered)
)
if hasattr(doc, "set_indicator"):
diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js
index 9ac54183a2..b1799cee6c 100644
--- a/erpnext/crm/doctype/lead/lead.js
+++ b/erpnext/crm/doctype/lead/lead.js
@@ -54,6 +54,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
}
add_lead_to_prospect () {
+ let me = this;
frappe.prompt([
{
fieldname: 'prospect',
@@ -67,12 +68,12 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
frappe.call({
method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect',
args: {
- 'lead': cur_frm.doc.name,
+ 'lead': me.frm.doc.name,
'prospect': data.prospect
},
callback: function(r) {
if (!r.exc) {
- frm.reload_doc();
+ me.frm.reload_doc();
}
},
freeze: true,
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index a98886c648..105c58d110 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -182,7 +182,7 @@ class Lead(SellingController, CRMNote):
"last_name": self.last_name,
"salutation": self.salutation,
"gender": self.gender,
- "job_title": self.job_title,
+ "designation": self.job_title,
"company_name": self.company_name,
}
)
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index b2617955a3..6ef82971f5 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -1,10 +1,10 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-
-{% include 'erpnext/selling/sales_common.js' %}
frappe.provide("erpnext.crm");
+erpnext.pre_sales.set_as_lost("Quotation");
+erpnext.sales_common.setup_selling_controller();
+
-cur_frm.email_field = "contact_email";
frappe.ui.form.on("Opportunity", {
setup: function(frm) {
frm.custom_make_buttons = {
@@ -19,6 +19,8 @@ frappe.ui.form.on("Opportunity", {
}
}
});
+
+ frm.email_field = "contact_email";
},
validate: function(frm) {
@@ -46,10 +48,6 @@ frappe.ui.form.on("Opportunity", {
}
},
- onload_post_render: function(frm) {
- frm.get_field("items").grid.set_multiple_add("item_code", "qty");
- },
-
status:function(frm){
if (frm.doc.status == "Lost"){
frm.trigger('set_as_lost_dialog');
@@ -252,13 +250,13 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
onload() {
if(!this.frm.doc.status) {
- frm.set_value('status', 'Open');
+ this.frm.set_value('status', 'Open');
}
if(!this.frm.doc.company && frappe.defaults.get_user_default("Company")) {
- frm.set_value('company', frappe.defaults.get_user_default("Company"));
+ this.frm.set_value('company', frappe.defaults.get_user_default("Company"));
}
if(!this.frm.doc.currency) {
- frm.set_value('currency', frappe.defaults.get_user_default("Currency"));
+ this.frm.set_value('currency', frappe.defaults.get_user_default("Currency"));
}
this.setup_queries();
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
index 42874ddeea..442aa77a5f 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
@@ -10,7 +10,6 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import get_url_to_form
from frappe.utils.file_manager import get_file_path
-from tweepy.error import TweepError
class TwitterSettings(Document):
@@ -21,20 +20,22 @@ class TwitterSettings(Document):
frappe.utils.get_url()
)
)
- auth = tweepy.OAuthHandler(
+ auth = tweepy.OAuth1UserHandler(
self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url
)
try:
redirect_url = auth.get_authorization_url()
return redirect_url
- except tweepy.TweepError as e:
+ except (tweepy.TweepyException, tweepy.HTTPException) as e:
frappe.msgprint(_("Error! Failed to get request token."))
frappe.throw(
_("Invalid {0} or {1}").format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))
)
def get_access_token(self, oauth_token, oauth_verifier):
- auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
+ auth = tweepy.OAuth1UserHandler(
+ self.consumer_key, self.get_password(fieldname="consumer_secret")
+ )
auth.request_token = {"oauth_token": oauth_token, "oauth_token_secret": oauth_verifier}
try:
@@ -59,13 +60,15 @@ class TwitterSettings(Document):
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings")
- except TweepError as e:
+ except (tweepy.TweepyException, tweepy.HTTPException) as e:
frappe.msgprint(_("Error! Failed to get access token."))
frappe.throw(_("Invalid Consumer Key or Consumer Secret Key"))
def get_api(self):
# authentication of consumer key and secret
- auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
+ auth = tweepy.OAuth1UserHandler(
+ self.consumer_key, self.get_password(fieldname="consumer_secret")
+ )
# authentication of access token and secret
auth.set_access_token(self.access_token, self.access_token_secret)
@@ -96,21 +99,21 @@ class TwitterSettings(Document):
return response
- except TweepError as e:
+ except (tweepy.TweepyException, tweepy.HTTPException) as e:
self.api_error(e)
def delete_tweet(self, tweet_id):
api = self.get_api()
try:
api.destroy_status(tweet_id)
- except TweepError as e:
+ except (tweepy.TweepyException, tweepy.HTTPException) as e:
self.api_error(e)
def get_tweet(self, tweet_id):
api = self.get_api()
try:
response = api.get_status(tweet_id, trim_user=True, include_entities=True)
- except TweepError as e:
+ except (tweepy.TweepyException, tweepy.HTTPException) as e:
self.api_error(e)
return response._json
diff --git a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js
index f29c2c64e1..0c4e7f2dab 100644
--- a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js
+++ b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.js
@@ -6,13 +6,13 @@ frappe.query_reports["Campaign Efficiency"] = {
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
}
]
};
diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
index fe5707af29..4bf82479a1 100644
--- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
+++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["First Response Time for Opportunity"] = {
"filters": [
diff --git a/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js b/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js
index eeb8984513..d7ff9ad538 100644
--- a/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js
+++ b/erpnext/crm/report/lead_conversion_time/lead_conversion_time.js
@@ -1,6 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Lead Conversion Time"] = {
"filters": [
diff --git a/erpnext/crm/report/lead_details/lead_details.js b/erpnext/crm/report/lead_details/lead_details.js
index 2f6d24224f..66611f6c6c 100644
--- a/erpnext/crm/report/lead_details/lead_details.js
+++ b/erpnext/crm/report/lead_details/lead_details.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Lead Details"] = {
"filters": [
diff --git a/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.js b/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.js
index bbfd6ac9ff..6fc52a1afc 100644
--- a/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.js
+++ b/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.js
@@ -6,12 +6,12 @@
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date"),
+ "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
}
]};
diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.js b/erpnext/crm/report/lost_opportunity/lost_opportunity.js
index 927c54df07..8d5923950a 100644
--- a/erpnext/crm/report/lost_opportunity/lost_opportunity.js
+++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Lost Opportunity"] = {
"filters": [
diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js
index 7cd1710a7f..0aa21436bc 100644
--- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js
+++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Opportunity Summary by Sales Stage"] = {
"filters": [
diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js
index 1426f4b6fd..3111121522 100644
--- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js
+++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Sales Pipeline Analytics"] = {
"filters": [
diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
index e560f4ad7d..fe4fee375b 100644
--- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
+++ b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
@@ -1,7 +1,7 @@
{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
{%- set align_class = resolve_class({
'text-right': align == 'Right',
- 'text-centre': align == 'Centre',
+ 'text-center': align == 'Centre',
'text-left': align == 'Left',
}) -%}
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
index 3ba6bb9987..015e943b80 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
@@ -96,7 +96,7 @@ erpnext.integrations.plaidLink = class plaidLink {
}
onScriptLoaded(me) {
- me.linkHandler = Plaid.create({
+ me.linkHandler = Plaid.create({ // eslint-disable-line no-undef
clientName: me.client_name,
product: me.product,
env: me.plaid_env,
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index dab166e49f..41db6b3a72 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -70,6 +70,19 @@ treeviews = [
"Department",
]
+demo_master_doctypes = [
+ "item_group",
+ "item",
+ "customer_group",
+ "supplier_group",
+ "customer",
+ "supplier",
+]
+demo_transaction_doctypes = [
+ "purchase_order",
+ "sales_order",
+]
+
jinja = {
"methods": [
"erpnext.stock.serial_batch_bundle.get_serial_or_batch_nos",
@@ -286,10 +299,34 @@ standard_queries = {
"Customer": "erpnext.controllers.queries.customer_query",
}
+period_closing_doctypes = [
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Journal Entry",
+ "Bank Clearance",
+ "Stock Entry",
+ "Dunning",
+ "Invoice Discounting",
+ "Payment Entry",
+ "Period Closing Voucher",
+ "Process Deferred Accounting",
+ "Asset",
+ "Asset Capitalization",
+ "Asset Repair",
+ "Delivery Note",
+ "Landed Cost Voucher",
+ "Purchase Receipt",
+ "Stock Reconciliation",
+ "Subcontracting Receipt",
+]
+
doc_events = {
"*": {
"validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
},
+ tuple(period_closing_doctypes): {
+ "validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save",
+ },
"Stock Entry": {
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
@@ -335,6 +372,7 @@ doc_events = {
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
],
+ "on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
"on_trash": "erpnext.regional.check_deletion_permission",
},
"Address": {
@@ -386,11 +424,11 @@ scheduler_events = {
],
},
"all": [
- "erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts",
],
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
+ "erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
],
@@ -405,7 +443,6 @@ scheduler_events = {
"erpnext.controllers.accounts_controller.update_invoice_status",
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
- "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
"erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status",
"erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards",
"erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history",
@@ -430,6 +467,7 @@ scheduler_events = {
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
+ "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
@@ -465,15 +503,6 @@ advance_payment_doctypes = ["Sales Order", "Purchase Order"]
invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]
-period_closing_doctypes = [
- "Sales Invoice",
- "Purchase Invoice",
- "Journal Entry",
- "Bank Clearance",
- "Asset",
- "Stock Entry",
-]
-
bank_reconciliation_doctypes = [
"Payment Entry",
"Journal Entry",
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index 17b5aae966..e9867468f9 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -93,6 +93,7 @@ class BOMUpdateLog(Document):
else:
frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
+ queue="long",
update_doc=self,
now=frappe.flags.in_test,
enqueue_after_commit=True,
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
index af115e3e42..a2919b79b8 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
@@ -157,12 +157,19 @@ def get_next_higher_level_boms(
def get_leaf_boms() -> List[str]:
"Get BOMs that have no dependencies."
- return frappe.db.sql_list(
- """select name from `tabBOM` bom
- where docstatus=1 and is_active=1
- and not exists(select bom_no from `tabBOM Item`
- where parent=bom.name and ifnull(bom_no, '')!='')"""
- )
+ bom = frappe.qb.DocType("BOM")
+ bom_item = frappe.qb.DocType("BOM Item")
+
+ boms = (
+ frappe.qb.from_(bom)
+ .left_join(bom_item)
+ .on((bom.name == bom_item.parent) & (bom_item.bom_no != ""))
+ .select(bom.name)
+ .where((bom.docstatus == 1) & (bom.is_active == 1) & (bom_item.bom_no.isnull()))
+ .distinct()
+ ).run(pluck=True)
+
+ return boms
def _generate_dependence_map() -> defaultdict:
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 8e9f542362..f1e6094813 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -344,6 +344,28 @@ frappe.ui.form.on('Job Card', {
if(frm.doc.__islocal)
return;
+ function setCurrentIncrement() {
+ currentIncrement += 1;
+ return currentIncrement;
+ }
+
+ function updateStopwatch(increment) {
+ var hours = Math.floor(increment / 3600);
+ var minutes = Math.floor((increment - (hours * 3600)) / 60);
+ var seconds = increment - (hours * 3600) - (minutes * 60);
+
+ $(section).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString());
+ $(section).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString());
+ $(section).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString());
+ }
+
+ function initialiseTimer() {
+ const interval = setInterval(function() {
+ var current = setCurrentIncrement();
+ updateStopwatch(current);
+ }, 1000);
+ }
+
frm.dashboard.refresh();
const timer = `
{
return {
+ query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
filters: {
- company: doc.company
+ company: frm.doc.company,
}
}
- }
+ });
frm.set_query('for_warehouse', function(doc) {
return {
@@ -42,32 +48,40 @@ frappe.ui.form.on('Production Plan', {
};
});
- frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) {
+ frm.set_query("item_code", "po_items", (doc, cdt, cdn) => {
return {
query: "erpnext.controllers.queries.item_query",
filters:{
'is_stock_item': 1,
}
}
- }
+ });
- frm.fields_dict['po_items'].grid.get_field('bom_no').get_query = function(doc, cdt, cdn) {
+ frm.set_query("bom_no", "po_items", (doc, cdt, cdn) => {
var d = locals[cdt][cdn];
if (d.item_code) {
return {
query: "erpnext.controllers.queries.bom",
- filters:{'item': cstr(d.item_code), 'docstatus': 1}
+ filters:{'item': d.item_code, 'docstatus': 1}
}
} else frappe.msgprint(__("Please enter Item first"));
- }
+ });
- frm.fields_dict['mr_items'].grid.get_field('warehouse').get_query = function(doc) {
+ frm.set_query("warehouse", "mr_items", (doc) => {
return {
filters: {
company: doc.company
}
}
- }
+ });
+
+ frm.set_query("warehouse", "po_items", (doc) => {
+ return {
+ filters: {
+ company: doc.company
+ }
+ }
+ });
},
refresh(frm) {
@@ -436,7 +450,7 @@ frappe.ui.form.on("Production Plan Item", {
}
});
}
- }
+ },
});
frappe.ui.form.on("Material Request Plan Item", {
@@ -467,31 +481,36 @@ frappe.ui.form.on("Material Request Plan Item", {
frappe.ui.form.on("Production Plan Sales Order", {
sales_order(frm, cdt, cdn) {
- const { sales_order } = locals[cdt][cdn];
+ let row = locals[cdt][cdn];
+ const sales_order = row.sales_order;
if (!sales_order) {
return;
}
- frappe.call({
- method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details",
- args: { sales_order },
- callback(r) {
- const {transaction_date, customer, grand_total} = r.message;
- frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date);
- frappe.model.set_value(cdt, cdn, 'customer', customer);
- frappe.model.set_value(cdt, cdn, 'grand_total', grand_total);
- }
- });
+
+ if (row.sales_order) {
+ frm.call({
+ method: "validate_sales_orders",
+ doc: frm.doc,
+ args: {
+ sales_order: row.sales_order,
+ },
+ callback(r) {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details",
+ args: { sales_order },
+ callback(r) {
+ const {transaction_date, customer, grand_total} = r.message;
+ frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date);
+ frappe.model.set_value(cdt, cdn, 'customer', customer);
+ frappe.model.set_value(cdt, cdn, 'grand_total', grand_total);
+ }
+ });
+ }
+ });
+ }
}
});
-cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function() {
- return{
- filters: [
- ['Sales Order','docstatus', '=' ,1]
- ]
- }
-};
-
frappe.tour['Production Plan'] = [
{
fieldname: "get_items_from",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 232f1cb2c4..0d0fd5e270 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -228,10 +228,10 @@
},
{
"default": "0",
- "description": "To know more about projected quantity,
click here.",
+ "description": "If enabled, the system won't create material requests for the available items.",
"fieldname": "ignore_existing_ordered_qty",
"fieldtype": "Check",
- "label": "Ignore Existing Projected Quantity"
+ "label": "Ignore Available Stock"
},
{
"fieldname": "column_break_25",
@@ -339,7 +339,7 @@
"depends_on": "eval:doc.get_items_from == 'Sales Order'",
"fieldname": "combine_items",
"fieldtype": "Check",
- "label": "Consolidate Items"
+ "label": "Consolidate Sales Order Items"
},
{
"fieldname": "section_break_25",
@@ -399,7 +399,7 @@
},
{
"default": "0",
- "description": "System consider the projected quantity to check available or will be available sub-assembly items ",
+ "description": "If this checkbox is enabled, then the system won\u2019t run the MRP for the available sub-assembly items.",
"fieldname": "skip_available_sub_assembly_item",
"fieldtype": "Check",
"label": "Skip Available Sub Assembly Items"
@@ -422,7 +422,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-05-22 23:36:31.770517",
+ "modified": "2023-07-28 13:37:43.926686",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index d8cc8f6d39..261aa76b70 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -39,6 +39,36 @@ class ProductionPlan(Document):
self.set_status()
self._rename_temporary_references()
validate_uom_is_integer(self, "stock_uom", "planned_qty")
+ self.validate_sales_orders()
+
+ @frappe.whitelist()
+ def validate_sales_orders(self, sales_order=None):
+ sales_orders = []
+
+ if sales_order:
+ sales_orders.append(sales_order)
+ else:
+ sales_orders = [row.sales_order for row in self.sales_orders if row.sales_order]
+
+ data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders})
+
+ title = _("Production Plan Already Submitted")
+ if not data:
+ msg = _("No items are available in the sales order {0} for production").format(sales_orders[0])
+ if len(sales_orders) > 1:
+ sales_orders = ", ".join(sales_orders)
+ msg = _("No items are available in sales orders {0} for production").format(sales_orders)
+
+ frappe.throw(msg, title=title)
+
+ data = [d[0] for d in data]
+
+ for sales_order in sales_orders:
+ if sales_order not in data:
+ frappe.throw(
+ _("No items are available in the sales order {0} for production").format(sales_order),
+ title=title,
+ )
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
@@ -205,6 +235,7 @@ class ProductionPlan(Document):
).as_("pending_qty"),
so_item.description,
so_item.name,
+ so_item.bom_no,
)
.distinct()
.where(
@@ -342,7 +373,7 @@ class ProductionPlan(Document):
"item_code": data.item_code,
"description": data.description or item_details.description,
"stock_uom": item_details and item_details.stock_uom or "",
- "bom_no": item_details and item_details.bom_no or "",
+ "bom_no": data.bom_no or item_details and item_details.bom_no or "",
"planned_qty": data.pending_qty,
"pending_qty": data.pending_qty,
"planned_start_date": now_datetime(),
@@ -401,11 +432,50 @@ class ProductionPlan(Document):
def on_submit(self):
self.update_bin_qty()
+ self.update_sales_order()
def on_cancel(self):
self.db_set("status", "Cancelled")
self.delete_draft_work_order()
self.update_bin_qty()
+ self.update_sales_order()
+
+ def update_sales_order(self):
+ sales_orders = [row.sales_order for row in self.po_items if row.sales_order]
+ if sales_orders:
+ so_wise_planned_qty = self.get_so_wise_planned_qty(sales_orders)
+
+ for row in self.po_items:
+ if not row.sales_order and not row.sales_order_item:
+ continue
+
+ key = (row.sales_order, row.sales_order_item)
+ frappe.db.set_value(
+ "Sales Order Item",
+ row.sales_order_item,
+ "production_plan_qty",
+ flt(so_wise_planned_qty.get(key)),
+ )
+
+ @staticmethod
+ def get_so_wise_planned_qty(sales_orders):
+ so_wise_planned_qty = frappe._dict()
+ data = frappe.get_all(
+ "Production Plan Item",
+ fields=["sales_order", "sales_order_item", "SUM(planned_qty) as qty"],
+ filters={
+ "sales_order": ("in", sales_orders),
+ "docstatus": 1,
+ "sales_order_item": ("is", "set"),
+ },
+ group_by="sales_order, sales_order_item",
+ )
+
+ for row in data:
+ key = (row.sales_order, row.sales_order_item)
+ so_wise_planned_qty[key] = row.qty
+
+ return so_wise_planned_qty
def update_bin_qty(self):
for d in self.mr_items:
@@ -719,6 +789,9 @@ class ProductionPlan(Document):
sub_assembly_items_store = [] # temporary store to process all subassembly items
for row in self.po_items:
+ if self.skip_available_sub_assembly_item and not row.warehouse:
+ frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx))
+
if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
@@ -1142,7 +1215,7 @@ def get_sales_orders(self):
& (so.docstatus == 1)
& (so.status.notin(["Stopped", "Closed"]))
& (so.company == self.company)
- & (so_item.qty > so_item.work_order_qty)
+ & (so_item.qty > so_item.production_plan_qty)
)
)
@@ -1566,7 +1639,6 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
def get_raw_materials_of_sub_assembly_items(
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
):
-
bei = frappe.qb.DocType("BOM Item")
bom = frappe.qb.DocType("BOM")
item = frappe.qb.DocType("Item")
@@ -1609,7 +1681,10 @@ def get_raw_materials_of_sub_assembly_items(
for item in items:
key = (item.item_code, item.bom_no)
- if item.bom_no and key in sub_assembly_items:
+ if item.bom_no and key not in sub_assembly_items:
+ continue
+
+ if item.bom_no:
planned_qty = flt(sub_assembly_items[key])
get_raw_materials_of_sub_assembly_items(
item_details,
@@ -1626,3 +1701,42 @@ def get_raw_materials_of_sub_assembly_items(
item_details.setdefault(item.get("item_code"), item)
return item_details
+
+
+@frappe.whitelist()
+def sales_order_query(
+ doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None
+):
+ frappe.has_permission("Production Plan", throw=True)
+
+ if not filters:
+ filters = {}
+
+ so_table = frappe.qb.DocType("Sales Order")
+ table = frappe.qb.DocType("Sales Order Item")
+
+ query = (
+ frappe.qb.from_(so_table)
+ .join(table)
+ .on(table.parent == so_table.name)
+ .select(table.parent)
+ .distinct()
+ .where((table.qty > table.production_plan_qty) & (table.docstatus == 1))
+ )
+
+ if filters.get("company"):
+ query = query.where(so_table.company == filters.get("company"))
+
+ if filters.get("sales_orders"):
+ query = query.where(so_table.name.isin(filters.get("sales_orders")))
+
+ if txt:
+ query = query.where(table.item_code.like(f"{txt}%"))
+
+ if page_len:
+ query = query.limit(page_len)
+
+ if start:
+ query = query.offset(start)
+
+ return query.run()
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index f60dbfc3f5..2871a29d76 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -225,6 +225,102 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(sales_orders, [])
+ def test_donot_allow_to_make_multiple_pp_against_same_so(self):
+ item = "Test SO Production Item 1"
+ create_item(item)
+
+ raw_material = "Test SO RM Production Item 1"
+ create_item(raw_material)
+
+ if not frappe.db.get_value("BOM", {"item": item}):
+ make_bom(item=item, raw_materials=[raw_material])
+
+ so = make_sales_order(item_code=item, qty=4)
+ pln = frappe.new_doc("Production Plan")
+ pln.company = so.company
+ pln.get_items_from = "Sales Order"
+
+ pln.append(
+ "sales_orders",
+ {
+ "sales_order": so.name,
+ "sales_order_date": so.transaction_date,
+ "customer": so.customer,
+ "grand_total": so.grand_total,
+ },
+ )
+
+ pln.get_so_items()
+ pln.submit()
+
+ pln = frappe.new_doc("Production Plan")
+ pln.company = so.company
+ pln.get_items_from = "Sales Order"
+
+ pln.append(
+ "sales_orders",
+ {
+ "sales_order": so.name,
+ "sales_order_date": so.transaction_date,
+ "customer": so.customer,
+ "grand_total": so.grand_total,
+ },
+ )
+
+ pln.get_so_items()
+ self.assertRaises(frappe.ValidationError, pln.save)
+
+ def test_so_based_bill_of_material(self):
+ item = "Test SO Production Item 1"
+ create_item(item)
+
+ raw_material = "Test SO RM Production Item 1"
+ create_item(raw_material)
+
+ bom1 = make_bom(item=item, raw_materials=[raw_material])
+
+ so = make_sales_order(item_code=item, qty=4)
+
+ # Create new BOM and assign to new sales order
+ bom2 = make_bom(item=item, raw_materials=[raw_material])
+ so2 = make_sales_order(item_code=item, qty=4)
+
+ pln1 = frappe.new_doc("Production Plan")
+ pln1.company = so.company
+ pln1.get_items_from = "Sales Order"
+
+ pln1.append(
+ "sales_orders",
+ {
+ "sales_order": so.name,
+ "sales_order_date": so.transaction_date,
+ "customer": so.customer,
+ "grand_total": so.grand_total,
+ },
+ )
+
+ pln1.get_so_items()
+
+ self.assertEqual(pln1.po_items[0].bom_no, bom1.name)
+
+ pln2 = frappe.new_doc("Production Plan")
+ pln2.company = so2.company
+ pln2.get_items_from = "Sales Order"
+
+ pln2.append(
+ "sales_orders",
+ {
+ "sales_order": so2.name,
+ "sales_order_date": so2.transaction_date,
+ "customer": so2.customer,
+ "grand_total": so2.grand_total,
+ },
+ )
+
+ pln2.get_so_items()
+
+ self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
+
def test_production_plan_combine_items(self):
"Test combining FG items in Production Plan."
item = "Test Production Item 1"
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index c1a078d65e..58945bba77 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -624,7 +624,7 @@ erpnext.work_order = {
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
frm.has_finish_btn = true;
- var finish_btn = frm.add_custom_button(__('Finish'), function() {
+ let finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
@@ -648,7 +648,7 @@ erpnext.work_order = {
}
} else {
if ((flt(doc.produced_qty) < flt(doc.qty))) {
- var finish_btn = frm.add_custom_button(__('Finish'), function() {
+ let finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
finish_btn.addClass('btn-primary');
@@ -756,13 +756,14 @@ erpnext.work_order = {
},
make_consumption_se: function(frm, backflush_raw_materials_based_on) {
+ let max = 0;
if(!frm.doc.skip_transfer){
- var max = (backflush_raw_materials_based_on === "Material Transferred for Manufacture") ?
+ max = (backflush_raw_materials_based_on === "Material Transferred for Manufacture") ?
flt(frm.doc.material_transferred_for_manufacturing) - flt(frm.doc.produced_qty) :
flt(frm.doc.qty) - flt(frm.doc.produced_qty);
// flt(frm.doc.qty) - flt(frm.doc.material_transferred_for_manufacturing);
} else {
- var max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
+ max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
}
frappe.call({
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index a236f2a339..1996e19c37 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -404,6 +404,8 @@
"read_only": 1
},
{
+ "fetch_from": "production_item.stock_uom",
+ "fetch_if_empty": 1,
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
@@ -590,7 +592,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2023-06-09 13:20:09.154362",
+ "modified": "2023-08-11 18:35:49.852069",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
@@ -610,7 +612,6 @@
"read": 1,
"report": 1,
"role": "Manufacturing User",
- "set_user_permissions": 1,
"share": 1,
"submit": 1,
"write": 1
diff --git a/erpnext/manufacturing/doctype/workstation/_test_workstation.js b/erpnext/manufacturing/doctype/workstation/_test_workstation.js
index 0f09bd1c61..f2dced81e2 100644
--- a/erpnext/manufacturing/doctype/workstation/_test_workstation.js
+++ b/erpnext/manufacturing/doctype/workstation/_test_workstation.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index d5b6d37d67..ac271b7144 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -114,7 +114,7 @@ class Workstation(Document):
if schedule_date in tuple(get_holidays(self.holiday_list)):
schedule_date = add_days(schedule_date, 1)
- self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
+ return self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
return schedule_date
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_list.js b/erpnext/manufacturing/doctype/workstation/workstation_list.js
index 6a89d21e1e..61f2062ec0 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation_list.js
+++ b/erpnext/manufacturing/doctype/workstation/workstation_list.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+
frappe.listview_settings['Workstation'] = {
// add_fields: ["status"],
// filters:[["status","=", "Open"]]
diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.js b/erpnext/manufacturing/report/bom_explorer/bom_explorer.js
index b94d3f3770..50191bf8ab 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.js
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["BOM Explorer"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
index 0eb22a22f7..34edb9d538 100644
--- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
+++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["BOM Operations Time"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js
index a0fd91e866..8e66f704c8 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Epoch Consulting and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["BOM Stock Calculated"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.js b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.js
index c6ecaef2fa..db6f4d7688 100644
--- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.js
+++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["BOM Variance Report"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
index 72eed5e0d7..d0249ba84b 100644
--- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Cost of Poor Quality Report"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js
index f6486743aa..0589260958 100644
--- a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js
+++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Downtime Analysis"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
index a3f0d00877..3795bd3df8 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Exponential Smoothing Forecasting"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
index a874f22482..4b3f86fcbf 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Job Card Summary"] = {
"filters": [
@@ -37,14 +37,14 @@ frappe.query_reports["Job Card Summary"] = {
label: __("From Posting Date"),
fieldname:"from_date",
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_start_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
reqd: 1
},
{
label: __("To Posting Date"),
fieldname:"to_date",
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_end_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
reqd: 1,
},
{
diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.js b/erpnext/manufacturing/report/process_loss_report/process_loss_report.js
index b0c2b94a25..c08413dc4a 100644
--- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.js
+++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Process Loss Report"] = {
filters: [
diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.js b/erpnext/manufacturing/report/production_analytics/production_analytics.js
index 99f9b1260a..77529a5e57 100644
--- a/erpnext/manufacturing/report/production_analytics/production_analytics.js
+++ b/erpnext/manufacturing/report/production_analytics/production_analytics.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Production Analytics"] = {
"filters": [
@@ -16,14 +16,14 @@ frappe.query_reports["Production Analytics"] = {
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_start_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
- default: frappe.defaults.get_user_default("year_end_date"),
+ default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
reqd: 1
},
{
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js
index 59396fef16..521543ab1b 100644
--- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Production Plan Summary"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js
index 675b8a1100..422583274b 100644
--- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js
+++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Production Planning Report"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js
index d4587aa661..905d185076 100644
--- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js
+++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Quality Inspection Summary"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
index 2fb4ec6791..70d7f92da0 100644
--- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Work Order Consumed Materials"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.js b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.js
index dbb7c23410..cf651cb394 100644
--- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.js
+++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Work Order Stock Report"] = {
"filters": [
diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
index 67bd24dd80..d0242f4d3b 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Work Order Summary"] = {
"filters": [
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 76e4dee9b6..c8cf7bc6be 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -262,6 +262,7 @@ erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
erpnext.patches.v15_0.saudi_depreciation_warning
erpnext.patches.v15_0.delete_saudi_doctypes
erpnext.patches.v14_0.show_loan_management_deprecation_warning
+execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
erpnext.patches.v14_0.delete_education_module_portal_menu_items
[post_model_sync]
@@ -320,8 +321,8 @@ erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances #14-07-2023
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
-# below migration patches should always run last
-erpnext.patches.v14_0.migrate_gl_to_payment_ledger
+erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
+erpnext.patches.v14_0.update_subscription_details
execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc
@@ -337,3 +338,7 @@ erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
execute:frappe.defaults.clear_default("fiscal_year")
erpnext.patches.v15_0.remove_exotel_integration
+erpnext.patches.v14_0.single_to_multi_dunning
+execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0)
+# below migration patch should always run last
+erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v12_0/update_bom_in_so_mr.py b/erpnext/patches/v12_0/update_bom_in_so_mr.py
index 114f65d100..d35b4bcdd6 100644
--- a/erpnext/patches/v12_0/update_bom_in_so_mr.py
+++ b/erpnext/patches/v12_0/update_bom_in_so_mr.py
@@ -6,7 +6,9 @@ def execute():
frappe.reload_doc("selling", "doctype", "sales_order_item")
for doctype in ["Sales Order", "Material Request"]:
- condition = " and child_doc.stock_qty > child_doc.produced_qty and doc.per_delivered < 100"
+ condition = (
+ " and child_doc.stock_qty > child_doc.produced_qty and ROUND(doc.per_delivered, 2) < 100"
+ )
if doctype == "Material Request":
condition = " and doc.per_ordered < 100 and doc.material_request_type = 'Manufacture'"
diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py
new file mode 100644
index 0000000000..3b01871d43
--- /dev/null
+++ b/erpnext/patches/v14_0/single_to_multi_dunning.py
@@ -0,0 +1,78 @@
+import frappe
+
+from erpnext.accounts.general_ledger import make_reverse_gl_entries
+
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "overdue_payment")
+ frappe.reload_doc("accounts", "doctype", "dunning")
+
+ # Migrate schema of all uncancelled dunnings
+ filters = {"docstatus": ("!=", 2)}
+
+ can_edit_accounts_after = get_accounts_closing_date()
+ if can_edit_accounts_after:
+ # Get dunnings after the date when accounts were frozen/closed
+ filters["posting_date"] = (">", can_edit_accounts_after)
+
+ all_dunnings = frappe.get_all("Dunning", filters=filters, pluck="name")
+
+ for dunning_name in all_dunnings:
+ dunning = frappe.get_doc("Dunning", dunning_name)
+ if not dunning.sales_invoice:
+ # nothing we can do
+ continue
+
+ if dunning.overdue_payments:
+ # something's already here, doesn't need patching
+ continue
+
+ payment_schedules = frappe.get_all(
+ "Payment Schedule",
+ filters={"parent": dunning.sales_invoice},
+ fields=[
+ "parent as sales_invoice",
+ "name as payment_schedule",
+ "payment_term",
+ "due_date",
+ "invoice_portion",
+ "payment_amount",
+ # at the time of creating this dunning, the full amount was outstanding
+ "payment_amount as outstanding",
+ "'0' as paid_amount",
+ "discounted_amount",
+ ],
+ )
+
+ dunning.extend("overdue_payments", payment_schedules)
+ dunning.validate()
+
+ dunning.flags.ignore_validate_update_after_submit = True
+ dunning.save()
+
+ # Reverse entries only if dunning is submitted and not resolved
+ if dunning.docstatus == 1 and dunning.status != "Resolved":
+ # With the new logic, dunning amount gets recorded as additional income
+ # at time of payment. We don't want to record the dunning amount twice,
+ # so we reverse previous GL Entries that recorded the dunning amount at
+ # time of submission of the Dunning.
+ make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name)
+
+
+def get_accounts_closing_date():
+ """Get the date when accounts were frozen/closed"""
+ accounts_frozen_till = frappe.db.get_single_value(
+ "Accounts Settings", "acc_frozen_upto"
+ ) # always returns datetime.date
+
+ period_closing_date = frappe.db.get_value(
+ "Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc"
+ )
+
+ # Set most recent frozen/closing date as filter
+ if accounts_frozen_till and period_closing_date:
+ can_edit_accounts_after = max(accounts_frozen_till, period_closing_date)
+ else:
+ can_edit_accounts_after = accounts_frozen_till or period_closing_date
+
+ return can_edit_accounts_after
diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py
index 2947b98740..2c84281483 100644
--- a/erpnext/patches/v14_0/update_closing_balances.py
+++ b/erpnext/patches/v14_0/update_closing_balances.py
@@ -69,7 +69,6 @@ def execute():
entries = gl_entries + closing_entries
- if entries:
- make_closing_entries(entries, voucher_name=pcv.name)
- i += 1
- company_wise_order[pcv.company].append(pcv.posting_date)
+ make_closing_entries(entries, pcv.name, pcv.company, pcv.posting_date)
+ company_wise_order[pcv.company].append(pcv.posting_date)
+ i += 1
diff --git a/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py b/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py
new file mode 100644
index 0000000000..48b6bcf755
--- /dev/null
+++ b/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py
@@ -0,0 +1,22 @@
+import frappe
+
+
+def execute():
+ """
+ Update Propery Setters for Journal Entry with new 'Entry Type'
+ """
+ new_reference_type = "Payment Entry"
+ prop_setter = frappe.db.get_list(
+ "Property Setter",
+ filters={
+ "doc_type": "Journal Entry Account",
+ "field_name": "reference_type",
+ "property": "options",
+ },
+ )
+ if prop_setter:
+ property_setter_doc = frappe.get_doc("Property Setter", prop_setter[0].get("name"))
+
+ if new_reference_type not in property_setter_doc.value.split("\n"):
+ property_setter_doc.value += "\n" + new_reference_type
+ property_setter_doc.save()
diff --git a/erpnext/patches/v14_0/update_subscription_details.py b/erpnext/patches/v14_0/update_subscription_details.py
new file mode 100644
index 0000000000..58ab16d39e
--- /dev/null
+++ b/erpnext/patches/v14_0/update_subscription_details.py
@@ -0,0 +1,17 @@
+import frappe
+
+
+def execute():
+ subscription_invoices = frappe.get_all(
+ "Subscription Invoice", fields=["document_type", "invoice", "parent"]
+ )
+
+ for subscription_invoice in subscription_invoices:
+ frappe.db.set_value(
+ subscription_invoice.document_type,
+ subscription_invoice.invoice,
+ "subscription",
+ subscription_invoice.parent,
+ )
+
+ frappe.delete_doc_if_exists("DocType", "Subscription Invoice")
diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py
index bc8f2afb8c..ac1524a49d 100644
--- a/erpnext/projects/report/billing_summary.py
+++ b/erpnext/projects/report/billing_summary.py
@@ -98,9 +98,11 @@ def get_timesheets(filters):
record_filters = [
["start_date", "<=", filters.to_date],
["end_date", ">=", filters.from_date],
- ["docstatus", "=", 1],
]
-
+ if not filters.get("include_draft_timesheets"):
+ record_filters.append(["docstatus", "=", 1])
+ else:
+ record_filters.append(["docstatus", "!=", 2])
if "employee" in filters:
record_filters.append(["employee", "=", filters.employee])
diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js
index 5aa44c0a8c..fa70b9394a 100644
--- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js
+++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Delayed Tasks Summary"] = {
"filters": [
diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
index 13f49ed6be..2c25465a61 100644
--- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
+++ b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Employee Billing Summary"] = {
"filters": [
@@ -25,5 +25,10 @@ frappe.query_reports["Employee Billing Summary"] = {
default: frappe.datetime.add_days(frappe.datetime.month_start(), -1),
reqd: 1
},
+ {
+ fieldname:"include_draft_timesheets",
+ label: __("Include Timesheets in Draft Status"),
+ fieldtype: "Check",
+ },
]
}
diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.js b/erpnext/projects/report/project_billing_summary/project_billing_summary.js
index caac1d86b4..fce0c68f11 100644
--- a/erpnext/projects/report/project_billing_summary/project_billing_summary.js
+++ b/erpnext/projects/report/project_billing_summary/project_billing_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Project Billing Summary"] = {
"filters": [
@@ -25,5 +25,10 @@ frappe.query_reports["Project Billing Summary"] = {
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
reqd: 1
},
+ {
+ fieldname:"include_draft_timesheets",
+ label: __("Include Timesheets in Draft Status"),
+ fieldtype: "Check",
+ },
]
}
diff --git a/erpnext/projects/report/project_summary/project_summary.js b/erpnext/projects/report/project_summary/project_summary.js
index 414b7b206a..21dbfda73f 100644
--- a/erpnext/projects/report/project_summary/project_summary.js
+++ b/erpnext/projects/report/project_summary/project_summary.js
@@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-/* eslint-disable */
+
frappe.query_reports["Project Summary"] = {
"filters": [
diff --git a/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py b/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py
index da609ca769..41a7c799d9 100644
--- a/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py
+++ b/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py
@@ -77,7 +77,7 @@ def get_issued_items_cost():
"""select se.project, sum(se_item.amount) as amount
from `tabStock Entry` se, `tabStock Entry Detail` se_item
where se.name = se_item.parent and se.docstatus = 1 and ifnull(se_item.t_warehouse, '') = ''
- and ifnull(se.project, '') != '' group by se.project""",
+ and se.project != '' group by se.project""",
as_dict=1,
)
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
deleted file mode 100644
index 1bed541831..0000000000
--- a/erpnext/public/build.json
+++ /dev/null
@@ -1,69 +0,0 @@
-{
- "css/erpnext.css": [
- "public/less/erpnext.less",
- "public/scss/call_popup.scss",
- "public/scss/point-of-sale.scss"
- ],
- "js/erpnext-web.min.js": [
- "public/js/website_utils.js",
- "public/js/shopping_cart.js",
- "public/js/wishlist.js"
- ],
- "css/erpnext-web.css": [
- "public/scss/website.scss",
- "public/scss/shopping_cart.scss"
- ],
- "js/erpnext.min.js": [
- "public/js/conf.js",
- "public/js/utils.js",
- "public/js/queries.js",
- "public/js/sms_manager.js",
- "public/js/utils/party.js",
- "public/js/controllers/stock_controller.js",
- "public/js/payment/payments.js",
- "public/js/controllers/taxes_and_totals.js",
- "public/js/controllers/transaction.js",
- "public/js/templates/item_selector.html",
- "public/js/utils/item_selector.js",
- "public/js/help_links.js",
- "public/js/agriculture/ternary_plot.js",
- "public/js/templates/item_quick_entry.html",
- "public/js/utils/customer_quick_entry.js",
- "public/js/utils/supplier_quick_entry.js",
- "public/js/education/student_button.html",
- "public/js/education/assessment_result_tool.html",
- "public/js/call_popup/call_popup.js",
- "public/js/utils/dimension_tree_filter.js",
- "public/js/telephony.js",
- "public/js/templates/call_link.html",
- "public/js/bulk_transaction_processing.js"
- ],
- "js/item-dashboard.min.js": [
- "stock/dashboard/item_dashboard.html",
- "stock/dashboard/item_dashboard_list.html",
- "stock/dashboard/item_dashboard.js",
- "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html",
- "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html"
- ],
- "js/point-of-sale.min.js": [
- "selling/page/point_of_sale/pos_item_selector.js",
- "selling/page/point_of_sale/pos_item_cart.js",
- "selling/page/point_of_sale/pos_item_details.js",
- "selling/page/point_of_sale/pos_number_pad.js",
- "selling/page/point_of_sale/pos_payment.js",
- "selling/page/point_of_sale/pos_past_order_list.js",
- "selling/page/point_of_sale/pos_past_order_summary.js",
- "selling/page/point_of_sale/pos_controller.js"
- ],
- "js/bank-reconciliation-tool.min.js": [
- "public/js/bank_reconciliation_tool/data_table_manager.js",
- "public/js/bank_reconciliation_tool/number_card.js",
- "public/js/bank_reconciliation_tool/dialog_manager.js"
- ],
- "js/e-commerce.min.js": [
- "e_commerce/product_ui/views.js",
- "e_commerce/product_ui/grid.js",
- "e_commerce/product_ui/list.js",
- "e_commerce/product_ui/search.js"
- ]
-}
diff --git a/erpnext/public/js/account_tree_grid.js b/erpnext/public/js/account_tree_grid.js
deleted file mode 100644
index 413a5ee971..0000000000
--- a/erpnext/public/js/account_tree_grid.js
+++ /dev/null
@@ -1,238 +0,0 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see
.
-
-erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridReport {
- constructor(wrapper, title) {
- super({
- title: title,
- parent: $(wrapper).find('.layout-main'),
- page: wrapper.page,
- doctypes: ["Company", "Fiscal Year", "Account", "GL Entry", "Cost Center"],
- tree_grid: {
- show: true,
- parent_field: "parent_account",
- formatter: function(item) {
- return repl("
\
- %(value)s", {
- value: item.name,
- });
- }
- },
- });
-
- this.filters = [
- {fieldtype: "Select", label: __("Company"), link:"Company", fieldname: "company",
- default_value: __("Select Company..."),
- filter: function(val, item, opts, me) {
- if (item.company == val || val == opts.default_value) {
- return me.apply_zero_filter(val, item, opts, me);
- }
- return false;
- }},
- {fieldtype: "Select", label: "Fiscal Year", link:"Fiscal Year", fieldname: "fiscal_year",
- default_value: __("Select Fiscal Year...")},
- {fieldtype: "Date", label: __("From Date"), fieldname: "from_date"},
- {fieldtype: "Label", label: __("To")},
- {fieldtype: "Date", label: __("To Date"), fieldname: "to_date"}
- ]
- }
- setup_columns() {
- this.columns = [
- {id: "name", name: __("Account"), field: "name", width: 300, cssClass: "cell-title"},
- {id: "opening_dr", name: __("Opening (Dr)"), field: "opening_dr", width: 100,
- formatter: this.currency_formatter},
- {id: "opening_cr", name: __("Opening (Cr)"), field: "opening_cr", width: 100,
- formatter: this.currency_formatter},
- {id: "debit", name: __("Debit"), field: "debit", width: 100,
- formatter: this.currency_formatter},
- {id: "credit", name: __("Credit"), field: "credit", width: 100,
- formatter: this.currency_formatter},
- {id: "closing_dr", name: __("Closing (Dr)"), field: "closing_dr", width: 100,
- formatter: this.currency_formatter},
- {id: "closing_cr", name: __("Closing (Cr)"), field: "closing_cr", width: 100,
- formatter: this.currency_formatter}
- ];
- }
-
- setup_filters() {
- super.setup_filters();
- var me = this;
- // default filters
- this.filter_inputs.fiscal_year.change(function() {
- var fy = $(this).val();
- $.each(frappe.report_dump.data["Fiscal Year"], function(i, v) {
- if (v.name==fy) {
- me.filter_inputs.from_date.val(frappe.datetime.str_to_user(v.year_start_date));
- me.filter_inputs.to_date.val(frappe.datetime.str_to_user(v.year_end_date));
- }
- });
- me.refresh();
- });
- me.show_zero_check()
- if(me.ignore_closing_entry) me.ignore_closing_entry();
- }
- prepare_data() {
- var me = this;
- if(!this.primary_data) {
- // make accounts list
- me.data = [];
- me.parent_map = {};
- me.item_by_name = {};
-
- $.each(frappe.report_dump.data["Account"], function(i, v) {
- var d = copy_dict(v);
-
- me.data.push(d);
- me.item_by_name[d.name] = d;
- if(d.parent_account) {
- me.parent_map[d.name] = d.parent_account;
- }
- });
-
- me.primary_data = [].concat(me.data);
- }
-
- me.data = [].concat(me.primary_data);
- $.each(me.data, function(i, d) {
- me.init_account(d);
- });
-
- this.set_indent();
- this.prepare_balances();
-
- }
- init_account(d) {
- this.reset_item_values(d);
- }
-
- prepare_balances() {
- var gl = frappe.report_dump.data['GL Entry'];
- var me = this;
-
- this.opening_date = frappe.datetime.user_to_obj(this.filter_inputs.from_date.val());
- this.closing_date = frappe.datetime.user_to_obj(this.filter_inputs.to_date.val());
- this.set_fiscal_year();
- if (!this.fiscal_year) return;
-
- $.each(this.data, function(i, v) {
- v.opening_dr = v.opening_cr = v.debit
- = v.credit = v.closing_dr = v.closing_cr = 0;
- });
-
- $.each(gl, function(i, v) {
- var posting_date = frappe.datetime.str_to_obj(v.posting_date);
- var account = me.item_by_name[v.account];
- me.update_balances(account, posting_date, v);
- });
-
- this.update_groups();
- }
- update_balances(account, posting_date, v) {
- // opening
- if (posting_date < this.opening_date || v.is_opening === "Yes") {
- if (account.report_type === "Profit and Loss" &&
- posting_date <= frappe.datetime.str_to_obj(this.fiscal_year[1])) {
- // balance of previous fiscal_year should
- // not be part of opening of pl account balance
- } else {
- var opening_bal = flt(account.opening_dr) - flt(account.opening_cr) +
- flt(v.debit) - flt(v.credit);
- this.set_debit_or_credit(account, "opening", opening_bal);
- }
- } else if (this.opening_date <= posting_date && posting_date <= this.closing_date) {
- // in between
- account.debit += flt(v.debit);
- account.credit += flt(v.credit);
- }
- // closing
- var closing_bal = flt(account.opening_dr) - flt(account.opening_cr) +
- flt(account.debit) - flt(account.credit);
- this.set_debit_or_credit(account, "closing", closing_bal);
- }
- set_debit_or_credit(account, field, balance) {
- if(balance > 0) {
- account[field+"_dr"] = balance;
- account[field+"_cr"] = 0;
- } else {
- account[field+"_cr"] = Math.abs(balance);
- account[field+"_dr"] = 0;
- }
- }
- update_groups() {
- // update groups
- var me= this;
- $.each(this.data, function(i, account) {
- // update groups
- if((account.is_group == 0) || (account.rgt - account.lft == 1)) {
- var parent = me.parent_map[account.name];
- while(parent) {
- var parent_account = me.item_by_name[parent];
- $.each(me.columns, function(c, col) {
- if (col.formatter == me.currency_formatter) {
- if(col.field=="opening_dr") {
- var bal = flt(parent_account.opening_dr) -
- flt(parent_account.opening_cr) +
- flt(account.opening_dr) - flt(account.opening_cr);
- me.set_debit_or_credit(parent_account, "opening", bal);
- } else if(col.field=="closing_dr") {
- var bal = flt(parent_account.closing_dr) -
- flt(parent_account.closing_cr) +
- flt(account.closing_dr) - flt(account.closing_cr);
- me.set_debit_or_credit(parent_account, "closing", bal);
- } else if(in_list(["debit", "credit"], col.field)) {
- parent_account[col.field] = flt(parent_account[col.field]) +
- flt(account[col.field]);
- }
- }
- });
- parent = me.parent_map[parent];
- }
- }
- });
- }
-
- set_fiscal_year() {
- if (this.opening_date > this.closing_date) {
- frappe.msgprint(__("Opening Date should be before Closing Date"));
- return;
- }
-
- this.fiscal_year = null;
- var me = this;
- $.each(frappe.report_dump.data["Fiscal Year"], function(i, v) {
- if (me.opening_date >= frappe.datetime.str_to_obj(v.year_start_date) &&
- me.closing_date <= frappe.datetime.str_to_obj(v.year_end_date)) {
- me.fiscal_year = v;
- }
- });
-
- if (!this.fiscal_year) {
- frappe.msgprint(__("Opening Date and Closing Date should be within same Fiscal Year"));
- return;
- }
- }
-
- show_general_ledger(account) {
- frappe.route_options = {
- account: account,
- company: this.company,
- from_date: this.from_date,
- to_date: this.to_date
- };
- frappe.set_route("query-report", "General Ledger");
- }
-};
diff --git a/erpnext/public/js/agriculture/ternary_plot.js b/erpnext/public/js/agriculture/ternary_plot.js
deleted file mode 100644
index b06a1fd7c8..0000000000
--- a/erpnext/public/js/agriculture/ternary_plot.js
+++ /dev/null
@@ -1,232 +0,0 @@
-frappe.provide('agriculture');
-
-agriculture.TernaryPlot = class TernaryPlot {
- constructor(opts) {
- Object.assign(this, opts);
-
- frappe.require('assets/frappe/js/lib/snap.svg-min.js', () => {
- this.make_svg();
- this.init_snap();
- this.init_config();
- this.make_plot();
- this.make_plot_marking();
- this.make_legend();
- this.mark_blip();
- });
- }
-
- make_svg() {
- this.$svg = $('