diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index a8776fa3448..e9734bb570b 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): return # check if books nor frozen till endate: - if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto): + if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto): end_date = get_last_day(add_days(accounts_frozen_upto, 1)) if via_journal_entry: diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 0efe086d94e..710c0146153 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -299,7 +299,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers): dict( account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"] ), - ["credit", "debit"], + ["credit_in_account_currency as credit", "debit_in_account_currency as debit"], as_dict=1, ) gl_amount, transaction_amount = ( diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index e43f18b5c74..69b611669f3 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -138,7 +138,7 @@ def get_paid_amount(payment_entry, currency, bank_account): ) elif doc.payment_type == "Pay": paid_amount_field = ( - "paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount" + "paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount" ) return frappe.db.get_value( diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 9b62c6a8f37..4fbb4ac1b50 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -843,7 +843,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-10 20:57:38.340026", + "modified": "2022-12-28 16:17:33.484531", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json index ce8c0c37086..a573e7fc763 100644 --- a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json @@ -36,11 +36,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-09-13 23:40:41.479208", + "modified": "2022-12-28 23:40:41.479208", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withheld Vouchers", - "naming_rule": "Autoincrement", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 1ca95099fe8..6293357cca0 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -5,7 +5,7 @@ from collections import OrderedDict import frappe -from frappe import _, scrub +from frappe import _, qb, scrub from frappe.utils import cint, cstr, flt, getdate, nowdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -95,6 +95,9 @@ class ReceivablePayableReport(object): # Get return entries self.get_return_entries() + # Get Exchange Rate Revaluations + self.get_exchange_rate_revaluations() + self.data = [] for gle in self.gl_entries: self.update_voucher_balance(gle) @@ -258,7 +261,8 @@ class ReceivablePayableReport(object): row.invoice_grand_total = row.invoiced if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( - abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision + (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) + or (row.voucher_no in self.err_journals) ): # non-zero oustanding, we must consider this row @@ -1033,3 +1037,17 @@ class ReceivablePayableReport(object): "data": {"labels": self.ageing_column_labels, "datasets": rows}, "type": "percentage", } + + def get_exchange_rate_revaluations(self): + je = qb.DocType("Journal Entry") + results = ( + qb.from_(je) + .select(je.name) + .where( + (je.company == self.filters.company) + & (je.posting_date.lte(self.filters.report_date)) + & (je.voucher_type == "Exchange Rate Revaluation") + ) + .run() + ) + self.err_journals = [x[0] for x in results] if results else [] diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index f38890e980c..09305a20ea1 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,18 +1,51 @@ import unittest import frappe -from frappe.utils import add_days, getdate, today +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, flt, getdate, today +from erpnext import get_default_cost_center 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.accounts_receivable import execute class TestAccountsReceivable(unittest.TestCase): - def test_accounts_receivable(self): + def setUp(self): frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'") + self.create_usd_account() + + def tearDown(self): + frappe.db.rollback() + + def create_usd_account(self): + name = "Debtors USD" + exists = frappe.db.get_list( + "Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"} + ) + if exists: + self.debtors_usd = exists[0].name + else: + debtors = frappe.get_doc( + "Account", + frappe.db.get_list( + "Account", filters={"company": "_Test Company 2", "account_name": "Debtors"} + )[0].name, + ) + + debtors_usd = frappe.new_doc("Account") + debtors_usd.company = debtors.company + debtors_usd.account_name = "Debtors USD" + debtors_usd.account_currency = "USD" + debtors_usd.parent_account = debtors.parent_account + debtors_usd.account_type = debtors.account_type + self.debtors_usd = debtors_usd.save().name + + def test_accounts_receivable(self): filters = { "company": "_Test Company 2", "based_on_payment_terms": 1, @@ -24,7 +57,7 @@ class TestAccountsReceivable(unittest.TestCase): } # check invoice grand total and invoiced column's value for 3 payment terms - name = make_sales_invoice() + name = make_sales_invoice().name report = execute(filters) expected_data = [[100, 30], [100, 50], [100, 20]] @@ -65,8 +98,74 @@ class TestAccountsReceivable(unittest.TestCase): ], ) + @change_settings( + "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1} + ) + def test_exchange_revaluation_for_party(self): + """ + Exchange Revaluation for party on Receivable/Payable shoule be included + """ -def make_sales_invoice(): + company = "_Test Company 2" + customer = "_Test Customer 2" + + # Using Exchange Gain/Loss account for unrealized as well. + company_doc = frappe.get_doc("Company", company) + company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account + company_doc.save() + + si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.currency = "USD" + si.conversion_rate = 0.90 + si.debit_to = self.debtors_usd + si = si.save().submit() + + # Exchange Revaluation + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = company + err.posting_date = today() + accounts = err.get_accounts_data() + err.extend("accounts", accounts) + err.accounts[0].new_exchange_rate = 0.95 + row = err.accounts[0] + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency) + ) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + err.set_total_gain_loss() + err = err.save().submit() + + # Submit JV for ERR + jv = frappe.get_doc(err.make_jv_entry()) + jv = jv.save() + for x in jv.accounts: + x.cost_center = get_default_cost_center(jv.company) + jv.submit() + + filters = { + "company": company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + report = execute(filters) + + expected_data_for_err = [0, -5, 0, 5] + row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0] + self.assertEqual( + expected_data_for_err, + [ + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + ], + ) + + +def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): frappe.set_user("Administrator") si = create_sales_invoice( @@ -81,22 +180,26 @@ def make_sales_invoice(): do_not_save=1, ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), - ) + if not no_payment_schedule: + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), + ) - si.submit() + si = si.save() - return si.name + if not do_not_submit: + si = si.submit() + + return si def make_payment(docname): diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 98c81086ab2..33d46bc901d 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -282,7 +282,7 @@ def get_conditions(filters): ): conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") - conditions.append("(posting_date <=%(to_date)s)") + conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") if filters.get("project"): conditions.append("project in %(project)s") diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index c04b9c71252..d34c21348c8 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum item_details = get_item_details() for d in item_list: - if not d.stock_qty: - continue - item_record = item_details.get(d.item_code) purchase_receipt = None @@ -94,7 +91,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum "expense_account": expense_account, "stock_qty": d.stock_qty, "stock_uom": d.stock_uom, - "rate": d.base_net_amount / d.stock_qty, + "rate": d.base_net_amount / d.stock_qty if d.stock_qty else d.base_net_amount, "amount": d.base_net_amount, } ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b2dea226f4e..74f6b6542e5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -573,7 +573,12 @@ class AccountsController(TransactionBase): if bool(uom) != bool(stock_uom): # xor item.stock_uom = item.uom = uom or stock_uom - item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom")) + # UOM cannot be zero so substitute as 1 + item.conversion_factor = ( + get_uom_conv_factor(item.get("uom"), item.get("stock_uom")) + or item.get("conversion_factor") + or 1 + ) if self.doctype == "Purchase Invoice": self.set_expense_account(for_validate) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 791e39fed88..57f8a3e1513 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -26,7 +26,7 @@ class SellingController(StockController): super(SellingController, self).onload() if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): for item in self.get("items"): - item.update(get_bin_details(item.item_code, item.warehouse)) + item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) def validate(self): super(SellingController, self).validate() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 850daf9efed..fb56860dae5 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -526,7 +526,6 @@ scheduler_events = { "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.doctype.lead.lead.daily_open_lead", - "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", ], "weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"], "monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"], diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 0a81fe5eec6..2e734f2aa30 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -5,7 +5,16 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate +from frappe.utils import ( + add_days, + cint, + cstr, + formatdate, + get_datetime, + get_link_to_form, + getdate, + nowdate, +) from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee @@ -106,8 +115,6 @@ class Attendance(Document): frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) def unlink_attendance_from_checkins(self): - from frappe.utils import get_link_to_form - EmployeeCheckin = frappe.qb.DocType("Employee Checkin") linked_logs = ( frappe.qb.from_(EmployeeCheckin) @@ -221,75 +228,39 @@ def mark_bulk_attendance(data): attendance.submit() -def get_month_map(): - return frappe._dict( - { - "January": 1, - "February": 2, - "March": 3, - "April": 4, - "May": 5, - "June": 6, - "July": 7, - "August": 8, - "September": 9, - "October": 10, - "November": 11, - "December": 12, - } - ) - - @frappe.whitelist() -def get_unmarked_days(employee, month, exclude_holidays=0): - import calendar - - month_map = get_month_map() - today = get_datetime() - +def get_unmarked_days(employee, from_date, to_date, exclude_holidays=0): joining_date, relieving_date = frappe.get_cached_value( "Employee", employee, ["date_of_joining", "relieving_date"] ) - start_day = 1 - end_day = calendar.monthrange(today.year, month_map[month])[1] + 1 - if joining_date and joining_date.year == today.year and joining_date.month == month_map[month]: - start_day = joining_date.day - - if ( - relieving_date and relieving_date.year == today.year and relieving_date.month == month_map[month] - ): - end_day = relieving_date.day + 1 - - dates_of_month = [ - "{}-{}-{}".format(today.year, month_map[month], r) for r in range(start_day, end_day) - ] - month_start, month_end = dates_of_month[0], dates_of_month[-1] + from_date = max(getdate(from_date), joining_date or getdate(from_date)) + to_date = min(getdate(to_date), relieving_date or getdate(to_date)) records = frappe.get_all( "Attendance", fields=["attendance_date", "employee"], filters=[ - ["attendance_date", ">=", month_start], - ["attendance_date", "<=", month_end], + ["attendance_date", ">=", from_date], + ["attendance_date", "<=", to_date], ["employee", "=", employee], ["docstatus", "!=", 2], ], ) - marked_days = [get_datetime(record.attendance_date) for record in records] + marked_days = [getdate(record.attendance_date) for record in records] + if cint(exclude_holidays): - holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end) - holidays = [get_datetime(record) for record in holiday_dates] + holiday_dates = get_holiday_dates_for_employee(employee, from_date, to_date) + holidays = [getdate(record) for record in holiday_dates] marked_days.extend(holidays) unmarked_days = [] - for date in dates_of_month: - date_time = get_datetime(date) - if today.day <= date_time.day and today.month <= date_time.month: - break - if date_time not in marked_days: - unmarked_days.append(date) + while from_date <= to_date: + if from_date not in marked_days: + unmarked_days.append(from_date) + + from_date = add_days(from_date, 1) return unmarked_days diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 7d69a83e35b..bf9bc690b1b 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -1,5 +1,6 @@ -frappe.listview_settings['Attendance'] = { +frappe.listview_settings["Attendance"] = { add_fields: ["status", "attendance_date"], + get_indicator: function (doc) { if (["Present", "Work From Home"].includes(doc.status)) { return [__(doc.status), "green", "status,=," + doc.status]; @@ -10,157 +11,185 @@ frappe.listview_settings['Attendance'] = { } }, - onload: function(list_view) { + onload: function (list_view) { let me = this; - const months = moment.months(); - const curMonth = moment().format("MMMM"); - months.splice(months.indexOf(curMonth) + 1); - list_view.page.add_inner_button(__("Mark Attendance"), function() { + + list_view.page.add_inner_button(__("Mark Attendance"), function () { + let first_day_of_month = moment().startOf('month'); + + if (moment().toDate().getDate() === 1) { + first_day_of_month = first_day_of_month.subtract(1, "month"); + } + let dialog = new frappe.ui.Dialog({ title: __("Mark Attendance"), - fields: [{ - fieldname: 'employee', - label: __('For Employee'), - fieldtype: 'Link', - options: 'Employee', - get_query: () => { - return {query: "erpnext.controllers.queries.employee_query"}; + fields: [ + { + fieldname: "employee", + label: __("For Employee"), + fieldtype: "Link", + options: "Employee", + get_query: () => { + return { + query: "erpnext.controllers.queries.employee_query", + }; + }, + reqd: 1, + onchange: () => me.reset_dialog(dialog), }, - reqd: 1, - onchange: function() { - dialog.set_df_property("unmarked_days", "hidden", 1); - dialog.set_df_property("status", "hidden", 1); - dialog.set_df_property("exclude_holidays", "hidden", 1); - dialog.set_df_property("month", "value", ''); - dialog.set_df_property("unmarked_days", "options", []); - dialog.no_unmarked_days_left = false; - } - }, - { - label: __("For Month"), - fieldtype: "Select", - fieldname: "month", - options: months, - reqd: 1, - onchange: function() { - if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { - dialog.set_df_property("status", "hidden", 0); - dialog.set_df_property("exclude_holidays", "hidden", 0); - dialog.set_df_property("unmarked_days", "options", []); - dialog.no_unmarked_days_left = false; - me.get_multi_select_options( - dialog.fields_dict.employee.value, - dialog.fields_dict.month.value, - dialog.fields_dict.exclude_holidays.get_value() - ).then(options => { - if (options.length > 0) { - dialog.set_df_property("unmarked_days", "hidden", 0); - dialog.set_df_property("unmarked_days", "options", options); - } else { - dialog.no_unmarked_days_left = true; - } - }); - } - } - }, - { - label: __("Status"), - fieldtype: "Select", - fieldname: "status", - options: ["Present", "Absent", "Half Day", "Work From Home"], - hidden: 1, - reqd: 1, - - }, - { - label: __("Exclude Holidays"), - fieldtype: "Check", - fieldname: "exclude_holidays", - hidden: 1, - onchange: function() { - if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { - dialog.set_df_property("status", "hidden", 0); - dialog.set_df_property("unmarked_days", "options", []); - dialog.no_unmarked_days_left = false; - me.get_multi_select_options( - dialog.fields_dict.employee.value, - dialog.fields_dict.month.value, - dialog.fields_dict.exclude_holidays.get_value() - ).then(options => { - if (options.length > 0) { - dialog.set_df_property("unmarked_days", "hidden", 0); - dialog.set_df_property("unmarked_days", "options", options); - } else { - dialog.no_unmarked_days_left = true; - } - }); - } - } - }, - { - label: __("Unmarked Attendance for days"), - fieldname: "unmarked_days", - fieldtype: "MultiCheck", - options: [], - columns: 2, - hidden: 1 - }], + { + fieldtype: "Section Break", + fieldname: "time_period_section", + hidden: 1, + }, + { + label: __("Start"), + fieldtype: "Date", + fieldname: "from_date", + reqd: 1, + default: first_day_of_month.toDate(), + onchange: () => me.get_unmarked_days(dialog), + }, + { + fieldtype: "Column Break", + fieldname: "time_period_column", + }, + { + label: __("End"), + fieldtype: "Date", + fieldname: "to_date", + reqd: 1, + default: moment().subtract(1, 'days').toDate(), + onchange: () => me.get_unmarked_days(dialog), + }, + { + fieldtype: "Section Break", + fieldname: "days_section", + hidden: 1, + }, + { + label: __("Status"), + fieldtype: "Select", + fieldname: "status", + options: ["Present", "Absent", "Half Day", "Work From Home"], + reqd: 1, + }, + { + label: __("Exclude Holidays"), + fieldtype: "Check", + fieldname: "exclude_holidays", + onchange: () => me.get_unmarked_days(dialog), + }, + { + label: __("Unmarked Attendance for days"), + fieldname: "unmarked_days", + fieldtype: "MultiCheck", + options: [], + columns: 2, + }, + ], primary_action(data) { if (cur_dialog.no_unmarked_days_left) { - frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}", - [dialog.fields_dict.month.value, dialog.fields_dict.employee.value])); + frappe.msgprint( + __( + "Attendance from {0} to {1} has already been marked for the Employee {2}", + [data.from_date, data.to_date, data.employee] + ) + ); } else { - frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => { - frappe.call({ - method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance", - args: { - data: data - }, - callback: function (r) { - if (r.message === 1) { - frappe.show_alert({ - message: __("Attendance Marked"), - indicator: 'blue' - }); - cur_dialog.hide(); - } - } - }); - }); + frappe.confirm( + __("Mark attendance as {0} for {1} on selected dates?", [ + data.status, + data.employee, + ]), + () => { + frappe.call({ + method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance", + args: { + data: data, + }, + callback: function (r) { + if (r.message === 1) { + frappe.show_alert({ + message: __("Attendance Marked"), + indicator: "blue", + }); + cur_dialog.hide(); + } + }, + }); + } + ); } dialog.hide(); list_view.refresh(); }, - primary_action_label: __('Mark Attendance') - + primary_action_label: __("Mark Attendance"), }); dialog.show(); }); }, - get_multi_select_options: function(employee, month, exclude_holidays) { - return new Promise(resolve => { - frappe.call({ - method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days', - async: false, - args: { - employee: employee, - month: month, - exclude_holidays: exclude_holidays - } - }).then(r => { - var options = []; - for (var d in r.message) { - var momentObj = moment(r.message[d], 'YYYY-MM-DD'); - var date = momentObj.format('DD-MM-YYYY'); - options.push({ - "label": date, - "value": r.message[d], - "checked": 1 - }); - } - resolve(options); - }); + reset_dialog: function (dialog) { + let fields = dialog.fields_dict; + + dialog.set_df_property( + "time_period_section", + "hidden", + fields.employee.value ? 0 : 1 + ); + + dialog.set_df_property("days_section", "hidden", 1); + dialog.set_df_property("unmarked_days", "options", []); + dialog.no_unmarked_days_left = false; + fields.exclude_holidays.value = false; + + fields.to_date.datepicker.update({ + maxDate: moment().subtract(1, 'days').toDate() }); - } + + this.get_unmarked_days(dialog) + }, + + get_unmarked_days: function (dialog) { + let fields = dialog.fields_dict; + if (fields.employee.value && fields.from_date.value && fields.to_date.value) { + dialog.set_df_property("days_section", "hidden", 0); + dialog.set_df_property("status", "hidden", 0); + dialog.set_df_property("exclude_holidays", "hidden", 0); + dialog.no_unmarked_days_left = false; + + frappe + .call({ + method: "erpnext.hr.doctype.attendance.attendance.get_unmarked_days", + async: false, + args: { + employee: fields.employee.value, + from_date: fields.from_date.value, + to_date: fields.to_date.value, + exclude_holidays: fields.exclude_holidays.value, + }, + }) + .then((r) => { + var options = []; + + for (var d in r.message) { + var momentObj = moment(r.message[d], "YYYY-MM-DD"); + var date = momentObj.format("DD-MM-YYYY"); + options.push({ + label: date, + value: r.message[d], + checked: 1, + }); + } + + dialog.set_df_property( + "unmarked_days", + "options", + options.length > 0 ? options : [] + ); + dialog.no_unmarked_days_left = options.length === 0; + }); + } + }, }; diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index b78f5c06206..88f4c387bde 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import ( add_days, add_months, + get_first_day, get_last_day, get_year_ending, get_year_start, @@ -13,11 +14,7 @@ from frappe.utils import ( nowdate, ) -from erpnext.hr.doctype.attendance.attendance import ( - get_month_map, - get_unmarked_days, - mark_attendance, -) +from erpnext.hr.doctype.attendance.attendance import get_unmarked_days, mark_attendance from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.tests.test_utils import get_first_sunday @@ -28,7 +25,7 @@ class TestAttendance(FrappeTestCase): def setUp(self): from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list - from_date = get_year_start(getdate()) + from_date = get_year_start(add_months(getdate(), -1)) to_date = get_year_ending(getdate()) self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date) @@ -55,9 +52,10 @@ class TestAttendance(FrappeTestCase): frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) mark_attendance(employee, attendance_date, "Present") - month_name = get_month_name(attendance_date) - unmarked_days = get_unmarked_days(employee, month_name) + unmarked_days = get_unmarked_days( + employee, get_first_day(attendance_date), get_last_day(attendance_date) + ) unmarked_days = [getdate(date) for date in unmarked_days] # attendance already marked for the day @@ -81,9 +79,10 @@ class TestAttendance(FrappeTestCase): frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) mark_attendance(employee, attendance_date, "Present") - month_name = get_month_name(attendance_date) - unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True) + unmarked_days = unmarked_days = get_unmarked_days( + employee, get_first_day(attendance_date), get_last_day(attendance_date), exclude_holidays=True + ) unmarked_days = [getdate(date) for date in unmarked_days] # attendance already marked for the day @@ -110,9 +109,10 @@ class TestAttendance(FrappeTestCase): attendance_date = add_days(date, 2) mark_attendance(employee, attendance_date, "Present") - month_name = get_month_name(attendance_date) - unmarked_days = get_unmarked_days(employee, month_name) + unmarked_days = get_unmarked_days( + employee, get_first_day(attendance_date), get_last_day(attendance_date) + ) unmarked_days = [getdate(date) for date in unmarked_days] # attendance already marked for the day @@ -124,10 +124,3 @@ class TestAttendance(FrappeTestCase): def tearDown(self): frappe.db.rollback() - - -def get_month_name(date): - month_number = date.month - for month, number in get_month_map().items(): - if number == month_number: - return month diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py index 91da08eee50..84c66a7bf3f 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py @@ -1,7 +1,7 @@ import frappe from dateutil.relativedelta import relativedelta from frappe.tests.utils import FrappeTestCase -from frappe.utils import now_datetime +from frappe.utils import add_months, getdate from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.hr.doctype.employee.test_employee import make_employee @@ -14,9 +14,7 @@ class TestMonthlyAttendanceSheet(FrappeTestCase): frappe.db.delete("Attendance", {"employee": self.employee}) def test_monthly_attendance_sheet_report(self): - now = now_datetime() - previous_month = now.month - 1 - previous_month_first = now.replace(day=1).replace(month=previous_month).date() + previous_month_first = add_months(getdate(), -1).replace(day=1) company = frappe.db.get_value("Employee", self.employee, "company") @@ -27,8 +25,8 @@ class TestMonthlyAttendanceSheet(FrappeTestCase): filters = frappe._dict( { - "month": previous_month, - "year": now.year, + "month": previous_month_first.month, + "year": previous_month_first.year, "company": company, } ) diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index bb799af36ea..9f5e3c882aa 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -371,12 +371,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "deposit", fieldtype: "Currency", label: "Deposit", + options: "currency", read_only: 1, }, { fieldname: "withdrawal", fieldtype: "Currency", label: "Withdrawal", + options: "currency", read_only: 1, }, { @@ -394,6 +396,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "allocated_amount", fieldtype: "Currency", label: "Allocated Amount", + options: "Currency", read_only: 1, }, @@ -401,8 +404,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "unallocated_amount", fieldtype: "Currency", label: "Unallocated Amount", + options: "Currency", read_only: 1, }, + { + fieldname: "currency", + fieldtype: "Link", + label: "Currency", + options: "Currency", + read_only: 1, + hidden: 1, + } ]; me.dialog = new frappe.ui.Dialog({ diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index b86659dd2c2..0b25afc52cb 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -217,7 +217,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ args: { item_code: item.item_code, warehouse: item.warehouse, - company: doc.company + company: doc.company, + include_child_warehouses: true } }); } diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index a4e23267cd4..b0ff1ccfcd7 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -153,7 +153,7 @@ def get_parent_item_groups(item_group_name, from_item=False): if from_item and frappe.request.environ.get("HTTP_REFERER"): # base page after 'Home' will vary on Item page - last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1] + last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0] if last_page and last_page in ("shop-by-category", "all-products"): base_nav_page_title = " ".join(last_page.split("-")).title() base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 911c8d0a665..462220c90e3 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -81,6 +81,7 @@ class TestItem(FrappeTestCase): def test_get_item_details(self): # delete modified item price record and make as per test_records frappe.db.sql("""delete from `tabItem Price`""") + frappe.db.sql("""delete from `tabBin`""") to_check = { "item_code": "_Test Item", @@ -101,9 +102,26 @@ class TestItem(FrappeTestCase): "batch_no": None, "uom": "_Test UOM", "conversion_factor": 1.0, + "reserved_qty": 1, + "actual_qty": 5, + "ordered_qty": 10, + "projected_qty": 14, } make_test_objects("Item Price") + make_test_objects( + "Bin", + [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "reserved_qty": 1, + "actual_qty": 5, + "ordered_qty": 10, + "projected_qty": 14, + } + ], + ) company = "_Test Company" currency = frappe.get_cached_value("Company", company, "default_currency") @@ -127,7 +145,7 @@ class TestItem(FrappeTestCase): ) for key, value in to_check.items(): - self.assertEqual(value, details.get(key)) + self.assertEqual(value, details.get(key), key) def test_item_tax_template(self): expected_item_tax_template = [ diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b27abae513e..3da24974919 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4,24 +4,12 @@ import json from collections import defaultdict -from typing import Dict import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import ( - add_days, - cint, - comma_or, - cstr, - flt, - format_time, - formatdate, - getdate, - nowdate, - today, -) +from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate from six import iteritems, itervalues, string_types import erpnext @@ -2566,62 +2554,3 @@ def get_supplied_items(purchase_order): ) return supplied_item_details - - -def audit_incorrect_valuation_entries(): - # Audit of stock transfer entries having incorrect valuation - from erpnext.controllers.stock_controller import create_repost_item_valuation_entry - - stock_entries = get_incorrect_stock_entries() - - for stock_entry, values in stock_entries.items(): - reposting_data = frappe._dict( - { - "posting_date": values.posting_date, - "posting_time": values.posting_time, - "voucher_type": "Stock Entry", - "voucher_no": stock_entry, - "company": values.company, - } - ) - - create_repost_item_valuation_entry(reposting_data) - - -def get_incorrect_stock_entries() -> Dict: - stock_entry = frappe.qb.DocType("Stock Entry") - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") - transfer_purposes = [ - "Material Transfer", - "Material Transfer for Manufacture", - "Send to Subcontractor", - ] - - query = ( - frappe.qb.from_(stock_entry) - .inner_join(stock_ledger_entry) - .on(stock_entry.name == stock_ledger_entry.voucher_no) - .select( - stock_entry.name, - stock_entry.company, - stock_entry.posting_date, - stock_entry.posting_time, - Sum(stock_ledger_entry.stock_value_difference).as_("stock_value"), - ) - .where( - (stock_entry.docstatus == 1) - & (stock_entry.purpose.isin(transfer_purposes)) - & (stock_ledger_entry.modified > add_days(today(), -2)) - ) - .groupby(stock_ledger_entry.voucher_detail_no) - .having(Sum(stock_ledger_entry.stock_value_difference) != 0) - ) - - data = query.run(as_dict=True) - stock_entries = {} - - for row in data: - if abs(row.stock_value) > 0.1 and row.name not in stock_entries: - stock_entries.setdefault(row.name, row) - - return stock_entries diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c38b3d997a3..4fe3cc1fdab 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -18,8 +18,6 @@ from erpnext.stock.doctype.item.test_item import ( from erpnext.stock.doctype.serial_no.serial_no import * # noqa from erpnext.stock.doctype.stock_entry.stock_entry import ( FinishedGoodError, - audit_incorrect_valuation_entries, - get_incorrect_stock_entries, move_sample_to_retention_warehouse, ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -1573,44 +1571,6 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(BatchExpiredError, se.save) - def test_audit_incorrect_stock_entries(self): - item_code = "Test Incorrect Valuation Rate Item - 001" - create_item(item_code=item_code, is_stock_item=1) - - make_stock_entry( - item_code=item_code, - purpose="Material Receipt", - posting_date=add_days(nowdate(), -10), - qty=2, - rate=500, - to_warehouse="_Test Warehouse - _TC", - ) - - transfer_entry = make_stock_entry( - item_code=item_code, - purpose="Material Transfer", - qty=2, - rate=500, - from_warehouse="_Test Warehouse - _TC", - to_warehouse="_Test Warehouse 1 - _TC", - ) - - sle_name = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": transfer_entry.name, "actual_qty": (">", 0)}, "name" - ) - - frappe.db.set_value( - "Stock Ledger Entry", sle_name, {"modified": add_days(now(), -1), "stock_value_difference": 10} - ) - - stock_entries = get_incorrect_stock_entries() - self.assertTrue(transfer_entry.name in stock_entries) - - audit_incorrect_valuation_entries() - - stock_entries = get_incorrect_stock_entries() - self.assertFalse(transfer_entry.name in stock_entries) - def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 47911b46146..ba07d01f469 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -102,9 +102,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru elif out.get("warehouse"): if doc and doc.get("doctype") == "Purchase Order": # calculate company_total_stock only for po - bin_details = get_bin_details(args.item_code, out.warehouse, args.company) + bin_details = get_bin_details( + args.item_code, out.warehouse, args.company, include_child_warehouses=True + ) else: - bin_details = get_bin_details(args.item_code, out.warehouse) + bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True) out.update(bin_details) @@ -1045,7 +1047,9 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa res[fieldname] = pos_profile.get(fieldname) if res.get("warehouse"): - res.actual_qty = get_bin_details(args.item_code, res.warehouse).get("actual_qty") + res.actual_qty = get_bin_details( + args.item_code, res.warehouse, include_child_warehouses=True + ).get("actual_qty") return res @@ -1156,16 +1160,31 @@ def get_projected_qty(item_code, warehouse): @frappe.whitelist() -def get_bin_details(item_code, warehouse, company=None): - bin_details = frappe.db.get_value( - "Bin", - {"item_code": item_code, "warehouse": warehouse}, - ["projected_qty", "actual_qty", "reserved_qty"], - as_dict=True, - cache=True, - ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} +def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False): + bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0} + + if warehouse: + from frappe.query_builder.functions import Coalesce, Sum + + from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses + + warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse] + + bin = frappe.qb.DocType("Bin") + bin_details = ( + frappe.qb.from_(bin) + .select( + Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"), + Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"), + Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"), + Coalesce(Sum(bin.ordered_qty), 0).as_("ordered_qty"), + ) + .where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses))) + ).run(as_dict=True)[0] + if company: bin_details["company_total_stock"] = get_company_total_stock(item_code, company) + return bin_details