Merge pull request #36763 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-08-23 08:28:36 +05:30
committed by GitHub
51 changed files with 1533 additions and 379 deletions

View File

@@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', {
}; };
}); });
frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) {
let d = locals[cdt][cdn];
return {
filters: {
company: d.company,
root_type: ["in", ["Asset", "Liability"]],
is_group: 0
}
}
});
if (!frm.is_new()) { if (!frm.is_new()) {
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () { frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
frappe.set_route("List", frm.doc.document_type); frappe.set_route("List", frm.doc.document_type);

View File

@@ -39,6 +39,8 @@ class AccountingDimension(Document):
if not self.is_new(): if not self.is_new():
self.validate_document_type_change() self.validate_document_type_change()
self.validate_dimension_defaults()
def validate_document_type_change(self): def validate_document_type_change(self):
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type") doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
if doctype_before_save != self.document_type: if doctype_before_save != self.document_type:
@@ -46,6 +48,14 @@ class AccountingDimension(Document):
message += _("Please create a new Accounting Dimension if required.") message += _("Please create a new Accounting Dimension if required.")
frappe.throw(message) frappe.throw(message)
def validate_dimension_defaults(self):
companies = []
for default in self.get("dimension_defaults"):
if default.company not in companies:
companies.append(default.company)
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def after_insert(self): def after_insert(self):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)

View File

@@ -8,7 +8,10 @@
"reference_document", "reference_document",
"default_dimension", "default_dimension",
"mandatory_for_bs", "mandatory_for_bs",
"mandatory_for_pl" "mandatory_for_pl",
"column_break_lqns",
"automatically_post_balancing_accounting_entry",
"offsetting_account"
], ],
"fields": [ "fields": [
{ {
@@ -50,6 +53,23 @@
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1, "in_list_view": 1,
"label": "Mandatory For Profit and Loss Account" "label": "Mandatory For Profit and Loss Account"
},
{
"default": "0",
"fieldname": "automatically_post_balancing_accounting_entry",
"fieldtype": "Check",
"label": "Automatically post balancing accounting entry"
},
{
"fieldname": "offsetting_account",
"fieldtype": "Link",
"label": "Offsetting Account",
"mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry",
"options": "Account"
},
{
"fieldname": "column_break_lqns",
"fieldtype": "Column Break"
} }
], ],
"istable": 1, "istable": 1,

View File

@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", { frappe.ui.form.on("Journal Entry", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("bank_account", "account", "account"); frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement']; frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger'];
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@@ -96,6 +96,8 @@ class JournalEntry(AccountsController):
"Payment Ledger Entry", "Payment Ledger Entry",
"Repost Payment Ledger", "Repost Payment Ledger",
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
) )
self.make_gl_entries(1) self.make_gl_entries(1)
self.update_advance_paid() self.update_advance_paid()

View File

@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"]; frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);

View File

