diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 81c2d8bb738..b528ee58e23 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -12,6 +12,7 @@ from frappe.utils import flt, get_link_to_form import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on +from erpnext.accounts.utils import get_currency_precision from erpnext.setup.utils import get_exchange_rate @@ -170,6 +171,15 @@ class ExchangeRateRevaluation(Document): .run(as_dict=True) ) + # round off balance based on currency precision + currency_precision = get_currency_precision() + for acc in account_details: + acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision) + acc.balance = flt(acc.balance, currency_precision) + acc.zero_balance = ( + True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False + ) + return account_details @staticmethod diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 866a94d04b1..46e2c50e40b 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -169,21 +169,18 @@ class PeriodClosingVoucher(AccountsController): return frappe.db.sql( """ select - t2.account_currency, + t1.account_currency, {dimension_fields}, sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency, sum(t1.debit) - sum(t1.credit) as bal_in_company_currency - from `tabGL Entry` t1, `tabAccount` t2 + from `tabGL Entry` t1 where t1.is_cancelled = 0 - and t1.account = t2.name - and t2.report_type = 'Profit and Loss' - and t2.docstatus < 2 - and t2.company = %s + and t1.account in (select name from `tabAccount` where report_type = 'Profit and Loss' and docstatus < 2 and company = %s) and t1.posting_date between %s and %s group by {dimension_fields} """.format( - dimension_fields=", ".join(dimension_fields) + dimension_fields=", ".join(dimension_fields), ), (self.company, self.get("year_start_date"), self.posting_date), as_dict=1, diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index a63039e0e3a..e8e80449292 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -469,7 +469,7 @@ "options": "UOM" }, { - "description": "If rate is zero them item will be treated as \"Free Item\"", + "description": "If rate is zero then item will be treated as \"Free Item\"", "fieldname": "free_item_rate", "fieldtype": "Currency", "label": "Free Item Rate" @@ -670,4 +670,4 @@ "sort_order": "DESC", "states": [], "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 946170340ee..25ea903dfec 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -647,12 +647,12 @@ def set_taxes( else: args.update(get_party_details(party, party_type)) - if party_type in ("Customer", "Lead"): + if party_type in ("Customer", "Lead", "Prospect"): args.update({"tax_type": "Sales"}) - if party_type == "Lead": + if party_type in ["Lead", "Prospect"]: args["customer"] = None - del args["lead"] + del args[frappe.scrub(party_type)] else: args.update({"tax_type": "Purchase"}) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2d56b597b0c..2bfb4105c1d 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from collections import OrderedDict import frappe from frappe import _, qb, scrub @@ -849,30 +850,30 @@ class GrossProfitGenerator(object): Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. """ - parents = [] + grouped = OrderedDict() for row in self.si_list: - if row.parent not in parents: - parents.append(row.parent) + # initialize list with a header row for each new parent + grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append( + row.update( + {"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code} + ) # descendant rows will have indent: 1.0 or greater + ) - parents_index = 0 - for index, row in enumerate(self.si_list): - if parents_index < len(parents) and row.parent == parents[parents_index]: - invoice = self.get_invoice_row(row) - self.si_list.insert(index, invoice) - parents_index += 1 + # if item is a bundle, add it's components as seperate rows + if frappe.db.exists("Product Bundle", row.item_code): + bundled_items = self.get_bundle_items(row) + for x in bundled_items: + bundle_item = self.get_bundle_item_row(row, x) + grouped.get(row.parent).append(bundle_item) - else: - # skipping the bundle items rows - if not row.indent: - row.indent = 1.0 - row.parent_invoice = row.parent - row.invoice_or_item = row.item_code + self.si_list.clear() - if frappe.db.exists("Product Bundle", row.item_code): - self.add_bundle_items(row, index) + for items in grouped.values(): + self.si_list.extend(items) def get_invoice_row(self, row): + # header row format return frappe._dict( { "parent_invoice": "", @@ -901,13 +902,6 @@ class GrossProfitGenerator(object): } ) - def add_bundle_items(self, product_bundle, index): - bundle_items = self.get_bundle_items(product_bundle) - - for i, item in enumerate(bundle_items): - bundle_item = self.get_bundle_item_row(product_bundle, item) - self.si_list.insert((index + i + 1), bundle_item) - def get_bundle_items(self, product_bundle): return frappe.get_all( "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index a4d2c82845a..2e1def98fc3 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -96,7 +96,6 @@ class AssetCategory(Document): frappe.throw(msg, title=_("Missing Account")) -@frappe.whitelist() def get_asset_category_account( fieldname, item=None, asset=None, account=None, asset_category=None, company=None ): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 78bb05671d8..10788c36eec 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -758,6 +758,7 @@ class AccountsController(TransactionBase): } ) + update_gl_dict_with_regional_fields(self, gl_dict) accounting_dimensions = get_accounting_dimensions() dimension_dict = frappe._dict() @@ -2835,3 +2836,8 @@ def validate_regional(doc): @erpnext.allow_regional def validate_einvoice_fields(doc): pass + + +@erpnext.allow_regional +def update_gl_dict_with_regional_fields(doc, gl_dict): + pass diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3d47ee797da..ef289ff6a67 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -43,7 +43,6 @@ class SellingController(StockController): self.validate_auto_repeat_subscription_dates() def set_missing_values(self, for_validate=False): - super(SellingController, self).set_missing_values(for_validate) # set contact and address details for customer, if they are not mentioned @@ -62,7 +61,7 @@ class SellingController(StockController): elif self.doctype == "Quotation" and self.party_name: if self.quotation_to == "Customer": customer = self.party_name - else: + elif self.quotation_to == "Lead": lead = self.party_name if customer: diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 4daa2edb28a..9cc6ec9d4b4 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -160,4 +160,3 @@ class TestLoanDisbursement(unittest.TestCase): interest = per_day_interest * 15 self.assertEqual(amounts["pending_principal_amount"], 1500000) - self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2)) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 15e9e3f4c1a..9d31ee195fc 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -22,7 +22,7 @@ class LoanInterestAccrual(AccountsController): frappe.throw(_("Interest Amount or Principal Amount is mandatory")) if not self.last_accrual_date: - self.last_accrual_date = get_last_accrual_date(self.loan) + self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date) def on_submit(self): self.make_gl_entries() @@ -271,14 +271,14 @@ def make_loan_interest_accrual_entry(args): def get_no_of_days_for_interest_accural(loan, posting_date): - last_interest_accrual_date = get_last_accrual_date(loan.name) + last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date) no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 return no_of_days -def get_last_accrual_date(loan): +def get_last_accrual_date(loan, posting_date): last_posting_date = frappe.db.sql( """ SELECT MAX(posting_date) from `tabLoan Interest Accrual` WHERE loan = %s and docstatus = 1""", @@ -286,12 +286,30 @@ def get_last_accrual_date(loan): ) if last_posting_date[0][0]: + last_interest_accrual_date = last_posting_date[0][0] # interest for last interest accrual date is already booked, so add 1 day - return add_days(last_posting_date[0][0], 1) + last_disbursement_date = get_last_disbursement_date(loan, posting_date) + + if last_disbursement_date and getdate(last_disbursement_date) > getdate( + last_interest_accrual_date + ): + last_interest_accrual_date = last_disbursement_date + + return add_days(last_interest_accrual_date, 1) else: return frappe.db.get_value("Loan", loan, "disbursement_date") +def get_last_disbursement_date(loan, posting_date): + last_disbursement_date = frappe.db.get_value( + "Loan Disbursement", + {"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)}, + "MAX(posting_date)", + ) + + return last_disbursement_date + + def days_in_year(year): days = 365 diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 017724da806..d7e11aafa81 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -101,7 +101,7 @@ class LoanRepayment(AccountsController): if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision): if not self.is_term_loan: # get last loan interest accrual date - last_accrual_date = get_last_accrual_date(self.against_loan) + last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date) # get posting date upto which interest has to be accrued per_day_interest = get_per_day_interest( @@ -722,7 +722,7 @@ def get_amounts(amounts, against_loan, posting_date): if due_date: pending_days = date_diff(posting_date, due_date) + 1 else: - last_accrual_date = get_last_accrual_date(against_loan_doc.name) + last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date) pending_days = date_diff(posting_date, last_accrual_date) + 1 if pending_days > 0: diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 22177f44148..a7f06486abc 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -7,13 +7,14 @@ from typing import Literal import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string -from frappe.utils.data import add_to_date, now +from frappe.utils.data import add_to_date, now, today from erpnext.manufacturing.doctype.job_card.job_card import ( JobCardOverTransferError, OperationMismatchError, OverlapError, make_corrective_job_card, + make_material_request, ) from erpnext.manufacturing.doctype.job_card.job_card import ( make_stock_entry as make_stock_entry_from_jc, @@ -449,6 +450,25 @@ class TestJobCard(FrappeTestCase): jc.docstatus = 2 assertStatus("Cancelled") + def test_job_card_material_request_and_bom_details(self): + from erpnext.stock.doctype.material_request.material_request import make_stock_entry + + create_bom_with_multiple_operations() + work_order = make_wo_with_transfer_against_jc() + + job_card_name = frappe.db.get_value("Job Card", {"work_order": work_order.name}, "name") + + mr = make_material_request(job_card_name) + mr.schedule_date = today() + mr.submit() + + ste = make_stock_entry(mr.name) + self.assertEqual(ste.purpose, "Material Transfer for Manufacture") + self.assertEqual(ste.work_order, work_order.name) + self.assertEqual(ste.job_card, job_card_name) + self.assertEqual(ste.from_bom, 1.0) + self.assertEqual(ste.bom_no, work_order.bom_no) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js index 98d2bbc81a4..5ab8bae2e1d 100644 --- a/erpnext/projects/doctype/task/task_list.js +++ b/erpnext/projects/doctype/task/task_list.js @@ -25,20 +25,38 @@ frappe.listview_settings['Task'] = { } return [__(doc.status), colors[doc.status], "status,=," + doc.status]; }, - gantt_custom_popup_html: function(ganttobj, task) { - var html = `
Project: ${task.project}
`; - html += `Progress: ${ganttobj.progress}
`; + if (task.project) { + html += `${__("Project")}: + + ${task.project} + +
`; + } + html += `+ ${__("Progress")}: + ${ganttobj.progress}% +
`; - if(task._assign_list) { - html += task._assign_list.reduce( - (html, user) => html + frappe.avatar(user) - , ''); + if (task._assign) { + const assign_list = JSON.parse(task._assign); + const assignment_wrapper = ` + Assigned to: + + ${assign_list.map((user) => frappe.user_info(user).fullname).join(", ")} + + `; + html += assignment_wrapper; } - return html; - } - + return `Are you sure you want to create Reposting Entries?
`; + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes.map(i => frappe.query_report.data[i]); + + if (!selected_rows.length) { + frappe.throw(__("Please select rows to create Reposting Entries")); + } frappe.confirm(__(message), () => { - let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); - let selected_rows = indexes.map(i => frappe.query_report.data[i]); - frappe.call({ method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries", args: { diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 2f64eddb300..272195e2a68 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -728,6 +728,8 @@ class update_entries_after(object): self.update_rate_on_purchase_receipt(sle, outgoing_rate) elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt": self.update_rate_on_subcontracting_receipt(sle, outgoing_rate) + elif sle.voucher_type == "Stock Reconciliation": + self.update_rate_on_stock_reconciliation(sle) def update_rate_on_stock_entry(self, sle, outgoing_rate): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) @@ -795,6 +797,38 @@ class update_entries_after(object): for d in scr.items: d.db_update() + def update_rate_on_stock_reconciliation(self, sle): + if not sle.serial_no and not sle.batch_no: + sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True) + + for item in sr.items: + # Skip for Serial and Batch Items + if item.serial_no or item.batch_no: + continue + + previous_sle = get_previous_sle( + { + "item_code": item.item_code, + "warehouse": item.warehouse, + "posting_date": sr.posting_date, + "posting_time": sr.posting_time, + "sle": sle.name, + } + ) + + item.current_qty = previous_sle.get("qty_after_transaction") or 0.0 + item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0 + item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate) + + item.amount = flt(item.qty) * flt(item.valuation_rate) + item.amount_difference = item.amount - item.current_amount + else: + sr.difference_amount = sum([item.amount_difference for item in sr.items]) + sr.db_update() + + for item in sr.items: + item.db_update() + def get_serialized_values(self, sle): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty)