mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-24 07:29:22 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into develop-ritvik-POS-runtime-effect
This commit is contained in:
@@ -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"
|
"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(
|
def _book_deferred_revenue_or_expense(
|
||||||
item,
|
item,
|
||||||
|
|||||||
@@ -1,67 +1,83 @@
|
|||||||
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Account', {
|
frappe.ui.form.on("Account", {
|
||||||
setup: function(frm) {
|
setup: function (frm) {
|
||||||
frm.add_fetch('parent_account', 'report_type', 'report_type');
|
frm.add_fetch("parent_account", "report_type", "report_type");
|
||||||
frm.add_fetch('parent_account', 'root_type', 'root_type');
|
frm.add_fetch("parent_account", "root_type", "root_type");
|
||||||
},
|
},
|
||||||
onload: function(frm) {
|
onload: function (frm) {
|
||||||
frm.set_query('parent_account', function(doc) {
|
frm.set_query("parent_account", function (doc) {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"is_group": 1,
|
is_group: 1,
|
||||||
"company": doc.company
|
company: doc.company,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
refresh: function(frm) {
|
refresh: function (frm) {
|
||||||
frm.toggle_display('account_name', frm.is_new());
|
frm.toggle_display("account_name", frm.is_new());
|
||||||
|
|
||||||
// hide fields if group
|
// 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
|
// disable fields
|
||||||
frm.toggle_enable(['is_group', 'company'], false);
|
frm.toggle_enable(["is_group", "company"], false);
|
||||||
|
|
||||||
if (cint(frm.doc.is_group) == 0) {
|
if (cint(frm.doc.is_group) == 0) {
|
||||||
frm.toggle_display('freeze_account', frm.doc.__onload
|
frm.toggle_display(
|
||||||
&& frm.doc.__onload.can_freeze_account);
|
"freeze_account",
|
||||||
|
frm.doc.__onload && frm.doc.__onload.can_freeze_account
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// read-only for root accounts
|
// read-only for root accounts
|
||||||
if (!frm.is_new()) {
|
if (!frm.is_new()) {
|
||||||
if (!frm.doc.parent_account) {
|
if (!frm.doc.parent_account) {
|
||||||
frm.set_read_only();
|
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 {
|
} else {
|
||||||
// credit days and type if customer or supplier
|
// credit days and type if customer or supplier
|
||||||
frm.set_intro(null);
|
frm.set_intro(null);
|
||||||
frm.trigger('account_type');
|
frm.trigger("account_type");
|
||||||
// show / hide convert buttons
|
// show / hide convert buttons
|
||||||
frm.trigger('add_toolbar_buttons');
|
frm.trigger("add_toolbar_buttons");
|
||||||
}
|
}
|
||||||
if (frm.has_perm('write')) {
|
if (frm.has_perm("write")) {
|
||||||
frm.add_custom_button(__('Merge Account'), function () {
|
frm.add_custom_button(
|
||||||
frm.trigger("merge_account");
|
__("Merge Account"),
|
||||||
}, __('Actions'));
|
function () {
|
||||||
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
frm.trigger("merge_account");
|
||||||
frm.trigger("update_account_number");
|
},
|
||||||
}, __('Actions'));
|
__("Actions")
|
||||||
|
);
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Update Account Name / Number"),
|
||||||
|
function () {
|
||||||
|
frm.trigger("update_account_number");
|
||||||
|
},
|
||||||
|
__("Actions")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
account_type: function (frm) {
|
account_type: function (frm) {
|
||||||
if (frm.doc.is_group == 0) {
|
if (frm.doc.is_group == 0) {
|
||||||
frm.toggle_display(['tax_rate'], frm.doc.account_type == 'Tax');
|
frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
|
||||||
frm.toggle_display('warehouse', frm.doc.account_type == 'Stock');
|
frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
add_toolbar_buttons: function(frm) {
|
add_toolbar_buttons: function (frm) {
|
||||||
frm.add_custom_button(__('Chart of Accounts'), () => {
|
frm.add_custom_button(
|
||||||
frappe.set_route("Tree", "Account");
|
__("Chart of Accounts"),
|
||||||
}, __('View'));
|
() => {
|
||||||
|
frappe.set_route("Tree", "Account");
|
||||||
|
},
|
||||||
|
__("View")
|
||||||
|
);
|
||||||
|
|
||||||
if (frm.doc.is_group == 1) {
|
if (frm.doc.is_group == 1) {
|
||||||
frm.add_custom_button(__('Convert to Non-Group'), function () {
|
frm.add_custom_button(__('Convert to Non-Group'), function () {
|
||||||
@@ -86,31 +102,35 @@ frappe.ui.form.on('Account', {
|
|||||||
frappe.set_route("query-report", "General Ledger");
|
frappe.set_route("query-report", "General Ledger");
|
||||||
}, __('View'));
|
}, __('View'));
|
||||||
|
|
||||||
frm.add_custom_button(__('Convert to Group'), function () {
|
frm.add_custom_button(
|
||||||
return frappe.call({
|
__("Convert to Group"),
|
||||||
doc: frm.doc,
|
function () {
|
||||||
method: 'convert_ledger_to_group',
|
return frappe.call({
|
||||||
callback: function() {
|
doc: frm.doc,
|
||||||
frm.refresh();
|
method: "convert_ledger_to_group",
|
||||||
}
|
callback: function () {
|
||||||
});
|
frm.refresh();
|
||||||
}, __('Actions'));
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
__("Actions")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
merge_account: function(frm) {
|
merge_account: function (frm) {
|
||||||
var d = new frappe.ui.Dialog({
|
var d = new frappe.ui.Dialog({
|
||||||
title: __('Merge with Existing Account'),
|
title: __("Merge with Existing Account"),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
"label" : "Name",
|
label: "Name",
|
||||||
"fieldname": "name",
|
fieldname: "name",
|
||||||
"fieldtype": "Data",
|
fieldtype: "Data",
|
||||||
"reqd": 1,
|
reqd: 1,
|
||||||
"default": frm.doc.name
|
default: frm.doc.name,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
primary_action: function() {
|
primary_action: function () {
|
||||||
var data = d.get_values();
|
var data = d.get_values();
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.accounts.doctype.account.account.merge_account",
|
method: "erpnext.accounts.doctype.account.account.merge_account",
|
||||||
@@ -119,44 +139,47 @@ frappe.ui.form.on('Account', {
|
|||||||
new: data.name,
|
new: data.name,
|
||||||
is_group: frm.doc.is_group,
|
is_group: frm.doc.is_group,
|
||||||
root_type: frm.doc.root_type,
|
root_type: frm.doc.root_type,
|
||||||
company: frm.doc.company
|
company: frm.doc.company,
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function (r) {
|
||||||
if(!r.exc) {
|
if (!r.exc) {
|
||||||
if(r.message) {
|
if (r.message) {
|
||||||
frappe.set_route("Form", "Account", r.message);
|
frappe.set_route("Form", "Account", r.message);
|
||||||
}
|
}
|
||||||
d.hide();
|
d.hide();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
primary_action_label: __('Merge')
|
primary_action_label: __("Merge"),
|
||||||
});
|
});
|
||||||
d.show();
|
d.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
update_account_number: function(frm) {
|
update_account_number: function (frm) {
|
||||||
var d = new frappe.ui.Dialog({
|
var d = new frappe.ui.Dialog({
|
||||||
title: __('Update Account Number / Name'),
|
title: __("Update Account Number / Name"),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
"label": "Account Name",
|
label: "Account Name",
|
||||||
"fieldname": "account_name",
|
fieldname: "account_name",
|
||||||
"fieldtype": "Data",
|
fieldtype: "Data",
|
||||||
"reqd": 1,
|
reqd: 1,
|
||||||
"default": frm.doc.account_name
|
default: frm.doc.account_name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Account Number",
|
label: "Account Number",
|
||||||
"fieldname": "account_number",
|
fieldname: "account_number",
|
||||||
"fieldtype": "Data",
|
fieldtype: "Data",
|
||||||
"default": frm.doc.account_number
|
default: frm.doc.account_number,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
primary_action: function() {
|
primary_action: function () {
|
||||||
var data = d.get_values();
|
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();
|
d.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -166,23 +189,29 @@ frappe.ui.form.on('Account', {
|
|||||||
args: {
|
args: {
|
||||||
account_number: data.account_number,
|
account_number: data.account_number,
|
||||||
account_name: data.account_name,
|
account_name: data.account_name,
|
||||||
name: frm.doc.name
|
name: frm.doc.name,
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function (r) {
|
||||||
if(!r.exc) {
|
if (!r.exc) {
|
||||||
if(r.message) {
|
if (r.message) {
|
||||||
frappe.set_route("Form", "Account", r.message);
|
frappe.set_route("Form", "Account", r.message);
|
||||||
} else {
|
} else {
|
||||||
frm.set_value("account_number", data.account_number);
|
frm.set_value(
|
||||||
frm.set_value("account_name", data.account_name);
|
"account_number",
|
||||||
|
data.account_number
|
||||||
|
);
|
||||||
|
frm.set_value(
|
||||||
|
"account_name",
|
||||||
|
data.account_name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
d.hide();
|
d.hide();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
primary_action_label: __('Update')
|
primary_action_label: __("Update"),
|
||||||
});
|
});
|
||||||
d.show();
|
d.show();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
"label": "Account Type",
|
"label": "Account Type",
|
||||||
"oldfieldname": "account_type",
|
"oldfieldname": "account_type",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\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",
|
"description": "Rate at which this tax is applied",
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-11 16:08:46.983677",
|
"modified": "2023-07-20 18:18:44.405723",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Account",
|
"name": "Account",
|
||||||
@@ -243,7 +243,6 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Accounts Manager",
|
"role": "Accounts Manager",
|
||||||
"set_user_permissions": 1,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class Account(NestedSet):
|
|||||||
if frappe.local.flags.allow_unverified_charts:
|
if frappe.local.flags.allow_unverified_charts:
|
||||||
return
|
return
|
||||||
self.validate_parent()
|
self.validate_parent()
|
||||||
|
self.validate_parent_child_account_type()
|
||||||
self.validate_root_details()
|
self.validate_root_details()
|
||||||
validate_field_number("Account", self.name, self.account_number, self.company, "account_number")
|
validate_field_number("Account", self.name, self.account_number, self.company, "account_number")
|
||||||
self.validate_group_or_ledger()
|
self.validate_group_or_ledger()
|
||||||
@@ -55,6 +56,20 @@ class Account(NestedSet):
|
|||||||
self.validate_account_currency()
|
self.validate_account_currency()
|
||||||
self.validate_root_company_and_sync_account_to_children()
|
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):
|
def validate_parent(self):
|
||||||
"""Fetch Parent Details and validate parent account"""
|
"""Fetch Parent Details and validate parent account"""
|
||||||
if self.parent_account:
|
if self.parent_account:
|
||||||
|
|||||||
@@ -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()) {
|
if (!frm.is_new()) {
|
||||||
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
|
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
|
||||||
frappe.set_route("List", frm.doc.document_type);
|
frappe.set_route("List", frm.doc.document_type);
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class AccountingDimension(Document):
|
|||||||
if not self.is_new():
|
if not self.is_new():
|
||||||
self.validate_document_type_change()
|
self.validate_document_type_change()
|
||||||
|
|
||||||
|
self.validate_dimension_defaults()
|
||||||
|
|
||||||
def validate_document_type_change(self):
|
def validate_document_type_change(self):
|
||||||
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
|
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
|
||||||
if doctype_before_save != self.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.")
|
message += _("Please create a new Accounting Dimension if required.")
|
||||||
frappe.throw(message)
|
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):
|
def after_insert(self):
|
||||||
if frappe.flags.in_test:
|
if frappe.flags.in_test:
|
||||||
make_dimension_in_accounting_doctypes(doc=self)
|
make_dimension_in_accounting_doctypes(doc=self)
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
"reference_document",
|
"reference_document",
|
||||||
"default_dimension",
|
"default_dimension",
|
||||||
"mandatory_for_bs",
|
"mandatory_for_bs",
|
||||||
"mandatory_for_pl"
|
"mandatory_for_pl",
|
||||||
|
"column_break_lqns",
|
||||||
|
"automatically_post_balancing_accounting_entry",
|
||||||
|
"offsetting_account"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -50,6 +53,23 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Mandatory For Profit and Loss Account"
|
"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,
|
"istable": 1,
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ class GLEntry(Document):
|
|||||||
validate_balance_type(self.account, adv_adj)
|
validate_balance_type(self.account, adv_adj)
|
||||||
validate_frozen_account(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 [
|
if frappe.get_cached_value("Account", self.account, "account_type") not in [
|
||||||
"Receivable",
|
"Receivable",
|
||||||
"Payable",
|
"Payable",
|
||||||
|
|||||||
@@ -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.party import get_party_account
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
|
cancel_exchange_gain_loss_journal,
|
||||||
get_account_currency,
|
get_account_currency,
|
||||||
get_balance_on,
|
get_balance_on,
|
||||||
get_stock_accounts,
|
get_stock_accounts,
|
||||||
@@ -87,9 +88,8 @@ class JournalEntry(AccountsController):
|
|||||||
self.update_invoice_discounting()
|
self.update_invoice_discounting()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||||
|
super(JournalEntry, self).on_cancel()
|
||||||
unlink_ref_doc_from_payment_entries(self)
|
|
||||||
self.ignore_linked_doctypes = (
|
self.ignore_linked_doctypes = (
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
"Stock Ledger Entry",
|
"Stock Ledger Entry",
|
||||||
@@ -499,11 +499,12 @@ class JournalEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not against_entries:
|
if not against_entries:
|
||||||
frappe.throw(
|
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)
|
"Journal Entry {0} does not have account {1} or already matched against other voucher"
|
||||||
)
|
).format(d.reference_name, d.account)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
dr_or_cr = "debit" if d.credit > 0 else "credit"
|
dr_or_cr = "debit" if d.credit > 0 else "credit"
|
||||||
valid = False
|
valid = False
|
||||||
@@ -586,7 +587,9 @@ class JournalEntry(AccountsController):
|
|||||||
else:
|
else:
|
||||||
party_account = against_voucher[1]
|
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(
|
frappe.throw(
|
||||||
_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
|
_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
|
||||||
d.idx,
|
d.idx,
|
||||||
@@ -768,18 +771,23 @@ class JournalEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
|
||||||
# Modified to include the posting date for which to retreive the exchange rate
|
ignore_exchange_rate = False
|
||||||
d.exchange_rate = get_exchange_rate(
|
if self.get("flags") and self.flags.get("ignore_exchange_rate"):
|
||||||
self.posting_date,
|
ignore_exchange_rate = True
|
||||||
d.account,
|
|
||||||
d.account_currency,
|
if not ignore_exchange_rate:
|
||||||
self.company,
|
# Modified to include the posting date for which to retreive the exchange rate
|
||||||
d.reference_type,
|
d.exchange_rate = get_exchange_rate(
|
||||||
d.reference_name,
|
self.posting_date,
|
||||||
d.debit,
|
d.account,
|
||||||
d.credit,
|
d.account_currency,
|
||||||
d.exchange_rate,
|
self.company,
|
||||||
)
|
d.reference_type,
|
||||||
|
d.reference_name,
|
||||||
|
d.debit,
|
||||||
|
d.credit,
|
||||||
|
d.exchange_rate,
|
||||||
|
)
|
||||||
|
|
||||||
if not d.exchange_rate:
|
if not d.exchange_rate:
|
||||||
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
|
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
|
||||||
@@ -935,6 +943,8 @@ class JournalEntry(AccountsController):
|
|||||||
merge_entries=merge_entries,
|
merge_entries=merge_entries,
|
||||||
update_outstanding=update_outstanding,
|
update_outstanding=update_outstanding,
|
||||||
)
|
)
|
||||||
|
if cancel:
|
||||||
|
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_balance(self, difference_account=None):
|
def get_balance(self, difference_account=None):
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import change_settings
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||||
@@ -13,6 +14,7 @@ from erpnext.exceptions import InvalidAccountCurrency
|
|||||||
|
|
||||||
|
|
||||||
class TestJournalEntry(unittest.TestCase):
|
class TestJournalEntry(unittest.TestCase):
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_journal_entry_with_against_jv(self):
|
def test_journal_entry_with_against_jv(self):
|
||||||
jv_invoice = frappe.copy_doc(test_records[2])
|
jv_invoice = frappe.copy_doc(test_records[2])
|
||||||
base_jv = frappe.copy_doc(test_records[0])
|
base_jv = frappe.copy_doc(test_records[0])
|
||||||
|
|||||||
@@ -203,7 +203,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Reference Type",
|
"label": "Reference Type",
|
||||||
"no_copy": 1,
|
"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",
|
"fieldname": "reference_name",
|
||||||
@@ -284,7 +284,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-10-26 20:03:10.906259",
|
"modified": "2023-06-16 14:11:13.507807",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry Account",
|
"name": "Journal Entry Account",
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if points_to_redeem > loyalty_program_details.loyalty_points:
|
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)
|
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
|
|||||||
|
|
||||||
frappe.ui.form.on('Payment Entry', {
|
frappe.ui.form.on('Payment Entry', {
|
||||||
onload: function(frm) {
|
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"];
|
||||||
|
|
||||||
if(frm.doc.__islocal) {
|
if(frm.doc.__islocal) {
|
||||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||||
@@ -903,12 +903,12 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
if(frm.doc.payment_type == "Receive"
|
if(frm.doc.payment_type == "Receive"
|
||||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
|
&& 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)) {
|
&& 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;
|
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
|
||||||
} else if (frm.doc.payment_type == "Pay"
|
} else if (frm.doc.payment_type == "Pay"
|
||||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
|
&& 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)) {
|
&& 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;
|
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ from erpnext.accounts.general_ledger import (
|
|||||||
process_gl_map,
|
process_gl_map,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.party import get_party_account
|
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 (
|
from erpnext.controllers.accounts_controller import (
|
||||||
AccountsController,
|
AccountsController,
|
||||||
get_supplier_block_status,
|
get_supplier_block_status,
|
||||||
@@ -143,6 +148,7 @@ class PaymentEntry(AccountsController):
|
|||||||
"Repost Payment Ledger",
|
"Repost Payment Ledger",
|
||||||
"Repost Payment Ledger Items",
|
"Repost Payment Ledger Items",
|
||||||
)
|
)
|
||||||
|
super(PaymentEntry, self).on_cancel()
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.make_advance_gl_entries(cancel=1)
|
self.make_advance_gl_entries(cancel=1)
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
@@ -277,12 +283,14 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||||
|
|
||||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
if (
|
||||||
frappe.throw(fail_message.format(d.idx))
|
d.payment_term
|
||||||
|
and (
|
||||||
if d.payment_term and (
|
(flt(d.allocated_amount)) > 0
|
||||||
(flt(d.allocated_amount)) > 0
|
and latest.payment_term_outstanding
|
||||||
and flt(d.allocated_amount) > flt(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(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
@@ -292,6 +300,9 @@ class PaymentEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
# Check for negative outstanding invoices as well
|
||||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||||
frappe.throw(fail_message.format(d.idx))
|
frappe.throw(fail_message.format(d.idx))
|
||||||
@@ -399,7 +410,7 @@ class PaymentEntry(AccountsController):
|
|||||||
else:
|
else:
|
||||||
if ref_doc:
|
if ref_doc:
|
||||||
if self.paid_from_account_currency == ref_doc.currency:
|
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:
|
if not self.source_exchange_rate:
|
||||||
self.source_exchange_rate = get_exchange_rate(
|
self.source_exchange_rate = get_exchange_rate(
|
||||||
@@ -412,7 +423,7 @@ class PaymentEntry(AccountsController):
|
|||||||
elif self.paid_to and not self.target_exchange_rate:
|
elif self.paid_to and not self.target_exchange_rate:
|
||||||
if ref_doc:
|
if ref_doc:
|
||||||
if self.paid_to_account_currency == ref_doc.currency:
|
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:
|
if not self.target_exchange_rate:
|
||||||
self.target_exchange_rate = get_exchange_rate(
|
self.target_exchange_rate = get_exchange_rate(
|
||||||
@@ -808,10 +819,25 @@ class PaymentEntry(AccountsController):
|
|||||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||||
)
|
)
|
||||||
else:
|
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(
|
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
|
return base_allocated_amount
|
||||||
|
|
||||||
def set_total_allocated_amount(self):
|
def set_total_allocated_amount(self):
|
||||||
@@ -1002,6 +1028,10 @@ class PaymentEntry(AccountsController):
|
|||||||
gl_entries = self.build_gl_map()
|
gl_entries = self.build_gl_map()
|
||||||
gl_entries = process_gl_map(gl_entries)
|
gl_entries = process_gl_map(gl_entries)
|
||||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
|
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):
|
def add_party_gl_entries(self, gl_entries):
|
||||||
if self.party_account:
|
if self.party_account:
|
||||||
@@ -1988,7 +2018,6 @@ def get_payment_entry(
|
|||||||
payment_type=None,
|
payment_type=None,
|
||||||
reference_date=None,
|
reference_date=None,
|
||||||
):
|
):
|
||||||
reference_doc = None
|
|
||||||
doc = frappe.get_doc(dt, dn)
|
doc = frappe.get_doc(dt, dn)
|
||||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
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) >= (
|
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
|
||||||
@@ -2128,7 +2157,7 @@ def get_payment_entry(
|
|||||||
update_accounting_dimensions(pe, doc)
|
update_accounting_dimensions(pe, doc)
|
||||||
|
|
||||||
if party_account and bank:
|
if party_account and bank:
|
||||||
pe.set_exchange_rate(ref_doc=reference_doc)
|
pe.set_exchange_rate(ref_doc=doc)
|
||||||
pe.set_amounts()
|
pe.set_amounts()
|
||||||
|
|
||||||
if discount_amount:
|
if discount_amount:
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
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):
|
def test_payment_entry_against_order(self):
|
||||||
so = make_sales_order()
|
so = make_sales_order()
|
||||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
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.target_exchange_rate = 45.263
|
||||||
pe.reference_no = "1"
|
pe.reference_no = "1"
|
||||||
pe.reference_date = "2016-01-01"
|
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()
|
pe.save()
|
||||||
|
|
||||||
self.assertEqual(flt(pe.difference_amount, 2), 0.0)
|
self.assertEqual(flt(pe.difference_amount, 2), 0.0)
|
||||||
self.assertEqual(flt(pe.unallocated_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):
|
def test_payment_entry_retrieves_last_exchange_rate(self):
|
||||||
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||||
save_new_records,
|
save_new_records,
|
||||||
@@ -792,33 +796,28 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
pe.reference_no = "1"
|
pe.reference_no = "1"
|
||||||
pe.reference_date = "2016-01-01"
|
pe.reference_date = "2016-01-01"
|
||||||
pe.source_exchange_rate = 55
|
pe.source_exchange_rate = 55
|
||||||
|
|
||||||
pe.append(
|
|
||||||
"deductions",
|
|
||||||
{
|
|
||||||
"account": "_Test Exchange Gain/Loss - _TC",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"amount": -500,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
pe.save()
|
pe.save()
|
||||||
|
|
||||||
self.assertEqual(pe.unallocated_amount, 0)
|
self.assertEqual(pe.unallocated_amount, 0)
|
||||||
self.assertEqual(pe.difference_amount, 0)
|
self.assertEqual(pe.difference_amount, 0)
|
||||||
|
self.assertEqual(pe.references[0].exchange_gain_loss, 500)
|
||||||
pe.submit()
|
pe.submit()
|
||||||
|
|
||||||
expected_gle = dict(
|
expected_gle = dict(
|
||||||
(d[0], d)
|
(d[0], d)
|
||||||
for d in [
|
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 Bank USD - _TC", 5500, 0, None],
|
||||||
["_Test Exchange Gain/Loss - _TC", 0, 500, None],
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.validate_gl_entries(pe.name, expected_gle)
|
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"))
|
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||||
self.assertEqual(outstanding_amount, 0)
|
self.assertEqual(outstanding_amount, 0)
|
||||||
|
|
||||||
@@ -1156,6 +1155,52 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
si3.cancel()
|
si3.cancel()
|
||||||
si3.delete()
|
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):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_rec
|
|||||||
)
|
)
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
QueryPaymentLedger,
|
QueryPaymentLedger,
|
||||||
|
create_gain_loss_journal,
|
||||||
get_outstanding_invoices,
|
get_outstanding_invoices,
|
||||||
reconcile_against_document,
|
reconcile_against_document,
|
||||||
)
|
)
|
||||||
@@ -276,6 +277,11 @@ class PaymentReconciliation(Document):
|
|||||||
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
|
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
|
||||||
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
|
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
|
||||||
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
|
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(
|
new_difference_amount = self.get_difference_amount(
|
||||||
payment_entry[0], invoice[0], allocated_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)
|
payment_details = self.get_payment_details(row, dr_or_cr)
|
||||||
reconciled_entry.append(payment_details)
|
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:
|
if entry_list:
|
||||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
||||||
|
|
||||||
@@ -656,6 +656,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
"reference_type": inv.against_voucher_type,
|
"reference_type": inv.against_voucher_type,
|
||||||
"reference_name": inv.against_voucher,
|
"reference_name": inv.against_voucher,
|
||||||
"cost_center": erpnext.get_default_cost_center(company),
|
"cost_center": erpnext.get_default_cost_center(company),
|
||||||
|
"exchange_rate": inv.exchange_rate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"account": inv.account,
|
"account": inv.account,
|
||||||
@@ -669,13 +670,38 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
"reference_type": inv.voucher_type,
|
"reference_type": inv.voucher_type,
|
||||||
"reference_name": inv.voucher_no,
|
"reference_name": inv.voucher_no,
|
||||||
"cost_center": erpnext.get_default_cost_center(company),
|
"cost_center": erpnext.get_default_cost_center(company),
|
||||||
|
"exchange_rate": inv.exchange_rate,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if difference_entry := get_difference_row(inv):
|
|
||||||
jv.append("accounts", difference_entry)
|
|
||||||
|
|
||||||
jv.flags.ignore_mandatory = True
|
jv.flags.ignore_mandatory = True
|
||||||
|
jv.flags.ignore_exchange_rate = True
|
||||||
jv.submit()
|
jv.submit()
|
||||||
|
|
||||||
|
if inv.difference_amount != 0:
|
||||||
|
# make gain/loss journal
|
||||||
|
if inv.party_type == "Customer":
|
||||||
|
dr_or_cr = "credit" if inv.difference_amount < 0 else "debit"
|
||||||
|
else:
|
||||||
|
dr_or_cr = "debit" if inv.difference_amount < 0 else "credit"
|
||||||
|
|
||||||
|
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
|
create_gain_loss_journal(
|
||||||
|
company,
|
||||||
|
inv.party_type,
|
||||||
|
inv.party,
|
||||||
|
inv.account,
|
||||||
|
inv.difference_account,
|
||||||
|
inv.difference_amount,
|
||||||
|
dr_or_cr,
|
||||||
|
reverse_dr_or_cr,
|
||||||
|
inv.voucher_type,
|
||||||
|
inv.voucher_no,
|
||||||
|
None,
|
||||||
|
inv.against_voucher_type,
|
||||||
|
inv.against_voucher,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|||||||
@@ -686,14 +686,24 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
|
|
||||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
total_debit_amount = frappe.db.get_all(
|
total_credit_amount = frappe.db.get_all(
|
||||||
"Journal Entry Account",
|
"Journal Entry Account",
|
||||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||||
"sum(debit) as amount",
|
"sum(credit) as amount",
|
||||||
group_by="reference_name",
|
group_by="reference_name",
|
||||||
)[0].amount
|
)[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):
|
def test_difference_amount_via_payment_entry(self):
|
||||||
# Make Sale Invoice
|
# Make Sale Invoice
|
||||||
|
|||||||
@@ -144,8 +144,7 @@ class TestPaymentRequest(unittest.TestCase):
|
|||||||
(d[0], d)
|
(d[0], d)
|
||||||
for d in [
|
for d in [
|
||||||
["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
|
["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
|
||||||
[pr.payment_account, 6290.0, 0, None],
|
[pr.payment_account, 5000.0, 0, None],
|
||||||
["_Test Exchange Gain/Loss - _TC", 0, 1290, None],
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
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_return
|
||||||
and not self.is_internal_supplier
|
and not self.is_internal_supplier
|
||||||
):
|
):
|
||||||
@@ -536,6 +536,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
merge_entries=False,
|
merge_entries=False,
|
||||||
from_repost=from_repost,
|
from_repost=from_repost,
|
||||||
)
|
)
|
||||||
|
self.make_exchange_gain_loss_journal()
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
|
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)
|
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.get_asset_gl_entry(gl_entries)
|
||||||
|
|
||||||
self.make_tax_gl_entries(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_internal_transfer_gl_entries(gl_entries)
|
||||||
|
|
||||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||||
|
|||||||
@@ -1273,10 +1273,11 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
pi.save()
|
pi.save()
|
||||||
pi.submit()
|
pi.submit()
|
||||||
|
|
||||||
|
creditors_account = pi.credit_to
|
||||||
|
|
||||||
expected_gle = [
|
expected_gle = [
|
||||||
["_Test Account Cost for Goods Sold - _TC", 37500.0],
|
["_Test Account Cost for Goods Sold - _TC", 37500.0],
|
||||||
["_Test Payable USD - _TC", -35000.0],
|
["_Test Payable USD - _TC", -37500.0],
|
||||||
["Exchange Gain/Loss - _TC", -2500.0],
|
|
||||||
]
|
]
|
||||||
|
|
||||||
gl_entries = frappe.db.sql(
|
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][0], gle.account)
|
||||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
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(
|
pi_2 = make_purchase_invoice(
|
||||||
supplier="_Test Supplier USD",
|
supplier="_Test Supplier USD",
|
||||||
currency="USD",
|
currency="USD",
|
||||||
@@ -1317,10 +1343,12 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
pi_2.save()
|
pi_2.save()
|
||||||
pi_2.submit()
|
pi_2.submit()
|
||||||
|
|
||||||
|
pi_2.reload()
|
||||||
|
self.assertEqual(pi_2.outstanding_amount, 0)
|
||||||
|
|
||||||
expected_gle = [
|
expected_gle = [
|
||||||
["_Test Account Cost for Goods Sold - _TC", 36500.0],
|
["_Test Account Cost for Goods Sold - _TC", 36500.0],
|
||||||
["_Test Payable USD - _TC", -35000.0],
|
["_Test Payable USD - _TC", -36500.0],
|
||||||
["Exchange Gain/Loss - _TC", -1500.0],
|
|
||||||
]
|
]
|
||||||
|
|
||||||
gl_entries = frappe.db.sql(
|
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][0], gle.account)
|
||||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
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.reload()
|
||||||
pi.cancel()
|
pi.cancel()
|
||||||
|
|
||||||
|
self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent.parent, "docstatus"), 2)
|
||||||
|
|
||||||
pi_2.reload()
|
pi_2.reload()
|
||||||
pi_2.cancel()
|
pi_2.cancel()
|
||||||
|
|
||||||
|
self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent_2.parent, "docstatus"), 2)
|
||||||
|
|
||||||
pay.reload()
|
pay.reload()
|
||||||
pay.cancel()
|
pay.cancel()
|
||||||
|
|
||||||
@@ -1736,6 +1791,107 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||||
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
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):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
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")
|
gl = frappe.qb.DocType("GL Entry")
|
||||||
q = (
|
query = (
|
||||||
frappe.qb.from_(gl)
|
frappe.qb.from_(gl)
|
||||||
.select(gl.account, gl.debit, gl.credit, gl.posting_date)
|
.select(gl.account, gl.debit, gl.credit, gl.posting_date)
|
||||||
.where(
|
.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)
|
.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):
|
for i, gle in enumerate(gl_entries):
|
||||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
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(expected_gle[i][2], gle.credit)
|
||||||
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
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):
|
def create_tax_witholding_category(category_name, company, account):
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|||||||
@@ -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.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.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 (
|
from erpnext.assets.doctype.asset.depreciation import (
|
||||||
depreciate_asset,
|
depreciate_asset,
|
||||||
get_disposal_account_and_cost_center,
|
get_disposal_account_and_cost_center,
|
||||||
@@ -32,6 +32,7 @@ from erpnext.assets.doctype.asset.depreciation import (
|
|||||||
reset_depreciation_schedule,
|
reset_depreciation_schedule,
|
||||||
reverse_depreciation_entry_made_after_disposal,
|
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.accounts_controller import validate_account_head
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
||||||
@@ -1029,7 +1030,10 @@ class SalesInvoice(SellingController):
|
|||||||
merge_entries=False,
|
merge_entries=False,
|
||||||
from_repost=from_repost,
|
from_repost=from_repost,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.make_exchange_gain_loss_journal()
|
||||||
elif self.docstatus == 2:
|
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)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
|
||||||
if update_outstanding == "No":
|
if update_outstanding == "No":
|
||||||
@@ -1054,7 +1058,6 @@ class SalesInvoice(SellingController):
|
|||||||
self.make_customer_gl_entry(gl_entries)
|
self.make_customer_gl_entry(gl_entries)
|
||||||
|
|
||||||
self.make_tax_gl_entries(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_internal_transfer_gl_entries(gl_entries)
|
||||||
|
|
||||||
self.make_item_gl_entries(gl_entries)
|
self.make_item_gl_entries(gl_entries)
|
||||||
@@ -1176,12 +1179,13 @@ class SalesInvoice(SellingController):
|
|||||||
self.get("posting_date"),
|
self.get("posting_date"),
|
||||||
)
|
)
|
||||||
asset.db_set("disposal_date", None)
|
asset.db_set("disposal_date", None)
|
||||||
|
add_asset_activity(asset.name, _("Asset returned"))
|
||||||
|
|
||||||
if asset.calculate_depreciation:
|
if asset.calculate_depreciation:
|
||||||
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||||
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
|
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
|
||||||
notes = _(
|
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(
|
).format(
|
||||||
get_link_to_form(asset.doctype, asset.name),
|
get_link_to_form(asset.doctype, asset.name),
|
||||||
get_link_to_form(self.doctype, self.get("name")),
|
get_link_to_form(self.doctype, self.get("name")),
|
||||||
@@ -1209,6 +1213,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.get("posting_date"),
|
self.get("posting_date"),
|
||||||
)
|
)
|
||||||
asset.db_set("disposal_date", self.posting_date)
|
asset.db_set("disposal_date", self.posting_date)
|
||||||
|
add_asset_activity(asset.name, _("Asset sold"))
|
||||||
|
|
||||||
for gle in fixed_asset_gl_entries:
|
for gle in fixed_asset_gl_entries:
|
||||||
gle["against"] = self.customer
|
gle["against"] = self.customer
|
||||||
@@ -1833,7 +1838,7 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
|
|||||||
doc = frappe.get_doc(ref_doc, inter_company_reference)
|
doc = frappe.get_doc(ref_doc, inter_company_reference)
|
||||||
ref_party = doc.supplier if doctype in ["Sales Invoice", "Sales Order"] else doc.customer
|
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:
|
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:
|
if not frappe.get_cached_value(ref_partytype, ref_party, "represents_company") == company:
|
||||||
frappe.throw(_("Invalid Company for Inter Company Transaction."))
|
frappe.throw(_("Invalid Company for Inter Company Transaction."))
|
||||||
|
|
||||||
@@ -1847,7 +1852,7 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
|
|||||||
if not company in companies:
|
if not company in companies:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("{0} not allowed to transact with {1}. Please change the Company.").format(
|
_("{0} not allowed to transact with {1}. Please change the Company.").format(
|
||||||
partytype, company
|
_(partytype), company
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ def get_data():
|
|||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Sales Order": ["items", "sales_order"],
|
"Sales Order": ["items", "sales_order"],
|
||||||
|
"Delivery Note": ["items", "delivery_note"],
|
||||||
"Timesheet": ["timesheets", "time_sheet"],
|
"Timesheet": ["timesheets", "time_sheet"],
|
||||||
},
|
},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
|
|||||||
@@ -3213,15 +3213,10 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
account.disabled = 0
|
account.disabled = 0
|
||||||
account.save()
|
account.save()
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_gain_loss_with_advance_entry(self):
|
def test_gain_loss_with_advance_entry(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
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 = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
|
||||||
|
|
||||||
jv.accounts[0].exchange_rate = 70
|
jv.accounts[0].exchange_rate = 70
|
||||||
@@ -3254,18 +3249,28 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
expected_gle = [
|
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", 7500.0, 0.0, nowdate()],
|
||||||
["_Test Receivable USD - _TC", 0.0, 500.0, nowdate()],
|
|
||||||
["Sales - _TC", 0.0, 7500.0, nowdate()],
|
["Sales - _TC", 0.0, 7500.0, nowdate()],
|
||||||
]
|
]
|
||||||
|
|
||||||
check_gl_entries(self, si.name, expected_gle, nowdate())
|
check_gl_entries(self, si.name, expected_gle, nowdate())
|
||||||
|
|
||||||
frappe.db.set_single_value(
|
si.reload()
|
||||||
"Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
|
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):
|
def test_batch_expiry_for_sales_invoice_return(self):
|
||||||
@@ -3371,6 +3376,13 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
||||||
|
|
||||||
|
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):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ def make_gl_entries(
|
|||||||
):
|
):
|
||||||
if gl_map:
|
if gl_map:
|
||||||
if not cancel:
|
if not cancel:
|
||||||
|
make_acc_dimensions_offsetting_entry(gl_map)
|
||||||
validate_accounting_period(gl_map)
|
validate_accounting_period(gl_map)
|
||||||
validate_disabled_accounts(gl_map)
|
validate_disabled_accounts(gl_map)
|
||||||
gl_map = process_gl_map(gl_map, merge_entries)
|
gl_map = process_gl_map(gl_map, merge_entries)
|
||||||
@@ -51,6 +52,63 @@ def make_gl_entries(
|
|||||||
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
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):
|
def validate_disabled_accounts(gl_map):
|
||||||
accounts = [d.account for d in gl_map if d.account]
|
accounts = [d.account for d in gl_map if d.account]
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from frappe.contacts.doctype.address.address import (
|
|||||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
|
from frappe.query_builder.functions import Date, Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
@@ -920,32 +921,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def get_partywise_advanced_payment_amount(
|
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 posting_date:
|
||||||
if future_payment:
|
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:
|
else:
|
||||||
cond = "posting_date <= '{0}'".format(posting_date)
|
query = query.where(gle.posting_date <= posting_date)
|
||||||
|
|
||||||
if company:
|
if company:
|
||||||
cond += "and company = {0}".format(frappe.db.escape(company))
|
query = query.where(gle.company == company)
|
||||||
|
|
||||||
if party:
|
if party:
|
||||||
cond += "and party = {0}".format(frappe.db.escape(party))
|
query = query.where(gle.party == party)
|
||||||
|
|
||||||
data = frappe.db.sql(
|
data = query.run(as_dict=True)
|
||||||
""" SELECT party, sum({0}) as amount
|
|
||||||
FROM `tabGL Entry`
|
|
||||||
WHERE
|
|
||||||
party_type = %s and against_voucher is null
|
|
||||||
and is_cancelled = 0
|
|
||||||
and {1} GROUP BY party""".format(
|
|
||||||
("credit") if party_type == "Customer" else "debit", cond
|
|
||||||
),
|
|
||||||
party_type,
|
|
||||||
)
|
|
||||||
if data:
|
if data:
|
||||||
return frappe._dict(data)
|
return frappe._dict(data)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
|||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
args = {
|
args = {
|
||||||
"party_type": "Supplier",
|
"account_type": "Payable",
|
||||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||||
}
|
}
|
||||||
return ReceivablePayableReport(filters).run(args)
|
return ReceivablePayableReport(filters).run(args)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
|
|||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
args = {
|
args = {
|
||||||
"party_type": "Supplier",
|
"account_type": "Payable",
|
||||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||||
}
|
}
|
||||||
return AccountsReceivableSummary(filters).run(args)
|
return AccountsReceivableSummary(filters).run(args)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, scrub
|
from frappe import _, qb, scrub
|
||||||
from frappe.query_builder import Criterion
|
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 frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
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):
|
def execute(filters=None):
|
||||||
args = {
|
args = {
|
||||||
"party_type": "Customer",
|
"account_type": "Receivable",
|
||||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||||
}
|
}
|
||||||
return ReceivablePayableReport(filters).run(args)
|
return ReceivablePayableReport(filters).run(args)
|
||||||
@@ -70,8 +70,11 @@ class ReceivablePayableReport(object):
|
|||||||
"Company", self.filters.get("company"), "default_currency"
|
"Company", self.filters.get("company"), "default_currency"
|
||||||
)
|
)
|
||||||
self.currency_precision = get_currency_precision() or 2
|
self.currency_precision = get_currency_precision() or 2
|
||||||
self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
|
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
|
||||||
self.party_type = self.filters.party_type
|
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.party_details = {}
|
||||||
self.invoices = set()
|
self.invoices = set()
|
||||||
self.skip_total_row = 0
|
self.skip_total_row = 0
|
||||||
@@ -197,6 +200,7 @@ class ReceivablePayableReport(object):
|
|||||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
# 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 = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||||
|
|
||||||
|
row.party_type = ple.party_type
|
||||||
return row
|
return row
|
||||||
|
|
||||||
def update_voucher_balance(self, ple):
|
def update_voucher_balance(self, ple):
|
||||||
@@ -207,8 +211,9 @@ class ReceivablePayableReport(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||||
if self.filters.get(scrub(self.party_type)):
|
for party_type in self.party_type:
|
||||||
amount = ple.amount_in_account_currency
|
if self.filters.get(scrub(party_type)):
|
||||||
|
amount = ple.amount_in_account_currency
|
||||||
else:
|
else:
|
||||||
amount = ple.amount
|
amount = ple.amount
|
||||||
amount_in_account_currency = ple.amount_in_account_currency
|
amount_in_account_currency = ple.amount_in_account_currency
|
||||||
@@ -362,7 +367,7 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def get_invoice_details(self):
|
def get_invoice_details(self):
|
||||||
self.invoice_details = frappe._dict()
|
self.invoice_details = frappe._dict()
|
||||||
if self.party_type == "Customer":
|
if self.account_type == "Receivable":
|
||||||
si_list = frappe.db.sql(
|
si_list = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select name, due_date, po_no
|
select name, due_date, po_no
|
||||||
@@ -390,7 +395,7 @@ class ReceivablePayableReport(object):
|
|||||||
d.sales_person
|
d.sales_person
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.party_type == "Supplier":
|
if self.account_type == "Payable":
|
||||||
for pi in frappe.db.sql(
|
for pi in frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select name, due_date, bill_no, bill_date
|
select name, due_date, bill_no, bill_date
|
||||||
@@ -421,8 +426,10 @@ class ReceivablePayableReport(object):
|
|||||||
# customer / supplier name
|
# customer / supplier name
|
||||||
party_details = self.get_party_details(row.party) or {}
|
party_details = self.get_party_details(row.party) or {}
|
||||||
row.update(party_details)
|
row.update(party_details)
|
||||||
if self.filters.get(scrub(self.filters.party_type)):
|
for party_type in self.party_type:
|
||||||
row.currency = row.account_currency
|
if self.filters.get(scrub(party_type)):
|
||||||
|
row.currency = row.account_currency
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
row.currency = self.company_currency
|
row.currency = self.company_currency
|
||||||
|
|
||||||
@@ -532,65 +539,67 @@ class ReceivablePayableReport(object):
|
|||||||
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
|
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
|
||||||
|
|
||||||
def get_future_payments_from_payment_entry(self):
|
def get_future_payments_from_payment_entry(self):
|
||||||
return frappe.db.sql(
|
pe = frappe.qb.DocType("Payment Entry")
|
||||||
"""
|
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||||
select
|
return (
|
||||||
ref.reference_name as invoice_no,
|
frappe.qb.from_(pe)
|
||||||
payment_entry.party,
|
.inner_join(pe_ref)
|
||||||
payment_entry.party_type,
|
.on(pe_ref.parent == pe.name)
|
||||||
payment_entry.posting_date as future_date,
|
.select(
|
||||||
ref.allocated_amount as future_amount,
|
(pe_ref.reference_name).as_("invoice_no"),
|
||||||
payment_entry.reference_no as future_ref
|
pe.party,
|
||||||
from
|
pe.party_type,
|
||||||
`tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref
|
(pe.posting_date).as_("future_date"),
|
||||||
on
|
(pe_ref.allocated_amount).as_("future_amount"),
|
||||||
(ref.parent = payment_entry.name)
|
(pe.reference_no).as_("future_ref"),
|
||||||
where
|
)
|
||||||
payment_entry.docstatus < 2
|
.where(
|
||||||
and payment_entry.posting_date > %s
|
(pe.docstatus < 2)
|
||||||
and payment_entry.party_type = %s
|
& (pe.posting_date > self.filters.report_date)
|
||||||
""",
|
& (pe.party_type.isin(self.party_type))
|
||||||
(self.filters.report_date, self.party_type),
|
)
|
||||||
as_dict=1,
|
).run(as_dict=True)
|
||||||
)
|
|
||||||
|
|
||||||
def get_future_payments_from_journal_entry(self):
|
def get_future_payments_from_journal_entry(self):
|
||||||
if self.filters.get("party"):
|
je = frappe.qb.DocType("Journal Entry")
|
||||||
amount_field = (
|
jea = frappe.qb.DocType("Journal Entry Account")
|
||||||
"jea.debit_in_account_currency - jea.credit_in_account_currency"
|
query = (
|
||||||
if self.party_type == "Supplier"
|
frappe.qb.from_(je)
|
||||||
else "jea.credit_in_account_currency - jea.debit_in_account_currency"
|
.inner_join(jea)
|
||||||
)
|
.on(jea.parent == je.name)
|
||||||
else:
|
.select(
|
||||||
amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit"
|
jea.reference_name.as_("invoice_no"),
|
||||||
|
|
||||||
return frappe.db.sql(
|
|
||||||
"""
|
|
||||||
select
|
|
||||||
jea.reference_name as invoice_no,
|
|
||||||
jea.party,
|
jea.party,
|
||||||
jea.party_type,
|
jea.party_type,
|
||||||
je.posting_date as future_date,
|
je.posting_date.as_("future_date"),
|
||||||
sum('{0}') as future_amount,
|
je.cheque_no.as_("future_ref"),
|
||||||
je.cheque_no as future_ref
|
)
|
||||||
from
|
.where(
|
||||||
`tabJournal Entry` as je inner join `tabJournal Entry Account` as jea
|
(je.docstatus < 2)
|
||||||
on
|
& (je.posting_date > self.filters.report_date)
|
||||||
(jea.parent = je.name)
|
& (jea.party_type.isin(self.party_type))
|
||||||
where
|
& (jea.reference_name.isnotnull())
|
||||||
je.docstatus < 2
|
& (jea.reference_name != "")
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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):
|
def allocate_future_payments(self, row):
|
||||||
# future payments are captured in additional columns
|
# future payments are captured in additional columns
|
||||||
# this method allocates pending future payments against a voucher to
|
# this method allocates pending future payments against a voucher to
|
||||||
@@ -619,13 +628,17 @@ class ReceivablePayableReport(object):
|
|||||||
row.future_ref = ", ".join(row.future_ref)
|
row.future_ref = ", ".join(row.future_ref)
|
||||||
|
|
||||||
def get_return_entries(self):
|
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}
|
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||||
party_field = scrub(self.filters.party_type)
|
or_filters = {}
|
||||||
if self.filters.get(party_field):
|
for party_type in self.party_type:
|
||||||
filters.update({party_field: self.filters.get(party_field)})
|
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(
|
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):
|
def set_ageing(self, row):
|
||||||
@@ -716,6 +729,7 @@ class ReceivablePayableReport(object):
|
|||||||
)
|
)
|
||||||
.where(ple.delinked == 0)
|
.where(ple.delinked == 0)
|
||||||
.where(Criterion.all(self.qb_selection_filter))
|
.where(Criterion.all(self.qb_selection_filter))
|
||||||
|
.where(Criterion.any(self.or_filters))
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.filters.get("group_by_party"):
|
if self.filters.get("group_by_party"):
|
||||||
@@ -746,16 +760,18 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def prepare_conditions(self):
|
def prepare_conditions(self):
|
||||||
self.qb_selection_filter = []
|
self.qb_selection_filter = []
|
||||||
party_type_field = scrub(self.party_type)
|
self.or_filters = []
|
||||||
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
|
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":
|
if party_type_field == "customer":
|
||||||
self.add_customer_filters()
|
self.add_customer_filters()
|
||||||
|
|
||||||
elif party_type_field == "supplier":
|
elif party_type_field == "supplier":
|
||||||
self.add_supplier_filters()
|
self.add_supplier_filters()
|
||||||
|
|
||||||
if self.filters.cost_center:
|
if self.filters.cost_center:
|
||||||
self.get_cost_center_conditions()
|
self.get_cost_center_conditions()
|
||||||
@@ -784,11 +800,10 @@ class ReceivablePayableReport(object):
|
|||||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||||
else:
|
else:
|
||||||
# get GL with "receivable" or "payable" account_type
|
# get GL with "receivable" or "payable" account_type
|
||||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
|
||||||
accounts = [
|
accounts = [
|
||||||
d.name
|
d.name
|
||||||
for d in frappe.get_all(
|
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 +893,7 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def get_party_details(self, party):
|
def get_party_details(self, party):
|
||||||
if not party in self.party_details:
|
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"]
|
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
|
||||||
|
|
||||||
if self.filters.get("sales_partner"):
|
if self.filters.get("sales_partner"):
|
||||||
@@ -901,14 +916,20 @@ class ReceivablePayableReport(object):
|
|||||||
self.columns = []
|
self.columns = []
|
||||||
self.add_column("Posting Date", fieldtype="Date")
|
self.add_column("Posting Date", fieldtype="Date")
|
||||||
self.add_column(
|
self.add_column(
|
||||||
label=_(self.party_type),
|
label="Party Type",
|
||||||
|
fieldname="party_type",
|
||||||
|
fieldtype="Data",
|
||||||
|
width=100,
|
||||||
|
)
|
||||||
|
self.add_column(
|
||||||
|
label="Party",
|
||||||
fieldname="party",
|
fieldname="party",
|
||||||
fieldtype="Link",
|
fieldtype="Dynamic Link",
|
||||||
options=self.party_type,
|
options="party_type",
|
||||||
width=180,
|
width=180,
|
||||||
)
|
)
|
||||||
self.add_column(
|
self.add_column(
|
||||||
label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
|
label=self.account_type + " Account",
|
||||||
fieldname="party_account",
|
fieldname="party_account",
|
||||||
fieldtype="Link",
|
fieldtype="Link",
|
||||||
options="Account",
|
options="Account",
|
||||||
@@ -916,13 +937,19 @@ class ReceivablePayableReport(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.party_naming_by == "Naming Series":
|
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(
|
self.add_column(
|
||||||
_("{0} Name").format(self.party_type),
|
label=label,
|
||||||
fieldname=scrub(self.party_type) + "_name",
|
fieldname=fieldname,
|
||||||
fieldtype="Data",
|
fieldtype="Data",
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.party_type == "Customer":
|
if self.account_type == "Receivable":
|
||||||
self.add_column(
|
self.add_column(
|
||||||
_("Customer Contact"),
|
_("Customer Contact"),
|
||||||
fieldname="customer_primary_contact",
|
fieldname="customer_primary_contact",
|
||||||
@@ -942,7 +969,7 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
self.add_column(label="Due Date", fieldtype="Date")
|
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 No"), fieldname="bill_no", fieldtype="Data")
|
||||||
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
|
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
|
||||||
|
|
||||||
@@ -952,7 +979,7 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
||||||
self.add_column(_("Paid Amount"), fieldname="paid")
|
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")
|
self.add_column(_("Credit Note"), fieldname="credit_note")
|
||||||
else:
|
else:
|
||||||
# note: fieldname is still `credit_note`
|
# note: fieldname is still `credit_note`
|
||||||
@@ -970,7 +997,7 @@ class ReceivablePayableReport(object):
|
|||||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
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")
|
self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
|
||||||
|
|
||||||
# comma separated list of linked delivery notes
|
# comma separated list of linked delivery notes
|
||||||
@@ -991,7 +1018,7 @@ class ReceivablePayableReport(object):
|
|||||||
if self.filters.sales_partner:
|
if self.filters.sales_partner:
|
||||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
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(
|
self.add_column(
|
||||||
label=_("Supplier Group"),
|
label=_("Supplier Group"),
|
||||||
fieldname="supplier_group",
|
fieldname="supplier_group",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
|||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
args = {
|
args = {
|
||||||
"party_type": "Customer",
|
"account_type": "Receivable",
|
||||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +21,10 @@ def execute(filters=None):
|
|||||||
|
|
||||||
class AccountsReceivableSummary(ReceivablePayableReport):
|
class AccountsReceivableSummary(ReceivablePayableReport):
|
||||||
def run(self, args):
|
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(
|
self.party_naming_by = frappe.db.get_value(
|
||||||
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
||||||
)
|
)
|
||||||
@@ -35,13 +38,19 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
self.get_party_total(args)
|
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 = (
|
party_advance_amount = (
|
||||||
get_partywise_advanced_payment_amount(
|
get_partywise_advanced_payment_amount(
|
||||||
self.party_type,
|
self.party_type,
|
||||||
self.filters.report_date,
|
self.filters.report_date,
|
||||||
self.filters.show_future_payments,
|
self.filters.show_future_payments,
|
||||||
self.filters.company,
|
self.filters.company,
|
||||||
party=self.filters.get(scrub(self.party_type)),
|
party=party,
|
||||||
|
account_type=self.account_type,
|
||||||
)
|
)
|
||||||
or {}
|
or {}
|
||||||
)
|
)
|
||||||
@@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
row.party = party
|
row.party = party
|
||||||
if self.party_naming_by == "Naming Series":
|
if self.party_naming_by == "Naming Series":
|
||||||
row.party_name = frappe.get_cached_value(
|
if self.account_type == "Payable":
|
||||||
self.party_type, party, scrub(self.party_type) + "_name"
|
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)
|
row.update(party_dict)
|
||||||
|
|
||||||
@@ -93,6 +106,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
# set territory, customer_group, sales person etc
|
# set territory, customer_group, sales person etc
|
||||||
self.set_party_details(d)
|
self.set_party_details(d)
|
||||||
|
self.party_total[d.party].update({"party_type": d.party_type})
|
||||||
|
|
||||||
def init_party_total(self, row):
|
def init_party_total(self, row):
|
||||||
self.party_total.setdefault(
|
self.party_total.setdefault(
|
||||||
@@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
self.columns = []
|
self.columns = []
|
||||||
self.add_column(
|
self.add_column(
|
||||||
label=_(self.party_type),
|
label="Party Type",
|
||||||
|
fieldname="party_type",
|
||||||
|
fieldtype="Data",
|
||||||
|
width=100,
|
||||||
|
)
|
||||||
|
self.add_column(
|
||||||
|
label="Party",
|
||||||
fieldname="party",
|
fieldname="party",
|
||||||
fieldtype="Link",
|
fieldtype="Dynamic Link",
|
||||||
options=self.party_type,
|
options="party_type",
|
||||||
width=180,
|
width=180,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.party_naming_by == "Naming Series":
|
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(_("Advance Amount"), fieldname="advance")
|
||||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
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=_("Future Payment Amount"), fieldname="future_amount")
|
||||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||||
|
|
||||||
if self.party_type == "Customer":
|
if self.account_type == "Receivable":
|
||||||
self.add_column(
|
self.add_column(
|
||||||
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
|
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
frappe.require("assets/erpnext/js/financial_statements.js", function () {
|
||||||
frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements);
|
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({
|
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||||
"fieldname": "accumulated_values",
|
fieldname: "accumulated_values",
|
||||||
"label": __("Accumulated Values"),
|
label: __("Accumulated Values"),
|
||||||
"fieldtype": "Check",
|
fieldtype: "Check",
|
||||||
"default": 1
|
default: 1,
|
||||||
});
|
});
|
||||||
|
console.log(frappe.query_reports["Balance Sheet"]["filters"]);
|
||||||
|
|
||||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||||
"fieldname": "include_default_book_entries",
|
fieldname: "include_default_book_entries",
|
||||||
"label": __("Include Default Book Entries"),
|
label: __("Include Default Book Entries"),
|
||||||
"fieldtype": "Check",
|
fieldtype: "Check",
|
||||||
"default": 1
|
default: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
51
erpnext/accounts/report/balance_sheet/test_balance_sheet.py
Normal file
51
erpnext/accounts/report/balance_sheet/test_balance_sheet.py
Normal file
@@ -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)
|
||||||
@@ -6,6 +6,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder import Criterion
|
||||||
from frappe.utils import flt, getdate
|
from frappe.utils import flt, getdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -359,6 +360,7 @@ def get_data(
|
|||||||
accounts_by_name,
|
accounts_by_name,
|
||||||
accounts,
|
accounts,
|
||||||
ignore_closing_entries=False,
|
ignore_closing_entries=False,
|
||||||
|
root_type=root_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
|
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_by_name,
|
||||||
accounts,
|
accounts,
|
||||||
ignore_closing_entries=False,
|
ignore_closing_entries=False,
|
||||||
|
root_type=None,
|
||||||
):
|
):
|
||||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||||
|
|
||||||
@@ -610,7 +613,6 @@ def set_gl_entries_by_account(
|
|||||||
"Company", filters.get("company"), ["lft", "rgt"]
|
"Company", filters.get("company"), ["lft", "rgt"]
|
||||||
)
|
)
|
||||||
|
|
||||||
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
|
|
||||||
companies = frappe.db.sql(
|
companies = frappe.db.sql(
|
||||||
""" select name, default_currency from `tabCompany`
|
""" select name, default_currency from `tabCompany`
|
||||||
where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
|
where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
|
||||||
@@ -626,27 +628,43 @@ def set_gl_entries_by_account(
|
|||||||
)
|
)
|
||||||
|
|
||||||
for d in companies:
|
for d in companies:
|
||||||
gl_entries = frappe.db.sql(
|
gle = frappe.qb.DocType("GL Entry")
|
||||||
"""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
|
account = frappe.qb.DocType("Account")
|
||||||
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
|
query = (
|
||||||
acc.account_name, acc.account_number
|
frappe.qb.from_(gle)
|
||||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
|
.inner_join(account)
|
||||||
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
|
.on(account.name == gle.account)
|
||||||
order by gl.account, gl.posting_date""".format(
|
.select(
|
||||||
additional_conditions=additional_conditions
|
gle.posting_date,
|
||||||
),
|
gle.account,
|
||||||
{
|
gle.debit,
|
||||||
"from_date": from_date,
|
gle.credit,
|
||||||
"to_date": to_date,
|
gle.is_opening,
|
||||||
"lft": root_lft,
|
gle.company,
|
||||||
"rgt": root_rgt,
|
gle.fiscal_year,
|
||||||
"company": d.name,
|
gle.debit_in_account_currency,
|
||||||
"finance_book": filters.get("finance_book"),
|
gle.credit_in_account_currency,
|
||||||
"company_fb": frappe.get_cached_value("Company", d.name, "default_finance_book"),
|
gle.account_currency,
|
||||||
},
|
account.account_name,
|
||||||
as_dict=True,
|
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:
|
if filters and filters.get("presentation_currency") != d.default_currency:
|
||||||
currency_info["company"] = d.name
|
currency_info["company"] = d.name
|
||||||
currency_info["company_currency"] = d.default_currency
|
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)
|
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 = []
|
additional_conditions = []
|
||||||
|
|
||||||
if ignore_closing_entries:
|
if ignore_closing_entries:
|
||||||
additional_conditions.append("gl.voucher_type != 'Period Closing Voucher'")
|
additional_conditions.append((gle.voucher_type != "Period Closing Voucher"))
|
||||||
|
|
||||||
if from_date:
|
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"):
|
if filters.get("include_default_book_entries"):
|
||||||
additional_conditions.append(
|
additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
|
||||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
|
||||||
)
|
|
||||||
else:
|
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):
|
def add_total_row(out, root_type, balance_must_be, companies, company_currency):
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import nowdate
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account
|
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 (
|
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
|
||||||
Deferred_Revenue_and_Expense_Report,
|
Deferred_Revenue_and_Expense_Report,
|
||||||
)
|
)
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
|
|
||||||
class TestDeferredRevenueAndExpense(unittest.TestCase):
|
class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
clear_accounts_and_items()
|
|
||||||
create_company()
|
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
|
|
||||||
def clear_old_entries(self):
|
def clear_old_entries(self):
|
||||||
@@ -51,55 +51,58 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
if deferred_invoices:
|
if deferred_invoices:
|
||||||
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
|
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
|
||||||
|
|
||||||
def test_deferred_revenue(self):
|
def setup_deferred_accounts_and_items(self):
|
||||||
self.clear_old_entries()
|
# 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
|
# created deferred expense accounts, if not found
|
||||||
deferred_revenue_account = create_account(
|
self.deferred_expense_account = create_account(
|
||||||
account_name="Deferred Revenue",
|
account_name="Deferred Expense",
|
||||||
parent_account="Current Liabilities - _CD",
|
parent_account="Current Assets - " + self.company_abbr,
|
||||||
company="_Test Company DR",
|
company=self.company,
|
||||||
)
|
)
|
||||||
|
|
||||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
def setUp(self):
|
||||||
acc_settings.book_deferred_entries_based_on = "Months"
|
self.create_company()
|
||||||
acc_settings.save()
|
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")
|
def tearDown(self):
|
||||||
customer.customer_name = "_Test Customer DR"
|
frappe.db.rollback()
|
||||||
customer.type = "Individual"
|
|
||||||
customer.insert()
|
|
||||||
|
|
||||||
item = create_item(
|
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||||
"_Test Internet Subscription",
|
def test_deferred_revenue(self):
|
||||||
is_stock_item=0,
|
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
|
||||||
warehouse="All Warehouses - _CD",
|
item = frappe.get_doc("Item", self.item)
|
||||||
company="_Test Company DR",
|
|
||||||
)
|
|
||||||
item.enable_deferred_revenue = 1
|
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.no_of_months = 3
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item=item.name,
|
item=self.item,
|
||||||
company="_Test Company DR",
|
company=self.company,
|
||||||
customer="_Test Customer DR",
|
customer=self.customer,
|
||||||
debit_to="Debtors - _CD",
|
debit_to=self.debit_to,
|
||||||
posting_date="2021-05-01",
|
posting_date="2021-05-01",
|
||||||
parent_cost_center="Main - _CD",
|
parent_cost_center=self.cost_center,
|
||||||
cost_center="Main - _CD",
|
cost_center=self.cost_center,
|
||||||
do_not_save=True,
|
do_not_save=True,
|
||||||
rate=300,
|
rate=300,
|
||||||
price_list_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].enable_deferred_revenue = 1
|
||||||
si.items[0].service_start_date = "2021-05-01"
|
si.items[0].service_start_date = "2021-05-01"
|
||||||
si.items[0].service_end_date = "2021-08-01"
|
si.items[0].service_end_date = "2021-08-01"
|
||||||
si.items[0].deferred_revenue_account = deferred_revenue_account
|
si.items[0].deferred_revenue_account = self.deferred_revenue_account
|
||||||
si.items[0].income_account = "Sales - _CD"
|
|
||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
@@ -110,7 +113,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
start_date="2021-05-01",
|
start_date="2021-05-01",
|
||||||
end_date="2021-08-01",
|
end_date="2021-08-01",
|
||||||
type="Income",
|
type="Income",
|
||||||
company="_Test Company DR",
|
company=self.company,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
pda.insert()
|
pda.insert()
|
||||||
@@ -120,7 +123,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
||||||
self.filters = frappe._dict(
|
self.filters = frappe._dict(
|
||||||
{
|
{
|
||||||
"company": frappe.defaults.get_user_default("Company"),
|
"company": self.company,
|
||||||
"filter_based_on": "Date Range",
|
"filter_based_on": "Date Range",
|
||||||
"period_start_date": "2021-05-01",
|
"period_start_date": "2021-05-01",
|
||||||
"period_end_date": "2021-08-01",
|
"period_end_date": "2021-08-01",
|
||||||
@@ -142,57 +145,36 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(report.period_total, expected)
|
self.assertEqual(report.period_total, expected)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||||
def test_deferred_expense(self):
|
def test_deferred_expense(self):
|
||||||
self.clear_old_entries()
|
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||||
|
item = frappe.get_doc("Item", self.item)
|
||||||
# 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",
|
|
||||||
)
|
|
||||||
item.enable_deferred_expense = 1
|
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.no_of_months_exp = 3
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
item=item.name,
|
item=self.item,
|
||||||
company="_Test Company DR",
|
company=self.company,
|
||||||
supplier="_Test Furniture Supplier",
|
supplier=self.supplier,
|
||||||
is_return=False,
|
is_return=False,
|
||||||
update_stock=False,
|
update_stock=False,
|
||||||
posting_date=frappe.utils.datetime.date(2021, 5, 1),
|
posting_date=frappe.utils.datetime.date(2021, 5, 1),
|
||||||
parent_cost_center="Main - _CD",
|
parent_cost_center=self.cost_center,
|
||||||
cost_center="Main - _CD",
|
cost_center=self.cost_center,
|
||||||
do_not_save=True,
|
do_not_save=True,
|
||||||
rate=300,
|
rate=300,
|
||||||
price_list_rate=300,
|
price_list_rate=300,
|
||||||
warehouse="All Warehouses - _CD",
|
warehouse=self.warehouse,
|
||||||
qty=1,
|
qty=1,
|
||||||
)
|
)
|
||||||
pi.set_posting_time = True
|
pi.set_posting_time = True
|
||||||
pi.items[0].enable_deferred_expense = 1
|
pi.items[0].enable_deferred_expense = 1
|
||||||
pi.items[0].service_start_date = "2021-05-01"
|
pi.items[0].service_start_date = "2021-05-01"
|
||||||
pi.items[0].service_end_date = "2021-08-01"
|
pi.items[0].service_end_date = "2021-08-01"
|
||||||
pi.items[0].deferred_expense_account = deferred_expense_account
|
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||||
pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
|
pi.items[0].expense_account = self.expense_account
|
||||||
pi.save()
|
pi.save()
|
||||||
pi.submit()
|
pi.submit()
|
||||||
|
|
||||||
@@ -203,7 +185,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
start_date="2021-05-01",
|
start_date="2021-05-01",
|
||||||
end_date="2021-08-01",
|
end_date="2021-08-01",
|
||||||
type="Expense",
|
type="Expense",
|
||||||
company="_Test Company DR",
|
company=self.company,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
pda.insert()
|
pda.insert()
|
||||||
@@ -213,7 +195,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
||||||
self.filters = frappe._dict(
|
self.filters = frappe._dict(
|
||||||
{
|
{
|
||||||
"company": frappe.defaults.get_user_default("Company"),
|
"company": self.company,
|
||||||
"filter_based_on": "Date Range",
|
"filter_based_on": "Date Range",
|
||||||
"period_start_date": "2021-05-01",
|
"period_start_date": "2021-05-01",
|
||||||
"period_end_date": "2021-08-01",
|
"period_end_date": "2021-08-01",
|
||||||
@@ -235,52 +217,31 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(report.period_total, expected)
|
self.assertEqual(report.period_total, expected)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||||
def test_zero_months(self):
|
def test_zero_months(self):
|
||||||
self.clear_old_entries()
|
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
|
||||||
# created deferred expense accounts, if not found
|
item = frappe.get_doc("Item", self.item)
|
||||||
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",
|
|
||||||
)
|
|
||||||
item.enable_deferred_revenue = 1
|
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.no_of_months = 0
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item=item.name,
|
item=item.name,
|
||||||
company="_Test Company DR",
|
company=self.company,
|
||||||
customer="_Test Customer DR",
|
customer=self.customer,
|
||||||
debit_to="Debtors - _CD",
|
debit_to=self.debit_to,
|
||||||
posting_date="2021-05-01",
|
posting_date="2021-05-01",
|
||||||
parent_cost_center="Main - _CD",
|
parent_cost_center=self.cost_center,
|
||||||
cost_center="Main - _CD",
|
cost_center=self.cost_center,
|
||||||
do_not_save=True,
|
do_not_save=True,
|
||||||
rate=300,
|
rate=300,
|
||||||
price_list_rate=300,
|
price_list_rate=300,
|
||||||
)
|
)
|
||||||
|
|
||||||
si.items[0].enable_deferred_revenue = 1
|
si.items[0].enable_deferred_revenue = 1
|
||||||
si.items[0].income_account = "Sales - _CD"
|
si.items[0].income_account = self.income_account
|
||||||
si.items[0].deferred_revenue_account = deferred_revenue_account
|
si.items[0].deferred_revenue_account = self.deferred_revenue_account
|
||||||
si.items[0].income_account = "Sales - _CD"
|
|
||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
@@ -291,7 +252,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
start_date="2021-05-01",
|
start_date="2021-05-01",
|
||||||
end_date="2021-08-01",
|
end_date="2021-08-01",
|
||||||
type="Income",
|
type="Income",
|
||||||
company="_Test Company DR",
|
company=self.company,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
pda.insert()
|
pda.insert()
|
||||||
@@ -301,7 +262,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
||||||
self.filters = frappe._dict(
|
self.filters = frappe._dict(
|
||||||
{
|
{
|
||||||
"company": frappe.defaults.get_user_default("Company"),
|
"company": self.company,
|
||||||
"filter_based_on": "Date Range",
|
"filter_based_on": "Date Range",
|
||||||
"period_start_date": "2021-05-01",
|
"period_start_date": "2021-05-01",
|
||||||
"period_end_date": "2021-08-01",
|
"period_end_date": "2021-08-01",
|
||||||
@@ -322,30 +283,3 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||||
]
|
]
|
||||||
self.assertEqual(report.period_total, expected)
|
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()
|
|
||||||
|
|||||||
72
erpnext/accounts/report/financial_ratios/financial_ratios.js
Normal file
72
erpnext/accounts/report/financial_ratios/financial_ratios.js
Normal file
@@ -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 = $(`<span>${value}</span>`);
|
||||||
|
let $value = $(value).css("font-weight", "bold");
|
||||||
|
value = $value.wrap("<p></p>").parent().html();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
|
||||||
|
column.fieldtype = "Data";
|
||||||
|
}
|
||||||
|
|
||||||
|
value = default_formatter(value, row, column, data);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
296
erpnext/accounts/report/financial_ratios/financial_ratios.py
Normal file
296
erpnext/accounts/report/financial_ratios/financial_ratios.py
Normal file
@@ -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
|
||||||
@@ -188,6 +188,7 @@ def get_data(
|
|||||||
filters,
|
filters,
|
||||||
gl_entries_by_account,
|
gl_entries_by_account,
|
||||||
ignore_closing_entries=ignore_closing_entries,
|
ignore_closing_entries=ignore_closing_entries,
|
||||||
|
root_type=root_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
calculate_values(
|
calculate_values(
|
||||||
@@ -417,13 +418,28 @@ def set_gl_entries_by_account(
|
|||||||
gl_entries_by_account,
|
gl_entries_by_account,
|
||||||
ignore_closing_entries=False,
|
ignore_closing_entries=False,
|
||||||
ignore_opening_entries=False,
|
ignore_opening_entries=False,
|
||||||
|
root_type=None,
|
||||||
):
|
):
|
||||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||||
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(
|
accounts_list = frappe.db.get_all(
|
||||||
"Account",
|
"Account",
|
||||||
filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
|
filters=account_filters,
|
||||||
pluck="name",
|
pluck="name",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
frappe.require("assets/erpnext/js/financial_statements.js", function () {
|
||||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
frappe.query_reports["Profit and Loss Statement"] = $.extend(
|
||||||
frappe.query_reports["Profit and Loss Statement"] = $.extend({},
|
{},
|
||||||
erpnext.financial_statements);
|
erpnext.financial_statements
|
||||||
|
|
||||||
erpnext.utils.add_dimensions('Profit and Loss Statement', 10);
|
|
||||||
|
|
||||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
|
||||||
{
|
|
||||||
"fieldname": "include_default_book_entries",
|
|
||||||
"label": __("Include Default Book Entries"),
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"default": 1
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
||||||
|
|
||||||
|
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||||
|
fieldname: "accumulated_values",
|
||||||
|
label: __("Accumulated Values"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
118
erpnext/accounts/report/trial_balance/test_trial_balance.py
Normal file
118
erpnext/accounts/report/trial_balance/test_trial_balance.py
Normal file
@@ -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()
|
||||||
@@ -259,7 +259,7 @@ def get_opening_balance(
|
|||||||
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
|
||||||
cost_center = frappe.qb.DocType("Cost Center")
|
cost_center = frappe.qb.DocType("Cost Center")
|
||||||
opening_balance = opening_balance.where(
|
opening_balance = opening_balance.where(
|
||||||
closing_balance.cost_center.in_(
|
closing_balance.cost_center.isin(
|
||||||
frappe.qb.from_(cost_center)
|
frappe.qb.from_(cost_center)
|
||||||
.select("name")
|
.select("name")
|
||||||
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
|
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ def get_data(filters):
|
|||||||
.select(
|
.select(
|
||||||
gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")
|
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)
|
.groupby(gle.voucher_no)
|
||||||
)
|
)
|
||||||
query = apply_filters(query, filters, gle)
|
query = apply_filters(query, filters, gle)
|
||||||
|
|||||||
80
erpnext/accounts/test/accounts_mixin.py
Normal file
80
erpnext/accounts/test/accounts_mixin.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsTestMixin:
|
||||||
|
def create_customer(self, customer_name, currency=None):
|
||||||
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
customer = frappe.new_doc("Customer")
|
||||||
|
customer.customer_name = customer_name
|
||||||
|
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, 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, 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
|
||||||
|
|
||||||
|
# create bank account
|
||||||
|
bank_account = "HDFC - " + abbr
|
||||||
|
if frappe.db.exists("Account", bank_account):
|
||||||
|
self.bank = bank_account
|
||||||
|
else:
|
||||||
|
bank_acc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Account",
|
||||||
|
"account_name": "HDFC",
|
||||||
|
"parent_account": "Bank Accounts - " + abbr,
|
||||||
|
"company": self.company,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
bank_acc.save()
|
||||||
|
self.bank = bank_acc.name
|
||||||
@@ -80,18 +80,27 @@ class TestUtils(unittest.TestCase):
|
|||||||
item = make_item().name
|
item = make_item().name
|
||||||
|
|
||||||
purchase_invoice = make_purchase_invoice(
|
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()
|
purchase_invoice.submit()
|
||||||
|
|
||||||
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
|
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.paid_amount = 15725
|
||||||
payment_entry.deductions = []
|
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 = []
|
payment_entry.references = []
|
||||||
|
self.assertEqual(payment_entry.difference_amount, 0.0)
|
||||||
payment_entry.submit()
|
payment_entry.submit()
|
||||||
|
|
||||||
payment_reconciliation = frappe.new_doc("Payment Reconciliation")
|
payment_reconciliation = frappe.new_doc("Payment Reconciliation")
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ def get_balance_on(
|
|||||||
in_account_currency=True,
|
in_account_currency=True,
|
||||||
cost_center=None,
|
cost_center=None,
|
||||||
ignore_account_permission=False,
|
ignore_account_permission=False,
|
||||||
|
account_type=None,
|
||||||
):
|
):
|
||||||
if not account and frappe.form_dict.get("account"):
|
if not account and frappe.form_dict.get("account"):
|
||||||
account = frappe.form_dict.get("account")
|
account = frappe.form_dict.get("account")
|
||||||
@@ -254,6 +255,21 @@ def get_balance_on(
|
|||||||
else:
|
else:
|
||||||
cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
|
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:
|
if party_type and party:
|
||||||
cond.append(
|
cond.append(
|
||||||
"""gle.party_type = %s and gle.party = %s """
|
"""gle.party_type = %s and gle.party = %s """
|
||||||
@@ -263,7 +279,8 @@ def get_balance_on(
|
|||||||
if company:
|
if company:
|
||||||
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
|
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:
|
if in_account_currency:
|
||||||
select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
|
select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
|
||||||
else:
|
else:
|
||||||
@@ -276,7 +293,6 @@ def get_balance_on(
|
|||||||
select_field, " and ".join(cond)
|
select_field, " and ".join(cond)
|
||||||
)
|
)
|
||||||
)[0][0]
|
)[0][0]
|
||||||
|
|
||||||
# if bal is None, return 0
|
# if bal is None, return 0
|
||||||
return flt(bal)
|
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
|
# update ref in advance entry
|
||||||
if voucher_type == "Journal Entry":
|
if voucher_type == "Journal Entry":
|
||||||
update_reference_in_journal_entry(entry, doc, do_not_save=True)
|
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:
|
else:
|
||||||
update_reference_in_payment_entry(
|
update_reference_in_payment_entry(
|
||||||
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
|
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
|
||||||
@@ -618,9 +637,7 @@ def update_reference_in_payment_entry(
|
|||||||
"total_amount": d.grand_total,
|
"total_amount": d.grand_total,
|
||||||
"outstanding_amount": d.outstanding_amount,
|
"outstanding_amount": d.outstanding_amount,
|
||||||
"allocated_amount": d.allocated_amount,
|
"allocated_amount": d.allocated_amount,
|
||||||
"exchange_rate": d.exchange_rate
|
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
||||||
if not d.exchange_gain_loss
|
|
||||||
else payment_entry.get_exchange_rate(),
|
|
||||||
"exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation
|
"exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation
|
||||||
"account": d.account,
|
"account": d.account,
|
||||||
}
|
}
|
||||||
@@ -642,28 +659,48 @@ def update_reference_in_payment_entry(
|
|||||||
new_row.docstatus = 1
|
new_row.docstatus = 1
|
||||||
new_row.update(reference_details)
|
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.flags.ignore_validate_update_after_submit = True
|
||||||
payment_entry.setup_party_account_field()
|
payment_entry.setup_party_account_field()
|
||||||
payment_entry.set_missing_values()
|
payment_entry.set_missing_values()
|
||||||
if not skip_ref_details_update_for_pe:
|
if not skip_ref_details_update_for_pe:
|
||||||
payment_entry.set_missing_ref_details()
|
payment_entry.set_missing_ref_details()
|
||||||
payment_entry.set_amounts()
|
payment_entry.set_amounts()
|
||||||
|
payment_entry.make_exchange_gain_loss_journal()
|
||||||
|
|
||||||
if not do_not_save:
|
if not do_not_save:
|
||||||
payment_entry.save(ignore_permissions=True)
|
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):
|
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_jv(ref_doc.doctype, ref_doc.name)
|
||||||
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
|
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
|
||||||
@@ -1820,3 +1857,74 @@ class QueryPaymentLedger(object):
|
|||||||
self.query_for_outstanding()
|
self.query_for_outstanding()
|
||||||
|
|
||||||
return self.voucher_outstandings
|
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
|
||||||
|
|||||||
@@ -207,34 +207,39 @@ frappe.ui.form.on('Asset', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
render_depreciation_schedule_view: function(frm, depr_schedule) {
|
render_depreciation_schedule_view: function(frm, depr_schedule) {
|
||||||
var wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
|
let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
|
||||||
|
|
||||||
let table = $(`<table class="table table-bordered" style="margin-top:0px;">
|
let data = [];
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td align="center">${__("No.")}</td>
|
|
||||||
<td>${__("Schedule Date")}</td>
|
|
||||||
<td align="right">${__("Depreciation Amount")}</td>
|
|
||||||
<td align="right">${__("Accumulated Depreciation Amount")}</td>
|
|
||||||
<td>${__("Journal Entry")}</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>`);
|
|
||||||
|
|
||||||
depr_schedule.forEach((sch) => {
|
depr_schedule.forEach((sch) => {
|
||||||
const row = $(`<tr>
|
const row = [
|
||||||
<td align="center">${sch['idx']}</td>
|
sch['idx'],
|
||||||
<td><b>${frappe.format(sch['schedule_date'], { fieldtype: 'Date' })}</b></td>
|
frappe.format(sch['schedule_date'], { fieldtype: 'Date' }),
|
||||||
<td><b>${frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' })}</b></td>
|
frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' }),
|
||||||
<td>${frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' })}</td>
|
frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' }),
|
||||||
<td><a href="/app/journal-entry/${sch['journal_entry'] || ''}">${sch['journal_entry'] || ''}</a></td>
|
sch['journal_entry'] || ''
|
||||||
</tr>`);
|
];
|
||||||
table.find("tbody").append(row);
|
data.push(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.append(table);
|
let datatable = new frappe.DataTable(wrapper.get(0), {
|
||||||
|
columns: [
|
||||||
|
{name: __("No."), editable: false, resizable: false, format: value => value, width: 60},
|
||||||
|
{name: __("Schedule Date"), editable: false, resizable: false, width: 270},
|
||||||
|
{name: __("Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||||
|
{name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||||
|
{name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 312}
|
||||||
|
],
|
||||||
|
data: data,
|
||||||
|
serialNoColumn: false,
|
||||||
|
checkboxColumn: true,
|
||||||
|
cellHeight: 35
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
setup_chart_and_depr_schedule_view: async function(frm) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"column_break_33",
|
"column_break_33",
|
||||||
"opening_accumulated_depreciation",
|
"opening_accumulated_depreciation",
|
||||||
"number_of_depreciations_booked",
|
"number_of_depreciations_booked",
|
||||||
|
"is_fully_depreciated",
|
||||||
"section_break_36",
|
"section_break_36",
|
||||||
"finance_books",
|
"finance_books",
|
||||||
"section_break_33",
|
"section_break_33",
|
||||||
@@ -205,6 +206,7 @@
|
|||||||
"fieldname": "disposal_date",
|
"fieldname": "disposal_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Disposal Date",
|
"label": "Disposal Date",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -244,19 +246,17 @@
|
|||||||
"label": "Is Existing Asset"
|
"label": "Is Existing Asset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "is_existing_asset",
|
"depends_on": "eval:(doc.is_existing_asset)",
|
||||||
"fieldname": "opening_accumulated_depreciation",
|
"fieldname": "opening_accumulated_depreciation",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Opening Accumulated Depreciation",
|
"label": "Opening Accumulated Depreciation",
|
||||||
"no_copy": 1,
|
|
||||||
"options": "Company:company:default_currency"
|
"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",
|
"fieldname": "number_of_depreciations_booked",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Number of Depreciations Booked",
|
"label": "Number of Depreciations Booked"
|
||||||
"no_copy": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
@@ -500,6 +500,13 @@
|
|||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Depreciation Schedule View"
|
"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,
|
"idx": 72,
|
||||||
@@ -526,6 +533,11 @@
|
|||||||
"link_doctype": "Asset Depreciation Schedule",
|
"link_doctype": "Asset Depreciation Schedule",
|
||||||
"link_fieldname": "asset"
|
"link_fieldname": "asset"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Activity",
|
||||||
|
"link_doctype": "Asset Activity",
|
||||||
|
"link_fieldname": "asset"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Journal Entry",
|
"group": "Journal Entry",
|
||||||
"link_doctype": "Journal Entry",
|
"link_doctype": "Journal Entry",
|
||||||
@@ -533,7 +545,7 @@
|
|||||||
"table_fieldname": "accounts"
|
"table_fieldname": "accounts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2023-07-26 13:33:36.821534",
|
"modified": "2023-07-28 20:12:44.819616",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
@@ -577,4 +589,4 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "asset_name",
|
"title_field": "asset_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from erpnext.assets.doctype.asset.depreciation import (
|
|||||||
get_depreciation_accounts,
|
get_depreciation_accounts,
|
||||||
get_disposal_account_and_cost_center,
|
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_category.asset_category import get_asset_category_account
|
||||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||||
cancel_asset_depr_schedules,
|
cancel_asset_depr_schedules,
|
||||||
@@ -59,7 +60,7 @@ class Asset(AccountsController):
|
|||||||
self.make_asset_movement()
|
self.make_asset_movement()
|
||||||
if not self.booked_fixed_asset and self.validate_make_gl_entry():
|
if not self.booked_fixed_asset and self.validate_make_gl_entry():
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
if not self.split_from:
|
if self.calculate_depreciation and not self.split_from:
|
||||||
asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self)
|
asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self)
|
||||||
convert_draft_asset_depr_schedules_into_active(self)
|
convert_draft_asset_depr_schedules_into_active(self)
|
||||||
if asset_depr_schedules_names:
|
if asset_depr_schedules_names:
|
||||||
@@ -71,6 +72,7 @@ class Asset(AccountsController):
|
|||||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||||
).format(asset_depr_schedules_links)
|
).format(asset_depr_schedules_links)
|
||||||
)
|
)
|
||||||
|
add_asset_activity(self.name, _("Asset submitted"))
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.validate_cancellation()
|
self.validate_cancellation()
|
||||||
@@ -81,9 +83,10 @@ class Asset(AccountsController):
|
|||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||||
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
|
||||||
self.db_set("booked_fixed_asset", 0)
|
self.db_set("booked_fixed_asset", 0)
|
||||||
|
add_asset_activity(self.name, _("Asset cancelled"))
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
if not self.split_from:
|
if self.calculate_depreciation and not self.split_from:
|
||||||
asset_depr_schedules_names = make_draft_asset_depr_schedules(self)
|
asset_depr_schedules_names = make_draft_asset_depr_schedules(self)
|
||||||
asset_depr_schedules_links = get_comma_separated_links(
|
asset_depr_schedules_links = get_comma_separated_links(
|
||||||
asset_depr_schedules_names, "Asset Depreciation Schedule"
|
asset_depr_schedules_names, "Asset Depreciation Schedule"
|
||||||
@@ -93,6 +96,16 @@ class Asset(AccountsController):
|
|||||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||||
).format(asset_depr_schedules_links)
|
).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):
|
def validate_asset_and_reference(self):
|
||||||
if self.purchase_invoice or self.purchase_receipt:
|
if self.purchase_invoice or self.purchase_receipt:
|
||||||
@@ -135,17 +148,33 @@ class Asset(AccountsController):
|
|||||||
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
|
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
|
||||||
|
|
||||||
def validate_cost_center(self):
|
def validate_cost_center(self):
|
||||||
if not self.cost_center:
|
if self.cost_center:
|
||||||
return
|
cost_center_company, cost_center_is_group = frappe.db.get_value(
|
||||||
|
"Cost Center", self.cost_center, ["company", "is_group"]
|
||||||
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 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):
|
def validate_in_use_date(self):
|
||||||
if not self.available_for_use_date:
|
if not self.available_for_use_date:
|
||||||
@@ -194,8 +223,11 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
if not self.calculate_depreciation:
|
if not self.calculate_depreciation:
|
||||||
return
|
return
|
||||||
elif not self.finance_books:
|
else:
|
||||||
frappe.throw(_("Enter depreciation details"))
|
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:
|
if self.is_existing_asset:
|
||||||
return
|
return
|
||||||
@@ -276,7 +308,7 @@ class Asset(AccountsController):
|
|||||||
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||||
frappe.throw(
|
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
|
depreciable_amount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -412,7 +444,9 @@ class Asset(AccountsController):
|
|||||||
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
|
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
|
||||||
value_after_depreciation = self.finance_books[idx].value_after_depreciation
|
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"
|
status = "Fully Depreciated"
|
||||||
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
|
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
|
||||||
status = "Partially Depreciated"
|
status = "Partially Depreciated"
|
||||||
@@ -898,6 +932,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"):
|
for row in asset.get("finance_books"):
|
||||||
value_after_depreciation = flt(
|
value_after_depreciation = flt(
|
||||||
(row.value_after_depreciation * remaining_qty) / asset.asset_quantity
|
(row.value_after_depreciation * remaining_qty) / asset.asset_quantity
|
||||||
@@ -965,6 +1006,15 @@ def create_new_asset_after_split(asset, split_qty):
|
|||||||
(row.expected_value_after_useful_life * split_qty) / asset.asset_quantity
|
(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.submit()
|
||||||
new_asset.set_status()
|
new_asset.set_status()
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
|||||||
get_checks_for_pl_and_bs_accounts,
|
get_checks_for_pl_and_bs_accounts,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
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 (
|
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||||
get_asset_depr_schedule_doc,
|
get_asset_depr_schedule_doc,
|
||||||
get_asset_depr_schedule_name,
|
get_asset_depr_schedule_name,
|
||||||
@@ -325,6 +326,8 @@ def scrap_asset(asset_name):
|
|||||||
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
|
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
|
||||||
asset.set_status("Scrapped")
|
asset.set_status("Scrapped")
|
||||||
|
|
||||||
|
add_asset_activity(asset_name, _("Asset scrapped"))
|
||||||
|
|
||||||
frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
|
frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
|
||||||
|
|
||||||
|
|
||||||
@@ -349,6 +352,8 @@ def restore_asset(asset_name):
|
|||||||
|
|
||||||
asset.set_status()
|
asset.set_status()
|
||||||
|
|
||||||
|
add_asset_activity(asset_name, _("Asset restored"))
|
||||||
|
|
||||||
|
|
||||||
def depreciate_asset(asset_doc, date, notes):
|
def depreciate_asset(asset_doc, date, notes):
|
||||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||||
@@ -398,6 +403,15 @@ def reverse_depreciation_entry_made_after_disposal(asset, date):
|
|||||||
|
|
||||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||||
reverse_journal_entry.posting_date = nowdate()
|
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
|
frappe.flags.is_reverse_depr_entry = True
|
||||||
reverse_journal_entry.submit()
|
reverse_journal_entry.submit()
|
||||||
|
|
||||||
|
|||||||
0
erpnext/assets/doctype/asset_activity/__init__.py
Normal file
0
erpnext/assets/doctype/asset_activity/__init__.py
Normal file
8
erpnext/assets/doctype/asset_activity/asset_activity.js
Normal file
8
erpnext/assets/doctype/asset_activity/asset_activity.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("Asset Activity", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
109
erpnext/assets/doctype/asset_activity/asset_activity.json
Normal file
109
erpnext/assets/doctype/asset_activity/asset_activity.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
20
erpnext/assets/doctype/asset_activity/asset_activity.py
Normal file
20
erpnext/assets/doctype/asset_activity/asset_activity.py
Normal file
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -18,6 +18,7 @@ from erpnext.assets.doctype.asset.depreciation import (
|
|||||||
reset_depreciation_schedule,
|
reset_depreciation_schedule,
|
||||||
reverse_depreciation_entry_made_after_disposal,
|
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.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
from erpnext.setup.doctype.brand.brand import get_brand_defaults
|
from erpnext.setup.doctype.brand.brand import get_brand_defaults
|
||||||
@@ -519,6 +520,13 @@ class AssetCapitalization(StockController):
|
|||||||
"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
|
"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(
|
frappe.msgprint(
|
||||||
_(
|
_(
|
||||||
"Asset {0} has been created. Please set the depreciation details if any and submit it."
|
"Asset {0} has been created. Please set the depreciation details if any and submit it."
|
||||||
@@ -542,9 +550,30 @@ class AssetCapitalization(StockController):
|
|||||||
|
|
||||||
def set_consumed_asset_status(self, asset):
|
def set_consumed_asset_status(self, asset):
|
||||||
if self.docstatus == 1:
|
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:
|
else:
|
||||||
asset.set_status()
|
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()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import get_link_to_form
|
||||||
|
|
||||||
|
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||||
|
|
||||||
|
|
||||||
class AssetMovement(Document):
|
class AssetMovement(Document):
|
||||||
@@ -128,5 +131,24 @@ class AssetMovement(Document):
|
|||||||
current_location = latest_movement_entry[0][0]
|
current_location = latest_movement_entry[0][0]
|
||||||
current_employee = latest_movement_entry[0][1]
|
current_employee = latest_movement_entry[0][1]
|
||||||
|
|
||||||
frappe.db.set_value("Asset", d.asset, "location", current_location)
|
frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False)
|
||||||
frappe.db.set_value("Asset", d.asset, "custodian", current_employee)
|
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)),
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_
|
|||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.general_ledger import make_gl_entries
|
from erpnext.accounts.general_ledger import make_gl_entries
|
||||||
from erpnext.assets.doctype.asset.asset import get_asset_account
|
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 (
|
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||||
get_depr_schedule,
|
get_depr_schedule,
|
||||||
make_new_active_asset_depr_schedules_and_cancel_current_ones,
|
make_new_active_asset_depr_schedules_and_cancel_current_ones,
|
||||||
@@ -25,8 +26,14 @@ class AssetRepair(AccountsController):
|
|||||||
self.calculate_total_repair_cost()
|
self.calculate_total_repair_cost()
|
||||||
|
|
||||||
def update_status(self):
|
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")
|
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:
|
else:
|
||||||
self.asset_doc.set_status()
|
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)
|
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||||
self.asset_doc.save()
|
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):
|
def before_cancel(self):
|
||||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
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)
|
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||||
self.asset_doc.save()
|
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):
|
def after_delete(self):
|
||||||
frappe.get_doc("Asset", self.asset).set_status()
|
frappe.get_doc("Asset", self.asset).set_status()
|
||||||
|
|
||||||
|
|||||||
@@ -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.asset import get_asset_value_after_depreciation
|
||||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||||
|
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||||
get_asset_depr_schedule_doc,
|
get_asset_depr_schedule_doc,
|
||||||
get_depreciation_amount,
|
get_depreciation_amount,
|
||||||
@@ -27,9 +28,21 @@ class AssetValueAdjustment(Document):
|
|||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.make_depreciation_entry()
|
self.make_depreciation_entry()
|
||||||
self.reschedule_depreciations(self.new_asset_value)
|
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):
|
def on_cancel(self):
|
||||||
self.reschedule_depreciations(self.current_asset_value)
|
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):
|
def validate_date(self):
|
||||||
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
|
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
|
||||||
@@ -74,12 +87,16 @@ class AssetValueAdjustment(Document):
|
|||||||
"account": accumulated_depreciation_account,
|
"account": accumulated_depreciation_account,
|
||||||
"credit_in_account_currency": self.difference_amount,
|
"credit_in_account_currency": self.difference_amount,
|
||||||
"cost_center": depreciation_cost_center or self.cost_center,
|
"cost_center": depreciation_cost_center or self.cost_center,
|
||||||
|
"reference_type": "Asset",
|
||||||
|
"reference_name": self.asset,
|
||||||
}
|
}
|
||||||
|
|
||||||
debit_entry = {
|
debit_entry = {
|
||||||
"account": depreciation_expense_account,
|
"account": depreciation_expense_account,
|
||||||
"debit_in_account_currency": self.difference_amount,
|
"debit_in_account_currency": self.difference_amount,
|
||||||
"cost_center": depreciation_cost_center or self.cost_center,
|
"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()
|
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||||
|
|||||||
0
erpnext/assets/report/asset_activity/__init__.py
Normal file
0
erpnext/assets/report/asset_activity/__init__.py
Normal file
33
erpnext/assets/report/asset_activity/asset_activity.json
Normal file
33
erpnext/assets/report/asset_activity/asset_activity.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -183,6 +183,17 @@
|
|||||||
"link_type": "Report",
|
"link_type": "Report",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"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",
|
"modified": "2023-05-24 14:47:20.243146",
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ frappe.ui.form.on("Purchase Order", {
|
|||||||
get_materials_from_supplier: function(frm) {
|
get_materials_from_supplier: function(frm) {
|
||||||
let po_details = [];
|
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 => {
|
frm.doc.supplied_items.forEach(d => {
|
||||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||||
po_details.push(d.name)
|
po_details.push(d.name)
|
||||||
@@ -184,7 +184,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!in_list(["Closed", "Delivered"], doc.status)) {
|
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.
|
// 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)) {
|
if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
|
||||||
this.frm.add_custom_button(__('Update Items'), () => {
|
this.frm.add_custom_button(__('Update Items'), () => {
|
||||||
@@ -198,7 +198,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.frm.has_perm("submit")) {
|
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") {
|
if (doc.status != "On Hold") {
|
||||||
this.frm.add_custom_button(__('Hold'), () => this.hold_purchase_order(), __("Status"));
|
this.frm.add_custom_button(__('Hold'), () => this.hold_purchase_order(), __("Status"));
|
||||||
} else{
|
} else{
|
||||||
@@ -221,7 +221,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
|||||||
}
|
}
|
||||||
if(doc.status != "Closed") {
|
if(doc.status != "Closed") {
|
||||||
if (doc.status != "On Hold") {
|
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'));
|
cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
|
||||||
if (doc.is_subcontracted) {
|
if (doc.is_subcontracted) {
|
||||||
if (doc.is_old_subcontracting_flow) {
|
if (doc.is_old_subcontracting_flow) {
|
||||||
@@ -234,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'),
|
cur_frm.add_custom_button(__('Purchase Invoice'),
|
||||||
this.make_purchase_invoice, __('Create'));
|
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(
|
this.frm.add_custom_button(
|
||||||
__('Payment'),
|
__('Payment'),
|
||||||
() => this.make_payment_entry(),
|
() => this.make_payment_entry(),
|
||||||
@@ -246,7 +246,7 @@ 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'),
|
this.frm.add_custom_button(__('Payment Request'),
|
||||||
function() { me.make_payment_request() }, __('Create'));
|
function() { me.make_payment_request() }, __('Create'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
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.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.query_builder.functions import Abs, Sum
|
from frappe.query_builder.functions import Abs, Sum
|
||||||
@@ -38,7 +38,12 @@ from erpnext.accounts.party import (
|
|||||||
get_party_gle_currency,
|
get_party_gle_currency,
|
||||||
validate_party_frozen_disabled,
|
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.buying.utils import update_last_purchase_rate
|
||||||
from erpnext.controllers.print_settings import (
|
from erpnext.controllers.print_settings import (
|
||||||
set_print_templates_for_item_table,
|
set_print_templates_for_item_table,
|
||||||
@@ -968,67 +973,133 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
d.exchange_gain_loss = difference
|
d.exchange_gain_loss = difference
|
||||||
|
|
||||||
def make_exchange_gain_loss_gl_entries(self, gl_entries):
|
def make_exchange_gain_loss_journal(self, args: dict = None) -> None:
|
||||||
if self.get("doctype") in ["Purchase Invoice", "Sales Invoice"]:
|
"""
|
||||||
for d in self.get("advances"):
|
Make Exchange Gain/Loss journal for Invoices and Payments
|
||||||
if d.exchange_gain_loss:
|
"""
|
||||||
is_purchase_invoice = self.get("doctype") == "Purchase Invoice"
|
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
||||||
party = self.supplier if is_purchase_invoice else self.customer
|
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
||||||
party_account = self.credit_to if is_purchase_invoice else self.debit_to
|
if self.docstatus == 1:
|
||||||
party_type = "Supplier" if is_purchase_invoice else "Customer"
|
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"):
|
||||||
|
|
||||||
gain_loss_account = frappe.get_cached_value(
|
party_account = arg.get("account")
|
||||||
"Company", self.company, "exchange_gain_loss_account"
|
gain_loss_account = arg.get("difference_account")
|
||||||
)
|
difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss")
|
||||||
if not gain_loss_account:
|
if difference_amount > 0:
|
||||||
frappe.throw(
|
dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit"
|
||||||
_("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company"))
|
else:
|
||||||
)
|
dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit"
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
# for purchase
|
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
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"
|
|
||||||
|
|
||||||
gl_entries.append(
|
je = create_gain_loss_journal(
|
||||||
self.get_gl_dict(
|
self.company,
|
||||||
{
|
arg.get("party_type"),
|
||||||
"account": gain_loss_account,
|
arg.get("party"),
|
||||||
"account_currency": account_currency,
|
party_account,
|
||||||
"against": party,
|
gain_loss_account,
|
||||||
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
|
difference_amount,
|
||||||
dr_or_cr: abs(d.exchange_gain_loss),
|
dr_or_cr,
|
||||||
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
|
reverse_dr_or_cr,
|
||||||
"project": self.project,
|
arg.get("against_voucher_type"),
|
||||||
},
|
arg.get("against_voucher"),
|
||||||
item=d,
|
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"
|
booked = []
|
||||||
|
if parents:
|
||||||
gl_entries.append(
|
booked = (
|
||||||
self.get_gl_dict(
|
qb.from_(je)
|
||||||
{
|
.inner_join(jea)
|
||||||
"account": party_account,
|
.on(je.name == jea.parent)
|
||||||
"party_type": party_type,
|
.select(jea.reference_type, jea.reference_name, jea.reference_detail_no)
|
||||||
"party": party,
|
.where(
|
||||||
"against": gain_loss_account,
|
(je.docstatus == 1)
|
||||||
dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate),
|
& (je.name.isin(parents))
|
||||||
dr_or_cr: abs(d.exchange_gain_loss),
|
& (je.voucher_type == "Exchange Gain or Loss")
|
||||||
"cost_center": self.cost_center,
|
)
|
||||||
"project": self.project,
|
.run()
|
||||||
},
|
)
|
||||||
self.party_account_currency,
|
|
||||||
item=self,
|
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):
|
def update_against_document_in_jv(self):
|
||||||
"""
|
"""
|
||||||
@@ -1090,9 +1161,15 @@ class AccountsController(TransactionBase):
|
|||||||
reconcile_against_document(lst)
|
reconcile_against_document(lst)
|
||||||
|
|
||||||
def on_cancel(self):
|
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"):
|
if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
|
||||||
unlink_ref_doc_from_payment_entries(self)
|
unlink_ref_doc_from_payment_entries(self)
|
||||||
|
|
||||||
@@ -1679,8 +1756,13 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
self.append("payment_schedule", data)
|
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 (
|
if not (
|
||||||
automatically_fetch_payment_terms
|
automatically_fetch_payment_terms
|
||||||
|
and allocate_payment_based_on_payment_terms
|
||||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||||
):
|
):
|
||||||
for d in self.get("payment_schedule"):
|
for d in self.get("payment_schedule"):
|
||||||
|
|||||||
@@ -233,6 +233,9 @@ class StatusUpdater(Document):
|
|||||||
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
|
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))
|
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
|
||||||
|
|
||||||
|
if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0:
|
||||||
|
frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code))
|
||||||
|
|
||||||
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
||||||
args["name"] = d.get(args["join_field"])
|
args["name"] = d.get(args["join_field"])
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from erpnext.accounts.general_ledger import (
|
|||||||
make_reverse_gl_entries,
|
make_reverse_gl_entries,
|
||||||
process_gl_map,
|
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.controllers.accounts_controller import AccountsController
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
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)
|
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
||||||
|
|
||||||
def make_gl_entries_on_cancel(self):
|
def make_gl_entries_on_cancel(self):
|
||||||
|
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||||
if frappe.db.sql(
|
if frappe.db.sql(
|
||||||
"""select name from `tabGL Entry` where voucher_type=%s
|
"""select name from `tabGL Entry` where voucher_type=%s
|
||||||
and voucher_no=%s""",
|
and voucher_no=%s""",
|
||||||
|
|||||||
980
erpnext/controllers/tests/test_accounts_controller.py
Normal file
980
erpnext/controllers/tests/test_accounts_controller.py
Normal file
@@ -0,0 +1,980 @@
|
|||||||
|
# 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)
|
||||||
|
si.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice Advance",
|
||||||
|
"reference_type": adv.doctype,
|
||||||
|
"reference_name": adv.name,
|
||||||
|
"advance_amount": 1,
|
||||||
|
"allocated_amount": 1,
|
||||||
|
"ref_exchange_rate": 85,
|
||||||
|
"remarks": "Test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
si.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice Advance",
|
||||||
|
"reference_type": adv.doctype,
|
||||||
|
"reference_name": adv.name,
|
||||||
|
"advance_amount": 1,
|
||||||
|
"allocated_amount": 1,
|
||||||
|
"ref_exchange_rate": 85,
|
||||||
|
"remarks": "Test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
si.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice Advance",
|
||||||
|
"reference_type": adv.doctype,
|
||||||
|
"reference_name": adv.name,
|
||||||
|
"advance_amount": 1,
|
||||||
|
"allocated_amount": 1,
|
||||||
|
"ref_exchange_rate": 85,
|
||||||
|
"remarks": "Test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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, 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)
|
||||||
|
si.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice Advance",
|
||||||
|
"reference_type": adv.doctype,
|
||||||
|
"reference_name": adv.name,
|
||||||
|
"reference_row": adv.accounts[0].name,
|
||||||
|
"advance_amount": 1,
|
||||||
|
"allocated_amount": 1,
|
||||||
|
"ref_exchange_rate": adv_exc_rate,
|
||||||
|
"remarks": "Test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
si.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice Advance",
|
||||||
|
"reference_type": adv.doctype,
|
||||||
|
"reference_name": adv.name,
|
||||||
|
"reference_row": adv.accounts[0].name,
|
||||||
|
"advance_amount": 1,
|
||||||
|
"allocated_amount": 1,
|
||||||
|
"ref_exchange_rate": adv_exc_rate,
|
||||||
|
"remarks": "Test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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)
|
||||||
@@ -206,9 +206,11 @@ def post_process(doctype, data):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if doc.get("per_delivered"):
|
if doc.get("per_delivered"):
|
||||||
doc.status_percent += flt(doc.per_delivered)
|
doc.status_percent += flt(doc.per_delivered, 2)
|
||||||
doc.status_display.append(
|
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"):
|
if hasattr(doc, "set_indicator"):
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class Lead(SellingController, CRMNote):
|
|||||||
"last_name": self.last_name,
|
"last_name": self.last_name,
|
||||||
"salutation": self.salutation,
|
"salutation": self.salutation,
|
||||||
"gender": self.gender,
|
"gender": self.gender,
|
||||||
"job_title": self.job_title,
|
"designation": self.job_title,
|
||||||
"company_name": self.company_name,
|
"company_name": self.company_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class BOMUpdateLog(Document):
|
|||||||
else:
|
else:
|
||||||
frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||||
|
queue="long",
|
||||||
update_doc=self,
|
update_doc=self,
|
||||||
now=frappe.flags.in_test,
|
now=frappe.flags.in_test,
|
||||||
enqueue_after_commit=True,
|
enqueue_after_commit=True,
|
||||||
|
|||||||
@@ -157,12 +157,19 @@ def get_next_higher_level_boms(
|
|||||||
def get_leaf_boms() -> List[str]:
|
def get_leaf_boms() -> List[str]:
|
||||||
"Get BOMs that have no dependencies."
|
"Get BOMs that have no dependencies."
|
||||||
|
|
||||||
return frappe.db.sql_list(
|
bom = frappe.qb.DocType("BOM")
|
||||||
"""select name from `tabBOM` bom
|
bom_item = frappe.qb.DocType("BOM Item")
|
||||||
where docstatus=1 and is_active=1
|
|
||||||
and not exists(select bom_no from `tabBOM Item`
|
boms = (
|
||||||
where parent=bom.name and bom_no !='')"""
|
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:
|
def _generate_dependence_map() -> defaultdict:
|
||||||
|
|||||||
@@ -544,12 +544,12 @@ class JobCard(Document):
|
|||||||
if self.for_quantity and flt(total_completed_qty, precision) != flt(
|
if self.for_quantity and flt(total_completed_qty, precision) != flt(
|
||||||
self.for_quantity, precision
|
self.for_quantity, precision
|
||||||
):
|
):
|
||||||
total_completed_qty = bold(_("Total Completed Qty"))
|
total_completed_qty_label = bold(_("Total Completed Qty"))
|
||||||
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
||||||
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("The {0} ({1}) must be equal to {2} ({3})").format(
|
_("The {0} ({1}) must be equal to {2} ({3})").format(
|
||||||
total_completed_qty,
|
total_completed_qty_label,
|
||||||
bold(flt(total_completed_qty, precision)),
|
bold(flt(total_completed_qty, precision)),
|
||||||
qty_to_manufacture,
|
qty_to_manufacture,
|
||||||
bold(self.for_quantity),
|
bold(self.for_quantity),
|
||||||
|
|||||||
@@ -9,19 +9,25 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
item.temporary_name = item.name;
|
item.temporary_name = item.name;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(frm) {
|
setup(frm) {
|
||||||
|
frm.trigger("setup_queries");
|
||||||
|
|
||||||
frm.custom_make_buttons = {
|
frm.custom_make_buttons = {
|
||||||
'Work Order': 'Work Order / Subcontract PO',
|
'Work Order': 'Work Order / Subcontract PO',
|
||||||
'Material Request': 'Material Request',
|
'Material Request': 'Material Request',
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
frm.fields_dict['po_items'].grid.get_field('warehouse').get_query = function(doc) {
|
setup_queries(frm) {
|
||||||
|
frm.set_query("sales_order", "sales_orders", () => {
|
||||||
return {
|
return {
|
||||||
|
query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
|
||||||
filters: {
|
filters: {
|
||||||
company: doc.company
|
company: frm.doc.company,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
frm.set_query('for_warehouse', function(doc) {
|
frm.set_query('for_warehouse', function(doc) {
|
||||||
return {
|
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 {
|
return {
|
||||||
query: "erpnext.controllers.queries.item_query",
|
query: "erpnext.controllers.queries.item_query",
|
||||||
filters:{
|
filters:{
|
||||||
'is_stock_item': 1,
|
'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];
|
var d = locals[cdt][cdn];
|
||||||
if (d.item_code) {
|
if (d.item_code) {
|
||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.bom",
|
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"));
|
} 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 {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
company: doc.company
|
company: doc.company
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
frm.set_query("warehouse", "po_items", (doc) => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: doc.company
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
@@ -436,7 +450,7 @@ frappe.ui.form.on("Production Plan Item", {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Material Request 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", {
|
frappe.ui.form.on("Production Plan Sales Order", {
|
||||||
sales_order(frm, cdt, cdn) {
|
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) {
|
if (!sales_order) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frappe.call({
|
|
||||||
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details",
|
if (row.sales_order) {
|
||||||
args: { sales_order },
|
frm.call({
|
||||||
callback(r) {
|
method: "validate_sales_orders",
|
||||||
const {transaction_date, customer, grand_total} = r.message;
|
doc: frm.doc,
|
||||||
frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date);
|
args: {
|
||||||
frappe.model.set_value(cdt, cdn, 'customer', customer);
|
sales_order: row.sales_order,
|
||||||
frappe.model.set_value(cdt, cdn, 'grand_total', grand_total);
|
},
|
||||||
}
|
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'] = [
|
frappe.tour['Production Plan'] = [
|
||||||
{
|
{
|
||||||
fieldname: "get_items_from",
|
fieldname: "get_items_from",
|
||||||
|
|||||||
@@ -228,10 +228,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "To know more about projected quantity, <a href=\"https://erpnext.com/docs/user/manual/en/stock/projected-quantity\" style=\"text-decoration: underline;\" target=\"_blank\">click here</a>.",
|
"description": "If enabled, the system won't create material requests for the available items.",
|
||||||
"fieldname": "ignore_existing_ordered_qty",
|
"fieldname": "ignore_existing_ordered_qty",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Ignore Existing Projected Quantity"
|
"label": "Ignore Available Stock"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_25",
|
"fieldname": "column_break_25",
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
"depends_on": "eval:doc.get_items_from == 'Sales Order'",
|
"depends_on": "eval:doc.get_items_from == 'Sales Order'",
|
||||||
"fieldname": "combine_items",
|
"fieldname": "combine_items",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Consolidate Items"
|
"label": "Consolidate Sales Order Items"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_25",
|
"fieldname": "section_break_25",
|
||||||
@@ -399,7 +399,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"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",
|
"fieldname": "skip_available_sub_assembly_item",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Skip Available Sub Assembly Items"
|
"label": "Skip Available Sub Assembly Items"
|
||||||
@@ -422,7 +422,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-22 23:36:31.770517",
|
"modified": "2023-07-28 13:37:43.926686",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan",
|
"name": "Production Plan",
|
||||||
|
|||||||
@@ -39,6 +39,36 @@ class ProductionPlan(Document):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
self._rename_temporary_references()
|
self._rename_temporary_references()
|
||||||
validate_uom_is_integer(self, "stock_uom", "planned_qty")
|
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):
|
def set_pending_qty_in_row_without_reference(self):
|
||||||
"Set Pending Qty in independent rows (not from SO or MR)."
|
"Set Pending Qty in independent rows (not from SO or MR)."
|
||||||
@@ -205,6 +235,7 @@ class ProductionPlan(Document):
|
|||||||
).as_("pending_qty"),
|
).as_("pending_qty"),
|
||||||
so_item.description,
|
so_item.description,
|
||||||
so_item.name,
|
so_item.name,
|
||||||
|
so_item.bom_no,
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
.where(
|
.where(
|
||||||
@@ -342,7 +373,7 @@ class ProductionPlan(Document):
|
|||||||
"item_code": data.item_code,
|
"item_code": data.item_code,
|
||||||
"description": data.description or item_details.description,
|
"description": data.description or item_details.description,
|
||||||
"stock_uom": item_details and item_details.stock_uom or "",
|
"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,
|
"planned_qty": data.pending_qty,
|
||||||
"pending_qty": data.pending_qty,
|
"pending_qty": data.pending_qty,
|
||||||
"planned_start_date": now_datetime(),
|
"planned_start_date": now_datetime(),
|
||||||
@@ -401,11 +432,50 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_bin_qty()
|
self.update_bin_qty()
|
||||||
|
self.update_sales_order()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.db_set("status", "Cancelled")
|
self.db_set("status", "Cancelled")
|
||||||
self.delete_draft_work_order()
|
self.delete_draft_work_order()
|
||||||
self.update_bin_qty()
|
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):
|
def update_bin_qty(self):
|
||||||
for d in self.mr_items:
|
for d in self.mr_items:
|
||||||
@@ -719,6 +789,9 @@ class ProductionPlan(Document):
|
|||||||
sub_assembly_items_store = [] # temporary store to process all subassembly items
|
sub_assembly_items_store = [] # temporary store to process all subassembly items
|
||||||
|
|
||||||
for row in self.po_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:
|
if not row.item_code:
|
||||||
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
|
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.docstatus == 1)
|
||||||
& (so.status.notin(["Stopped", "Closed"]))
|
& (so.status.notin(["Stopped", "Closed"]))
|
||||||
& (so.company == self.company)
|
& (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(
|
def get_raw_materials_of_sub_assembly_items(
|
||||||
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
|
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
|
||||||
):
|
):
|
||||||
|
|
||||||
bei = frappe.qb.DocType("BOM Item")
|
bei = frappe.qb.DocType("BOM Item")
|
||||||
bom = frappe.qb.DocType("BOM")
|
bom = frappe.qb.DocType("BOM")
|
||||||
item = frappe.qb.DocType("Item")
|
item = frappe.qb.DocType("Item")
|
||||||
@@ -1609,7 +1681,10 @@ def get_raw_materials_of_sub_assembly_items(
|
|||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
key = (item.item_code, item.bom_no)
|
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])
|
planned_qty = flt(sub_assembly_items[key])
|
||||||
get_raw_materials_of_sub_assembly_items(
|
get_raw_materials_of_sub_assembly_items(
|
||||||
item_details,
|
item_details,
|
||||||
@@ -1626,3 +1701,42 @@ def get_raw_materials_of_sub_assembly_items(
|
|||||||
item_details.setdefault(item.get("item_code"), item)
|
item_details.setdefault(item.get("item_code"), item)
|
||||||
|
|
||||||
return item_details
|
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()
|
||||||
|
|||||||
@@ -225,6 +225,102 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(sales_orders, [])
|
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):
|
def test_production_plan_combine_items(self):
|
||||||
"Test combining FG items in Production Plan."
|
"Test combining FG items in Production Plan."
|
||||||
item = "Test Production Item 1"
|
item = "Test Production Item 1"
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class Workstation(Document):
|
|||||||
|
|
||||||
if schedule_date in tuple(get_holidays(self.holiday_list)):
|
if schedule_date in tuple(get_holidays(self.holiday_list)):
|
||||||
schedule_date = add_days(schedule_date, 1)
|
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
|
return schedule_date
|
||||||
|
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
|
|||||||
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
|
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
|
||||||
erpnext.patches.v14_0.update_closing_balances #14-07-2023
|
erpnext.patches.v14_0.update_closing_balances #14-07-2023
|
||||||
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||||
|
erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
|
||||||
# below migration patches should always run last
|
# below migration patches should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
|
execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ def execute():
|
|||||||
frappe.reload_doc("selling", "doctype", "sales_order_item")
|
frappe.reload_doc("selling", "doctype", "sales_order_item")
|
||||||
|
|
||||||
for doctype in ["Sales Order", "Material Request"]:
|
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":
|
if doctype == "Material Request":
|
||||||
condition = " and doc.per_ordered < 100 and doc.material_request_type = 'Manufacture'"
|
condition = " and doc.per_ordered < 100 and doc.material_request_type = 'Manufacture'"
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -24,12 +24,14 @@ erpnext.setup.slides_settings = [
|
|||||||
fieldtype: 'Data',
|
fieldtype: 'Data',
|
||||||
reqd: 1
|
reqd: 1
|
||||||
},
|
},
|
||||||
|
{ fieldtype: "Column Break" },
|
||||||
{
|
{
|
||||||
fieldname: 'company_abbr',
|
fieldname: 'company_abbr',
|
||||||
label: __('Company Abbreviation'),
|
label: __('Company Abbreviation'),
|
||||||
fieldtype: 'Data',
|
fieldtype: 'Data',
|
||||||
hidden: 1
|
reqd: 1
|
||||||
},
|
},
|
||||||
|
{ fieldtype: "Section Break" },
|
||||||
{
|
{
|
||||||
fieldname: 'chart_of_accounts', label: __('Chart of Accounts'),
|
fieldname: 'chart_of_accounts', label: __('Chart of Accounts'),
|
||||||
options: "", fieldtype: 'Select'
|
options: "", fieldtype: 'Select'
|
||||||
@@ -134,18 +136,20 @@ erpnext.setup.slides_settings = [
|
|||||||
me.charts_modal(slide, chart_template);
|
me.charts_modal(slide, chart_template);
|
||||||
});
|
});
|
||||||
|
|
||||||
slide.get_input("company_name").on("change", function () {
|
slide.get_input("company_name").on("input", function () {
|
||||||
let parts = slide.get_input("company_name").val().split(" ");
|
let parts = slide.get_input("company_name").val().split(" ");
|
||||||
let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
|
let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
|
||||||
slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
|
slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
|
||||||
}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
|
}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
|
||||||
|
|
||||||
slide.get_input("company_abbr").on("change", function () {
|
slide.get_input("company_abbr").on("change", function () {
|
||||||
if (slide.get_input("company_abbr").val().length > 10) {
|
let abbr = slide.get_input("company_abbr").val();
|
||||||
|
if (abbr.length > 10) {
|
||||||
frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
|
frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
|
||||||
slide.get_field("company_abbr").set_value("");
|
abbr = abbr.slice(0, 10);
|
||||||
}
|
}
|
||||||
});
|
slide.get_field("company_abbr").set_value(abbr);
|
||||||
|
}).val(frappe.boot.sysdefaults.company_abbr || "").trigger("change");
|
||||||
},
|
},
|
||||||
|
|
||||||
charts_modal: function(slide, chart_template) {
|
charts_modal: function(slide, chart_template) {
|
||||||
|
|||||||
@@ -62,10 +62,10 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "process_owner.full_name",
|
"fetch_from": "procedure.process_owner_full_name",
|
||||||
"fieldname": "full_name",
|
"fieldname": "full_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"read_only": 1,
|
||||||
"label": "Full Name"
|
"label": "Full Name"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-02-26 15:27:47.247814",
|
"modified": "2023-07-31 08:10:47.247814",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Quality Management",
|
"module": "Quality Management",
|
||||||
"name": "Non Conformance",
|
"name": "Non Conformance",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"autoname": "format:{####}",
|
"autoname": "format:{####}",
|
||||||
"creation": "2019-05-26 15:03:43.996455",
|
"creation": "2019-05-26 15:03:43.996455",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@@ -12,7 +13,6 @@
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fetch_from": "goal.objective",
|
|
||||||
"fieldname": "objective",
|
"fieldname": "objective",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -38,14 +38,17 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"modified": "2019-05-26 16:12:54.832058",
|
"links": [],
|
||||||
|
"modified": "2023-07-28 18:10:23.351246",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Quality Management",
|
"module": "Quality Management",
|
||||||
"name": "Quality Goal Objective",
|
"name": "Quality Goal Objective",
|
||||||
|
"naming_rule": "Expression",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "Open",
|
||||||
"columns": 2,
|
"columns": 2,
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-27 16:28:20.908637",
|
"modified": "2023-07-31 09:20:20.908637",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Quality Management",
|
"module": "Quality Management",
|
||||||
"name": "Quality Review Objective",
|
"name": "Quality Review Objective",
|
||||||
@@ -76,4 +77,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if(frm.doc.docstatus === 1) {
|
if(frm.doc.docstatus === 1) {
|
||||||
if (frm.doc.status !== 'Closed' && flt(frm.doc.per_delivered, 6) < 100 && flt(frm.doc.per_billed, 6) < 100) {
|
if (frm.doc.status !== 'Closed' && flt(frm.doc.per_delivered, 2) < 100 && flt(frm.doc.per_billed, 2) < 100) {
|
||||||
frm.add_custom_button(__('Update Items'), () => {
|
frm.add_custom_button(__('Update Items'), () => {
|
||||||
erpnext.utils.update_child_items({
|
erpnext.utils.update_child_items({
|
||||||
frm: frm,
|
frm: frm,
|
||||||
@@ -309,7 +309,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
me.frm.cscript.update_status('Resume', 'Draft')
|
me.frm.cscript.update_status('Resume', 'Draft')
|
||||||
}, __("Status"));
|
}, __("Status"));
|
||||||
|
|
||||||
if(flt(doc.per_delivered, 6) < 100 || flt(doc.per_billed) < 100) {
|
if(flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) {
|
||||||
// close
|
// close
|
||||||
this.frm.add_custom_button(__('Close'), () => this.close_sales_order(), __("Status"))
|
this.frm.add_custom_button(__('Close'), () => this.close_sales_order(), __("Status"))
|
||||||
}
|
}
|
||||||
@@ -327,7 +327,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
&& !this.frm.doc.skip_delivery_note
|
&& !this.frm.doc.skip_delivery_note
|
||||||
|
|
||||||
if (this.frm.has_perm("submit")) {
|
if (this.frm.has_perm("submit")) {
|
||||||
if(flt(doc.per_delivered, 6) < 100 || flt(doc.per_billed) < 100) {
|
if(flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) {
|
||||||
// hold
|
// hold
|
||||||
this.frm.add_custom_button(__('Hold'), () => this.hold_sales_order(), __("Status"))
|
this.frm.add_custom_button(__('Hold'), () => this.hold_sales_order(), __("Status"))
|
||||||
// close
|
// close
|
||||||
@@ -335,7 +335,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) {
|
if (flt(doc.per_picked, 2) < 100 && flt(doc.per_delivered, 2) < 100) {
|
||||||
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
|
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,18 +345,18 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
const order_is_a_custom_sale = ["Sales", "Shopping Cart", "Maintenance"].indexOf(doc.order_type) === -1;
|
const order_is_a_custom_sale = ["Sales", "Shopping Cart", "Maintenance"].indexOf(doc.order_type) === -1;
|
||||||
|
|
||||||
// delivery note
|
// delivery note
|
||||||
if(flt(doc.per_delivered, 6) < 100 && (order_is_a_sale || order_is_a_custom_sale) && allow_delivery) {
|
if(flt(doc.per_delivered, 2) < 100 && (order_is_a_sale || order_is_a_custom_sale) && allow_delivery) {
|
||||||
this.frm.add_custom_button(__('Delivery Note'), () => this.make_delivery_note_based_on_delivery_date(), __('Create'));
|
this.frm.add_custom_button(__('Delivery Note'), () => this.make_delivery_note_based_on_delivery_date(), __('Create'));
|
||||||
this.frm.add_custom_button(__('Work Order'), () => this.make_work_order(), __('Create'));
|
this.frm.add_custom_button(__('Work Order'), () => this.make_work_order(), __('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// sales invoice
|
// sales invoice
|
||||||
if(flt(doc.per_billed, 6) < 100) {
|
if(flt(doc.per_billed, 2) < 100) {
|
||||||
this.frm.add_custom_button(__('Sales Invoice'), () => me.make_sales_invoice(), __('Create'));
|
this.frm.add_custom_button(__('Sales Invoice'), () => me.make_sales_invoice(), __('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// material request
|
// material request
|
||||||
if(!doc.order_type || (order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered, 6) < 100) {
|
if(!doc.order_type || (order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered, 2) < 100) {
|
||||||
this.frm.add_custom_button(__('Material Request'), () => this.make_material_request(), __('Create'));
|
this.frm.add_custom_button(__('Material Request'), () => this.make_material_request(), __('Create'));
|
||||||
this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create'));
|
this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class SalesOrder(SellingController):
|
|||||||
super(SalesOrder, self).__init__(*args, **kwargs)
|
super(SalesOrder, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def onload(self) -> None:
|
def onload(self) -> None:
|
||||||
if frappe.get_cached_value("Stock Settings", None, "enable_stock_reservation"):
|
if frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
||||||
if self.has_unreserved_stock():
|
if self.has_unreserved_stock():
|
||||||
self.set_onload("has_unreserved_stock", True)
|
self.set_onload("has_unreserved_stock", True)
|
||||||
|
|
||||||
@@ -733,7 +733,7 @@ def make_material_request(source_name, target_doc=None):
|
|||||||
# qty is for packed items, because packed items don't have stock_qty field
|
# qty is for packed items, because packed items don't have stock_qty field
|
||||||
qty = source.get("qty")
|
qty = source.get("qty")
|
||||||
target.project = source_parent.project
|
target.project = source_parent.project
|
||||||
target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty
|
target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty"))
|
||||||
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
|
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
|
||||||
|
|
||||||
args = target.as_dict().copy()
|
args = target.as_dict().copy()
|
||||||
@@ -767,7 +767,7 @@ def make_material_request(source_name, target_doc=None):
|
|||||||
"doctype": "Material Request Item",
|
"doctype": "Material Request Item",
|
||||||
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
|
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
|
||||||
"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
|
"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
|
||||||
and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0),
|
and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0),
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ frappe.listview_settings['Sales Order'] = {
|
|||||||
return [__("On Hold"), "orange", "status,=,On Hold"];
|
return [__("On Hold"), "orange", "status,=,On Hold"];
|
||||||
} else if (doc.status === "Completed") {
|
} else if (doc.status === "Completed") {
|
||||||
return [__("Completed"), "green", "status,=,Completed"];
|
return [__("Completed"), "green", "status,=,Completed"];
|
||||||
} else if (!doc.skip_delivery_note && flt(doc.per_delivered, 6) < 100) {
|
} else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) {
|
||||||
if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
|
if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
|
||||||
// not delivered & overdue
|
// not delivered & overdue
|
||||||
return [__("Overdue"), "red",
|
return [__("Overdue"), "red",
|
||||||
@@ -19,7 +19,7 @@ frappe.listview_settings['Sales Order'] = {
|
|||||||
// not delivered (zeroount order)
|
// not delivered (zeroount order)
|
||||||
return [__("To Deliver"), "orange",
|
return [__("To Deliver"), "orange",
|
||||||
"per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
|
"per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
|
||||||
} else if (flt(doc.per_billed, 6) < 100) {
|
} else if (flt(doc.per_billed, 2) < 100) {
|
||||||
// not delivered & not billed
|
// not delivered & not billed
|
||||||
return [__("To Deliver and Bill"), "orange",
|
return [__("To Deliver and Bill"), "orange",
|
||||||
"per_delivered,<,100|per_billed,<,100|status,!=,Closed"];
|
"per_delivered,<,100|per_billed,<,100|status,!=,Closed"];
|
||||||
@@ -28,12 +28,12 @@ frappe.listview_settings['Sales Order'] = {
|
|||||||
return [__("To Deliver"), "orange",
|
return [__("To Deliver"), "orange",
|
||||||
"per_delivered,<,100|per_billed,=,100|status,!=,Closed"];
|
"per_delivered,<,100|per_billed,=,100|status,!=,Closed"];
|
||||||
}
|
}
|
||||||
} else if ((flt(doc.per_delivered, 6) === 100) && flt(doc.grand_total) !== 0
|
} else if ((flt(doc.per_delivered, 2) === 100) && flt(doc.grand_total) !== 0
|
||||||
&& flt(doc.per_billed, 6) < 100) {
|
&& flt(doc.per_billed, 2) < 100) {
|
||||||
// to bill
|
// to bill
|
||||||
return [__("To Bill"), "orange",
|
return [__("To Bill"), "orange",
|
||||||
"per_delivered,=,100|per_billed,<,100|status,!=,Closed"];
|
"per_delivered,=,100|per_billed,<,100|status,!=,Closed"];
|
||||||
} else if (doc.skip_delivery_note && flt(doc.per_billed, 6) < 100){
|
} else if (doc.skip_delivery_note && flt(doc.per_billed, 2) < 100){
|
||||||
return [__("To Bill"), "orange", "per_billed,<,100|status,!=,Closed"];
|
return [__("To Bill"), "orange", "per_billed,<,100|status,!=,Closed"];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -549,6 +549,26 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
workflow.is_active = 0
|
workflow.is_active = 0
|
||||||
workflow.save()
|
workflow.save()
|
||||||
|
|
||||||
|
def test_material_request_for_product_bundle(self):
|
||||||
|
# Create the Material Request from the sales order for the Packing Items
|
||||||
|
# Check whether the material request has the correct packing item or not.
|
||||||
|
if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"):
|
||||||
|
bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0})
|
||||||
|
bundle_item.append(
|
||||||
|
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||||
|
)
|
||||||
|
bundle_item.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
make_item("_Packed Item New 2", {"is_stock_item": 1})
|
||||||
|
make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New 2"], 2)
|
||||||
|
|
||||||
|
so = make_sales_order(
|
||||||
|
item_code="_Test Product Bundle Item New 1",
|
||||||
|
)
|
||||||
|
|
||||||
|
mr = make_material_request(so.name)
|
||||||
|
self.assertEqual(mr.items[0].item_code, "_Packed Item New 2")
|
||||||
|
|
||||||
def test_bin_details_of_packed_item(self):
|
def test_bin_details_of_packed_item(self):
|
||||||
# test Update Items with product bundle
|
# test Update Items with product bundle
|
||||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
"actual_qty",
|
"actual_qty",
|
||||||
"ordered_qty",
|
"ordered_qty",
|
||||||
"planned_qty",
|
"planned_qty",
|
||||||
|
"production_plan_qty",
|
||||||
"column_break_69",
|
"column_break_69",
|
||||||
"work_order_qty",
|
"work_order_qty",
|
||||||
"delivered_qty",
|
"delivered_qty",
|
||||||
@@ -882,12 +883,19 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "production_plan_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Production Plan Qty",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-04 10:44:05.707488",
|
"modified": "2023-07-28 14:56:42.031636",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
|||||||
@@ -605,7 +605,6 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
i => i.item_code === item_code
|
i => i.item_code === item_code
|
||||||
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
|
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
|
||||||
&& (i.uom === uom)
|
&& (i.uom === uom)
|
||||||
&& (i.rate == rate)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ def get_data():
|
|||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Sales Order": ["items", "against_sales_order"],
|
"Sales Order": ["items", "against_sales_order"],
|
||||||
|
"Sales Invoice": ["items", "against_sales_invoice"],
|
||||||
"Material Request": ["items", "material_request"],
|
"Material Request": ["items", "material_request"],
|
||||||
"Purchase Order": ["items", "purchase_order"],
|
"Purchase Order": ["items", "purchase_order"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -395,16 +395,16 @@ class Item(Document):
|
|||||||
|
|
||||||
def validate_warehouse_for_reorder(self):
|
def validate_warehouse_for_reorder(self):
|
||||||
"""Validate Reorder level table for duplicate and conditional mandatory"""
|
"""Validate Reorder level table for duplicate and conditional mandatory"""
|
||||||
warehouse = []
|
warehouse_material_request_type: list[tuple[str, str]] = []
|
||||||
for d in self.get("reorder_levels"):
|
for d in self.get("reorder_levels"):
|
||||||
if not d.warehouse_group:
|
if not d.warehouse_group:
|
||||||
d.warehouse_group = d.warehouse
|
d.warehouse_group = d.warehouse
|
||||||
if d.get("warehouse") and d.get("warehouse") not in warehouse:
|
if (d.get("warehouse"), d.get("material_request_type")) not in warehouse_material_request_type:
|
||||||
warehouse += [d.get("warehouse")]
|
warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))]
|
||||||
else:
|
else:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row {0}: An Reorder entry already exists for this warehouse {1}").format(
|
_("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format(
|
||||||
d.idx, d.warehouse
|
d.idx, d.warehouse, d.material_request_type
|
||||||
),
|
),
|
||||||
DuplicateReorderRows,
|
DuplicateReorderRows,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class PriceList(Document):
|
|||||||
|
|
||||||
doc_before_save = self.get_doc_before_save()
|
doc_before_save = self.get_doc_before_save()
|
||||||
currency_changed = self.currency != doc_before_save.currency
|
currency_changed = self.currency != doc_before_save.currency
|
||||||
affects_cart = self.name == frappe.get_cached_value("E Commerce Settings", None, "price_list")
|
affects_cart = self.name == frappe.db.get_single_value("E Commerce Settings", "price_list")
|
||||||
|
|
||||||
if currency_changed and affects_cart:
|
if currency_changed and affects_cart:
|
||||||
validate_cart_settings()
|
validate_cart_settings()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def execute(filters=None):
|
|||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
sle_count = frappe.db.count("Stock Ledger Entry", {"is_cancelled": 0})
|
sle_count = frappe.db.count("Stock Ledger Entry")
|
||||||
|
|
||||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
||||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
||||||
|
|||||||
@@ -446,10 +446,9 @@ class StockBalanceReport(object):
|
|||||||
{
|
{
|
||||||
"label": _("Valuation Rate"),
|
"label": _("Valuation Rate"),
|
||||||
"fieldname": "val_rate",
|
"fieldname": "val_rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Float",
|
||||||
"width": 90,
|
"width": 90,
|
||||||
"convertible": "rate",
|
"convertible": "rate",
|
||||||
"options": "currency",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Reserved Stock"),
|
"label": _("Reserved Stock"),
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ def get_columns(filters):
|
|||||||
{
|
{
|
||||||
"label": _("Avg Rate (Balance Stock)"),
|
"label": _("Avg Rate (Balance Stock)"),
|
||||||
"fieldname": "valuation_rate",
|
"fieldname": "valuation_rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Float",
|
||||||
"width": 180,
|
"width": 180,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"convertible": "rate",
|
"convertible": "rate",
|
||||||
@@ -204,7 +204,7 @@ def get_columns(filters):
|
|||||||
{
|
{
|
||||||
"label": _("Valuation Rate"),
|
"label": _("Valuation Rate"),
|
||||||
"fieldname": "in_out_rate",
|
"fieldname": "in_out_rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Float",
|
||||||
"width": 140,
|
"width": 140,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"convertible": "rate",
|
"convertible": "rate",
|
||||||
|
|||||||
@@ -358,6 +358,8 @@ def update_args_in_repost_item_valuation(
|
|||||||
"current_index": index,
|
"current_index": index,
|
||||||
"total_reposting_count": len(args),
|
"total_reposting_count": len(args),
|
||||||
},
|
},
|
||||||
|
doctype=doc.doctype,
|
||||||
|
docname=doc.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
title = "Warehouse",
|
title = "Warehouse",
|
||||||
actual_qty = (frm.doc.doctype==="Sales Order"
|
actual_qty = (frm.doc.doctype==="Sales Order"
|
||||||
? doc.projected_qty : doc.actual_qty);
|
? doc.projected_qty : doc.actual_qty);
|
||||||
if(flt(frm.doc.per_delivered) < 100
|
if(flt(frm.doc.per_delivered, 2) < 100
|
||||||
&& in_list(["Sales Order Item", "Delivery Note Item"], doc.doctype)) {
|
&& in_list(["Sales Order Item", "Delivery Note Item"], doc.doctype)) {
|
||||||
if(actual_qty != undefined) {
|
if(actual_qty != undefined) {
|
||||||
if(actual_qty >= doc.qty) {
|
if(actual_qty >= doc.qty) {
|
||||||
|
|||||||
@@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
|||||||
You cannot delete Project Type 'External',Jy kan nie projektipe 'eksterne' uitvee nie,
|
You cannot delete Project Type 'External',Jy kan nie projektipe 'eksterne' uitvee nie,
|
||||||
You cannot edit root node.,U kan nie wortelknoop wysig nie.,
|
You cannot edit root node.,U kan nie wortelknoop wysig nie.,
|
||||||
You cannot restart a Subscription that is not cancelled.,U kan nie 'n intekening herlaai wat nie gekanselleer is nie.,
|
You cannot restart a Subscription that is not cancelled.,U kan nie 'n intekening herlaai wat nie gekanselleer is nie.,
|
||||||
You don't have enought Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie,
|
You don't have enough Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie,
|
||||||
You have already assessed for the assessment criteria {}.,U het reeds geassesseer vir die assesseringskriteria ().,
|
You have already assessed for the assessment criteria {}.,U het reeds geassesseer vir die assesseringskriteria ().,
|
||||||
You have already selected items from {0} {1},Jy het reeds items gekies van {0} {1},
|
You have already selected items from {0} {1},Jy het reeds items gekies van {0} {1},
|
||||||
You have been invited to collaborate on the project: {0},U is genooi om saam te werk aan die projek: {0},
|
You have been invited to collaborate on the project: {0},U is genooi om saam te werk aan die projek: {0},
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
@@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
|||||||
You cannot delete Project Type 'External',የፕሮጀክት አይነት «ውጫዊ» ን መሰረዝ አይችሉም.,
|
You cannot delete Project Type 'External',የፕሮጀክት አይነት «ውጫዊ» ን መሰረዝ አይችሉም.,
|
||||||
You cannot edit root node.,የስር ሥፍራ ማረም አይችሉም.,
|
You cannot edit root node.,የስር ሥፍራ ማረም አይችሉም.,
|
||||||
You cannot restart a Subscription that is not cancelled.,የማይሰረዝ የደንበኝነት ምዝገባን ዳግም ማስጀመር አይችሉም.,
|
You cannot restart a Subscription that is not cancelled.,የማይሰረዝ የደንበኝነት ምዝገባን ዳግም ማስጀመር አይችሉም.,
|
||||||
You don't have enought Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም,
|
You don't have enough Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም,
|
||||||
You have already assessed for the assessment criteria {}.,ቀድሞውንም ግምገማ መስፈርት ከገመገምን {}.,
|
You have already assessed for the assessment criteria {}.,ቀድሞውንም ግምገማ መስፈርት ከገመገምን {}.,
|
||||||
You have already selected items from {0} {1},ከዚህ ቀደም ከ ንጥሎች ተመርጠዋል ሊሆን {0} {1},
|
You have already selected items from {0} {1},ከዚህ ቀደም ከ ንጥሎች ተመርጠዋል ሊሆን {0} {1},
|
||||||
You have been invited to collaborate on the project: {0},እርስዎ ፕሮጀክት ላይ ተባበር ተጋብዘዋል: {0},
|
You have been invited to collaborate on the project: {0},እርስዎ ፕሮጀክት ላይ ተባበር ተጋብዘዋል: {0},
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
@@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
|||||||
You cannot delete Project Type 'External',لا يمكنك حذف مشروع من نوع 'خارجي',
|
You cannot delete Project Type 'External',لا يمكنك حذف مشروع من نوع 'خارجي',
|
||||||
You cannot edit root node.,لا يمكنك تحرير عقدة الجذر.,
|
You cannot edit root node.,لا يمكنك تحرير عقدة الجذر.,
|
||||||
You cannot restart a Subscription that is not cancelled.,لا يمكنك إعادة تشغيل اشتراك غير ملغى.,
|
You cannot restart a Subscription that is not cancelled.,لا يمكنك إعادة تشغيل اشتراك غير ملغى.,
|
||||||
You don't have enought Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها,
|
You don't have enough Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها,
|
||||||
You have already assessed for the assessment criteria {}.,لقد سبق أن قيمت معايير التقييم {}.,
|
You have already assessed for the assessment criteria {}.,لقد سبق أن قيمت معايير التقييم {}.,
|
||||||
You have already selected items from {0} {1},لقد حددت العناصر من {0} {1},
|
You have already selected items from {0} {1},لقد حددت العناصر من {0} {1},
|
||||||
You have been invited to collaborate on the project: {0},لقد وجهت الدعوة إلى التعاون في هذا المشروع: {0},
|
You have been invited to collaborate on the project: {0},لقد وجهت الدعوة إلى التعاون في هذا المشروع: {0},
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user