mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-15 04:45:09 +00:00
Merge pull request #36763 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -714,6 +714,7 @@
|
||||
"fieldtype": "Table",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Items",
|
||||
"oldfieldname": "entries",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Sales Invoice Item",
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
118
erpnext/accounts/report/trial_balance/test_trial_balance.py
Normal file
118
erpnext/accounts/report/trial_balance/test_trial_balance.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import execute
|
||||
|
||||
|
||||
class TestTrialBalance(FrappeTestCase):
|
||||
def setUp(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
self.company = create_company()
|
||||
create_cost_center(
|
||||
cost_center_name="Test Cost Center",
|
||||
company="Trial Balance Company",
|
||||
parent_cost_center="Trial Balance Company - TBC",
|
||||
)
|
||||
create_account(
|
||||
account_name="Offsetting",
|
||||
company="Trial Balance Company",
|
||||
parent_account="Temporary Accounts - TBC",
|
||||
)
|
||||
self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0]
|
||||
create_accounting_dimension()
|
||||
|
||||
def test_offsetting_entries_for_accounting_dimensions(self):
|
||||
"""
|
||||
Checks if Trial Balance Report is balanced when filtered using a particular Accounting Dimension
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'")
|
||||
|
||||
branch1 = frappe.new_doc("Branch")
|
||||
branch1.branch = "Location 1"
|
||||
branch1.insert(ignore_if_duplicate=True)
|
||||
branch2 = frappe.new_doc("Branch")
|
||||
branch2.branch = "Location 2"
|
||||
branch2.insert(ignore_if_duplicate=True)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=self.company,
|
||||
debit_to="Debtors - TBC",
|
||||
cost_center="Test Cost Center - TBC",
|
||||
income_account="Sales - TBC",
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.branch = "Location 1"
|
||||
si.items[0].branch = "Location 2"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
{"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]}
|
||||
)
|
||||
total_row = execute(filters)[1][-1]
|
||||
self.assertEqual(total_row["debit"], total_row["credit"])
|
||||
|
||||
def tearDown(self):
|
||||
clear_dimension_defaults("Branch")
|
||||
disable_dimension()
|
||||
|
||||
|
||||
def create_company(**args):
|
||||
args = frappe._dict(args)
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": args.company_name or "Trial Balance Company",
|
||||
"country": args.country or "India",
|
||||
"default_currency": args.currency or "INR",
|
||||
}
|
||||
)
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
return company.name
|
||||
|
||||
|
||||
def create_accounting_dimension(**args):
|
||||
args = frappe._dict(args)
|
||||
document_type = args.document_type or "Branch"
|
||||
if frappe.db.exists("Accounting Dimension", document_type):
|
||||
accounting_dimension = frappe.get_doc("Accounting Dimension", document_type)
|
||||
accounting_dimension.disabled = 0
|
||||
else:
|
||||
accounting_dimension = frappe.new_doc("Accounting Dimension")
|
||||
accounting_dimension.document_type = document_type
|
||||
accounting_dimension.insert()
|
||||
|
||||
accounting_dimension.set("dimension_defaults", [])
|
||||
accounting_dimension.append(
|
||||
"dimension_defaults",
|
||||
{
|
||||
"company": args.company or "Trial Balance Company",
|
||||
"automatically_post_balancing_accounting_entry": 1,
|
||||
"offsetting_account": args.offsetting_account or "Offsetting - TBC",
|
||||
},
|
||||
)
|
||||
accounting_dimension.save()
|
||||
|
||||
|
||||
def disable_dimension(**args):
|
||||
args = frappe._dict(args)
|
||||
document_type = args.document_type or "Branch"
|
||||
dimension = frappe.get_doc("Accounting Dimension", document_type)
|
||||
dimension.disabled = 1
|
||||
dimension.save()
|
||||
|
||||
|
||||
def clear_dimension_defaults(dimension_name):
|
||||
accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name)
|
||||
accounting_dimension.dimension_defaults = []
|
||||
accounting_dimension.save()
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
|
||||
automatically be attached with the email.</p>`);
|
||||
|
||||
dialog.set_value("subject", frm.doc.subject);
|
||||
dialog.show();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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": "<p>In your <b>Email Template</b>, you can use the following special variables:\n</p>\n<ul>\n <li>\n <code>{{ update_password_link }}</code>: A link where your supplier can set a new password to log into your portal.\n </li>\n <li>\n <code>{{ portal_link }}</code>: A link to this RFQ in your supplier portal.\n </li>\n <li>\n <code>{{ supplier_name }}</code>: The company name of your supplier.\n </li>\n <li>\n <code>{{ contact.salutation }} {{ contact.last_name }}</code>: The contact person of your supplier.\n </li><li>\n <code>{{ user_fullname }}</code>: Your full name.\n </li>\n </ul>\n<p></p>\n<p>Apart from these, you can access all values in this RFQ, like <code>{{ message_for_supplier }}</code> or <code>{{ terms }}</code>.</p>",
|
||||
"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",
|
||||
|
||||
@@ -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'<a href="{update_password_link}" class="btn btn-default btn-xs" target="_blank">{_("Set Password")}</a>',
|
||||
"portal_link": f'<a href="{rfq_link}" class="btn btn-default btn-xs" target="_blank"> {_("Submit your Quotation")} </a>',
|
||||
"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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<h4>{{_("Request for Quotation")}}</h4>
|
||||
<p>{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},</p>
|
||||
<p>{{ message }}</p>
|
||||
<p>{{_("The Request for Quotation can be accessed by clicking on the following button")}}:</p>
|
||||
<br>
|
||||
<a
|
||||
href="{{ rfq_link }}"
|
||||
class="btn btn-default btn-sm"
|
||||
target="_blank">
|
||||
{{ _("Submit your Quotation") }}
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
{% if update_password_link %}
|
||||
<br>
|
||||
<p>{{_("Please click on the following button to set your new password")}}:</p>
|
||||
<a
|
||||
href="{{ update_password_link }}"
|
||||
class="btn btn-default btn-xs"
|
||||
target="_blank">
|
||||
{{_("Set Password") }}
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
{% endif %}
|
||||
<p>
|
||||
{{_("Regards")}},<br>
|
||||
{{ user_fullname }}
|
||||
</p>
|
||||
Reference in New Issue
Block a user