@@ -105,6 +105,8 @@ class PaymentEntry(AccountsController):
"Payment Ledger Entry", "Payment Ledger Entry",
"Repost Payment Ledger", "Repost Payment Ledger",
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
) )
super(PaymentEntry, self).on_cancel() super(PaymentEntry, self).on_cancel()
self.make_gl_entries(cancel=1) self.make_gl_entries(cancel=1)
@@ -185,84 +187,87 @@ class PaymentEntry(AccountsController):
return False return False
def validate_allocated_amount_with_latest_data(self): def validate_allocated_amount_with_latest_data(self):
latest_references = get_outstanding_reference_documents( if self.references:
{ uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
"posting_date": self.posting_date, vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
"company": self.company, latest_references = get_outstanding_reference_documents(
"party_type": self.party_type, {
"payment_type": self.payment_type, "posting_date": self.posting_date,
"party": self.party, "company": self.company,
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, "party_type": self.party_type,
"get_outstanding_invoices": True, "payment_type": self.payment_type,
"get_orders_to_be_billed": True, "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) # Group latest_references by (voucher_type, voucher_no)
latest_lookup = {} latest_lookup = {}
for d in latest_references: for d in latest_references:
d = frappe._dict(d) d = frappe._dict(d)
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = 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): for idx, d in enumerate(self.get("references"), start=1):
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
# If term based allocation is enabled, throw # If term based allocation is enabled, throw
if ( if (
d.payment_term is None or d.payment_term == "" d.payment_term is None or d.payment_term == ""
) and self.term_based_allocation_enabled_for_reference( ) and self.term_based_allocation_enabled_for_reference(
d.reference_doctype, d.reference_name d.reference_doctype, d.reference_name
): ):
frappe.throw( frappe.throw(
_( _(
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" "{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)) ).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 (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
frappe.throw(fail_message.format(d.idx)) latest = latest.get(d.payment_term) or latest.get(None)
# Check for negative outstanding invoices as well # The reference has already been fully paid
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): if not latest:
frappe.throw(fail_message.format(d.idx)) 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): def delink_advance_entry_references(self):
for reference in self.references: for reference in self.references:
@@ -1463,6 +1468,7 @@ def get_outstanding_reference_documents(args):
min_outstanding=args.get("outstanding_amt_greater_than"), min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"), max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter, accounting_dimensions=accounting_dimensions_filter,
vouchers=args.get("vouchers") or None,
) )
outstanding_invoices = split_invoices_based_on_payment_terms( outstanding_invoices = split_invoices_based_on_payment_terms(

View File

@@ -385,59 +385,6 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries() 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): def get_payment_details(self, row, dr_or_cr):
return frappe._dict( return frappe._dict(
{ {
@@ -603,16 +550,6 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company): 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: for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"

View File

@@ -130,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
args: { "pos_profile": frm.pos_profile }, args: { "pos_profile": frm.pos_profile },
callback: ({ message: profile }) => { callback: ({ message: profile }) => {
this.update_customer_groups_settings(profile?.customer_groups); this.update_customer_groups_settings(profile?.customer_groups);
this.frm.set_value("company", profile?.company);
}, },
}); });
} }

View File

@@ -54,6 +54,7 @@ class POSInvoice(SalesInvoice):
self.validate_pos() self.validate_pos()
self.validate_payment_amount() self.validate_payment_amount()
self.validate_loyalty_transaction() self.validate_loyalty_transaction()
self.validate_company_with_pos_company()
if self.coupon_code: if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_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: 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)) 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): def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and ( if self.redeem_loyalty_points and (
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
@@ -448,6 +457,7 @@ class POSInvoice(SalesInvoice):
profile = {} profile = {}
if self.pos_profile: if self.pos_profile:
profile = frappe.get_doc("POS Profile", 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: if not self.get("payments") and not for_validate:
update_multi_mode_option(self, profile) update_multi_mode_option(self, profile)

View File

@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload(); super.onload();
// Ignore linked advances // Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"]; this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal) { if(!this.frm.doc.__islocal) {
// show credit_to in print format // show credit_to in print format

View File

@@ -1422,6 +1422,8 @@ class PurchaseInvoice(BuyingController):
"Repost Item Valuation", "Repost Item Valuation",
"Repost Payment Ledger", "Repost Payment Ledger",
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Payment Ledger Entry", "Payment Ledger Entry",
"Tax Withheld Vouchers", "Tax Withheld Vouchers",
) )

View File

@@ -1771,23 +1771,101 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
0, 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): create_account(
gl_entries = frappe.db.sql( account_name="Offsetting",
"""select account, debit, credit, posting_date company="_Test Company",
from `tabGL Entry` parent_account="Temporary Accounts - _TC",
where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s )
order by posting_date asc, account asc""",
(voucher_no, posting_date), create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC")
as_dict=1,
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): for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][0], gle.account)
doc.assertEqual(expected_gle[i][1], gle.debit) doc.assertEqual(expected_gle[i][1], gle.debit)
doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
if additional_columns:
j = 4
for col in additional_columns:
doc.assertEqual(expected_gle[i][j], gle[col])
j += 1
def create_tax_witholding_category(category_name, company, account): def create_tax_witholding_category(category_name, company, account):
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year

View File

@@ -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>

View File

@@ -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);
}
}
});
});
}
});

View File

@@ -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": []
}

