diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index ea427aa7d803..07b4318a9dc8 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -26,6 +26,7 @@
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
+ "book_tax_discount_loss",
"period_closing_settings_section",
"acc_frozen_upto",
"frozen_accounts_modifier",
@@ -284,6 +285,13 @@
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account"
+ },
+ {
+ "default": "0",
+ "description": "Split Early Payment Discount Loss into Income and Tax Loss",
+ "fieldname": "book_tax_discount_loss",
+ "fieldtype": "Check",
+ "label": "Book Tax Loss on Early Payment Discount"
}
],
"icon": "icon-cog",
@@ -291,7 +299,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-07-11 13:37:50.605141",
+ "modified": "2023-03-28 09:50:20.375233",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 6be0920d2a8e..2e5674874ccc 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -256,8 +256,6 @@ frappe.ui.form.on('Payment Entry', {
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
party_account_currency, "references");
- frm.set_currency_labels(["amount"], company_currency, "deductions");
-
cur_frm.set_df_property("source_exchange_rate", "description",
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index f0d7d57fc648..44b8dbe5326f 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -440,7 +440,7 @@ def update_payment_schedule(self, cancel=0):
for ref in self.get("references"):
if ref.payment_term and ref.reference_name:
- key = (ref.payment_term, ref.reference_name)
+ key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += ref.allocated_amount
@@ -448,20 +448,37 @@ def update_payment_schedule(self, cancel=0):
payment_schedule = frappe.get_all(
"Payment Schedule",
filters={"parent": ref.reference_name},
- fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
+ fields=[
+ "paid_amount",
+ "payment_amount",
+ "payment_term",
+ "discount",
+ "outstanding",
+ "discount_type",
+ ],
)
for term in payment_schedule:
- invoice_key = (term.payment_term, ref.reference_name)
+ invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
invoice_paid_amount_map.setdefault(invoice_key, {})
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
- invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
- term.discount / 100
- )
+ if not (term.discount_type and term.discount):
+ continue
+
+ if term.discount_type == "Percentage":
+ invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
+ term.discount / 100
+ )
+ else:
+ invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
for idx, (key, allocated_amount) in enumerate(iteritems(invoice_payment_amount_map), 1):
if not invoice_paid_amount_map.get(key):
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
+ allocated_amount = self.get_allocated_amount_in_transaction_currency(
+ allocated_amount, key[2], key[1]
+ )
+
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
@@ -496,6 +513,33 @@ def update_payment_schedule(self, cancel=0):
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
)
+ def get_allocated_amount_in_transaction_currency(
+ self, allocated_amount, reference_doctype, reference_docname
+ ):
+ """
+ Payment Entry could be in base currency while reference's payment schedule
+ is always in transaction currency.
+ E.g.
+ * SI with base=INR and currency=USD
+ * SI with payment schedule in USD
+ * PE in INR (accounting done in base currency)
+ """
+ ref_currency, ref_exchange_rate = frappe.db.get_value(
+ reference_doctype, reference_docname, ["currency", "conversion_rate"]
+ )
+ is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
+ # PE in different currency
+ reference_is_multi_currency = self.paid_from_account_currency != ref_currency
+
+ if not (is_single_currency and reference_is_multi_currency):
+ return allocated_amount
+
+ allocated_amount = flt(
+ allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
+ )
+
+ return allocated_amount
+
def set_status(self):
if self.docstatus == 2:
self.status = "Cancelled"
@@ -1801,7 +1845,14 @@ def get_bill_no_and_update_amounts(
@frappe.whitelist()
-def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None):
+def get_payment_entry(
+ dt,
+ dn,
+ party_amount=None,
+ bank_account=None,
+ bank_amount=None,
+ reference_date=None,
+):
reference_doc = None
doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
@@ -1822,8 +1873,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
)
- paid_amount, received_amount, discount_amount = apply_early_payment_discount(
- paid_amount, received_amount, doc
+ reference_date = getdate(reference_date)
+ paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
+ paid_amount, received_amount, doc, party_account_currency, reference_date
)
pe = frappe.new_doc("Payment Entry")
@@ -1831,6 +1883,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.company = doc.company
pe.cost_center = doc.get("cost_center")
pe.posting_date = nowdate()
+ pe.reference_date = reference_date
pe.mode_of_payment = doc.get("mode_of_payment")
pe.party_type = party_type
pe.party = doc.get(scrub(party_type))
@@ -1871,7 +1924,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
):
for reference in get_reference_as_per_payment_terms(
- doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+ doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
):
pe.append("references", reference)
else:
@@ -1922,16 +1975,17 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
reference_doc = doc
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
+
if discount_amount:
- pe.set_gain_or_loss(
- account_details={
- "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
- "cost_center": pe.cost_center
- or frappe.get_cached_value("Company", pe.company, "cost_center"),
- "amount": discount_amount * (-1 if payment_type == "Pay" else 1),
- }
+ base_total_discount_loss = 0
+ if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
+ base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
+
+ set_pending_discount_loss(
+ pe, doc, discount_amount, base_total_discount_loss, party_account_currency
)
- pe.set_difference_amount()
+
+ pe.set_difference_amount()
return pe
@@ -2067,20 +2121,30 @@ def set_paid_amount_and_received_amount(
return paid_amount, received_amount
-def apply_early_payment_discount(paid_amount, received_amount, doc):
+def apply_early_payment_discount(
+ paid_amount, received_amount, doc, party_account_currency, reference_date
+):
total_discount = 0
+ valid_discounts = []
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
if doc.doctype in eligible_for_payments and has_payment_schedule:
+ # Non eligible documents may not have `company_currency` field
+ is_multi_currency = party_account_currency != doc.company_currency
+
for term in doc.payment_schedule:
- if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
+ if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
+
if term.discount_type == "Percentage":
- discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
+ grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
+ discount_amount = flt(grand_total) * (term.discount / 100)
else:
discount_amount = term.discount
- discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
+ # if accounting is done in the same currency, paid_amount = received_amount
+ conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
+ discount_amount_in_foreign_currency = discount_amount * conversion_rate
if doc.doctype == "Sales Invoice":
paid_amount -= discount_amount
@@ -2089,23 +2153,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
received_amount -= discount_amount
paid_amount -= discount_amount_in_foreign_currency
+ valid_discounts.append({"type": term.discount_type, "discount": term.discount})
total_discount += discount_amount
if total_discount:
- money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
+ currency = doc.get("currency") if is_multi_currency else doc.company_currency
+ money = frappe.utils.fmt_money(total_discount, currency=currency)
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
- return paid_amount, received_amount, total_discount
+ return paid_amount, received_amount, total_discount, valid_discounts
+
+
+def set_pending_discount_loss(
+ pe, doc, discount_amount, base_total_discount_loss, party_account_currency
+):
+ # If multi-currency, get base discount amount to adjust with base currency deductions/losses
+ if party_account_currency != doc.company_currency:
+ discount_amount = discount_amount * doc.get("conversion_rate", 1)
+
+ # Avoid considering miniscule losses
+ discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
+
+ # Set base discount amount (discount loss/pending rounding loss) in deductions
+ if discount_amount > 0.0:
+ positive_negative = -1 if pe.payment_type == "Pay" else 1
+
+ # If tax loss booking is enabled, pending loss will be rounding loss.
+ # Otherwise it will be the total discount loss.
+ book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
+ account_type = "round_off_account" if book_tax_loss else "default_discount_account"
+
+ pe.set_gain_or_loss(
+ account_details={
+ "account": frappe.get_cached_value("Company", pe.company, account_type),
+ "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+ "amount": discount_amount * positive_negative,
+ }
+ )
+
+
+def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
+ """Split early payment discount into Income Loss & Tax Loss."""
+ total_discount_percent = get_total_discount_percent(doc, valid_discounts)
+
+ if not total_discount_percent:
+ return 0.0
+
+ base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
+ base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
+
+ # Round off total loss rather than individual losses to reduce rounding error
+ return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
+
+
+def get_total_discount_percent(doc, valid_discounts) -> float:
+ """Get total percentage and amount discount applied as a percentage."""
+ total_discount_percent = (
+ sum(
+ discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
+ )
+ or 0.0
+ )
+
+ # Operate in percentages only as it makes the income & tax split easier
+ total_discount_amount = (
+ sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
+ or 0.0
+ )
+
+ if total_discount_amount:
+ discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
+ total_discount_percent += discount_percentage
+ return total_discount_percent
+
+ return total_discount_percent
+
+
+def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
+ """Add loss on income discount in base currency."""
+ precision = doc.precision("total")
+ base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
+
+ pe.append(
+ "deductions",
+ {
+ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
+ "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+ "amount": flt(base_loss_on_income, precision),
+ },
+ )
+
+ return base_loss_on_income # Return loss without rounding
+
+
+def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
+ """Add loss on tax discount in base currency."""
+ tax_discount_loss = {}
+ base_total_tax_loss = 0
+ precision = doc.precision("tax_amount_after_discount_amount", "taxes")
+
+ # The same account head could be used more than once
+ for tax in doc.get("taxes", []):
+ base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
+ total_discount_percentage / 100
+ )
+
+ account = tax.get("account_head")
+ if not tax_discount_loss.get(account):
+ tax_discount_loss[account] = base_tax_loss
+ else:
+ tax_discount_loss[account] += base_tax_loss
+
+ for account, loss in tax_discount_loss.items():
+ base_total_tax_loss += loss
+ if loss == 0.0:
+ continue
+
+ pe.append(
+ "deductions",
+ {
+ "account": account,
+ "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+ "amount": flt(loss, precision),
+ },
+ )
+
+ return base_total_tax_loss # Return loss without rounding
def get_reference_as_per_payment_terms(
- payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+ payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
):
references = []
+ is_multi_currency_acc = (doc.currency != doc.company_currency) and (
+ party_account_currency != doc.company_currency
+ )
+
for payment_term in payment_schedule:
payment_term_outstanding = flt(
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
)
+ if not is_multi_currency_acc:
+ # If accounting is done in company currency for multi-currency transaction
+ payment_term_outstanding = flt(
+ payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
+ )
if payment_term_outstanding:
references.append(
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 004c84c0221f..740f62a360a7 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -5,6 +5,7 @@
import frappe
from frappe import qb
+from frappe.tests.utils import change_settings
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
@@ -252,10 +253,25 @@ def test_payment_entry_against_payment_terms_with_discount(self):
},
)
si.save()
-
si.submit()
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
+ pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+
+ self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
+ self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
+ self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
+ self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
+ self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
+ self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
+
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+
+ self.assertEqual(pe.references[0].allocated_amount, 236.0)
+ self.assertEqual(pe.paid_amount, 212.4)
+ self.assertEqual(pe.deductions[0].amount, 23.6)
+
pe.submit()
si.load_from_db()
@@ -265,6 +281,190 @@ def test_payment_entry_against_payment_terms_with_discount(self):
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
+ def test_payment_entry_against_payment_terms_with_discount_amount(self):
+ si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
+
+ si.payment_terms_template = "Test Discount Amount Template"
+ create_payment_terms_template_with_discount(
+ name="30 Credit Days with Rs.50 Discount",
+ discount_type="Amount",
+ discount=50,
+ template_name="Test Discount Amount Template",
+ )
+ frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18,
+ },
+ )
+ si.save()
+ si.submit()
+
+ # Set reference date past discount cut off date
+ pe_1 = get_payment_entry(
+ "Sales Invoice",
+ si.name,
+ bank_account="_Test Cash - _TC",
+ reference_date=frappe.utils.add_days(si.posting_date, 2),
+ )
+ self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
+
+ # Test if tax loss is booked on enabling configuration
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
+ pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+ self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
+ self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
+ self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
+
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+ self.assertEqual(pe.references[0].allocated_amount, 236.0)
+ self.assertEqual(pe.paid_amount, 186)
+ self.assertEqual(pe.deductions[0].amount, 50.0)
+
+ pe.submit()
+ si.load_from_db()
+
+ self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
+ self.assertEqual(si.payment_schedule[0].paid_amount, 186)
+ self.assertEqual(si.payment_schedule[0].outstanding, 0)
+ self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
+
+ @change_settings(
+ "Accounts Settings",
+ {
+ "allow_multi_currency_invoices_against_single_party_account": 1,
+ "book_tax_discount_loss": 1,
+ },
+ )
+ def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
+ self,
+ ):
+ """
+ 1. Multi-currency SI with single currency accounting (company currency)
+ 2. PE with early payment discount
+ 3. Test if Paid Amount is calculated in company currency
+ 4. Test if deductions are calculated in company currency
+
+ SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
+ """
+ si = create_sales_invoice(
+ customer="_Test Customer",
+ currency="USD",
+ conversion_rate=50,
+ do_not_save=1,
+ )
+ create_payment_terms_template_with_discount()
+ si.payment_terms_template = "Test Discount Template"
+
+ frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+ si.save()
+ si.submit()
+
+ pe = get_payment_entry(
+ "Sales Invoice",
+ si.name,
+ bank_account="_Test Bank - _TC",
+ )
+ pe.reference_no = si.name
+ pe.reference_date = nowdate()
+
+ # Early payment discount loss on income
+ self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
+ self.assertEqual(pe.received_amount, 4500.0)
+ self.assertEqual(pe.deductions[0].amount, 500.0)
+ self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
+ self.assertEqual(pe.difference_amount, 0.0)
+
+ pe.insert()
+ pe.submit()
+
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["Debtors - _TC", 0, 5000, si.name],
+ ["_Test Bank - _TC", 4500, 0, None],
+ ["Write Off - _TC", 500.0, 0, None],
+ ]
+ )
+
+ self.validate_gl_entries(pe.name, expected_gle)
+
+ outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
+ self.assertEqual(outstanding_amount, 0)
+
+ def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
+ """
+ 1. Multi-currency SI with multi-currency accounting
+ 2. PE with early payment discount and also exchange loss
+ 3. Test if Paid Amount is calculated in transaction currency
+ 4. Test if deductions are calculated in base/company currency
+ 5. Test if exchange loss is reflected in difference
+ """
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ do_not_save=1,
+ )
+ create_payment_terms_template_with_discount()
+ si.payment_terms_template = "Test Discount Template"
+
+ frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+ si.save()
+ si.submit()
+
+ pe = get_payment_entry(
+ "Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
+ )
+ pe.reference_no = si.name
+ pe.reference_date = nowdate()
+
+ # Early payment discount loss on income
+ self.assertEqual(pe.paid_amount, 90.0)
+ self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
+ self.assertEqual(pe.deductions[0].amount, 500.0)
+ self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
+
+ # Exchange loss
+ self.assertEqual(pe.difference_amount, 300.0)
+
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": 300.0,
+ },
+ )
+
+ pe.insert()
+ pe.submit()
+
+ self.assertEqual(pe.difference_amount, 0.0)
+
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 5000, si.name],
+ ["_Test Bank - _TC", 4200, 0, None],
+ ["Write Off - _TC", 500.0, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
+ ]
+ )
+
+ self.validate_gl_entries(pe.name, expected_gle)
+
+ outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
+ self.assertEqual(outstanding_amount, 0)
+
def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -856,24 +1056,27 @@ def create_payment_terms_template():
).insert()
-def create_payment_terms_template_with_discount():
-
- create_payment_term("30 Credit Days with 10% Discount")
+def create_payment_terms_template_with_discount(
+ name=None, discount_type=None, discount=None, template_name=None
+):
+ create_payment_term(name or "30 Credit Days with 10% Discount")
+ template_name = template_name or "Test Discount Template"
- if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
- payment_term_template = frappe.get_doc(
+ if not frappe.db.exists("Payment Terms Template", template_name):
+ frappe.get_doc(
{
"doctype": "Payment Terms Template",
- "template_name": "Test Discount Template",
+ "template_name": template_name,
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
- "payment_term": "30 Credit Days with 10% Discount",
+ "payment_term": name or "30 Credit Days with 10% Discount",
"invoice_portion": 100,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 2,
- "discount": 10,
+ "discount_type": discount_type or "Percentage",
+ "discount": discount or 10,
"discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 1,
}
diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
index 61a1462dd7a4..1c31829f0ea6 100644
--- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
+++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
@@ -3,6 +3,7 @@
"creation": "2016-06-15 15:56:30.815503",
"doctype": "DocType",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"account",
"cost_center",
@@ -17,9 +18,7 @@
"in_list_view": 1,
"label": "Account",
"options": "Account",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fieldname": "cost_center",
@@ -28,37 +27,30 @@
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1,
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Amount",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "label": "Amount (Company Currency)",
+ "options": "Company:company:default_currency",
+ "reqd": 1
},
{
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
- "label": "Description",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Description"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-12 20:38:08.110674",
+ "modified": "2023-03-06 07:11:57.739619",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
@@ -66,5 +58,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 0208975513b0..76b85ecf0e25 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -81,8 +81,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}
if(doc.docstatus == 1 && doc.outstanding_amount != 0
- && !(doc.is_return && doc.return_against)) {
- this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
+ && !(doc.is_return && doc.return_against) && !doc.on_hold) {
+ this.frm.add_custom_button(
+ __('Payment'),
+ () => this.make_payment_entry(),
+ __('Create')
+ );
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 462233524f80..a624c638c369 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -75,9 +75,12 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
if (doc.docstatus == 1 && doc.outstanding_amount!=0
&& !(cint(doc.is_return) && doc.return_against)) {
- cur_frm.add_custom_button(__('Payment'),
- this.make_payment_entry, __('Create'));
- cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
+ this.frm.add_custom_button(
+ __('Payment'),
+ () => this.make_payment_entry(),
+ __('Create')
+ );
+ this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if(doc.docstatus==1 && !doc.is_return) {
diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
index 57d80492ae0e..f21c94b4940c 100644
--- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
+++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
@@ -25,6 +25,7 @@ def get_data(filters):
["posting_date", "<=", filters.get("to_date")],
["against_voucher_type", "=", "Asset"],
["account", "in", depreciation_accounts],
+ ["is_cancelled", "=", 0],
]
if filters.get("asset"):
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 6d0b77abcd72..01d05c1327c3 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -421,6 +421,9 @@ frappe.ui.form.on('Asset', {
} else {
frm.set_value('purchase_date', purchase_doc.posting_date);
}
+ if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
+ frm.set_value('available_for_use_date', frm.doc.purchase_date);
+ }
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) {
doctype_field = frappe.scrub(doctype)
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 511afdf08540..c6d1154387e1 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -79,6 +79,9 @@
"options": "ACC-ASS-.YYYY.-"
},
{
+ "depends_on": "item_code",
+ "fetch_from": "item_code.item_name",
+ "fetch_if_empty": 1,
"fieldname": "asset_name",
"fieldtype": "Data",
"in_list_view": 1,
@@ -512,7 +515,7 @@
"table_fieldname": "accounts"
}
],
- "modified": "2023-01-31 01:03:09.467817",
+ "modified": "2023-03-30 15:07:41.542374",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -554,4 +557,4 @@
"sort_order": "DESC",
"title_field": "asset_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 662411e05102..38a66e470cff 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -296,17 +296,42 @@ def make_depreciation_schedule(self, date_of_disposal):
if has_pro_rata:
number_of_pending_depreciations += 1
+ has_wdv_or_dd_non_yearly_pro_rata = False
+ if (
+ finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance")
+ and cint(finance_book.frequency_of_depreciation) != 12
+ ):
+ has_wdv_or_dd_non_yearly_pro_rata = self.check_is_pro_rata(
+ finance_book, wdv_or_dd_non_yearly=True
+ )
+
skip_row = False
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
+ depreciation_amount = 0
+
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
- depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
+ if n > 0 and len(self.get("schedules")) > n - 1:
+ prev_depreciation_amount = self.get("schedules")[n - 1].depreciation_amount
+ else:
+ prev_depreciation_amount = 0
+
+ depreciation_amount = get_depreciation_amount(
+ self,
+ value_after_depreciation,
+ finance_book,
+ n,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ )
- if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
+ if not has_pro_rata or (
+ n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
+ ):
schedule_date = add_months(
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
)
@@ -322,7 +347,10 @@ def make_depreciation_schedule(self, date_of_disposal):
if date_of_disposal:
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(
- finance_book, depreciation_amount, from_date, date_of_disposal
+ finance_book,
+ depreciation_amount,
+ from_date,
+ date_of_disposal,
)
if depreciation_amount > 0:
@@ -340,12 +368,20 @@ def make_depreciation_schedule(self, date_of_disposal):
break
# For first row
- if has_pro_rata and not self.opening_accumulated_depreciation and n == 0:
+ if (
+ (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
+ and not self.opening_accumulated_depreciation
+ and n == 0
+ ):
from_date = add_days(
self.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(
- finance_book, depreciation_amount, from_date, finance_book.depreciation_start_date
+ finance_book,
+ depreciation_amount,
+ from_date,
+ finance_book.depreciation_start_date,
+ has_wdv_or_dd_non_yearly_pro_rata,
)
# For first depr schedule date will be the start date
@@ -364,7 +400,11 @@ def make_depreciation_schedule(self, date_of_disposal):
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(
- finance_book, depreciation_amount, schedule_date, self.to_date
+ finance_book,
+ depreciation_amount,
+ schedule_date,
+ self.to_date,
+ has_wdv_or_dd_non_yearly_pro_rata,
)
depreciation_amount = self.get_adjusted_depreciation_amount(
@@ -469,28 +509,37 @@ def get_from_date(self, finance_book):
return add_days(self.available_for_use_date, -1)
# if it returns True, depreciation_amount will not be equal for the first and last rows
- def check_is_pro_rata(self, row):
+ def check_is_pro_rata(self, row, wdv_or_dd_non_yearly=False):
has_pro_rata = False
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
- from_date = self.get_modified_available_for_use_date(row)
+ from_date = self.get_modified_available_for_use_date(row, wdv_or_dd_non_yearly)
days = date_diff(row.depreciation_start_date, from_date) + 1
- # if frequency_of_depreciation is 12 months, total_days = 365
- total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
+ if wdv_or_dd_non_yearly:
+ total_days = get_total_days(row.depreciation_start_date, 12)
+ else:
+ # if frequency_of_depreciation is 12 months, total_days = 365
+ total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
- def get_modified_available_for_use_date(self, row):
- return add_months(
- self.available_for_use_date,
- (self.number_of_depreciations_booked * row.frequency_of_depreciation),
- )
+ def get_modified_available_for_use_date(self, row, wdv_or_dd_non_yearly=False):
+ if wdv_or_dd_non_yearly:
+ return add_months(
+ self.available_for_use_date,
+ (self.number_of_depreciations_booked * 12),
+ )
+ else:
+ return add_months(
+ self.available_for_use_date,
+ (self.number_of_depreciations_booked * row.frequency_of_depreciation),
+ )
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
@@ -893,7 +942,12 @@ def get_depreciation_rate(self, args, on_validate=False):
float_precision = cint(frappe.db.get_default("float_precision")) or 2
if args.get("depreciation_method") == "Double Declining Balance":
- return 200.0 / args.get("total_number_of_depreciations")
+ return 200.0 / (
+ (
+ flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation"))
+ )
+ / 12
+ )
if args.get("depreciation_method") == "Written Down Value":
if (
@@ -910,14 +964,29 @@ def get_depreciation_rate(self, args, on_validate=False):
else:
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
- depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
+ depreciation_rate = math.pow(
+ value,
+ 1.0
+ / (
+ (
+ flt(args.get("total_number_of_depreciations"), 2)
+ * flt(args.get("frequency_of_depreciation"))
+ )
+ / 12
+ ),
+ )
return flt((100 * (1 - depreciation_rate)), float_precision)
- def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
+ def get_pro_rata_amt(
+ self, row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False
+ ):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
- total_days = get_total_days(to_date, row.frequency_of_depreciation)
+ if has_wdv_or_dd_non_yearly_pro_rata:
+ total_days = get_total_days(to_date, 12)
+ else:
+ total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
@@ -1178,24 +1247,69 @@ def get_total_days(date, frequency):
@erpnext.allow_regional
-def get_depreciation_amount(asset, depreciable_value, row):
+def get_depreciation_amount(
+ asset,
+ depreciable_value,
+ row,
+ schedule_idx=0,
+ prev_depreciation_amount=0,
+ has_wdv_or_dd_non_yearly_pro_rata=False,
+):
if row.depreciation_method in ("Straight Line", "Manual"):
- # 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:
- depreciation_amount = (
- flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
- # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
- elif asset.flags.increase_in_asset_value_due_to_repair:
- depreciation_amount = (
- flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations)
- # if the Depreciation Schedule is being prepared for the first time
- else:
- depreciation_amount = (
- flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations)
+ return get_straight_line_or_manual_depr_amount(asset, row)
else:
- depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
+ return get_wdv_or_dd_depr_amount(
+ depreciable_value,
+ row.rate_of_depreciation,
+ row.frequency_of_depreciation,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ )
+
- return depreciation_amount
+def get_straight_line_or_manual_depr_amount(asset, row):
+ # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
+ if asset.flags.increase_in_asset_life:
+ return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
+ date_diff(asset.to_date, asset.available_for_use_date) / 365
+ )
+ # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
+ elif asset.flags.increase_in_asset_value_due_to_repair:
+ return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
+ row.total_number_of_depreciations
+ )
+ # if the Depreciation Schedule is being prepared for the first time
+ else:
+ return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt(
+ row.total_number_of_depreciations
+ )
+
+
+def get_wdv_or_dd_depr_amount(
+ depreciable_value,
+ rate_of_depreciation,
+ frequency_of_depreciation,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+):
+ if cint(frequency_of_depreciation) == 12:
+ return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
+ else:
+ if has_wdv_or_dd_non_yearly_pro_rata:
+ if schedule_idx == 0:
+ return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
+ elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
+ return (
+ flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
+ )
+ else:
+ return prev_depreciation_amount
+ else:
+ if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
+ return (
+ flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
+ )
+ else:
+ return prev_depreciation_amount
diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js
index 4302cb2c5181..c45698c0236c 100644
--- a/erpnext/assets/doctype/asset/asset_list.js
+++ b/erpnext/assets/doctype/asset/asset_list.js
@@ -33,7 +33,7 @@ frappe.listview_settings['Asset'] = {
}
},
onload: function(me) {
- me.page.add_action_item('Make Asset Movement', function() {
+ me.page.add_action_item(__("Make Asset Movement"), function() {
const assets = me.get_checked_items();
frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index fc3af44947dd..ca0cd4d978d8 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -225,10 +225,16 @@ def notify_depr_entry_posting_error(failed_asset_names):
asset_links = get_comma_separated_asset_links(failed_asset_names)
message = (
- _("Hi,")
- + "
"
- + _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
+ _("Hello,")
+ + "
"
+ + _("The following assets have failed to automatically post depreciation entries: {0}").format(
+ asset_links
+ )
+ "."
+ + "
"
+ + _(
+ "Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table."
+ )
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 625a45b50989..a441856b2419 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -806,12 +806,12 @@ def test_monthly_depreciation_by_wdv_method(self):
)
expected_schedules = [
- ["2022-02-28", 647.25, 647.25],
- ["2022-03-31", 1210.71, 1857.96],
- ["2022-04-30", 1053.99, 2911.95],
- ["2022-05-31", 917.55, 3829.5],
- ["2022-06-30", 798.77, 4628.27],
- ["2022-07-15", 371.73, 5000.0],
+ ["2022-02-28", 310.89, 310.89],
+ ["2022-03-31", 654.45, 965.34],
+ ["2022-04-30", 654.45, 1619.79],
+ ["2022-05-31", 654.45, 2274.24],
+ ["2022-06-30", 654.45, 2928.69],
+ ["2022-07-15", 2071.31, 5000.0],
]
schedules = [
diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
index e603d3462663..9bc35bc7360e 100644
--- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
+++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
@@ -82,6 +82,8 @@ def calculate_next_due_date(
next_due_date = add_years(start_date, 1)
if periodicity == "2 Yearly":
next_due_date = add_years(start_date, 2)
+ if periodicity == "3 Yearly":
+ next_due_date = add_years(start_date, 3)
if periodicity == "Quarterly":
next_due_date = add_months(start_date, 3)
if end_date and (
diff --git a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json
index 20963e3fdc7a..b7cb23e66878 100644
--- a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json
+++ b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json
@@ -1,664 +1,156 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2017-10-20 07:10:55.903571",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-10-20 07:10:55.903571",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "maintenance_task",
+ "maintenance_type",
+ "column_break_2",
+ "maintenance_status",
+ "section_break_2",
+ "start_date",
+ "periodicity",
+ "column_break_4",
+ "end_date",
+ "certificate_required",
+ "section_break_9",
+ "assign_to",
+ "column_break_10",
+ "assign_to_name",
+ "section_break_10",
+ "next_due_date",
+ "column_break_14",
+ "last_completion_date",
+ "section_break_7",
+ "description"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "maintenance_task",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Maintenance Task",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "maintenance_task",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Maintenance Task",
+ "reqd": 1
+ },
+ {
+ "fieldname": "maintenance_type",
+ "fieldtype": "Select",
+ "label": "Maintenance Type",
+ "options": "Preventive Maintenance\nCalibration"
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "maintenance_status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Maintenance Status",
+ "options": "Planned\nOverdue\nCancelled",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "Today",
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "label": "Start Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "periodicity",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Periodicity",
+ "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "end_date",
+ "fieldtype": "Date",
+ "label": "End Date"
+ },
+ {
+ "default": "0",
+ "fieldname": "certificate_required",
+ "fieldtype": "Check",
+ "label": "Certificate Required",
+ "search_index": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "assign_to",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Assign To",
+ "options": "User"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "maintenance_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Maintenance Type",
- "length": 0,
- "no_copy": 0,
- "options": "Preventive Maintenance\nCalibration",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "maintenance_status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Maintenance Status",
- "length": 0,
- "no_copy": 0,
- "options": "Planned\nOverdue\nCancelled",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_2",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Today",
- "fieldname": "start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Start Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "periodicity",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Periodicity",
- "length": 0,
- "no_copy": 0,
- "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "end_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "End Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "certificate_required",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Certificate Required",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_9",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "assign_to",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Assign To",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_10",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "assign_to.full_name",
- "fieldname": "assign_to_name",
- "fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Assign to Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_10",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "next_due_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Next Due Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_14",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "last_completion_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Last Completion Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_7",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "assign_to_name",
+ "fieldtype": "Read Only",
+ "label": "Assign to Name"
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "next_due_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Next Due Date"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "last_completion_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Last Completion Date"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-06-18 16:12:04.330021",
- "modified_by": "Administrator",
- "module": "Assets",
- "name": "Asset Maintenance Task",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2023-03-23 07:03:07.113452",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Maintenance Task",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
index ae0e1bda0204..d07f40cdf422 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
@@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
frm.call({
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
args: {
- asset: frm.doc.asset,
+ asset_name: frm.doc.asset,
finance_book: frm.doc.finance_book
},
callback: function(r) {
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
index 06989a95da70..65a4226ebdf0 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
@@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = {
"label": __("Period Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
- "default": ["Fiscal Year"],
+ "default": "Fiscal Year",
"reqd": 1
},
{
@@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldtype: "Link",
options: "Asset Category"
},
- {
- fieldname:"finance_book",
- label: __("Finance Book"),
- fieldtype: "Link",
- options: "Finance Book"
- },
{
fieldname:"cost_center",
label: __("Cost Center"),
@@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = {
reqd: 1
},
{
- fieldname:"is_existing_asset",
- label: __("Is Existing Asset"),
+ fieldname:"finance_book",
+ label: __("Finance Book"),
+ fieldtype: "Link",
+ options: "Finance Book",
+ depends_on: "eval: doc.only_depreciable_assets == 1",
+ },
+ {
+ fieldname:"only_depreciable_assets",
+ label: __("Only depreciable assets"),
+ fieldtype: "Check"
+ },
+ {
+ fieldname:"only_existing_assets",
+ label: __("Only existing assets"),
fieldtype: "Check"
},
]
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index ae99c491a93f..63f9889f054c 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -45,8 +45,10 @@ def get_conditions(filters):
filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
- if filters.get("is_existing_asset"):
- conditions["is_existing_asset"] = filters.get("is_existing_asset")
+ if filters.get("only_depreciable_assets"):
+ conditions["calculate_depreciation"] = filters.get("only_depreciable_assets")
+ if filters.get("only_existing_assets"):
+ conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"):
conditions["asset_category"] = filters.get("asset_category")
if filters.get("cost_center"):
@@ -102,19 +104,18 @@ def get_data(filters):
]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
- assets_linked_to_fb = frappe.db.get_all(
- doctype="Asset Finance Book",
- filters={"finance_book": filters.finance_book or ("is", "not set")},
- pluck="parent",
- )
+ assets_linked_to_fb = None
+
+ if filters.only_depreciable_assets:
+ assets_linked_to_fb = frappe.db.get_all(
+ doctype="Asset Finance Book",
+ filters={"finance_book": filters.finance_book or ("is", "not set")},
+ pluck="parent",
+ )
for asset in assets_record:
- if filters.finance_book:
- if asset.asset_id not in assets_linked_to_fb:
- continue
- else:
- if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
- continue
+ if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
+ continue
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
row = {
@@ -172,11 +173,11 @@ def prepare_chart_data(data, filters):
"datasets": [
{
"name": _("Asset Value"),
- "values": [d.get("asset_value") for d in labels_values_map.values()],
+ "values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
},
{
"name": _("Depreciatied Amount"),
- "values": [d.get("depreciated_amount") for d in labels_values_map.values()],
+ "values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
},
],
},
@@ -310,7 +311,7 @@ def get_columns(filters):
return [
{
- "label": _("Asset Id"),
+ "label": _("Asset ID"),
"fieldtype": "Link",
"fieldname": "asset_id",
"options": "Asset",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 2559ce76da6e..72329e9a22e6 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -191,8 +191,12 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
cur_frm.add_custom_button(__('Purchase Invoice'),
this.make_purchase_invoice, __('Create'));
- if(flt(doc.per_billed)==0 && doc.status != "Delivered") {
- cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
+ if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
+ this.frm.add_custom_button(
+ __('Payment'),
+ () => this.make_payment_entry(),
+ __('Create')
+ );
}
if(flt(doc.per_billed)==0) {
diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py
index 8660c7331038..7b8c43b2d65f 100644
--- a/erpnext/crm/report/lead_details/lead_details.py
+++ b/erpnext/crm/report/lead_details/lead_details.py
@@ -98,7 +98,7 @@ def get_data(filters):
`tabAddress`.name=`tabDynamic Link`.parent)
WHERE
company = %(company)s
- AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s
+ AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s
{conditions}
ORDER BY
`tabLead`.creation asc """.format(
diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py
index a57b44be4774..ad8d8484e0e4 100644
--- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py
+++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py
@@ -90,7 +90,7 @@ def get_data(filters):
{join}
WHERE
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
- AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s
+ AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s
{conditions}
GROUP BY
`tabOpportunity`.name
diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py
index ebf01bf43fbc..93c8b439b15f 100644
--- a/erpnext/e_commerce/doctype/website_item/test_website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -198,8 +198,14 @@ def test_website_item_breadcrumbs(self):
breadcrumbs = get_parent_item_groups(item.item_group)
+ settings = frappe.get_cached_doc("E Commerce Settings")
+ if settings.enable_field_filters:
+ base_breadcrumb = "Shop by Category"
+ else:
+ base_breadcrumb = "All Products"
+
self.assertEqual(breadcrumbs[0]["name"], "Home")
- self.assertEqual(breadcrumbs[1]["name"], "All Products")
+ self.assertEqual(breadcrumbs[1]["name"], base_breadcrumb)
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
index c6bd7d23a767..5c7cf2038e82 100644
--- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
@@ -4,6 +4,7 @@
import unittest
import frappe
+from frappe.tests.utils import change_settings
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -99,6 +100,16 @@ def test_employee_history(self):
self.assertEqual(data.from_date, dt[0])
self.assertEqual(data.to_date, None)
+ @change_settings("System Settings", {"number_format": "#.###,##"})
+ def test_data_formatting_in_history(self):
+ from erpnext.hr.utils import get_formatted_value
+
+ value = get_formatted_value("12.500,00", "Float")
+ self.assertEqual(value, 12500.0)
+
+ value = get_formatted_value("12.500,00", "Currency")
+ self.assertEqual(value, 12500.0)
+
def create_company():
if not frappe.db.exists("Company", "Test Company"):
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 08bc93760a3a..d6f8c25b4246 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -873,6 +873,9 @@ def get_leave_allocation_records(employee, date, leave_type=None):
| (
(Ledger.is_carry_forward == 1)
& (Ledger.to_date.between(LeaveAllocation.from_date, LeaveAllocation.to_date))
+ # only consider cf leaves from current allocation
+ & (LeaveAllocation.from_date <= date)
+ & (date <= LeaveAllocation.to_date)
)
)
)
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index e30b84bbf347..8d1adaca62bb 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -1170,25 +1170,51 @@ def test_get_leave_allocation_records(self):
details = get_leave_allocation_records(employee.name, add_days(cf_expiry, 1), leave_type.name)
self.assertEqual(details.get(leave_type.name), expected_data)
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_filtered_old_cf_entries_in_get_leave_allocation_records(self):
+ """Tests whether old cf entries are ignored while fetching current allocation records"""
+ employee = get_employee()
+ leave_type = create_leave_type(
+ leave_type_name="_Test_CF_leave_expiry",
+ is_carry_forward=1,
+ expire_carry_forwarded_leaves_after_days=90,
+ )
+
+ # old allocation with cf leaves
+ create_carry_forwarded_allocation(employee, leave_type, date="2019-01-01")
+ # new allocation with cf leaves
+ leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
+ cf_expiry = frappe.db.get_value(
+ "Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
+ )
+
+ # test total leaves allocated before cf leave expiry
+ details = get_leave_allocation_records(employee.name, add_days(cf_expiry, -1), leave_type.name)
+ # filters out old CF leaves (15 i.e total 45)
+ self.assertEqual(details[leave_type.name]["total_leaves_allocated"], 30.0)
+
+
+def create_carry_forwarded_allocation(employee, leave_type, date=None):
+ date = date or nowdate()
-def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation
leave_allocation = create_leave_allocation(
leave_type="_Test_CF_leave_expiry",
employee=employee.name,
employee_name=employee.employee_name,
- from_date=add_months(nowdate(), -24),
- to_date=add_months(nowdate(), -12),
+ from_date=add_months(date, -24),
+ to_date=add_months(date, -12),
carry_forward=0,
)
leave_allocation.submit()
+ # carry forward leave allocation
leave_allocation = create_leave_allocation(
leave_type="_Test_CF_leave_expiry",
employee=employee.name,
employee_name=employee.employee_name,
- from_date=add_days(nowdate(), -84),
- to_date=add_days(nowdate(), 100),
+ from_date=add_days(date, -84),
+ to_date=add_days(date, 100),
carry_forward=1,
)
leave_allocation.submit()
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index d256f34732ea..181e3b12b981 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -13,6 +13,7 @@
formatdate,
get_datetime,
get_link_to_form,
+ get_number_format_info,
getdate,
nowdate,
today,
@@ -185,15 +186,11 @@ def update_employee_work_history(employee, details, date=None, cancel=False):
field = frappe.get_meta("Employee").get_field(item.fieldname)
if not field:
continue
- fieldtype = field.fieldtype
- new_data = item.new if not cancel else item.current
- if fieldtype == "Date" and new_data:
- new_data = getdate(new_data)
- elif fieldtype == "Datetime" and new_data:
- new_data = get_datetime(new_data)
- elif fieldtype in ["Currency", "Float"] and new_data:
- new_data = flt(new_data)
- setattr(employee, item.fieldname, new_data)
+
+ new_value = item.new if not cancel else item.current
+ new_value = get_formatted_value(new_value, field.fieldtype)
+ setattr(employee, item.fieldname, new_value)
+
if item.fieldname in ["department", "designation", "branch"]:
internal_work_history[item.fieldname] = item.new
@@ -207,6 +204,34 @@ def update_employee_work_history(employee, details, date=None, cancel=False):
return employee
+def get_formatted_value(value, fieldtype):
+ """
+ Since the fields in Internal Work History table are `Data` fields
+ format them as per relevant field types
+ """
+ if not value:
+ return
+
+ if fieldtype == "Date":
+ value = getdate(value)
+ elif fieldtype == "Datetime":
+ value = get_datetime(value)
+ elif fieldtype in ["Currency", "Float"]:
+ # in case of currency/float, the value might be in user's prefered number format
+ # instead of machine readable format. Convert it into a machine readable format
+ number_format = frappe.db.get_default("number_format") or "#,###.##"
+ decimal_str, comma_str, _number_format_precision = get_number_format_info(number_format)
+
+ if comma_str == "." and decimal_str == ",":
+ value = value.replace(",", "#$")
+ value = value.replace(".", ",")
+ value = value.replace("#$", ".")
+
+ value = flt(value)
+
+ return value
+
+
def delete_employee_work_history(details, employee, date):
filters = {}
for d in details:
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 36bac4c68404..9ae9b1a4af22 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -949,7 +949,8 @@ def get_valuation_rate(data):
2) If no value, get last valuation rate from SLE
3) If no value, get valuation rate from Item
"""
- from frappe.query_builder.functions import Sum
+ from frappe.query_builder.functions import Count, IfNull, Sum
+ from pypika import Case
item_code, company = data.get("item_code"), data.get("company")
valuation_rate = 0.0
@@ -960,7 +961,14 @@ def get_valuation_rate(data):
frappe.qb.from_(bin_table)
.join(wh_table)
.on(bin_table.warehouse == wh_table.name)
- .select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
+ .select(
+ Case()
+ .when(
+ Count(bin_table.name) > 0, IfNull(Sum(bin_table.stock_value) / Sum(bin_table.actual_qty), 0.0)
+ )
+ .else_(None)
+ .as_("valuation_rate")
+ )
.where((bin_table.item_code == item_code) & (wh_table.company == company))
).run(as_dict=True)[0]
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index af5ff8e1c21c..9c35e49b20b4 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -151,7 +151,7 @@ def queue_bom_cost_jobs(
while current_boms_list:
batch_no += 1
- batch_size = 20_000
+ batch_size = 7_000
boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs
# update list to exclude 20K (queued) BOMs
diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
index 84dee4ad284c..15ef20794cb8 100644
--- a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
+++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
@@ -28,7 +28,7 @@
"fieldname": "qty",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "qty"
+ "label": "Qty"
},
{
"fieldname": "item_reference",
@@ -40,7 +40,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-07 17:03:49.707487",
+ "modified": "2023-03-31 10:30:14.604051",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item Reference",
@@ -48,5 +48,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 7b64087102fd..7ea12a86b8a4 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -125,7 +125,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
}
else {
// allow for '0' qty on Credit/Debit notes
- let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1;
+ let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1);
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index eb78ca72bc17..972e6b1516c9 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1987,22 +1987,62 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
},
- make_payment_entry: function() {
+ make_payment_entry() {
+ let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry;
+ if(this.has_discount_in_schedule() && !via_journal_entry) {
+ // If early payment discount is applied, ask user for reference date
+ this.prompt_user_for_reference_date();
+ } else {
+ this.make_mapped_payment_entry();
+ }
+ },
+
+ make_mapped_payment_entry(args) {
+ var me = this;
+ args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name };
return frappe.call({
- method: cur_frm.cscript.get_method_for_payment(),
- args: {
- "dt": cur_frm.doc.doctype,
- "dn": cur_frm.doc.name
- },
+ method: me.get_method_for_payment(),
+ args: args,
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
- // cur_frm.refresh_fields()
}
});
},
- make_quality_inspection: function () {
+ prompt_user_for_reference_date(){
+ var me = this;
+ frappe.prompt({
+ label: __("Cheque/Reference Date"),
+ fieldname: "reference_date",
+ fieldtype: "Date",
+ reqd: 1,
+ }, (values) => {
+ let args = {
+ "dt": me.frm.doc.doctype,
+ "dn": me.frm.doc.name,
+ "reference_date": values.reference_date
+ }
+ me.make_mapped_payment_entry(args);
+ },
+ __("Reference Date for Early Payment Discount"),
+ __("Continue")
+ );
+ },
+
+ has_discount_in_schedule() {
+ let is_eligible = in_list(
+ ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"],
+ this.frm.doctype
+ );
+ let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length;
+ if(!is_eligible || !has_payment_schedule) return false;
+
+ let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date);
+ return has_discount;
+ },
+
+ make_quality_inspection() {
let data = [];
const fields = [
{
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index d5ef3981faf8..107c700b5212 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -17,6 +17,10 @@
)
from six import string_types
+from erpnext.assets.doctype.asset.asset import (
+ get_straight_line_or_manual_depr_amount,
+ get_wdv_or_dd_depr_amount,
+)
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
from erpnext.hr.utils import get_salary_assignments
@@ -1099,23 +1103,16 @@ def update_taxable_values(doc, method):
doc.get("items")[item_count - 1].taxable_value += diff
-def get_depreciation_amount(asset, depreciable_value, row):
+def get_depreciation_amount(
+ asset,
+ depreciable_value,
+ row,
+ schedule_idx=0,
+ prev_depreciation_amount=0,
+ has_wdv_or_dd_non_yearly_pro_rata=False,
+):
if row.depreciation_method in ("Straight Line", "Manual"):
- # 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:
- depreciation_amount = (
- flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
- # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
- elif asset.flags.increase_in_asset_value_due_to_repair:
- depreciation_amount = (
- flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations)
- # if the Depreciation Schedule is being prepared for the first time
- else:
- depreciation_amount = (
- flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations)
+ return get_straight_line_or_manual_depr_amount(asset, row)
else:
rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation
@@ -1130,10 +1127,14 @@ def get_depreciation_amount(asset, depreciable_value, row):
"As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%."
)
)
-
- depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100))
-
- return depreciation_amount
+ return get_wdv_or_dd_depr_amount(
+ depreciable_value,
+ rate_of_depreciation,
+ row.frequency_of_depreciation,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ )
def set_item_tax_from_hsn_code(item):
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index a11cfc3407a6..36b64c55ddc9 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -37,8 +37,24 @@ def validate(self):
self.make_route()
self.validate_item_group_defaults()
+ self.check_item_tax()
ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True)
+ def check_item_tax(self):
+ """Check whether Tax Rate is not entered twice for same Tax Type"""
+ check_list = []
+ for d in self.get("taxes"):
+ if d.item_tax_template:
+ if (d.item_tax_template, d.tax_category) in check_list:
+ frappe.throw(
+ _("{0} entered twice {1} in Item Taxes").format(
+ frappe.bold(d.item_tax_template),
+ "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "",
+ )
+ )
+ else:
+ check_list.append((d.item_tax_template, d.tax_category))
+
def on_update(self):
NestedSet.on_update(self)
invalidate_cache_for(self)
@@ -149,12 +165,17 @@ def get_item_for_list_in_html(context):
def get_parent_item_groups(item_group_name, from_item=False):
- base_nav_page = {"name": _("All Products"), "route": "/all-products"}
+ settings = frappe.get_cached_doc("E Commerce Settings")
+
+ if settings.enable_field_filters:
+ base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"}
+ else:
+ base_nav_page = {"name": _("All Products"), "route": "/all-products"}
if from_item and frappe.request.environ.get("HTTP_REFERER"):
# base page after 'Home' will vary on Item page
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
- if last_page and last_page == "shop-by-category":
+ if last_page and last_page in ("shop-by-category", "all-products"):
base_nav_page_title = " ".join(last_page.split("-")).title()
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 768ea9b60244..d13edd5c3260 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
-from frappe.utils import cint, flt, get_link_to_form
+from frappe.utils import cint, flt, get_link_to_form, nowtime
from frappe.utils.data import add_days
from frappe.utils.jinja import render_template
from six import text_type
@@ -173,7 +173,11 @@ def get_batch_qty(
out = 0
if batch_no and warehouse:
cond = ""
- if posting_date and posting_time:
+
+ if posting_date:
+ if posting_time is None:
+ posting_time = nowtime()
+
cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format(
posting_date, posting_time
)
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 65930a404b46..aa9b21ced172 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -354,10 +354,15 @@ def check_item_tax(self):
check_list = []
for d in self.get("taxes"):
if d.item_tax_template:
- if d.item_tax_template in check_list:
- frappe.throw(_("{0} entered twice in Item Tax").format(d.item_tax_template))
+ if (d.item_tax_template, d.tax_category) in check_list:
+ frappe.throw(
+ _("{0} entered twice {1} in Item Taxes").format(
+ frappe.bold(d.item_tax_template),
+ "for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "",
+ )
+ )
else:
- check_list.append(d.item_tax_template)
+ check_list.append((d.item_tax_template, d.tax_category))
def validate_barcode(self):
from stdnum import ean
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index b8ba53475108..0248cdfbcf02 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _, msgprint
+from frappe import _, bold, msgprint
from frappe.utils import cint, cstr, flt
import erpnext
@@ -88,7 +88,7 @@ def _changed(item):
if item_dict.get("serial_nos"):
item.current_serial_no = item_dict.get("serial_nos")
- if self.purpose == "Stock Reconciliation" and not item.serial_no:
+ if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty:
item.serial_no = item.current_serial_no
item.current_qty = item_dict.get("qty")
@@ -139,6 +139,14 @@ def _get_msg(row_num, msg):
self.validate_item(row.item_code, row)
+ if row.serial_no and not row.qty:
+ self.validation_messages.append(
+ _get_msg(
+ row_num,
+ f"Quantity should not be zero for the {bold(row.item_code)} since serial nos are specified",
+ )
+ )
+
# validate warehouse
if not frappe.db.get_value("Warehouse", row.warehouse):
self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system")))
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 7ba79d0530d9..d2e4cd7d13aa 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -606,7 +606,9 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
taxes_with_no_validity.append(tax)
if taxes_with_validity:
- taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from, reverse=True)
+ taxes = sorted(
+ taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True
+ )
else:
taxes = taxes_with_no_validity
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 7ca771f0a73a..e9c53fb2c5dc 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -30,6 +30,9 @@ def execute(filters=None):
conversion_factors.append(0)
actual_qty = stock_value = 0
+ if opening_row:
+ actual_qty = opening_row.get("qty_after_transaction")
+ stock_value = opening_row.get("stock_value")
available_serial_nos = {}
for sle in sl_entries:
diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py
index 4295188dc0be..ae74ffa27721 100644
--- a/erpnext/templates/utils.py
+++ b/erpnext/templates/utils.py
@@ -6,13 +6,12 @@
@frappe.whitelist(allow_guest=True)
-def send_message(subject="Website Query", message="", sender="", status="Open"):
+def send_message(sender, message, subject="Website Query"):
from frappe.www.contact import send_message as website_send_message
- lead = customer = None
-
- website_send_message(subject, message, sender)
+ website_send_message(sender, message, subject)
+ lead = customer = None
customer = frappe.db.sql(
"""select distinct dl.link_name from `tabDynamic Link` dl
left join `tabContact` c on dl.parent=c.name where dl.link_doctype='Customer'
@@ -59,5 +58,3 @@ def send_message(subject="Website Query", message="", sender="", status="Open"):
}
)
comm.insert(ignore_permissions=True)
-
- return "okay"
diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py
index 219747c9f8a5..913c1836acdf 100644
--- a/erpnext/www/shop-by-category/index.py
+++ b/erpnext/www/shop-by-category/index.py
@@ -53,6 +53,7 @@ def get_tabs(categories):
def get_category_records(categories: list):
categorical_data = {}
+ website_item_meta = frappe.get_meta("Website Item", cached=True)
for c in categories:
if c == "item_group":
@@ -64,7 +65,16 @@ def get_category_records(categories: list):
continue
- doctype = frappe.unscrub(c)
+ field_type = website_item_meta.get_field(c).fieldtype
+
+ if field_type == "Table MultiSelect":
+ child_doc = website_item_meta.get_field(c).options
+ for field in frappe.get_meta(child_doc, cached=True).fields:
+ if field.fieldtype == "Link" and field.reqd:
+ doctype = field.options
+ else:
+ doctype = website_item_meta.get_field(c).options
+
fields = ["name"]
try: