diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 3a9e80a9d94..0c62ba96acb 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -401,59 +401,6 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries()
- def make_difference_entry(self, row):
- journal_entry = frappe.new_doc("Journal Entry")
- journal_entry.voucher_type = "Exchange Gain Or Loss"
- journal_entry.company = self.company
- journal_entry.posting_date = nowdate()
- journal_entry.multi_currency = 1
-
- party_account_currency = frappe.get_cached_value(
- "Account", self.receivable_payable_account, "account_currency"
- )
- difference_account_currency = frappe.get_cached_value(
- "Account", row.difference_account, "account_currency"
- )
-
- # Account Currency has balance
- dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
- reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
-
- journal_account = frappe._dict(
- {
- "account": self.receivable_payable_account,
- "party_type": self.party_type,
- "party": self.party,
- "account_currency": party_account_currency,
- "exchange_rate": 0,
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": row.against_voucher_type,
- "reference_name": row.against_voucher,
- dr_or_cr: flt(row.difference_amount),
- dr_or_cr + "_in_account_currency": 0,
- }
- )
-
- journal_entry.append("accounts", journal_account)
-
- journal_account = frappe._dict(
- {
- "account": row.difference_account,
- "account_currency": difference_account_currency,
- "exchange_rate": 1,
- "cost_center": erpnext.get_default_cost_center(self.company),
- reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
- reverse_dr_or_cr: flt(row.difference_amount),
- }
- )
-
- journal_entry.append("accounts", journal_account)
-
- journal_entry.save()
- journal_entry.submit()
-
- return journal_entry
-
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
{
@@ -619,16 +566,6 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company):
- def get_difference_row(inv):
- if inv.difference_amount != 0 and inv.difference_account:
- difference_row = {
- "account": inv.difference_account,
- inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
- reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
- "cost_center": erpnext.get_default_cost_center(company),
- }
- return difference_row
-
for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 6f0b8019b86..ae132eb6eb2 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -131,6 +131,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
args: { "pos_profile": frm.pos_profile },
callback: ({ message: profile }) => {
this.update_customer_groups_settings(profile?.customer_groups);
+ this.frm.set_value("company", profile?.company);
},
});
}
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 89a96118ec4..842f159967f 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -49,6 +49,7 @@ class POSInvoice(SalesInvoice):
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
+ self.validate_company_with_pos_company()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -281,6 +282,14 @@ class POSInvoice(SalesInvoice):
if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
+ def validate_company_with_pos_company(self):
+ if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
+ frappe.throw(
+ _("Company {} does not match with POS Profile Company {}").format(
+ self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company")
+ )
+ )
+
def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
@@ -359,6 +368,7 @@ class POSInvoice(SalesInvoice):
profile = {}
if self.pos_profile:
profile = frappe.get_doc("POS Profile", self.pos_profile)
+ self.company = profile.get("company")
if not self.get("payments") and not for_validate:
update_multi_mode_option(self, profile)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 7581366bc08..e5adeae501b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -716,6 +716,7 @@
"fieldtype": "Table",
"hide_days": 1,
"hide_seconds": 1,
+ "label": "Items",
"oldfieldname": "entries",
"oldfieldtype": "Table",
"options": "Sales Invoice Item",
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 0d67752ba74..8bd7b5a3fee 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -14,7 +14,7 @@ from frappe.contacts.doctype.address.address import (
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
-from frappe.query_builder.functions import Date, Sum
+from frappe.query_builder.functions import Abs, Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -922,35 +922,34 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
def get_partywise_advanced_payment_amount(
- party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
+ party_type, posting_date=None, future_payment=0, company=None, party=None
):
- gle = frappe.qb.DocType("GL Entry")
+ ple = frappe.qb.DocType("Payment Ledger Entry")
query = (
- frappe.qb.from_(gle)
- .select(gle.party)
+ frappe.qb.from_(ple)
+ .select(ple.party, Abs(Sum(ple.amount).as_("amount")))
.where(
- (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
+ (ple.party_type.isin(party_type))
+ & (ple.amount < 0)
+ & (ple.against_voucher_no == ple.voucher_no)
+ & (ple.delinked == 0)
)
- .groupby(gle.party)
+ .groupby(ple.party)
)
- if account_type == "Receivable":
- query = query.select(Sum(gle.credit).as_("amount"))
- else:
- query = query.select(Sum(gle.debit).as_("amount"))
if posting_date:
if future_payment:
- query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
+ query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date))
else:
- query = query.where(gle.posting_date <= posting_date)
+ query = query.where(ple.posting_date <= posting_date)
if company:
- query = query.where(gle.company == company)
+ query = query.where(ple.company == company)
if party:
- query = query.where(gle.party == party)
+ query = query.where(ple.party == party)
- data = query.run(as_dict=True)
+ data = query.run()
if data:
return frappe._dict(data)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index f78a84086a9..a7b35a579a9 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -1090,7 +1090,10 @@ class ReceivablePayableReport(object):
.where(
(je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date))
- & (je.voucher_type == "Exchange Rate Revaluation")
+ & (
+ (je.voucher_type == "Exchange Rate Revaluation")
+ | (je.voucher_type == "Exchange Gain Or Loss")
+ )
)
.run()
)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index da4c9dabbf6..cffc87895ef 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -50,13 +50,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.filters.show_future_payments,
self.filters.company,
party=party,
- account_type=self.account_type,
)
or {}
)
if self.filters.show_gl_balance:
- gl_balance_map = get_gl_balance(self.filters.report_date)
+ gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0:
@@ -233,12 +232,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(label="Total Amount Due", fieldname="total_due")
-def get_gl_balance(report_date):
+def get_gl_balance(report_date, company):
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", "sum(debit - credit)"],
- filters={"posting_date": ("<=", report_date), "is_cancelled": 0},
+ filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,
)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
new file mode 100644
index 00000000000..3ee35a114d1
--- /dev/null
+++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
@@ -0,0 +1,203 @@
+import unittest
+
+import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import today
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.maxDiff = None
+ self.create_company()
+ self.create_customer()
+ self.create_item()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def test_01_receivable_summary_output(self):
+ """
+ Test for Invoices, Paid, Advance and Outstanding
+ """
+ filters = {
+ "company": self.company,
+ "customer": self.customer,
+ "posting_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=200,
+ price_list_rate=200,
+ )
+
+ customer_group, customer_territory = frappe.db.get_all(
+ "Customer",
+ filters={"name": self.customer},
+ fields=["customer_group", "territory"],
+ as_list=True,
+ )[0]
+
+ report = execute(filters)
+ rpt_output = report[1]
+ expected_data = {
+ "party_type": "Customer",
+ "advance": 0,
+ "party": self.customer,
+ "invoiced": 200.0,
+ "paid": 0.0,
+ "credit_note": 0.0,
+ "outstanding": 200.0,
+ "range1": 200.0,
+ "range2": 0.0,
+ "range3": 0.0,
+ "range4": 0.0,
+ "range5": 0.0,
+ "total_due": 200.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "currency": si.currency,
+ "territory": customer_territory,
+ "customer_group": customer_group,
+ }
+
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # simulate advance payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 50
+ pe.references[0].allocated_amount = 0 # this essitially removes the reference
+ pe.save().submit()
+
+ # update expected data with advance
+ expected_data.update(
+ {
+ "advance": 50.0,
+ "outstanding": 150.0,
+ "range1": 150.0,
+ "total_due": 150.0,
+ }
+ )
+
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # make partial payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 125
+ pe.references[0].allocated_amount = 125
+ pe.save().submit()
+
+ # update expected data after advance and partial payment
+ expected_data.update(
+ {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0}
+ )
+
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ @change_settings("Selling Settings", {"cust_master_name": "Naming Series"})
+ def test_02_various_filters_and_output(self):
+ filters = {
+ "company": self.company,
+ "customer": self.customer,
+ "posting_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=200,
+ price_list_rate=200,
+ )
+ # make partial payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 150
+ pe.references[0].allocated_amount = 150
+ pe.save().submit()
+
+ customer_group, customer_territory = frappe.db.get_all(
+ "Customer",
+ filters={"name": self.customer},
+ fields=["customer_group", "territory"],
+ as_list=True,
+ )[0]
+
+ report = execute(filters)
+ rpt_output = report[1]
+ expected_data = {
+ "party_type": "Customer",
+ "advance": 0,
+ "party": self.customer,
+ "party_name": self.customer,
+ "invoiced": 200.0,
+ "paid": 150.0,
+ "credit_note": 0.0,
+ "outstanding": 50.0,
+ "range1": 50.0,
+ "range2": 0.0,
+ "range3": 0.0,
+ "range4": 0.0,
+ "range5": 0.0,
+ "total_due": 50.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "currency": si.currency,
+ "territory": customer_territory,
+ "customer_group": customer_group,
+ }
+
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # with gl balance filter
+ filters.update({"show_gl_balance": True})
+ expected_data.update({"gl_balance": 50.0, "diff": 0.0})
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # with gl balance and future payments filter
+ filters.update({"show_future_payments": True})
+ expected_data.update({"remaining_balance": 50.0})
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # invoice fully paid
+ pe = get_payment_entry(si.doctype, si.name).save().submit()
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 0)
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index 70bbf7e694d..debfffdcbb3 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -1,4 +1,5 @@
import frappe
+from frappe import qb
from erpnext.stock.doctype.item.test_item import create_item
@@ -103,3 +104,15 @@ class AccountsTestMixin:
)
new_acc.save()
setattr(self, acc.attribute_name, new_acc.name)
+
+ def clear_old_entries(self):
+ doctype_list = [
+ "GL Entry",
+ "Payment Ledger Entry",
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Payment Entry",
+ "Journal Entry",
+ ]
+ for doctype in doctype_list:
+ qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 2060c6ca835..ce894eb00d9 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -56,7 +56,6 @@ class Asset(AccountsController):
def on_submit(self):
self.validate_in_use_date()
- self.set_status()
self.make_asset_movement()
if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries()
@@ -72,6 +71,7 @@ class Asset(AccountsController):
"Asset Depreciation Schedules created:
{0}
Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
+ self.set_status()
add_asset_activity(self.name, _("Asset submitted"))
def on_cancel(self):
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index cd66f1d136a..90eae2db388 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -19,6 +19,7 @@ from frappe.utils import (
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import (
+ get_asset_value_after_depreciation,
make_sales_invoice,
split_asset,
update_maintenance_status,
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 39ebd4ec0ec..2b4b248a302 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -107,7 +107,7 @@ class AssetDepreciationSchedule(Document):
have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
- self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+ self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return)
def have_asset_details_been_modified(self, asset_doc):
return (
@@ -157,7 +157,12 @@ class AssetDepreciationSchedule(Document):
self.status = "Draft"
def make_depr_schedule(
- self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
+ self,
+ asset_doc,
+ row,
+ date_of_disposal,
+ update_asset_finance_book_row=True,
+ value_after_depreciation=None,
):
if not self.get("depreciation_schedule"):
self.depreciation_schedule = []
@@ -167,7 +172,9 @@ class AssetDepreciationSchedule(Document):
start = self.clear_depr_schedule()
- self._make_depr_schedule(asset_doc, row, start, date_of_disposal, update_asset_finance_book_row)
+ self._make_depr_schedule(
+ asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation
+ )
def clear_depr_schedule(self):
start = 0
@@ -187,23 +194,30 @@ class AssetDepreciationSchedule(Document):
return start
def _make_depr_schedule(
- self, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
+ self,
+ asset_doc,
+ row,
+ start,
+ date_of_disposal,
+ update_asset_finance_book_row,
+ value_after_depreciation,
):
asset_doc.validate_asset_finance_books(row)
- value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
+ if not value_after_depreciation:
+ value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row:
row.db_update()
- number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
+ final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
self.number_of_depreciations_booked
)
has_pro_rata = _check_is_pro_rata(asset_doc, row)
if has_pro_rata:
- number_of_pending_depreciations += 1
+ final_number_of_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False
if (
@@ -219,7 +233,9 @@ class AssetDepreciationSchedule(Document):
depreciation_amount = 0
- for n in range(start, number_of_pending_depreciations):
+ number_of_pending_depreciations = final_number_of_depreciations - start
+
+ for n in range(start, final_number_of_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
@@ -236,10 +252,11 @@ class AssetDepreciationSchedule(Document):
n,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
+ number_of_pending_depreciations,
)
if not has_pro_rata or (
- n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
+ n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
):
schedule_date = add_months(
row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
@@ -310,7 +327,7 @@ class AssetDepreciationSchedule(Document):
)
# 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 asset_doc.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months(
@@ -343,7 +360,7 @@ class AssetDepreciationSchedule(Document):
# Adjust depreciation amount in the last period based on the expected value after useful life
if row.expected_value_after_useful_life and (
(
- n == cint(number_of_pending_depreciations) - 1
+ n == cint(final_number_of_depreciations) - 1
and value_after_depreciation != row.expected_value_after_useful_life
)
or value_after_depreciation < row.expected_value_after_useful_life
@@ -392,6 +409,7 @@ class AssetDepreciationSchedule(Document):
def set_accumulated_depreciation(
self,
+ asset_doc,
row,
date_of_disposal=None,
date_of_return=None,
@@ -403,13 +421,21 @@ class AssetDepreciationSchedule(Document):
if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual"
]
- accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+ accumulated_depreciation = None
value_after_depreciation = flt(row.value_after_depreciation)
for i, d in enumerate(self.get("depreciation_schedule")):
if ignore_booked_entry and d.journal_entry:
continue
+ if not accumulated_depreciation:
+ if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment:
+ accumulated_depreciation = self.get("depreciation_schedule")[
+ i - 1
+ ].accumulated_depreciation_amount
+ else:
+ accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
@@ -507,9 +533,12 @@ def get_depreciation_amount(
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
+ number_of_pending_depreciations=0,
):
if fb_row.depreciation_method in ("Straight Line", "Manual"):
- return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx)
+ return get_straight_line_or_manual_depr_amount(
+ asset, fb_row, schedule_idx, number_of_pending_depreciations
+ )
else:
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row
@@ -529,7 +558,9 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb
return fb_row.rate_of_depreciation
-def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
+def get_straight_line_or_manual_depr_amount(
+ asset, row, schedule_idx, number_of_pending_depreciations
+):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
@@ -540,6 +571,36 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
+ # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
+ elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
+ if row.daily_depreciation:
+ daily_depr_amount = (
+ flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
+ ) / date_diff(
+ add_months(
+ row.depreciation_start_date,
+ flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
+ * row.frequency_of_depreciation,
+ ),
+ add_months(
+ row.depreciation_start_date,
+ flt(
+ row.total_number_of_depreciations
+ - asset.number_of_depreciations_booked
+ - number_of_pending_depreciations
+ )
+ * row.frequency_of_depreciation,
+ ),
+ )
+ to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
+ from_date = add_months(
+ row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
+ )
+ return daily_depr_amount * date_diff(to_date, from_date)
+ else:
+ return (
+ flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
+ ) / number_of_pending_depreciations
# if the Depreciation Schedule is being prepared for the first time
else:
if row.daily_depreciation:
@@ -669,7 +730,12 @@ def cancel_asset_depr_schedules(asset_doc):
def make_new_active_asset_depr_schedules_and_cancel_current_ones(
- asset_doc, notes, date_of_disposal=None, date_of_return=None
+ asset_doc,
+ notes,
+ date_of_disposal=None,
+ date_of_return=None,
+ value_after_depreciation=None,
+ ignore_booked_entry=False,
):
for row in asset_doc.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
@@ -695,8 +761,12 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
row.rate_of_depreciation = new_rate_of_depreciation
new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation
- new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal)
- new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+ new_asset_depr_schedule_doc.make_depr_schedule(
+ asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation
+ )
+ new_asset_depr_schedule_doc.set_accumulated_depreciation(
+ asset_doc, row, date_of_disposal, date_of_return, ignore_booked_entry
+ )
new_asset_depr_schedule_doc.notes = notes
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 823b6e9e21d..9be7243602e 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate
+from frappe.utils import flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
@@ -14,8 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- get_asset_depr_schedule_doc,
- get_depreciation_amount,
+ make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
@@ -27,7 +26,7 @@ class AssetValueAdjustment(Document):
def on_submit(self):
self.make_depreciation_entry()
- self.reschedule_depreciations(self.new_asset_value)
+ self.update_asset(self.new_asset_value)
add_asset_activity(
self.asset,
_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
@@ -36,7 +35,7 @@ class AssetValueAdjustment(Document):
)
def on_cancel(self):
- self.reschedule_depreciations(self.current_asset_value)
+ self.update_asset(self.current_asset_value)
add_asset_activity(
self.asset,
_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
@@ -124,73 +123,33 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
- def reschedule_depreciations(self, asset_value):
+ def update_asset(self, asset_value):
asset = frappe.get_doc("Asset", self.asset)
- country = frappe.get_value("Company", self.company, "country")
- for d in asset.finance_books:
- d.value_after_depreciation = asset_value
+ if not asset.calculate_depreciation:
+ asset.value_after_depreciation = asset_value
+ asset.save()
+ return
- current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
- asset.name, "Active", d.finance_book
+ asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
+
+ if self.docstatus == 1:
+ notes = _(
+ "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
+ ).format(
+ get_link_to_form("Asset", asset.name),
+ get_link_to_form(self.get("doctype"), self.get("name")),
+ )
+ elif self.docstatus == 2:
+ notes = _(
+ "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
+ ).format(
+ get_link_to_form("Asset", asset.name),
+ get_link_to_form(self.get("doctype"), self.get("name")),
)
- new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- new_asset_depr_schedule_doc.status = "Draft"
- new_asset_depr_schedule_doc.docstatus = 0
-
- current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
- current_asset_depr_schedule_doc.cancel()
-
- if self.docstatus == 1:
- notes = _(
- "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
- ).format(
- get_link_to_form(asset.doctype, asset.name),
- get_link_to_form(self.get("doctype"), self.get("name")),
- )
- elif self.docstatus == 2:
- notes = _(
- "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
- ).format(
- get_link_to_form(asset.doctype, asset.name),
- get_link_to_form(self.get("doctype"), self.get("name")),
- )
- new_asset_depr_schedule_doc.notes = notes
-
- new_asset_depr_schedule_doc.insert()
-
- depr_schedule = new_asset_depr_schedule_doc.get("depreciation_schedule")
-
- if d.depreciation_method in ("Straight Line", "Manual"):
- end_date = max(s.schedule_date for s in depr_schedule)
- total_days = date_diff(end_date, self.date)
- rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
- total_days
- )
- from_date = self.date
- else:
- no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry])
-
- value_after_depreciation = d.value_after_depreciation
- for data in depr_schedule:
- if 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()
-
- new_asset_depr_schedule_doc.set_accumulated_depreciation(d, ignore_booked_entry=True)
- for asset_data in depr_schedule:
- if not asset_data.journal_entry:
- asset_data.db_update()
-
- new_asset_depr_schedule_doc.submit()
+ make_new_active_asset_depr_schedules_and_cancel_current_ones(
+ asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True
+ )
+ asset.flags.ignore_validate_update_after_submit = True
+ asset.save()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
index 0b3dcba024c..5d49759727c 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
@@ -4,9 +4,10 @@
import unittest
import frappe
-from frappe.utils import add_days, get_last_day, nowdate
+from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
+from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
@@ -49,27 +50,23 @@ class TestAssetValueAdjustment(unittest.TestCase):
def test_asset_depreciation_value_adjustment(self):
pr = make_purchase_receipt(
- item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
+ item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location"
)
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name)
asset_doc.calculate_depreciation = 1
+ asset_doc.available_for_use_date = "2023-01-15"
+ asset_doc.purchase_date = "2023-01-15"
- month_end_date = get_last_day(nowdate())
- purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
-
- asset_doc.available_for_use_date = purchase_date
- asset_doc.purchase_date = purchase_date
- asset_doc.calculate_depreciation = 1
asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line",
- "total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": month_end_date,
+ "total_number_of_depreciations": 12,
+ "frequency_of_depreciation": 1,
+ "depreciation_start_date": "2023-01-31",
},
)
asset_doc.submit()
@@ -77,9 +74,15 @@ class TestAssetValueAdjustment(unittest.TestCase):
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
+ post_depreciation_entries(getdate("2023-08-21"))
+
current_value = get_asset_value_after_depreciation(asset_doc.name)
+
adj_doc = make_asset_value_adjustment(
- asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
+ asset=asset_doc.name,
+ current_asset_value=current_value,
+ new_asset_value=50000.0,
+ date="2023-08-21",
)
adj_doc.submit()
@@ -90,8 +93,8 @@ class TestAssetValueAdjustment(unittest.TestCase):
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
expected_gle = (
- ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
- ("_Test Depreciations - _TC", 50000.0, 0.0),
+ ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
+ ("_Test Depreciations - _TC", 4625.29, 0.0),
)
gle = frappe.db.sql(
@@ -103,6 +106,29 @@ class TestAssetValueAdjustment(unittest.TestCase):
self.assertSequenceEqual(gle, expected_gle)
+ expected_schedules = [
+ ["2023-01-31", 5474.73, 5474.73],
+ ["2023-02-28", 9983.33, 15458.06],
+ ["2023-03-31", 9983.33, 25441.39],
+ ["2023-04-30", 9983.33, 35424.72],
+ ["2023-05-31", 9983.33, 45408.05],
+ ["2023-06-30", 9983.33, 55391.38],
+ ["2023-07-31", 9983.33, 65374.71],
+ ["2023-08-31", 8300.0, 73674.71],
+ ["2023-09-30", 8300.0, 81974.71],
+ ["2023-10-31", 8300.0, 90274.71],
+ ["2023-11-30", 8300.0, 98574.71],
+ ["2023-12-31", 8300.0, 106874.71],
+ ["2024-01-15", 8300.0, 115174.71],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in second_asset_depr_schedule.get("depreciation_schedule")
+ ]
+
+ self.assertEqual(schedules, expected_schedules)
+
def make_asset_value_adjustment(**args):
args = frappe._dict(args)
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index fbfc1ac1693..06dbd86ba12 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -25,6 +25,7 @@
"col_break_email_1",
"html_llwp",
"send_attached_files",
+ "send_document_print",
"sec_break_email_2",
"message_for_supplier",
"terms_section_break",
@@ -283,13 +284,21 @@
"fieldname": "send_attached_files",
"fieldtype": "Check",
"label": "Send Attached Files"
+ },
+ {
+ "default": "0",
+ "description": "If enabled, a print of this document will be attached to each email",
+ "fieldname": "send_document_print",
+ "fieldtype": "Check",
+ "label": "Send Document Print",
+ "print_hide": 1
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-08-08 16:30:10.870429",
+ "modified": "2023-08-09 12:20:26.850623",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 56840c11a6e..6b39982bb81 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -205,10 +205,24 @@ class RequestforQuotation(BuyingController):
if preview:
return {"message": message, "subject": subject}
- attachments = None
+ attachments = []
if self.send_attached_files:
attachments = self.get_attachments()
+ if self.send_document_print:
+ supplier_language = frappe.db.get_value("Supplier", data.supplier, "language")
+ system_language = frappe.db.get_single_value("System Settings", "language")
+ attachments.append(
+ frappe.attach_print(
+ self.doctype,
+ self.name,
+ doc=self,
+ print_format=self.meta.default_print_format or "Standard",
+ lang=supplier_language or system_language,
+ letterhead=self.letter_head,
+ )
+ )
+
self.send_email(data, sender, subject, message, attachments)
def send_email(self, data, sender, subject, message, attachments):
@@ -218,7 +232,6 @@ class RequestforQuotation(BuyingController):
recipients=data.email_id,
sender=sender,
attachments=attachments,
- print_format=self.meta.default_print_format or "Standard",
send_email=True,
doctype=self.doctype,
name=self.name,
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 7b7c53ecfe1..b396b27da7a 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -759,7 +759,7 @@ class BuyingController(SubcontractingController):
"company": self.company,
"supplier": self.supplier,
"purchase_date": self.posting_date,
- "calculate_depreciation": 1,
+ "calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount,
"gross_purchase_amount": purchase_amount,
"asset_quantity": row.qty if is_grouped_asset else 0,
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 61d2acefae4..11d5f6a9c4d 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -237,14 +237,15 @@ def new_bank_transaction(transaction):
deposit = abs(amount)
withdrawal = 0.0
- status = "Pending" if transaction["pending"] == "True" else "Settled"
+ status = "Pending" if transaction["pending"] == True else "Settled"
tags = []
- try:
- tags += transaction["category"]
- tags += [f'Plaid Cat. {transaction["category_id"]}']
- except KeyError:
- pass
+ if transaction["category"]:
+ try:
+ tags += transaction["category"]
+ tags += [f'Plaid Cat. {transaction["category_id"]}']
+ except KeyError:
+ pass
if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])):
try:
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 261aa76b706..131f438e206 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -347,7 +347,7 @@ class ProductionPlan(Document):
if not data.pending_qty:
continue
- item_details = get_item_details(data.item_code)
+ item_details = get_item_details(data.item_code, throw=False)
if self.combine_items:
if item_details.bom_no in refs:
refs[item_details.bom_no]["so_details"].append(
@@ -795,6 +795,9 @@ class ProductionPlan(Document):
if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
+ if not row.bom_no:
+ frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx))
+
bom_data = []
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 7c15bf9234b..5ad79f94b79 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1082,7 +1082,7 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
-def get_item_details(item, project=None, skip_bom_info=False):
+def get_item_details(item, project=None, skip_bom_info=False, throw=True):
res = frappe.db.sql(
"""
select stock_uom, description, item_name, allow_alternative_item,
@@ -1118,12 +1118,15 @@ def get_item_details(item, project=None, skip_bom_info=False):
if not res["bom_no"]:
if project:
- res = get_item_details(item)
+ res = get_item_details(item, throw=throw)
frappe.msgprint(
_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1
)
else:
- frappe.throw(_("Default BOM for {0} not found").format(item))
+ msg = _("Default BOM for {0} not found").format(item)
+ frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw))
+
+ return res
bom_data = frappe.db.get_value(
"BOM",
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 248b7056a0b..258a503dd5f 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -647,7 +647,7 @@ class update_entries_after(object):
def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse)
- val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []})
+ val = frappe._dict({"sle": dependant_sle})
if key not in self.distinct_item_warehouses:
self.distinct_item_warehouses[key] = val
@@ -661,6 +661,8 @@ class update_entries_after(object):
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
val.sle_changed = True
+ dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
+ val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
self.distinct_item_warehouses[key] = val
self.new_items_found = True
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):