View File

@@ -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()

View File

@@ -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}))

View File

@@ -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": []
}

View File

@@ -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

View File

@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload(); super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"]; 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format

View File

@@ -714,6 +714,7 @@
"fieldtype": "Table", "fieldtype": "Table",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Items",
"oldfieldname": "entries", "oldfieldname": "entries",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Sales Invoice Item", "options": "Sales Invoice Item",

View File

@@ -399,6 +399,8 @@ class SalesInvoice(SellingController):
"Repost Item Valuation", "Repost Item Valuation",
"Repost Payment Ledger", "Repost Payment Ledger",
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Payment Ledger Entry", "Payment Ledger Entry",
) )

View File

@@ -28,6 +28,7 @@ def make_gl_entries(
): ):
if gl_map: if gl_map:
if not cancel: if not cancel:
make_acc_dimensions_offsetting_entry(gl_map)
validate_accounting_period(gl_map) validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map) validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries) gl_map = process_gl_map(gl_map, merge_entries)
@@ -51,6 +52,63 @@ def make_gl_entries(
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
def make_acc_dimensions_offsetting_entry(gl_map):
accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(
gl_map, gl_map[0].company
)
no_of_dimensions = len(accounting_dimensions_to_offset)
if no_of_dimensions == 0:
return
offsetting_entries = []
for gle in gl_map:
for dimension in accounting_dimensions_to_offset:
offsetting_entry = gle.copy()
debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0
credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0
offsetting_entry.update(
{
"account": dimension.offsetting_account,
"debit": debit,
"credit": credit,
"debit_in_account_currency": debit,
"credit_in_account_currency": credit,
"remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name),
"against_voucher": None,
}
)
offsetting_entry["against_voucher_type"] = None
offsetting_entries.append(offsetting_entry)
gl_map += offsetting_entries
def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
acc_dimension = frappe.qb.DocType("Accounting Dimension")
dimension_detail = frappe.qb.DocType("Accounting Dimension Detail")
acc_dimensions = (
frappe.qb.from_(acc_dimension)
.inner_join(dimension_detail)
.on(acc_dimension.name == dimension_detail.parent)
.select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account)
.where(
(acc_dimension.disabled == 0)
& (dimension_detail.company == company)
& (dimension_detail.automatically_post_balancing_accounting_entry == 1)
)
).run(as_dict=True)
accounting_dimensions_to_offset = []
for acc_dimension in acc_dimensions:
values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
if len(values) > 1:
accounting_dimensions_to_offset.append(acc_dimension)
return accounting_dimensions_to_offset
def validate_disabled_accounts(gl_map): def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account] accounts = [d.account for d in gl_map if d.account]

View File

