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