feat: utility to repost accounting ledgers without cancellation (#36469)
* feat: introduce doctypes for repost * refactor: basic filters and validations * chore: basic validations * chore: added barebones function to generate ledger entries * chore: repost on submit * chore: repost in background * chore: include payment entry and journal entry * chore: ignore repost doc on cancel * chore: preview method * chore: rudimentary form of preview * refactor: preview template * refactor: basic background colors to differentiate old and new * chore: remove commented code * test: basic functionality * chore: fix conflict * chore: prevent repost on invoices with deferred accounting * refactor(test): rename and test basic validations and methods * refactor(test): test all validations * fix(test): use proper name account name * refactor(test): fix failing test case * refactor(test): clear old entries * refactor(test): simpler logic to clear old records * refactor(test): make use of deletion flag * refactor(test): split into multiple test cases
This commit is contained in:
parent
c1dd06065b
commit
e64b004eca
@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
|||||||
frappe.ui.form.on("Journal Entry", {
|
frappe.ui.form.on("Journal Entry", {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.add_fetch("bank_account", "account", "account");
|
frm.add_fetch("bank_account", "account", "account");
|
||||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule'];
|
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"];
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
|
@ -96,6 +96,8 @@ class JournalEntry(AccountsController):
|
|||||||
"Payment Ledger Entry",
|
"Payment Ledger Entry",
|
||||||
"Repost Payment Ledger",
|
"Repost Payment Ledger",
|
||||||
"Repost Payment Ledger Items",
|
"Repost Payment Ledger Items",
|
||||||
|
"Repost Accounting Ledger",
|
||||||
|
"Repost Accounting Ledger Items",
|
||||||
)
|
)
|
||||||
self.make_gl_entries(1)
|
self.make_gl_entries(1)
|
||||||
self.update_advance_paid()
|
self.update_advance_paid()
|
||||||
|
@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
|
|||||||
|
|
||||||
frappe.ui.form.on('Payment Entry', {
|
frappe.ui.form.on('Payment Entry', {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "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.__islocal) {
|
||||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||||
|
@ -147,6 +147,8 @@ class PaymentEntry(AccountsController):
|
|||||||
"Payment Ledger Entry",
|
"Payment Ledger Entry",
|
||||||
"Repost Payment Ledger",
|
"Repost Payment Ledger",
|
||||||
"Repost Payment Ledger Items",
|
"Repost Payment Ledger Items",
|
||||||
|
"Repost Accounting Ledger",
|
||||||
|
"Repost Accounting Ledger Items",
|
||||||
)
|
)
|
||||||
super(PaymentEntry, self).on_cancel()
|
super(PaymentEntry, self).on_cancel()
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
|
@ -35,7 +35,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
super.onload();
|
super.onload();
|
||||||
|
|
||||||
// Ignore linked advances
|
// 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) {
|
if(!this.frm.doc.__islocal) {
|
||||||
// show credit_to in print format
|
// show credit_to in print format
|
||||||
|
@ -1415,6 +1415,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"Repost Item Valuation",
|
"Repost Item Valuation",
|
||||||
"Repost Payment Ledger",
|
"Repost Payment Ledger",
|
||||||
"Repost Payment Ledger Items",
|
"Repost Payment Ledger Items",
|
||||||
|
"Repost Accounting Ledger",
|
||||||
|
"Repost Accounting Ledger Items",
|
||||||
"Payment Ledger Entry",
|
"Payment Ledger Entry",
|
||||||
"Tax Withheld Vouchers",
|
"Tax Withheld Vouchers",
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
<style>
|
||||||
|
.print-format {
|
||||||
|
padding: 4mm;
|
||||||
|
font-size: 8.0pt !important;
|
||||||
|
}
|
||||||
|
.print-format td {
|
||||||
|
vertical-align:middle !important;
|
||||||
|
}
|
||||||
|
.old {
|
||||||
|
background-color: #FFB3C0;
|
||||||
|
}
|
||||||
|
.new {
|
||||||
|
background-color: #B3FFCC;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<table class="table table-bordered table-condensed">
|
||||||
|
<colgroup>
|
||||||
|
{% for col in gl_columns%}
|
||||||
|
<col style="width: 18mm;">
|
||||||
|
{% endfor %}
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for col in gl_columns%}
|
||||||
|
<td>{{ col.label }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for gl in gl_data%}
|
||||||
|
{% if gl["old"]%}
|
||||||
|
<tr class="old">
|
||||||
|
{% else %}
|
||||||
|
<tr class="new">
|
||||||
|
{% endif %}
|
||||||
|
{% for col in gl_columns %}
|
||||||
|
<td class="text-right">
|
||||||
|
{{ gl[col.fieldname] }}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -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": []
|
||||||
|
}
|
@ -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()
|
@ -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}))
|
@ -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": []
|
||||||
|
}
|
@ -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
|
@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
super.onload();
|
super.onload();
|
||||||
|
|
||||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
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) {
|
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||||
// show debit_to in print format
|
// show debit_to in print format
|
||||||
|
@ -386,6 +386,8 @@ class SalesInvoice(SellingController):
|
|||||||
"Repost Item Valuation",
|
"Repost Item Valuation",
|
||||||
"Repost Payment Ledger",
|
"Repost Payment Ledger",
|
||||||
"Repost Payment Ledger Items",
|
"Repost Payment Ledger Items",
|
||||||
|
"Repost Accounting Ledger",
|
||||||
|
"Repost Accounting Ledger Items",
|
||||||
"Payment Ledger Entry",
|
"Payment Ledger Entry",
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,7 @@ from erpnext.stock.doctype.item.test_item import create_item
|
|||||||
|
|
||||||
|
|
||||||
class AccountsTestMixin:
|
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):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
customer = frappe.new_doc("Customer")
|
customer = frappe.new_doc("Customer")
|
||||||
customer.customer_name = customer_name
|
customer.customer_name = customer_name
|
||||||
@ -17,7 +17,7 @@ class AccountsTestMixin:
|
|||||||
else:
|
else:
|
||||||
self.customer = customer_name
|
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):
|
if not frappe.db.exists("Supplier", supplier_name):
|
||||||
supplier = frappe.new_doc("Supplier")
|
supplier = frappe.new_doc("Supplier")
|
||||||
supplier.supplier_name = supplier_name
|
supplier.supplier_name = supplier_name
|
||||||
@ -31,7 +31,7 @@ class AccountsTestMixin:
|
|||||||
else:
|
else:
|
||||||
self.supplier = supplier_name
|
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)
|
item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
|
||||||
self.item = item.name
|
self.item = item.name
|
||||||
|
|
||||||
@ -62,19 +62,44 @@ class AccountsTestMixin:
|
|||||||
self.debit_usd = "Debtors USD - " + abbr
|
self.debit_usd = "Debtors USD - " + abbr
|
||||||
self.cash = "Cash - " + abbr
|
self.cash = "Cash - " + abbr
|
||||||
self.creditors = "Creditors - " + abbr
|
self.creditors = "Creditors - " + abbr
|
||||||
|
self.retained_earnings = "Retained Earnings - " + abbr
|
||||||
|
|
||||||
# create bank account
|
# Deferred revenue, expense and bank accounts
|
||||||
bank_account = "HDFC - " + abbr
|
other_accounts = [
|
||||||
if frappe.db.exists("Account", bank_account):
|
frappe._dict(
|
||||||
self.bank = bank_account
|
|
||||||
else:
|
|
||||||
bank_acc = frappe.get_doc(
|
|
||||||
{
|
{
|
||||||
"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",
|
"account_name": "HDFC",
|
||||||
"parent_account": "Bank Accounts - " + abbr,
|
"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)
|
||||||
|
Loading…
Reference in New Issue
Block a user