@@ -14,7 +14,7 @@ from frappe.contacts.doctype.address.address import (
from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Date, Sum from frappe.query_builder.functions import Abs, Date, Sum
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
add_months, add_months,
@@ -884,35 +884,34 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
def get_partywise_advanced_payment_amount( def get_partywise_advanced_payment_amount(
party_type, posting_date=None, future_payment=0, company=None, party=None, 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 = ( query = (
frappe.qb.from_(gle) frappe.qb.from_(ple)
.select(gle.party) .select(ple.party, Abs(Sum(ple.amount).as_("amount")))
.where( .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 posting_date:
if future_payment: 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: else:
query = query.where(gle.posting_date <= posting_date) query = query.where(ple.posting_date <= posting_date)
if company: if company:
query = query.where(gle.company == company) query = query.where(ple.company == company)
if party: 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: if data:
return frappe._dict(data) return frappe._dict(data)

View File

@@ -214,8 +214,8 @@ class ReceivablePayableReport(object):
for party_type in self.party_type: for party_type in self.party_type:
if self.filters.get(scrub(party_type)): if self.filters.get(scrub(party_type)):
amount = ple.amount_in_account_currency amount = ple.amount_in_account_currency
else: else:
amount = ple.amount amount = ple.amount
amount_in_account_currency = ple.amount_in_account_currency amount_in_account_currency = ple.amount_in_account_currency
# update voucher # update voucher
@@ -1090,7 +1090,10 @@ class ReceivablePayableReport(object):
.where( .where(
(je.company == self.filters.company) (je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date)) & (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() .run()
) )

View File

@@ -50,13 +50,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.filters.show_future_payments, self.filters.show_future_payments,
self.filters.company, self.filters.company,
party=party, party=party,
account_type=self.account_type,
) )
or {} or {}
) )
if self.filters.show_gl_balance: 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(): for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0: if party_dict.outstanding == 0:
@@ -233,12 +232,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(label="Total Amount Due", fieldname="total_due") 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( return frappe._dict(
frappe.db.get_all( frappe.db.get_all(
"GL Entry", "GL Entry",
fields=["party", "sum(debit - credit)"], 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", group_by="party",
as_list=1, as_list=1,
) )

View File

@@ -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)

View File

@@ -749,13 +749,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
if from_date: if from_date:
additional_conditions.append(gle.posting_date >= from_date) additional_conditions.append(gle.posting_date >= from_date)
finance_book = filters.get("finance_book") finance_books = []
company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book") finance_books.append("")
if filter_fb := filters.get("finance_book"):
finance_books.append(filter_fb)
if filters.get("include_default_book_entries"): 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: 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 return additional_conditions

View 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()

View File

@@ -1,10 +1,11 @@
import frappe import frappe
from frappe import qb
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
class AccountsTestMixin: class AccountsTestMixin:
def create_customer(self, customer_name, currency=None): def create_customer(self, customer_name="_Test Customer", currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer") customer = frappe.new_doc("Customer")
customer.customer_name = customer_name customer.customer_name = customer_name
@@ -17,7 +18,7 @@ class AccountsTestMixin:
else: else:
self.customer = customer_name self.customer = customer_name
def create_supplier(self, supplier_name, currency=None): def create_supplier(self, supplier_name="_Test Supplier", currency=None):
if not frappe.db.exists("Supplier", supplier_name): if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier") supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name supplier.supplier_name = supplier_name
@@ -31,7 +32,7 @@ class AccountsTestMixin:
else: else:
self.supplier = supplier_name self.supplier = supplier_name
def create_item(self, item_name, is_stock=0, warehouse=None, company=None): def create_item(self, item_name="_Test Item", is_stock=0, warehouse=None, company=None):
item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company) item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
self.item = item.name self.item = item.name
@@ -62,19 +63,56 @@ class AccountsTestMixin:
self.debit_usd = "Debtors USD - " + abbr self.debit_usd = "Debtors USD - " + abbr
self.cash = "Cash - " + abbr self.cash = "Cash - " + abbr
self.creditors = "Creditors - " + abbr self.creditors = "Creditors - " + abbr
self.retained_earnings = "Retained Earnings - " + abbr
# create bank account # Deferred revenue, expense and bank accounts
bank_account = "HDFC - " + abbr other_accounts = [
if frappe.db.exists("Account", bank_account): frappe._dict(
self.bank = bank_account
else:
bank_acc = frappe.get_doc(
{ {
"doctype": "Account", "attribute_name": "deferred_revenue",
"account_name": "Deferred Revenue",
"parent_account": "Current Liabilities - " + abbr,
}
),
frappe._dict(
{
"attribute_name": "deferred_expense",
"account_name": "Deferred Expense",
"parent_account": "Current Assets - " + abbr,
}
),
frappe._dict(
{
"attribute_name": "bank",
"account_name": "HDFC", "account_name": "HDFC",
"parent_account": "Bank Accounts - " + abbr, "parent_account": "Bank Accounts - " + abbr,
"company": self.company,
} }
) ),
bank_acc.save() ]
self.bank = bank_acc.name for acc in other_accounts:
acc_name = acc.account_name + " - " + abbr
if frappe.db.exists("Account", acc_name):
setattr(self, acc.attribute_name, acc_name)
else:
new_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": acc.account_name,
"parent_account": acc.parent_account,
"company": self.company,
}
)
new_acc.save()
setattr(self, acc.attribute_name, new_acc.name)
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()

View File

@@ -884,6 +884,7 @@ def get_outstanding_invoices(
min_outstanding=None, min_outstanding=None,
max_outstanding=None, max_outstanding=None,
accounting_dimensions=None, accounting_dimensions=None,
vouchers=None,
): ):
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
@@ -909,6 +910,7 @@ def get_outstanding_invoices(
ple_query = QueryPaymentLedger() ple_query = QueryPaymentLedger()
invoice_list = ple_query.get_voucher_outstandings( invoice_list = ple_query.get_voucher_outstandings(
vouchers=vouchers,
common_filter=common_filter, common_filter=common_filter,
posting_date=posting_date, posting_date=posting_date,
min_outstanding=min_outstanding, min_outstanding=min_outstanding,

View File

@@ -81,18 +81,27 @@ class Asset(AccountsController):
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) _("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: if self.calculate_depreciation:
self.value_after_depreciation = 0 self.value_after_depreciation = 0
self.set_depreciation_rate() self.set_depreciation_rate()
if self.should_prepare_depreciation_schedule(): if self.should_prepare_depreciation_schedule():
self.make_depreciation_schedule(date_of_disposal) self.make_depreciation_schedule(date_of_disposal, value_after_depreciation)
self.set_accumulated_depreciation(date_of_disposal, date_of_return) self.set_accumulated_depreciation(date_of_disposal, date_of_return, ignore_booked_entry)
else: else:
self.finance_books = [] self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( if value_after_depreciation:
self.opening_accumulated_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): def should_prepare_depreciation_schedule(self):
if not self.get("schedules"): 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") 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"): if not self.get("schedules"):
self.schedules = [] self.schedules = []
@@ -295,24 +304,30 @@ class Asset(AccountsController):
start = self.clear_depreciation_schedule() start = self.clear_depreciation_schedule()
for finance_book in self.get("finance_books"): 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): if len(self.get("finance_books")) > 1 and any(start):
self.sort_depreciation_schedule() 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) 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 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 self.number_of_depreciations_booked
) )
has_pro_rata = self.check_is_pro_rata(finance_book) has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata: if has_pro_rata:
number_of_pending_depreciations += 1 final_number_of_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False has_wdv_or_dd_non_yearly_pro_rata = False
if ( if (
@@ -328,7 +343,9 @@ class Asset(AccountsController):
depreciation_amount = 0 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 depreciation is already completed (for double declining balance)
if skip_row: if skip_row:
continue continue
@@ -345,10 +362,11 @@ class Asset(AccountsController):
n, n,
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
number_of_pending_depreciations,
) )
if not has_pro_rata or ( 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( schedule_date = add_months(
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
@@ -416,7 +434,7 @@ class Asset(AccountsController):
) )
# For last row # 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: 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 # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months( 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 # Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and ( 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 and value_after_depreciation != finance_book.expected_value_after_useful_life
) )
or 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 if s.finance_book_id == d.finance_book_id
and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual") 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( value_after_depreciation = flt(
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation
) )
@@ -1296,11 +1317,14 @@ def get_depreciation_amount(
schedule_idx=0, schedule_idx=0,
prev_depreciation_amount=0, prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False, has_wdv_or_dd_non_yearly_pro_rata=False,
number_of_pending_depreciations=0,
): ):
frappe.flags.company = asset.company frappe.flags.company = asset.company
if fb_row.depreciation_method in ("Straight Line", "Manual"): 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: else:
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row 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 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 the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life: if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_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( return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations 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 # if the Depreciation Schedule is being prepared for the first time
else: else:
if row.daily_depreciation: if row.daily_depreciation:

View File

@@ -5,15 +5,12 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, date_diff, flt, formatdate, getdate from frappe.utils import flt, formatdate, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, get_checks_for_pl_and_bs_accounts,
) )
from erpnext.assets.doctype.asset.asset import ( from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
get_asset_value_after_depreciation,
get_depreciation_amount,
)
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
@@ -25,10 +22,10 @@ class AssetValueAdjustment(Document):
def on_submit(self): def on_submit(self):
self.make_depreciation_entry() self.make_depreciation_entry()
self.reschedule_depreciations(self.new_asset_value) self.update_asset(self.new_asset_value)
def on_cancel(self): def on_cancel(self):
self.reschedule_depreciations(self.current_asset_value) self.update_asset(self.current_asset_value)
def validate_date(self): def validate_date(self):
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date") asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
@@ -71,12 +68,16 @@ class AssetValueAdjustment(Document):
"account": accumulated_depreciation_account, "account": accumulated_depreciation_account,
"credit_in_account_currency": self.difference_amount, "credit_in_account_currency": self.difference_amount,
"cost_center": depreciation_cost_center or self.cost_center, "cost_center": depreciation_cost_center or self.cost_center,
"reference_type": "Asset",
"reference_name": asset.name,
} }
debit_entry = { debit_entry = {
"account": depreciation_expense_account, "account": depreciation_expense_account,
"debit_in_account_currency": self.difference_amount, "debit_in_account_currency": self.difference_amount,
"cost_center": depreciation_cost_center or self.cost_center, "cost_center": depreciation_cost_center or self.cost_center,
"reference_type": "Asset",
"reference_name": asset.name,
} }
accounting_dimensions = get_checks_for_pl_and_bs_accounts() accounting_dimensions = get_checks_for_pl_and_bs_accounts()
@@ -106,44 +107,11 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name) 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) asset = frappe.get_doc("Asset", self.asset)
country = frappe.get_value("Company", self.company, "country")
for d in asset.finance_books: asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
d.value_after_depreciation = asset_value
if d.depreciation_method in ("Straight Line", "Manual"): asset.prepare_depreciation_data(value_after_depreciation=asset_value, ignore_booked_entry=True)
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx) asset.flags.ignore_validate_update_after_submit = True
total_days = date_diff(end_date, self.date) asset.save()
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()

