diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
index 2fa1d53c60c..2f53f7b640d 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
@@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', {
};
});
+ frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ return {
+ filters: {
+ company: d.company,
+ root_type: ["in", ["Asset", "Liability"]],
+ is_group: 0
+ }
+ }
+ });
+
if (!frm.is_new()) {
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
frappe.set_route("List", frm.doc.document_type);
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 15c84d462f1..cfe5e6e8009 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -39,6 +39,8 @@ class AccountingDimension(Document):
if not self.is_new():
self.validate_document_type_change()
+ self.validate_dimension_defaults()
+
def validate_document_type_change(self):
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
if doctype_before_save != self.document_type:
@@ -46,6 +48,14 @@ class AccountingDimension(Document):
message += _("Please create a new Accounting Dimension if required.")
frappe.throw(message)
+ def validate_dimension_defaults(self):
+ companies = []
+ for default in self.get("dimension_defaults"):
+ if default.company not in companies:
+ companies.append(default.company)
+ else:
+ frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
+
def after_insert(self):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
diff --git a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
index e9e1f43f990..7b6120a583b 100644
--- a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
+++ b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
@@ -8,7 +8,10 @@
"reference_document",
"default_dimension",
"mandatory_for_bs",
- "mandatory_for_pl"
+ "mandatory_for_pl",
+ "column_break_lqns",
+ "automatically_post_balancing_accounting_entry",
+ "offsetting_account"
],
"fields": [
{
@@ -50,6 +53,23 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Mandatory For Profit and Loss Account"
+ },
+ {
+ "default": "0",
+ "fieldname": "automatically_post_balancing_accounting_entry",
+ "fieldtype": "Check",
+ "label": "Automatically post balancing accounting entry"
+ },
+ {
+ "fieldname": "offsetting_account",
+ "fieldtype": "Link",
+ "label": "Offsetting Account",
+ "mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry",
+ "options": "Account"
+ },
+ {
+ "fieldname": "column_break_lqns",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 302acc4f1f7..199068529d4 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
- frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement'];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger'];
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index f6898026134..4328635aaa6 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -96,6 +96,8 @@ class JournalEntry(AccountsController):
"Payment Ledger Entry",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
+ "Repost Accounting Ledger",
+ "Repost Accounting Ledger Items",
)
self.make_gl_entries(1)
self.update_advance_paid()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 3c2fb1dd0ed..cd788a896a8 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
- frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 379903dade3..127768071a0 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -105,6 +105,8 @@ class PaymentEntry(AccountsController):
"Payment Ledger Entry",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
+ "Repost Accounting Ledger",
+ "Repost Accounting Ledger Items",
)
super(PaymentEntry, self).on_cancel()
self.make_gl_entries(cancel=1)
@@ -185,84 +187,87 @@ class PaymentEntry(AccountsController):
return False
def validate_allocated_amount_with_latest_data(self):
- latest_references = get_outstanding_reference_documents(
- {
- "posting_date": self.posting_date,
- "company": self.company,
- "party_type": self.party_type,
- "payment_type": self.payment_type,
- "party": self.party,
- "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
- "get_outstanding_invoices": True,
- "get_orders_to_be_billed": True,
- }
- )
+ if self.references:
+ uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
+ vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
+ latest_references = get_outstanding_reference_documents(
+ {
+ "posting_date": self.posting_date,
+ "company": self.company,
+ "party_type": self.party_type,
+ "payment_type": self.payment_type,
+ "party": self.party,
+ "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
+ "get_outstanding_invoices": True,
+ "get_orders_to_be_billed": True,
+ "vouchers": vouchers,
+ }
+ )
- # Group latest_references by (voucher_type, voucher_no)
- latest_lookup = {}
- for d in latest_references:
- d = frappe._dict(d)
- latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
+ # Group latest_references by (voucher_type, voucher_no)
+ latest_lookup = {}
+ for d in latest_references:
+ d = frappe._dict(d)
+ latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
- for idx, d in enumerate(self.get("references"), start=1):
- latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
+ for idx, d in enumerate(self.get("references"), start=1):
+ latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
- # If term based allocation is enabled, throw
- if (
- d.payment_term is None or d.payment_term == ""
- ) and self.term_based_allocation_enabled_for_reference(
- d.reference_doctype, d.reference_name
- ):
- frappe.throw(
- _(
- "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
- ).format(frappe.bold(d.reference_name), frappe.bold(idx))
- )
-
- # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
- latest = latest.get(d.payment_term) or latest.get(None)
-
- # The reference has already been fully paid
- if not latest:
- frappe.throw(
- _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
- )
- # The reference has already been partly paid
- elif latest.outstanding_amount < latest.invoice_amount and flt(
- d.outstanding_amount, d.precision("outstanding_amount")
- ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
-
- frappe.throw(
- _(
- "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
- ).format(_(d.reference_doctype), d.reference_name)
- )
-
- fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
-
- if (
- d.payment_term
- and (
- (flt(d.allocated_amount)) > 0
- and latest.payment_term_outstanding
- and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
- )
- and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
- ):
- frappe.throw(
- _(
- "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
- ).format(
- d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
+ # If term based allocation is enabled, throw
+ if (
+ d.payment_term is None or d.payment_term == ""
+ ) and self.term_based_allocation_enabled_for_reference(
+ d.reference_doctype, d.reference_name
+ ):
+ frappe.throw(
+ _(
+ "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
+ ).format(frappe.bold(d.reference_name), frappe.bold(idx))
)
- )
- if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
- frappe.throw(fail_message.format(d.idx))
+ # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
+ latest = latest.get(d.payment_term) or latest.get(None)
- # Check for negative outstanding invoices as well
- if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
- frappe.throw(fail_message.format(d.idx))
+ # The reference has already been fully paid
+ if not latest:
+ frappe.throw(
+ _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
+ )
+ # The reference has already been partly paid
+ elif latest.outstanding_amount < latest.invoice_amount and flt(
+ d.outstanding_amount, d.precision("outstanding_amount")
+ ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
+ frappe.throw(
+ _(
+ "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
+ ).format(_(d.reference_doctype), d.reference_name)
+ )
+
+ fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
+
+ if (
+ d.payment_term
+ and (
+ (flt(d.allocated_amount)) > 0
+ and latest.payment_term_outstanding
+ and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
+ )
+ and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
+ ):
+ frappe.throw(
+ _(
+ "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
+ ).format(
+ d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
+ )
+ )
+
+ if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
+ frappe.throw(fail_message.format(d.idx))
+
+ # Check for negative outstanding invoices as well
+ if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
+ frappe.throw(fail_message.format(d.idx))
def delink_advance_entry_references(self):
for reference in self.references:
@@ -1463,6 +1468,7 @@ def get_outstanding_reference_documents(args):
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter,
+ vouchers=args.get("vouchers") or None,
)
outstanding_invoices = split_invoices_based_on_payment_terms(
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index b6708ce24b1..cc3ec26066f 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -385,59 +385,6 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries()
- def make_difference_entry(self, row):
- journal_entry = frappe.new_doc("Journal Entry")
- journal_entry.voucher_type = "Exchange Gain Or Loss"
- journal_entry.company = self.company
- journal_entry.posting_date = nowdate()
- journal_entry.multi_currency = 1
-
- party_account_currency = frappe.get_cached_value(
- "Account", self.receivable_payable_account, "account_currency"
- )
- difference_account_currency = frappe.get_cached_value(
- "Account", row.difference_account, "account_currency"
- )
-
- # Account Currency has balance
- dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
- reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
-
- journal_account = frappe._dict(
- {
- "account": self.receivable_payable_account,
- "party_type": self.party_type,
- "party": self.party,
- "account_currency": party_account_currency,
- "exchange_rate": 0,
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": row.against_voucher_type,
- "reference_name": row.against_voucher,
- dr_or_cr: flt(row.difference_amount),
- dr_or_cr + "_in_account_currency": 0,
- }
- )
-
- journal_entry.append("accounts", journal_account)
-
- journal_account = frappe._dict(
- {
- "account": row.difference_account,
- "account_currency": difference_account_currency,
- "exchange_rate": 1,
- "cost_center": erpnext.get_default_cost_center(self.company),
- reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
- reverse_dr_or_cr: flt(row.difference_amount),
- }
- )
-
- journal_entry.append("accounts", journal_account)
-
- journal_entry.save()
- journal_entry.submit()
-
- return journal_entry
-
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
{
@@ -603,16 +550,6 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company):
- def get_difference_row(inv):
- if inv.difference_amount != 0 and inv.difference_account:
- difference_row = {
- "account": inv.difference_account,
- inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
- reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
- "cost_center": erpnext.get_default_cost_center(company),
- }
- return difference_row
-
for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index cced37589ba..720b549ccb3 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -130,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
args: { "pos_profile": frm.pos_profile },
callback: ({ message: profile }) => {
this.update_customer_groups_settings(profile?.customer_groups);
+ this.frm.set_value("company", profile?.company);
},
});
}
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index aea801af9dd..0ff230bb18b 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -54,6 +54,7 @@ class POSInvoice(SalesInvoice):
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
+ self.validate_company_with_pos_company()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -370,6 +371,14 @@ class POSInvoice(SalesInvoice):
if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
+ def validate_company_with_pos_company(self):
+ if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
+ frappe.throw(
+ _("Company {} does not match with POS Profile Company {}").format(
+ self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company")
+ )
+ )
+
def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
@@ -448,6 +457,7 @@ class POSInvoice(SalesInvoice):
profile = {}
if self.pos_profile:
profile = frappe.get_doc("POS Profile", self.pos_profile)
+ self.company = profile.get("company")
if not self.get("payments") and not for_validate:
update_multi_mode_option(self, profile)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index ab7884d5209..5c82cf99438 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload();
// Ignore linked advances
- this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
+ this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index cefb502ede1..6161e5b36ee 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1422,6 +1422,8 @@ class PurchaseInvoice(BuyingController):
"Repost Item Valuation",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
+ "Repost Accounting Ledger",
+ "Repost Accounting Ledger Items",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index f60c83dcf5c..0f8e77952cf 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1771,23 +1771,101 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
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,
+ )
-def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
- gl_entries = frappe.db.sql(
- """select account, debit, credit, posting_date
- from `tabGL Entry`
- where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s
- order by posting_date asc, account asc""",
- (voucher_no, posting_date),
- as_dict=1,
+ 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 check_gl_entries(
+ doc,
+ voucher_no,
+ expected_gle,
+ posting_date,
+ voucher_type="Purchase Invoice",
+ additional_columns=None,
+):
+ gl = frappe.qb.DocType("GL Entry")
+ query = (
+ frappe.qb.from_(gl)
+ .select(gl.account, gl.debit, gl.credit, gl.posting_date)
+ .where(
+ (gl.voucher_type == voucher_type)
+ & (gl.voucher_no == voucher_no)
+ & (gl.posting_date >= posting_date)
+ & (gl.is_cancelled == 0)
+ )
+ .orderby(gl.posting_date, gl.account, gl.creation)
)
+ if additional_columns:
+ for col in additional_columns:
+ query = query.select(gl[col])
+
+ gl_entries = query.run(as_dict=True)
+
for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account)
doc.assertEqual(expected_gle[i][1], gle.debit)
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
+ if additional_columns:
+ j = 4
+ for col in additional_columns:
+ doc.assertEqual(expected_gle[i][j], gle[col])
+ j += 1
+
def create_tax_witholding_category(category_name, company, account):
from erpnext.accounts.utils import get_fiscal_year
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html
new file mode 100644
index 00000000000..2dec8f753f2
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+ {% for col in gl_columns%}
+
+ {% endfor %}
+
+
+
+ {% for col in gl_columns%}
+ | {{ col.label }} |
+ {% endfor %}
+
+
+{% for gl in gl_data%}
+{% if gl["old"]%}
+
+{% else %}
+
+{% endif %}
+ {% for col in gl_columns %}
+ |
+ {{ gl[col.fieldname] }}
+ |
+ {% endfor %}
+
+{% endfor %}
+
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
new file mode 100644
index 00000000000..3a87a380d19
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
@@ -0,0 +1,50 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Repost Accounting Ledger", {
+ setup: function(frm) {
+ frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
+ return {
+ filters: {
+ name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
+ }
+ }
+ }
+
+ frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
+ if (doc.company) {
+ return {
+ filters: {
+ company: doc.company,
+ docstatus: 1
+ }
+ }
+ }
+ }
+ },
+
+ refresh: function(frm) {
+ frm.add_custom_button(__('Show Preview'), () => {
+ frm.call({
+ method: 'generate_preview',
+ doc: frm.doc,
+ freeze: true,
+ freeze_message: __('Generating Preview'),
+ callback: function(r) {
+ if (r && r.message) {
+ let content = r.message;
+ let opts = {
+ title: "Preview",
+ subtitle: "preview",
+ content: content,
+ print_settings: {orientation: "landscape"},
+ columns: [],
+ data: [],
+ }
+ frappe.render_grid(opts);
+ }
+ }
+ });
+ });
+ }
+});
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
new file mode 100644
index 00000000000..8d56c9bb11d
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
@@ -0,0 +1,81 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:ACC-REPOST-{#####}",
+ "creation": "2023-07-04 13:07:32.923675",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "column_break_vpup",
+ "delete_cancelled_entries",
+ "section_break_metl",
+ "vouchers",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Repost Accounting Ledger",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "vouchers",
+ "fieldtype": "Table",
+ "label": "Vouchers",
+ "options": "Repost Accounting Ledger Items"
+ },
+ {
+ "fieldname": "column_break_vpup",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_metl",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "delete_cancelled_entries",
+ "fieldtype": "Check",
+ "label": "Delete Cancelled Ledger Entries"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-07-27 15:47:58.975034",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Accounting Ledger",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
new file mode 100644
index 00000000000..4cf2ed2f46c
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.model.document import Document
+from frappe.utils.data import comma_and
+
+
+class RepostAccountingLedger(Document):
+ def __init__(self, *args, **kwargs):
+ super(RepostAccountingLedger, self).__init__(*args, **kwargs)
+ self._allowed_types = set(
+ ["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
+ )
+
+ def validate(self):
+ self.validate_vouchers()
+ self.validate_for_closed_fiscal_year()
+ self.validate_for_deferred_accounting()
+
+ def validate_for_deferred_accounting(self):
+ sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
+ docs_with_deferred_revenue = frappe.db.get_all(
+ "Sales Invoice Item",
+ filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
+ fields=["parent"],
+ as_list=1,
+ )
+
+ purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
+ docs_with_deferred_expense = frappe.db.get_all(
+ "Purchase Invoice Item",
+ filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
+ fields=["parent"],
+ as_list=1,
+ )
+
+ if docs_with_deferred_revenue or docs_with_deferred_expense:
+ frappe.throw(
+ _("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
+ frappe.bold(
+ comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
+ )
+ )
+ )
+
+ def validate_for_closed_fiscal_year(self):
+ if self.vouchers:
+ latest_pcv = (
+ frappe.db.get_all(
+ "Period Closing Voucher",
+ filters={"company": self.company},
+ order_by="posting_date desc",
+ pluck="posting_date",
+ limit=1,
+ )
+ or None
+ )
+ if not latest_pcv:
+ return
+
+ for vtype in self._allowed_types:
+ if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]:
+ latest_voucher = frappe.db.get_all(
+ vtype,
+ filters={"name": ["in", names]},
+ pluck="posting_date",
+ order_by="posting_date desc",
+ limit=1,
+ )[0]
+ if latest_voucher and latest_pcv[0] >= latest_voucher:
+ frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year."))
+
+ def validate_vouchers(self):
+ if self.vouchers:
+ # Validate voucher types
+ voucher_types = set([x.voucher_type for x in self.vouchers])
+ if disallowed_types := voucher_types.difference(self._allowed_types):
+ frappe.throw(
+ _("{0} types are not allowed. Only {1} are.").format(
+ frappe.bold(comma_and(list(disallowed_types))),
+ frappe.bold(comma_and(list(self._allowed_types))),
+ )
+ )
+
+ def get_existing_ledger_entries(self):
+ vouchers = [x.voucher_no for x in self.vouchers]
+ gl = qb.DocType("GL Entry")
+ existing_gles = (
+ qb.from_(gl)
+ .select(gl.star)
+ .where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0))
+ .run(as_dict=True)
+ )
+ self.gles = frappe._dict({})
+
+ for gle in existing_gles:
+ self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault(
+ "existing", []
+ ).append(gle.update({"old": True}))
+
+ def generate_preview_data(self):
+ self.gl_entries = []
+ self.get_existing_ledger_entries()
+ for x in self.vouchers:
+ doc = frappe.get_doc(x.voucher_type, x.voucher_no)
+ if doc.doctype in ["Payment Entry", "Journal Entry"]:
+ gle_map = doc.build_gl_map()
+ else:
+ gle_map = doc.get_gl_entries()
+
+ old_entries = self.gles.get((x.voucher_type, x.voucher_no))
+ if old_entries:
+ self.gl_entries.extend(old_entries.existing)
+ self.gl_entries.extend(gle_map)
+
+ @frappe.whitelist()
+ def generate_preview(self):
+ from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
+
+ gl_columns = []
+ gl_data = []
+
+ self.generate_preview_data()
+ if self.gl_entries:
+ filters = {"company": self.company, "include_dimensions": 1}
+ for x in get_gl_columns(filters):
+ if x["fieldname"] == "gl_entry":
+ x["fieldname"] = "name"
+ gl_columns.append(x)
+
+ gl_data = self.gl_entries
+ rendered_page = frappe.render_template(
+ "erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html",
+ {"gl_columns": gl_columns, "gl_data": gl_data},
+ )
+
+ return rendered_page
+
+ def on_submit(self):
+ job_name = "repost_accounting_ledger_" + self.name
+ frappe.enqueue(
+ method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
+ account_repost_doc=self.name,
+ is_async=True,
+ job_name=job_name,
+ )
+ frappe.msgprint(_("Repost has started in the background"))
+
+
+@frappe.whitelist()
+def start_repost(account_repost_doc=str) -> None:
+ if account_repost_doc:
+ repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
+
+ if repost_doc.docstatus == 1:
+ # Prevent repost on invoices with deferred accounting
+ repost_doc.validate_for_deferred_accounting()
+
+ for x in repost_doc.vouchers:
+ doc = frappe.get_doc(x.voucher_type, x.voucher_no)
+
+ if repost_doc.delete_cancelled_entries:
+ frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name})
+ frappe.db.delete(
+ "Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
+ )
+
+ if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
+ if not repost_doc.delete_cancelled_entries:
+ doc.docstatus = 2
+ doc.make_gl_entries_on_cancel()
+
+ doc.docstatus = 1
+ doc.make_gl_entries()
+
+ elif doc.doctype in ["Payment Entry", "Journal Entry"]:
+ if not repost_doc.delete_cancelled_entries:
+ doc.make_gl_entries(1)
+ doc.make_gl_entries()
+
+ frappe.db.commit()
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
new file mode 100644
index 00000000000..0e75dd2e3e1
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
@@ -0,0 +1,202 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe import qb
+from frappe.query_builder.functions import Sum
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, nowdate, today
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
+from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+from erpnext.accounts.utils import get_fiscal_year
+
+
+class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.create_item()
+
+ def teadDown(self):
+ frappe.db.rollback()
+
+ def test_01_basic_functions(self):
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ )
+
+ preq = frappe.get_doc(
+ make_payment_request(
+ dt=si.doctype,
+ dn=si.name,
+ payment_request_type="Inward",
+ party_type="Customer",
+ party=si.customer,
+ )
+ )
+ preq.save().submit()
+
+ # Test Validation Error
+ ral = frappe.new_doc("Repost Accounting Ledger")
+ ral.company = self.company
+ ral.delete_cancelled_entries = True
+ ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+ ral.append(
+ "vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name}
+ ) # this should throw validation error
+ self.assertRaises(frappe.ValidationError, ral.save)
+ ral.vouchers.pop()
+ preq.cancel()
+ preq.delete()
+
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.save().submit()
+ ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+ ral.save()
+
+ # manually set an incorrect debit amount in DB
+ gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
+ frappe.db.set_value("GL Entry", gle[0], "debit", 90)
+
+ gl = qb.DocType("GL Entry")
+ res = (
+ qb.from_(gl)
+ .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
+ .where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
+ .run()
+ )
+
+ # Assert incorrect ledger balance
+ self.assertNotEqual(res[0], (si.name, 100, 100))
+
+ # Submit repost document
+ ral.save().submit()
+
+ # background jobs don't run on test cases. Manually triggering repost function.
+ start_repost(ral.name)
+
+ res = (
+ qb.from_(gl)
+ .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
+ .where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
+ .run()
+ )
+
+ # Ledger should reflect correct amount post repost
+ self.assertEqual(res[0], (si.name, 100, 100))
+
+ def test_02_deferred_accounting_valiations(self):
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ do_not_submit=True,
+ )
+ si.items[0].enable_deferred_revenue = True
+ si.items[0].deferred_revenue_account = self.deferred_revenue
+ si.items[0].service_start_date = nowdate()
+ si.items[0].service_end_date = add_days(nowdate(), 90)
+ si.save().submit()
+
+ ral = frappe.new_doc("Repost Accounting Ledger")
+ ral.company = self.company
+ ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+ self.assertRaises(frappe.ValidationError, ral.save)
+
+ @change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
+ def test_04_pcv_validation(self):
+ # Clear old GL entries so PCV can be submitted.
+ gl = frappe.qb.DocType("GL Entry")
+ qb.from_(gl).delete().where(gl.company == self.company).run()
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ )
+ pcv = frappe.get_doc(
+ {
+ "doctype": "Period Closing Voucher",
+ "transaction_date": today(),
+ "posting_date": today(),
+ "company": self.company,
+ "fiscal_year": get_fiscal_year(today(), company=self.company)[0],
+ "cost_center": self.cost_center,
+ "closing_account_head": self.retained_earnings,
+ "remarks": "test",
+ }
+ )
+ pcv.save().submit()
+
+ ral = frappe.new_doc("Repost Accounting Ledger")
+ ral.company = self.company
+ ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+ self.assertRaises(frappe.ValidationError, ral.save)
+
+ pcv.reload()
+ pcv.cancel()
+ pcv.delete()
+
+ def test_03_deletion_flag_and_preview_function(self):
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ )
+
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.save().submit()
+
+ # without deletion flag set
+ ral = frappe.new_doc("Repost Accounting Ledger")
+ ral.company = self.company
+ ral.delete_cancelled_entries = False
+ ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+ ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+ ral.save()
+
+ # assert preview data is generated
+ preview = ral.generate_preview()
+ self.assertIsNotNone(preview)
+
+ ral.save().submit()
+
+ # background jobs don't run on test cases. Manually triggering repost function.
+ start_repost(ral.name)
+
+ self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
+ self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
+
+ # with deletion flag set
+ ral = frappe.new_doc("Repost Accounting Ledger")
+ ral.company = self.company
+ ral.delete_cancelled_entries = True
+ ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+ ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+ ral.save().submit()
+
+ start_repost(ral.name)
+ self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
+ self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
new file mode 100644
index 00000000000..4a2041f88c6
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-07-04 14:14:01.243848",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "voucher_type",
+ "voucher_no"
+ ],
+ "fields": [
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Voucher Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Voucher No",
+ "options": "voucher_type"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-07-04 14:15:51.165584",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Accounting Ledger Items",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py
new file mode 100644
index 00000000000..9221f447355
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class RepostAccountingLedgerItems(Document):
+ pass
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index df3db37bc65..d6977d39a9f 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
- 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
+ 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index fb60dd58724..5fc711d893b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -714,6 +714,7 @@
"fieldtype": "Table",
"hide_days": 1,
"hide_seconds": 1,
+ "label": "Items",
"oldfieldname": "entries",
"oldfieldtype": "Table",
"options": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index ab629913cd4..3f9fe0441d1 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -399,6 +399,8 @@ class SalesInvoice(SellingController):
"Repost Item Valuation",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
+ "Repost Accounting Ledger",
+ "Repost Accounting Ledger Items",
"Payment Ledger Entry",
)
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 5d4035ee65e..ed6d9dbe026 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -28,6 +28,7 @@ def make_gl_entries(
):
if gl_map:
if not cancel:
+ make_acc_dimensions_offsetting_entry(gl_map)
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
@@ -51,6 +52,63 @@ def make_gl_entries(
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
+def make_acc_dimensions_offsetting_entry(gl_map):
+ accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(
+ gl_map, gl_map[0].company
+ )
+ no_of_dimensions = len(accounting_dimensions_to_offset)
+ if no_of_dimensions == 0:
+ return
+
+ offsetting_entries = []
+
+ for gle in gl_map:
+ for dimension in accounting_dimensions_to_offset:
+ offsetting_entry = gle.copy()
+ debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0
+ credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0
+ offsetting_entry.update(
+ {
+ "account": dimension.offsetting_account,
+ "debit": debit,
+ "credit": credit,
+ "debit_in_account_currency": debit,
+ "credit_in_account_currency": credit,
+ "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name),
+ "against_voucher": None,
+ }
+ )
+ offsetting_entry["against_voucher_type"] = None
+ offsetting_entries.append(offsetting_entry)
+
+ gl_map += offsetting_entries
+
+
+def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
+ acc_dimension = frappe.qb.DocType("Accounting Dimension")
+ dimension_detail = frappe.qb.DocType("Accounting Dimension Detail")
+
+ acc_dimensions = (
+ frappe.qb.from_(acc_dimension)
+ .inner_join(dimension_detail)
+ .on(acc_dimension.name == dimension_detail.parent)
+ .select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account)
+ .where(
+ (acc_dimension.disabled == 0)
+ & (dimension_detail.company == company)
+ & (dimension_detail.automatically_post_balancing_accounting_entry == 1)
+ )
+ ).run(as_dict=True)
+
+ accounting_dimensions_to_offset = []
+ for acc_dimension in acc_dimensions:
+ values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
+ if len(values) > 1:
+ accounting_dimensions_to_offset.append(acc_dimension)
+
+ return accounting_dimensions_to_offset
+
+
def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account]
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 3aea40316b5..0c51e727c92 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -14,7 +14,7 @@ from frappe.contacts.doctype.address.address import (
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
-from frappe.query_builder.functions import Date, Sum
+from frappe.query_builder.functions import Abs, Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -884,35 +884,34 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
def get_partywise_advanced_payment_amount(
- party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
+ party_type, posting_date=None, future_payment=0, company=None, party=None
):
- gle = frappe.qb.DocType("GL Entry")
+ ple = frappe.qb.DocType("Payment Ledger Entry")
query = (
- frappe.qb.from_(gle)
- .select(gle.party)
+ frappe.qb.from_(ple)
+ .select(ple.party, Abs(Sum(ple.amount).as_("amount")))
.where(
- (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
+ (ple.party_type.isin(party_type))
+ & (ple.amount < 0)
+ & (ple.against_voucher_no == ple.voucher_no)
+ & (ple.delinked == 0)
)
- .groupby(gle.party)
+ .groupby(ple.party)
)
- if account_type == "Receivable":
- query = query.select(Sum(gle.credit).as_("amount"))
- else:
- query = query.select(Sum(gle.debit).as_("amount"))
if posting_date:
if future_payment:
- query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
+ query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date))
else:
- query = query.where(gle.posting_date <= posting_date)
+ query = query.where(ple.posting_date <= posting_date)
if company:
- query = query.where(gle.company == company)
+ query = query.where(ple.company == company)
if party:
- query = query.where(gle.party == party)
+ query = query.where(ple.party == party)
- data = query.run(as_dict=True)
+ data = query.run()
if data:
return frappe._dict(data)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index f78a84086a9..751063ad8e6 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -214,8 +214,8 @@ class ReceivablePayableReport(object):
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
amount = ple.amount_in_account_currency
- else:
- amount = ple.amount
+ else:
+ amount = ple.amount
amount_in_account_currency = ple.amount_in_account_currency
# update voucher
@@ -1090,7 +1090,10 @@ class ReceivablePayableReport(object):
.where(
(je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date))
- & (je.voucher_type == "Exchange Rate Revaluation")
+ & (
+ (je.voucher_type == "Exchange Rate Revaluation")
+ | (je.voucher_type == "Exchange Gain Or Loss")
+ )
)
.run()
)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index da4c9dabbf6..cffc87895ef 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -50,13 +50,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.filters.show_future_payments,
self.filters.company,
party=party,
- account_type=self.account_type,
)
or {}
)
if self.filters.show_gl_balance:
- gl_balance_map = get_gl_balance(self.filters.report_date)
+ gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0:
@@ -233,12 +232,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(label="Total Amount Due", fieldname="total_due")
-def get_gl_balance(report_date):
+def get_gl_balance(report_date, company):
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", "sum(debit - credit)"],
- filters={"posting_date": ("<=", report_date), "is_cancelled": 0},
+ filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,
)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
new file mode 100644
index 00000000000..3ee35a114d1
--- /dev/null
+++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
@@ -0,0 +1,203 @@
+import unittest
+
+import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import today
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.maxDiff = None
+ self.create_company()
+ self.create_customer()
+ self.create_item()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def test_01_receivable_summary_output(self):
+ """
+ Test for Invoices, Paid, Advance and Outstanding
+ """
+ filters = {
+ "company": self.company,
+ "customer": self.customer,
+ "posting_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=200,
+ price_list_rate=200,
+ )
+
+ customer_group, customer_territory = frappe.db.get_all(
+ "Customer",
+ filters={"name": self.customer},
+ fields=["customer_group", "territory"],
+ as_list=True,
+ )[0]
+
+ report = execute(filters)
+ rpt_output = report[1]
+ expected_data = {
+ "party_type": "Customer",
+ "advance": 0,
+ "party": self.customer,
+ "invoiced": 200.0,
+ "paid": 0.0,
+ "credit_note": 0.0,
+ "outstanding": 200.0,
+ "range1": 200.0,
+ "range2": 0.0,
+ "range3": 0.0,
+ "range4": 0.0,
+ "range5": 0.0,
+ "total_due": 200.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "currency": si.currency,
+ "territory": customer_territory,
+ "customer_group": customer_group,
+ }
+
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # simulate advance payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 50
+ pe.references[0].allocated_amount = 0 # this essitially removes the reference
+ pe.save().submit()
+
+ # update expected data with advance
+ expected_data.update(
+ {
+ "advance": 50.0,
+ "outstanding": 150.0,
+ "range1": 150.0,
+ "total_due": 150.0,
+ }
+ )
+
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # make partial payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 125
+ pe.references[0].allocated_amount = 125
+ pe.save().submit()
+
+ # update expected data after advance and partial payment
+ expected_data.update(
+ {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0}
+ )
+
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ @change_settings("Selling Settings", {"cust_master_name": "Naming Series"})
+ def test_02_various_filters_and_output(self):
+ filters = {
+ "company": self.company,
+ "customer": self.customer,
+ "posting_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=200,
+ price_list_rate=200,
+ )
+ # make partial payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 150
+ pe.references[0].allocated_amount = 150
+ pe.save().submit()
+
+ customer_group, customer_territory = frappe.db.get_all(
+ "Customer",
+ filters={"name": self.customer},
+ fields=["customer_group", "territory"],
+ as_list=True,
+ )[0]
+
+ report = execute(filters)
+ rpt_output = report[1]
+ expected_data = {
+ "party_type": "Customer",
+ "advance": 0,
+ "party": self.customer,
+ "party_name": self.customer,
+ "invoiced": 200.0,
+ "paid": 150.0,
+ "credit_note": 0.0,
+ "outstanding": 50.0,
+ "range1": 50.0,
+ "range2": 0.0,
+ "range3": 0.0,
+ "range4": 0.0,
+ "range5": 0.0,
+ "total_due": 50.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "currency": si.currency,
+ "territory": customer_territory,
+ "customer_group": customer_group,
+ }
+
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # with gl balance filter
+ filters.update({"show_gl_balance": True})
+ expected_data.update({"gl_balance": 50.0, "diff": 0.0})
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # with gl balance and future payments filter
+ filters.update({"show_future_payments": True})
+ expected_data.update({"remaining_balance": 50.0})
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # invoice fully paid
+ pe = get_payment_entry(si.doctype, si.name).save().submit()
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 0)
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index cdcf73f6620..fe4b6c71ebc 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -749,13 +749,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
if from_date:
additional_conditions.append(gle.posting_date >= from_date)
- finance_book = filters.get("finance_book")
- company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
+ finance_books = []
+ finance_books.append("")
+ if filter_fb := filters.get("finance_book"):
+ finance_books.append(filter_fb)
if filters.get("include_default_book_entries"):
- additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
+ if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"):
+ finance_books.append(company_fb)
+
+ additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
else:
- additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
+ additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
return additional_conditions
diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py
new file mode 100644
index 00000000000..4682ac4500a
--- /dev/null
+++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py
@@ -0,0 +1,118 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import today
+
+from erpnext.accounts.report.trial_balance.trial_balance import execute
+
+
+class TestTrialBalance(FrappeTestCase):
+ def setUp(self):
+ from erpnext.accounts.doctype.account.test_account import create_account
+ from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+ from erpnext.accounts.utils import get_fiscal_year
+
+ self.company = create_company()
+ create_cost_center(
+ cost_center_name="Test Cost Center",
+ company="Trial Balance Company",
+ parent_cost_center="Trial Balance Company - TBC",
+ )
+ create_account(
+ account_name="Offsetting",
+ company="Trial Balance Company",
+ parent_account="Temporary Accounts - TBC",
+ )
+ self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0]
+ create_accounting_dimension()
+
+ def test_offsetting_entries_for_accounting_dimensions(self):
+ """
+ Checks if Trial Balance Report is balanced when filtered using a particular Accounting Dimension
+ """
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'")
+ frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'")
+
+ branch1 = frappe.new_doc("Branch")
+ branch1.branch = "Location 1"
+ branch1.insert(ignore_if_duplicate=True)
+ branch2 = frappe.new_doc("Branch")
+ branch2.branch = "Location 2"
+ branch2.insert(ignore_if_duplicate=True)
+
+ si = create_sales_invoice(
+ company=self.company,
+ debit_to="Debtors - TBC",
+ cost_center="Test Cost Center - TBC",
+ income_account="Sales - TBC",
+ do_not_submit=1,
+ )
+ si.branch = "Location 1"
+ si.items[0].branch = "Location 2"
+ si.save()
+ si.submit()
+
+ filters = frappe._dict(
+ {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]}
+ )
+ total_row = execute(filters)[1][-1]
+ self.assertEqual(total_row["debit"], total_row["credit"])
+
+ def tearDown(self):
+ clear_dimension_defaults("Branch")
+ disable_dimension()
+
+
+def create_company(**args):
+ args = frappe._dict(args)
+ company = frappe.get_doc(
+ {
+ "doctype": "Company",
+ "company_name": args.company_name or "Trial Balance Company",
+ "country": args.country or "India",
+ "default_currency": args.currency or "INR",
+ }
+ )
+ company.insert(ignore_if_duplicate=True)
+ return company.name
+
+
+def create_accounting_dimension(**args):
+ args = frappe._dict(args)
+ document_type = args.document_type or "Branch"
+ if frappe.db.exists("Accounting Dimension", document_type):
+ accounting_dimension = frappe.get_doc("Accounting Dimension", document_type)
+ accounting_dimension.disabled = 0
+ else:
+ accounting_dimension = frappe.new_doc("Accounting Dimension")
+ accounting_dimension.document_type = document_type
+ accounting_dimension.insert()
+
+ accounting_dimension.set("dimension_defaults", [])
+ accounting_dimension.append(
+ "dimension_defaults",
+ {
+ "company": args.company or "Trial Balance Company",
+ "automatically_post_balancing_accounting_entry": 1,
+ "offsetting_account": args.offsetting_account or "Offsetting - TBC",
+ },
+ )
+ accounting_dimension.save()
+
+
+def disable_dimension(**args):
+ args = frappe._dict(args)
+ document_type = args.document_type or "Branch"
+ dimension = frappe.get_doc("Accounting Dimension", document_type)
+ dimension.disabled = 1
+ dimension.save()
+
+
+def clear_dimension_defaults(dimension_name):
+ accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name)
+ accounting_dimension.dimension_defaults = []
+ accounting_dimension.save()
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index c82164ef644..debfffdcbb3 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -1,10 +1,11 @@
import frappe
+from frappe import qb
from erpnext.stock.doctype.item.test_item import create_item
class AccountsTestMixin:
- def create_customer(self, customer_name, currency=None):
+ def create_customer(self, customer_name="_Test Customer", currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
@@ -17,7 +18,7 @@ class AccountsTestMixin:
else:
self.customer = customer_name
- def create_supplier(self, supplier_name, currency=None):
+ def create_supplier(self, supplier_name="_Test Supplier", currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
@@ -31,7 +32,7 @@ class AccountsTestMixin:
else:
self.supplier = supplier_name
- def create_item(self, item_name, is_stock=0, warehouse=None, company=None):
+ def create_item(self, item_name="_Test Item", is_stock=0, warehouse=None, company=None):
item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
self.item = item.name
@@ -62,19 +63,56 @@ class AccountsTestMixin:
self.debit_usd = "Debtors USD - " + abbr
self.cash = "Cash - " + abbr
self.creditors = "Creditors - " + abbr
+ self.retained_earnings = "Retained Earnings - " + 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(
+ # Deferred revenue, expense and bank accounts
+ other_accounts = [
+ frappe._dict(
{
- "doctype": "Account",
+ "attribute_name": "deferred_revenue",
+ "account_name": "Deferred Revenue",
+ "parent_account": "Current Liabilities - " + abbr,
+ }
+ ),
+ frappe._dict(
+ {
+ "attribute_name": "deferred_expense",
+ "account_name": "Deferred Expense",
+ "parent_account": "Current Assets - " + abbr,
+ }
+ ),
+ frappe._dict(
+ {
+ "attribute_name": "bank",
"account_name": "HDFC",
"parent_account": "Bank Accounts - " + abbr,
- "company": self.company,
}
- )
- bank_acc.save()
- self.bank = bank_acc.name
+ ),
+ ]
+ for acc in other_accounts:
+ acc_name = acc.account_name + " - " + abbr
+ if frappe.db.exists("Account", acc_name):
+ setattr(self, acc.attribute_name, acc_name)
+ else:
+ new_acc = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": acc.account_name,
+ "parent_account": acc.parent_account,
+ "company": self.company,
+ }
+ )
+ new_acc.save()
+ setattr(self, acc.attribute_name, new_acc.name)
+
+ 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()
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 3e06a36e67e..2df3387b83e 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -884,6 +884,7 @@ def get_outstanding_invoices(
min_outstanding=None,
max_outstanding=None,
accounting_dimensions=None,
+ vouchers=None,
):
ple = qb.DocType("Payment Ledger Entry")
@@ -909,6 +910,7 @@ def get_outstanding_invoices(
ple_query = QueryPaymentLedger()
invoice_list = ple_query.get_voucher_outstandings(
+ vouchers=vouchers,
common_filter=common_filter,
posting_date=posting_date,
min_outstanding=min_outstanding,
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index f4a1e3cc190..34d5430210c 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -81,18 +81,27 @@ class Asset(AccountsController):
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
)
- def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None):
+ def prepare_depreciation_data(
+ self,
+ date_of_disposal=None,
+ date_of_return=None,
+ value_after_depreciation=None,
+ ignore_booked_entry=False,
+ ):
if self.calculate_depreciation:
self.value_after_depreciation = 0
self.set_depreciation_rate()
if self.should_prepare_depreciation_schedule():
- self.make_depreciation_schedule(date_of_disposal)
- self.set_accumulated_depreciation(date_of_disposal, date_of_return)
+ self.make_depreciation_schedule(date_of_disposal, value_after_depreciation)
+ self.set_accumulated_depreciation(date_of_disposal, date_of_return, ignore_booked_entry)
else:
self.finance_books = []
- self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
- self.opening_accumulated_depreciation
- )
+ if value_after_depreciation:
+ self.value_after_depreciation = value_after_depreciation
+ else:
+ self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
+ self.opening_accumulated_depreciation
+ )
def should_prepare_depreciation_schedule(self):
if not self.get("schedules"):
@@ -285,7 +294,7 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
- def make_depreciation_schedule(self, date_of_disposal):
+ def make_depreciation_schedule(self, date_of_disposal, value_after_depreciation=None):
if not self.get("schedules"):
self.schedules = []
@@ -295,24 +304,30 @@ class Asset(AccountsController):
start = self.clear_depreciation_schedule()
for finance_book in self.get("finance_books"):
- self._make_depreciation_schedule(finance_book, start, date_of_disposal)
+ self._make_depreciation_schedule(
+ finance_book, start, date_of_disposal, value_after_depreciation
+ )
if len(self.get("finance_books")) > 1 and any(start):
self.sort_depreciation_schedule()
- def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
+ def _make_depreciation_schedule(
+ self, finance_book, start, date_of_disposal, value_after_depreciation=None
+ ):
self.validate_asset_finance_books(finance_book)
- value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book)
+ if not value_after_depreciation:
+ value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book)
+
finance_book.value_after_depreciation = value_after_depreciation
- number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
+ final_number_of_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
self.number_of_depreciations_booked
)
has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
- number_of_pending_depreciations += 1
+ final_number_of_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False
if (
@@ -328,7 +343,9 @@ class Asset(AccountsController):
depreciation_amount = 0
- for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
+ number_of_pending_depreciations = final_number_of_depreciations - start[finance_book.idx - 1]
+
+ for n in range(start[finance_book.idx - 1], final_number_of_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
@@ -345,10 +362,11 @@ class Asset(AccountsController):
n,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
+ number_of_pending_depreciations,
)
if not has_pro_rata or (
- n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
+ n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
):
schedule_date = add_months(
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
@@ -416,7 +434,7 @@ class Asset(AccountsController):
)
# For last row
- elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
+ elif has_pro_rata and n == cint(final_number_of_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(
@@ -447,7 +465,7 @@ class Asset(AccountsController):
# Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and (
(
- n == cint(number_of_pending_depreciations) - 1
+ n == cint(final_number_of_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life
)
or value_after_depreciation < finance_book.expected_value_after_useful_life
@@ -690,7 +708,10 @@ class Asset(AccountsController):
if s.finance_book_id == d.finance_book_id
and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual")
]
- accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+ if i > 0 and self.flags.decrease_in_asset_value_due_to_value_adjustment:
+ accumulated_depreciation = self.get("schedules")[i - 1].accumulated_depreciation_amount
+ else:
+ accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation
)
@@ -1296,11 +1317,14 @@ def get_depreciation_amount(
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
+ number_of_pending_depreciations=0,
):
frappe.flags.company = asset.company
if fb_row.depreciation_method in ("Straight Line", "Manual"):
- return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx)
+ return get_straight_line_or_manual_depr_amount(
+ asset, fb_row, schedule_idx, number_of_pending_depreciations
+ )
else:
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row
@@ -1320,7 +1344,9 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb
return fb_row.rate_of_depreciation
-def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
+def get_straight_line_or_manual_depr_amount(
+ asset, row, schedule_idx, number_of_pending_depreciations
+):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
@@ -1331,6 +1357,36 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
+ # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
+ elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
+ if row.daily_depreciation:
+ daily_depr_amount = (
+ flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
+ ) / date_diff(
+ add_months(
+ row.depreciation_start_date,
+ flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
+ * row.frequency_of_depreciation,
+ ),
+ add_months(
+ row.depreciation_start_date,
+ flt(
+ row.total_number_of_depreciations
+ - asset.number_of_depreciations_booked
+ - number_of_pending_depreciations
+ )
+ * row.frequency_of_depreciation,
+ ),
+ )
+ to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
+ from_date = add_months(
+ row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
+ )
+ return daily_depr_amount * date_diff(to_date, from_date)
+ else:
+ return (
+ flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
+ ) / number_of_pending_depreciations
# if the Depreciation Schedule is being prepared for the first time
else:
if row.daily_depreciation:
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 9928b2f5f38..29e7a9bdfd6 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -5,15 +5,12 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cint, date_diff, flt, formatdate, getdate
+from frappe.utils import flt, formatdate, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
-from erpnext.assets.doctype.asset.asset import (
- get_asset_value_after_depreciation,
- get_depreciation_amount,
-)
+from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
@@ -25,10 +22,10 @@ class AssetValueAdjustment(Document):
def on_submit(self):
self.make_depreciation_entry()
- self.reschedule_depreciations(self.new_asset_value)
+ self.update_asset(self.new_asset_value)
def on_cancel(self):
- self.reschedule_depreciations(self.current_asset_value)
+ self.update_asset(self.current_asset_value)
def validate_date(self):
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
@@ -71,12 +68,16 @@ class AssetValueAdjustment(Document):
"account": accumulated_depreciation_account,
"credit_in_account_currency": self.difference_amount,
"cost_center": depreciation_cost_center or self.cost_center,
+ "reference_type": "Asset",
+ "reference_name": asset.name,
}
debit_entry = {
"account": depreciation_expense_account,
"debit_in_account_currency": self.difference_amount,
"cost_center": depreciation_cost_center or self.cost_center,
+ "reference_type": "Asset",
+ "reference_name": asset.name,
}
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
@@ -106,44 +107,11 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
- def reschedule_depreciations(self, asset_value):
+ def update_asset(self, asset_value):
asset = frappe.get_doc("Asset", self.asset)
- country = frappe.get_value("Company", self.company, "country")
- for d in asset.finance_books:
- d.value_after_depreciation = asset_value
+ asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
- if d.depreciation_method in ("Straight Line", "Manual"):
- end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
- total_days = date_diff(end_date, self.date)
- rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
- total_days
- )
- from_date = self.date
- else:
- no_of_depreciations = len(
- [
- s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry)
- ]
- )
-
- value_after_depreciation = d.value_after_depreciation
- for data in asset.schedules:
- if cint(data.finance_book_id) == d.idx and not data.journal_entry:
- if d.depreciation_method in ("Straight Line", "Manual"):
- days = date_diff(data.schedule_date, from_date)
- depreciation_amount = days * rate_per_day
- from_date = data.schedule_date
- else:
- depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
-
- if depreciation_amount:
- value_after_depreciation -= flt(depreciation_amount)
- data.depreciation_amount = depreciation_amount
-
- d.db_update()
-
- asset.set_accumulated_depreciation(ignore_booked_entry=True)
- for asset_data in asset.schedules:
- if not asset_data.journal_entry:
- asset_data.db_update()
+ asset.prepare_depreciation_data(value_after_depreciation=asset_value, ignore_booked_entry=True)
+ asset.flags.ignore_validate_update_after_submit = True
+ asset.save()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
index b2aa3958080..977a9b3714b 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
@@ -4,9 +4,10 @@
import unittest
import frappe
-from frappe.utils import add_days, get_last_day, nowdate
+from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
+from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -46,40 +47,44 @@ class TestAssetValueAdjustment(unittest.TestCase):
def test_asset_depreciation_value_adjustment(self):
pr = make_purchase_receipt(
- item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
+ item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location"
)
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name)
asset_doc.calculate_depreciation = 1
- month_end_date = get_last_day(nowdate())
- purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
-
- asset_doc.available_for_use_date = purchase_date
- asset_doc.purchase_date = purchase_date
+ asset_doc.available_for_use_date = "2023-01-15"
+ asset_doc.purchase_date = "2023-01-15"
asset_doc.calculate_depreciation = 1
asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line",
- "total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": month_end_date,
+ "total_number_of_depreciations": 12,
+ "frequency_of_depreciation": 1,
+ "depreciation_start_date": "2023-01-31",
},
)
asset_doc.submit()
+ post_depreciation_entries(getdate("2023-08-21"))
+
current_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment(
- asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
+ asset=asset_doc.name,
+ current_asset_value=current_value,
+ new_asset_value=50000.0,
+ date="2023-08-21",
)
adj_doc.submit()
+ asset_doc.reload()
+
expected_gle = (
- ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
- ("_Test Depreciations - _TC", 50000.0, 0.0),
+ ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
+ ("_Test Depreciations - _TC", 4625.29, 0.0),
)
gle = frappe.db.sql(
@@ -91,6 +96,29 @@ class TestAssetValueAdjustment(unittest.TestCase):
self.assertSequenceEqual(gle, expected_gle)
+ expected_schedules = [
+ ["2023-01-31", 5474.73, 5474.73],
+ ["2023-02-28", 9983.33, 15458.06],
+ ["2023-03-31", 9983.33, 25441.39],
+ ["2023-04-30", 9983.33, 35424.72],
+ ["2023-05-31", 9983.33, 45408.05],
+ ["2023-06-30", 9983.33, 55391.38],
+ ["2023-07-31", 9983.33, 65374.71],
+ ["2023-08-31", 8300.0, 73674.71],
+ ["2023-09-30", 8300.0, 81974.71],
+ ["2023-10-31", 8300.0, 90274.71],
+ ["2023-11-30", 8300.0, 98574.71],
+ ["2023-12-31", 8300.0, 106874.71],
+ ["2024-01-15", 8300.0, 115174.71],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in asset_doc.get("schedules")
+ ]
+
+ self.assertEqual(schedules, expected_schedules)
+
def make_asset_value_adjustment(**args):
args = frappe._dict(args)
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 2f0b7862a82..30abad528bf 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -245,19 +245,21 @@ frappe.ui.form.on("Request for Quotation",{
]
});
- dialog.fields_dict['supplier'].df.onchange = () => {
- var supplier = dialog.get_value('supplier');
- frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => {
+ dialog.fields_dict["supplier"].df.onchange = () => {
+ frm.call("get_supplier_email_preview", {
+ supplier: dialog.get_value("supplier"),
+ }).then(({ message }) => {
dialog.fields_dict.email_preview.$wrapper.empty();
- dialog.fields_dict.email_preview.$wrapper.append(result.message);
+ dialog.fields_dict.email_preview.$wrapper.append(
+ message.message
+ );
+ dialog.set_value("subject", message.subject);
});
-
- }
+ };
dialog.fields_dict.note.$wrapper.append(`This is a preview of the email to be sent. A PDF of the document will
automatically be attached with the email.
`);
- dialog.set_value("subject", frm.doc.subject);
dialog.show();
}
})
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index c16abb2f41b..fbfc1ac1693 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -20,11 +20,10 @@
"items_section",
"items",
"supplier_response_section",
- "salutation",
- "subject",
- "col_break_email_1",
"email_template",
"preview",
+ "col_break_email_1",
+ "html_llwp",
"send_attached_files",
"sec_break_email_2",
"message_for_supplier",
@@ -237,23 +236,6 @@
"print_hide": 1,
"read_only": 1
},
- {
- "fetch_from": "email_template.subject",
- "fetch_if_empty": 1,
- "fieldname": "subject",
- "fieldtype": "Data",
- "label": "Subject",
- "print_hide": 1
- },
- {
- "description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.",
- "fieldname": "salutation",
- "fieldtype": "Link",
- "label": "Salutation",
- "no_copy": 1,
- "options": "Salutation",
- "print_hide": 1
- },
{
"fieldname": "col_break_email_1",
"fieldtype": "Column Break"
@@ -287,6 +269,14 @@
"fieldtype": "Data",
"label": "Named Place"
},
+ {
+ "fieldname": "html_llwp",
+ "fieldtype": "HTML",
+ "options": "In your Email Template, you can use the following special variables:\n
\n\n - \n
{{ update_password_link }}: A link where your supplier can set a new password to log into your portal.\n \n - \n
{{ portal_link }}: A link to this RFQ in your supplier portal.\n \n - \n
{{ supplier_name }}: The company name of your supplier.\n \n - \n
{{ contact.salutation }} {{ contact.last_name }}: The contact person of your supplier.\n - \n
{{ user_fullname }}: Your full name.\n \n
\n\nApart from these, you can access all values in this RFQ, like {{ message_for_supplier }} or {{ terms }}.
",
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1
+ },
{
"default": "1",
"description": "If enabled, all files attached to this document will be attached to each email",
@@ -299,7 +289,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-07-27 16:41:48.468873",
+ "modified": "2023-08-08 16:30:10.870429",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 63e393aecd6..56840c11a6e 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -182,35 +182,28 @@ class RequestforQuotation(BuyingController):
if full_name == "Guest":
full_name = "Administrator"
- # send document dict and some important data from suppliers row
- # to render message_for_supplier from any template
doc_args = self.as_dict()
- doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
- # Get Contact Full Name
- supplier_name = None
if data.get("contact"):
- contact_name = frappe.db.get_value(
- "Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
- )
- supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
+ contact = frappe.get_doc("Contact", data.get("contact"))
+ doc_args["contact"] = contact.as_dict()
- args = {
- "update_password_link": update_password_link,
- "message": frappe.render_template(self.message_for_supplier, doc_args),
- "rfq_link": rfq_link,
- "user_fullname": full_name,
- "supplier_name": supplier_name or data.get("supplier_name"),
- "supplier_salutation": self.salutation or "Dear Mx.",
- }
-
- subject = self.subject or _("Request for Quotation")
- template = "templates/emails/request_for_quotation.html"
+ doc_args.update(
+ {
+ "supplier": data.get("supplier"),
+ "supplier_name": data.get("supplier_name"),
+ "update_password_link": f'{_("Set Password")}',
+ "portal_link": f' {_("Submit your Quotation")} ',
+ "user_fullname": full_name,
+ }
+ )
+ email_template = frappe.get_doc("Email Template", self.email_template)
+ message = frappe.render_template(email_template.response_, doc_args)
+ subject = frappe.render_template(email_template.subject, doc_args)
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
- message = frappe.get_template(template).render(args)
if preview:
- return message
+ return {"message": message, "subject": subject}
attachments = None
if self.send_attached_files:
diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
index 71019e80377..a7e03c08fac 100644
--- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
@@ -154,31 +154,35 @@ def get_data(filters):
procurement_record = []
if procurement_record_against_mr:
procurement_record += procurement_record_against_mr
+
for po in purchase_order_entry:
# fetch material records linked to the purchase order item
- mr_record = mr_records.get(po.material_request_item, [{}])[0]
- procurement_detail = {
- "material_request_date": mr_record.get("transaction_date"),
- "cost_center": po.cost_center,
- "project": po.project,
- "requesting_site": po.warehouse,
- "requestor": po.owner,
- "material_request_no": po.material_request,
- "item_code": po.item_code,
- "quantity": flt(po.qty),
- "unit_of_measurement": po.stock_uom,
- "status": po.status,
- "purchase_order_date": po.transaction_date,
- "purchase_order": po.parent,
- "supplier": po.supplier,
- "estimated_cost": flt(mr_record.get("amount")),
- "actual_cost": flt(pi_records.get(po.name)),
- "purchase_order_amt": flt(po.amount),
- "purchase_order_amt_in_company_currency": flt(po.base_amount),
- "expected_delivery_date": po.schedule_date,
- "actual_delivery_date": pr_records.get(po.name),
- }
- procurement_record.append(procurement_detail)
+ material_requests = mr_records.get(po.material_request_item, [{}])
+
+ for mr_record in material_requests:
+ procurement_detail = {
+ "material_request_date": mr_record.get("transaction_date"),
+ "cost_center": po.cost_center,
+ "project": po.project,
+ "requesting_site": po.warehouse,
+ "requestor": po.owner,
+ "material_request_no": po.material_request,
+ "item_code": po.item_code,
+ "quantity": flt(po.qty),
+ "unit_of_measurement": po.stock_uom,
+ "status": po.status,
+ "purchase_order_date": po.transaction_date,
+ "purchase_order": po.parent,
+ "supplier": po.supplier,
+ "estimated_cost": flt(mr_record.get("amount")),
+ "actual_cost": flt(pi_records.get(po.name)),
+ "purchase_order_amt": flt(po.amount),
+ "purchase_order_amt_in_company_currency": flt(po.base_amount),
+ "expected_delivery_date": po.schedule_date,
+ "actual_delivery_date": pr_records.get(po.name),
+ }
+ procurement_record.append(procurement_detail)
+
return procurement_record
@@ -301,7 +305,7 @@ def get_po_entries(filters):
& (parent.name == child.parent)
& (parent.status.notin(("Closed", "Completed", "Cancelled")))
)
- .groupby(parent.name, child.item_code)
+ .groupby(parent.name, child.material_request_item)
)
query = apply_filters_on_query(filters, parent, child, query)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 7afd80b4bcf..76fe6a91182 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -715,7 +715,9 @@ class AccountsController(TransactionBase):
def validate_enabled_taxes_and_charges(self):
taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges")
- if frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"):
+ if self.taxes_and_charges and frappe.get_cached_value(
+ taxes_and_charges_doctype, self.taxes_and_charges, "disabled"
+ ):
frappe.throw(
_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges)
)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 8d75c3cb60d..db4003bc585 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -347,7 +347,7 @@ class ProductionPlan(Document):
if not data.pending_qty:
continue
- item_details = get_item_details(data.item_code)
+ item_details = get_item_details(data.item_code, throw=False)
if self.combine_items:
if item_details.bom_no in refs:
refs[item_details.bom_no]["so_details"].append(
@@ -795,6 +795,9 @@ class ProductionPlan(Document):
if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
+ if not row.bom_no:
+ frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx))
+
bom_data = []
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 92678e44c84..c4b6846376f 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1075,7 +1075,7 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
-def get_item_details(item, project=None, skip_bom_info=False):
+def get_item_details(item, project=None, skip_bom_info=False, throw=True):
res = frappe.db.sql(
"""
select stock_uom, description, item_name, allow_alternative_item,
@@ -1111,12 +1111,15 @@ def get_item_details(item, project=None, skip_bom_info=False):
if not res["bom_no"]:
if project:
- res = get_item_details(item)
+ res = get_item_details(item, throw=throw)
frappe.msgprint(
_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1
)
else:
- frappe.throw(_("Default BOM for {0} not found").format(item))
+ msg = _("Default BOM for {0} not found").format(item)
+ frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw))
+
+ return res
bom_data = frappe.db.get_value(
"BOM",
diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py
index bc8f2afb8c9..ac1524a49dd 100644
--- a/erpnext/projects/report/billing_summary.py
+++ b/erpnext/projects/report/billing_summary.py
@@ -98,9 +98,11 @@ def get_timesheets(filters):
record_filters = [
["start_date", "<=", filters.to_date],
["end_date", ">=", filters.from_date],
- ["docstatus", "=", 1],
]
-
+ if not filters.get("include_draft_timesheets"):
+ record_filters.append(["docstatus", "=", 1])
+ else:
+ record_filters.append(["docstatus", "!=", 2])
if "employee" in filters:
record_filters.append(["employee", "=", filters.employee])
diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
index 13f49ed6bed..9c904c57872 100644
--- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
+++ b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
@@ -25,5 +25,10 @@ frappe.query_reports["Employee Billing Summary"] = {
default: frappe.datetime.add_days(frappe.datetime.month_start(), -1),
reqd: 1
},
+ {
+ fieldname:"include_draft_timesheets",
+ label: __("Include Timesheets in Draft Status"),
+ fieldtype: "Check",
+ },
]
}
diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.js b/erpnext/projects/report/project_billing_summary/project_billing_summary.js
index caac1d86b45..6a6f3677e3f 100644
--- a/erpnext/projects/report/project_billing_summary/project_billing_summary.js
+++ b/erpnext/projects/report/project_billing_summary/project_billing_summary.js
@@ -25,5 +25,10 @@ frappe.query_reports["Project Billing Summary"] = {
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
reqd: 1
},
+ {
+ fieldname:"include_draft_timesheets",
+ label: __("Include Timesheets in Draft Status"),
+ fieldtype: "Check",
+ },
]
}
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index 1271e38049a..ddc10530581 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -117,6 +117,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
name: __("Document Name"),
editable: false,
width: 1,
+ format: (value, row) => {
+ return frappe.form.formatters.Link(value, {options: row[2].content});
+ },
},
{
name: __("Reference Date"),
diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js
index d346357a8f8..2674df9c4af 100644
--- a/erpnext/public/js/controllers/stock_controller.js
+++ b/erpnext/public/js/controllers/stock_controller.js
@@ -57,7 +57,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company,
- show_cancelled_entries: me.frm.doc.docstatus === 2
+ show_cancelled_entries: me.frm.doc.docstatus === 2,
+ ignore_prepared_report: true
};
frappe.set_route("query-report", "Stock Ledger");
}, __("View"));
@@ -75,7 +76,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company,
group_by: "Group by Voucher (Consolidated)",
- show_cancelled_entries: me.frm.doc.docstatus === 2
+ show_cancelled_entries: me.frm.doc.docstatus === 2,
+ ignore_prepared_report: true
};
frappe.set_route("query-report", "General Ledger");
}, __("View"));
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 159fd32c123..bfb0d4e1853 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -660,7 +660,10 @@ def make_stock_entry(source_name, target_doc=None):
"job_card_item": "job_card_item",
},
"postprocess": update_item,
- "condition": lambda doc: doc.ordered_qty < doc.stock_qty,
+ "condition": lambda doc: (
+ flt(doc.ordered_qty, doc.precision("ordered_qty"))
+ < flt(doc.stock_qty, doc.precision("ordered_qty"))
+ ),
},
},
target_doc,
diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html
deleted file mode 100644
index 5b073e604ff..00000000000
--- a/erpnext/templates/emails/request_for_quotation.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{{_("Request for Quotation")}}
-{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},
-{{ message }}
-{{_("The Request for Quotation can be accessed by clicking on the following button")}}:
-
-
- {{ _("Submit your Quotation") }}
-
-
-
-{% if update_password_link %}
-
-{{_("Please click on the following button to set your new password")}}:
-
- {{_("Set Password") }}
-
-
-
-{% endif %}
-
- {{_("Regards")}},
- {{ user_fullname }}
-