View File

@@ -4,9 +4,10 @@
import unittest import unittest
import frappe 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.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.assets.doctype.asset.test_asset import create_asset_data
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt 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): def test_asset_depreciation_value_adjustment(self):
pr = make_purchase_receipt( 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_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name) asset_doc = frappe.get_doc("Asset", asset_name)
asset_doc.calculate_depreciation = 1 asset_doc.calculate_depreciation = 1
month_end_date = get_last_day(nowdate()) asset_doc.available_for_use_date = "2023-01-15"
purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) asset_doc.purchase_date = "2023-01-15"
asset_doc.available_for_use_date = purchase_date
asset_doc.purchase_date = purchase_date
asset_doc.calculate_depreciation = 1 asset_doc.calculate_depreciation = 1
asset_doc.append( asset_doc.append(
"finance_books", "finance_books",
{ {
"expected_value_after_useful_life": 200, "expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 12,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 1,
"depreciation_start_date": month_end_date, "depreciation_start_date": "2023-01-31",
}, },
) )
asset_doc.submit() asset_doc.submit()
post_depreciation_entries(getdate("2023-08-21"))
current_value = get_asset_value_after_depreciation(asset_doc.name) current_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment( 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() adj_doc.submit()
asset_doc.reload()
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 50000.0), ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
("_Test Depreciations - _TC", 50000.0, 0.0), ("_Test Depreciations - _TC", 4625.29, 0.0),
) )
gle = frappe.db.sql( gle = frappe.db.sql(
@@ -91,6 +96,29 @@ class TestAssetValueAdjustment(unittest.TestCase):
self.assertSequenceEqual(gle, expected_gle) 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): def make_asset_value_adjustment(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -245,19 +245,21 @@ frappe.ui.form.on("Request for Quotation",{
] ]
}); });
dialog.fields_dict['supplier'].df.onchange = () => { dialog.fields_dict["supplier"].df.onchange = () => {
var supplier = dialog.get_value('supplier'); frm.call("get_supplier_email_preview", {
frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => { supplier: dialog.get_value("supplier"),
}).then(({ message }) => {
dialog.fields_dict.email_preview.$wrapper.empty(); 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 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>`); automatically be attached with the email.</p>`);
dialog.set_value("subject", frm.doc.subject);
dialog.show(); dialog.show();
} }
}) })

View File

@@ -20,11 +20,10 @@
"items_section", "items_section",
"items", "items",
"supplier_response_section", "supplier_response_section",
"salutation",
"subject",
"col_break_email_1",
"email_template", "email_template",
"preview", "preview",
"col_break_email_1",
"html_llwp",
"send_attached_files", "send_attached_files",
"sec_break_email_2", "sec_break_email_2",
"message_for_supplier", "message_for_supplier",
@@ -237,23 +236,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 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", "fieldname": "col_break_email_1",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -287,6 +269,14 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "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", "default": "1",
"description": "If enabled, all files attached to this document will be attached to each email", "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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-07-27 16:41:48.468873", "modified": "2023-08-08 16:30:10.870429",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",

View File

@@ -182,35 +182,28 @@ class RequestforQuotation(BuyingController):
if full_name == "Guest": if full_name == "Guest":
full_name = "Administrator" 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 = 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"): if data.get("contact"):
contact_name = frappe.db.get_value( contact = frappe.get_doc("Contact", data.get("contact"))
"Contact", data.get("contact"), ["first_name", "middle_name", "last_name"] doc_args["contact"] = contact.as_dict()
)
supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
args = { doc_args.update(
"update_password_link": update_password_link, {
"message": frappe.render_template(self.message_for_supplier, doc_args), "supplier": data.get("supplier"),
"rfq_link": rfq_link, "supplier_name": data.get("supplier_name"),
"user_fullname": full_name, "update_password_link": f'<a href="{update_password_link}" class="btn btn-default btn-xs" target="_blank">{_("Set Password")}</a>',
"supplier_name": supplier_name or data.get("supplier_name"), "portal_link": f'<a href="{rfq_link}" class="btn btn-default btn-xs" target="_blank"> {_("Submit your Quotation")} </a>',
"supplier_salutation": self.salutation or "Dear Mx.", "user_fullname": full_name,
} }
)
subject = self.subject or _("Request for Quotation") email_template = frappe.get_doc("Email Template", self.email_template)
template = "templates/emails/request_for_quotation.html" 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 sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
message = frappe.get_template(template).render(args)
if preview: if preview:
return message return {"message": message, "subject": subject}
attachments = None attachments = None
if self.send_attached_files: if self.send_attached_files:

View File

@@ -154,31 +154,35 @@ def get_data(filters):
procurement_record = [] procurement_record = []
if procurement_record_against_mr: if procurement_record_against_mr:
procurement_record += procurement_record_against_mr procurement_record += procurement_record_against_mr
for po in purchase_order_entry: for po in purchase_order_entry:
# fetch material records linked to the purchase order item # fetch material records linked to the purchase order item
mr_record = mr_records.get(po.material_request_item, [{}])[0] material_requests = mr_records.get(po.material_request_item, [{}])
procurement_detail = {
"material_request_date": mr_record.get("transaction_date"), for mr_record in material_requests:
"cost_center": po.cost_center, procurement_detail = {
"project": po.project, "material_request_date": mr_record.get("transaction_date"),
"requesting_site": po.warehouse, "cost_center": po.cost_center,
"requestor": po.owner, "project": po.project,
"material_request_no": po.material_request, "requesting_site": po.warehouse,
"item_code": po.item_code, "requestor": po.owner,
"quantity": flt(po.qty), "material_request_no": po.material_request,
"unit_of_measurement": po.stock_uom, "item_code": po.item_code,
"status": po.status, "quantity": flt(po.qty),
"purchase_order_date": po.transaction_date, "unit_of_measurement": po.stock_uom,
"purchase_order": po.parent, "status": po.status,
"supplier": po.supplier, "purchase_order_date": po.transaction_date,
"estimated_cost": flt(mr_record.get("amount")), "purchase_order": po.parent,
"actual_cost": flt(pi_records.get(po.name)), "supplier": po.supplier,
"purchase_order_amt": flt(po.amount), "estimated_cost": flt(mr_record.get("amount")),
"purchase_order_amt_in_company_currency": flt(po.base_amount), "actual_cost": flt(pi_records.get(po.name)),
"expected_delivery_date": po.schedule_date, "purchase_order_amt": flt(po.amount),
"actual_delivery_date": pr_records.get(po.name), "purchase_order_amt_in_company_currency": flt(po.base_amount),
} "expected_delivery_date": po.schedule_date,
procurement_record.append(procurement_detail) "actual_delivery_date": pr_records.get(po.name),
}
procurement_record.append(procurement_detail)
return procurement_record return procurement_record
@@ -301,7 +305,7 @@ def get_po_entries(filters):
& (parent.name == child.parent) & (parent.name == child.parent)
& (parent.status.notin(("Closed", "Completed", "Cancelled"))) & (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) query = apply_filters_on_query(filters, parent, child, query)

View File

@@ -715,7 +715,9 @@ class AccountsController(TransactionBase):
def validate_enabled_taxes_and_charges(self): def validate_enabled_taxes_and_charges(self):
taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges") 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( frappe.throw(
_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges) _("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges)
) )

View File

@@ -347,7 +347,7 @@ class ProductionPlan(Document):
if not data.pending_qty: if not data.pending_qty:
continue continue
item_details = get_item_details(data.item_code) item_details = get_item_details(data.item_code, throw=False)
if self.combine_items: if self.combine_items:
if item_details.bom_no in refs: if item_details.bom_no in refs:
refs[item_details.bom_no]["so_details"].append( refs[item_details.bom_no]["so_details"].append(
@@ -795,6 +795,9 @@ class ProductionPlan(Document):
if not row.item_code: if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
if not row.bom_no:
frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx))
bom_data = [] bom_data = []
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None warehouse = row.warehouse if self.skip_available_sub_assembly_item else None

View File

@@ -1075,7 +1075,7 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @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( res = frappe.db.sql(
""" """
select stock_uom, description, item_name, allow_alternative_item, 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 not res["bom_no"]:
if project: if project:
res = get_item_details(item) res = get_item_details(item, throw=throw)
frappe.msgprint( frappe.msgprint(
_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1 _("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1
) )
else: 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_data = frappe.db.get_value(
"BOM", "BOM",

View File

@@ -98,9 +98,11 @@ def get_timesheets(filters):
record_filters = [ record_filters = [
["start_date", "<=", filters.to_date], ["start_date", "<=", filters.to_date],
["end_date", ">=", filters.from_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: if "employee" in filters:
record_filters.append(["employee", "=", filters.employee]) record_filters.append(["employee", "=", filters.employee])

View File

@@ -25,5 +25,10 @@ frappe.query_reports["Employee Billing Summary"] = {
default: frappe.datetime.add_days(frappe.datetime.month_start(), -1), default: frappe.datetime.add_days(frappe.datetime.month_start(), -1),
reqd: 1 reqd: 1
}, },
{
fieldname:"include_draft_timesheets",
label: __("Include Timesheets in Draft Status"),
fieldtype: "Check",
},
] ]
} }

View File

@@ -25,5 +25,10 @@ frappe.query_reports["Project Billing Summary"] = {
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
reqd: 1 reqd: 1
}, },
{
fieldname:"include_draft_timesheets",
label: __("Include Timesheets in Draft Status"),
fieldtype: "Check",
},
] ]
} }

View File

@@ -117,6 +117,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
name: __("Document Name"), name: __("Document Name"),
editable: false, editable: false,
width: 1, width: 1,
format: (value, row) => {
return frappe.form.formatters.Link(value, {options: row[2].content});
},
}, },
{ {
name: __("Reference Date"), name: __("Reference Date"),

View File

@@ -57,7 +57,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
from_date: me.frm.doc.posting_date, from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company, 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"); frappe.set_route("query-report", "Stock Ledger");
}, __("View")); }, __("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'), to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company, company: me.frm.doc.company,
group_by: "Group by Voucher (Consolidated)", 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"); frappe.set_route("query-report", "General Ledger");
}, __("View")); }, __("View"));

View File

@@ -660,7 +660,10 @@ def make_stock_entry(source_name, target_doc=None):
"job_card_item": "job_card_item", "job_card_item": "job_card_item",
}, },
"postprocess": update_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, target_doc,

View File

@@ -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>