mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-07 07:02:54 +00:00
Merge pull request #33512 from frappe/version-13-hotfix
chore: release v13
This commit is contained in:
@@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# check if books nor frozen till endate:
|
# 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))
|
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||||
|
|
||||||
if via_journal_entry:
|
if via_journal_entry:
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
|||||||
dict(
|
dict(
|
||||||
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
|
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,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
gl_amount, transaction_amount = (
|
gl_amount, transaction_amount = (
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
|||||||
)
|
)
|
||||||
elif doc.payment_type == "Pay":
|
elif doc.payment_type == "Pay":
|
||||||
paid_amount_field = (
|
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(
|
return frappe.db.get_value(
|
||||||
|
|||||||
@@ -843,7 +843,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-10-10 20:57:38.340026",
|
"modified": "2022-12-28 16:17:33.484531",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
|||||||
@@ -36,11 +36,11 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-09-13 23:40:41.479208",
|
"modified": "2022-12-28 23:40:41.479208",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Tax Withheld Vouchers",
|
"name": "Tax Withheld Vouchers",
|
||||||
"naming_rule": "Autoincrement",
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, qb, scrub
|
||||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -95,6 +95,9 @@ class ReceivablePayableReport(object):
|
|||||||
# Get return entries
|
# Get return entries
|
||||||
self.get_return_entries()
|
self.get_return_entries()
|
||||||
|
|
||||||
|
# Get Exchange Rate Revaluations
|
||||||
|
self.get_exchange_rate_revaluations()
|
||||||
|
|
||||||
self.data = []
|
self.data = []
|
||||||
for gle in self.gl_entries:
|
for gle in self.gl_entries:
|
||||||
self.update_voucher_balance(gle)
|
self.update_voucher_balance(gle)
|
||||||
@@ -258,7 +261,8 @@ class ReceivablePayableReport(object):
|
|||||||
row.invoice_grand_total = row.invoiced
|
row.invoice_grand_total = row.invoiced
|
||||||
|
|
||||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
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
|
# non-zero oustanding, we must consider this row
|
||||||
|
|
||||||
@@ -1033,3 +1037,17 @@ class ReceivablePayableReport(object):
|
|||||||
"data": {"labels": self.ageing_column_labels, "datasets": rows},
|
"data": {"labels": self.ageing_column_labels, "datasets": rows},
|
||||||
"type": "percentage",
|
"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 []
|
||||||
|
|||||||
@@ -1,18 +1,51 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
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.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
||||||
|
|
||||||
|
|
||||||
class TestAccountsReceivable(unittest.TestCase):
|
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 `tabSales Invoice` where company='_Test Company 2'")
|
||||||
frappe.db.sql("delete from `tabGL Entry` 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 = {
|
filters = {
|
||||||
"company": "_Test Company 2",
|
"company": "_Test Company 2",
|
||||||
"based_on_payment_terms": 1,
|
"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
|
# 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)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data = [[100, 30], [100, 50], [100, 20]]
|
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")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
@@ -81,6 +180,7 @@ def make_sales_invoice():
|
|||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not no_payment_schedule:
|
||||||
si.append(
|
si.append(
|
||||||
"payment_schedule",
|
"payment_schedule",
|
||||||
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
||||||
@@ -94,9 +194,12 @@ def make_sales_invoice():
|
|||||||
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
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):
|
def make_payment(docname):
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ def get_conditions(filters):
|
|||||||
):
|
):
|
||||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
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"):
|
if filters.get("project"):
|
||||||
conditions.append("project in %(project)s")
|
conditions.append("project in %(project)s")
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
|||||||
item_details = get_item_details()
|
item_details = get_item_details()
|
||||||
|
|
||||||
for d in item_list:
|
for d in item_list:
|
||||||
if not d.stock_qty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
item_record = item_details.get(d.item_code)
|
item_record = item_details.get(d.item_code)
|
||||||
|
|
||||||
purchase_receipt = None
|
purchase_receipt = None
|
||||||
@@ -94,7 +91,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
|||||||
"expense_account": expense_account,
|
"expense_account": expense_account,
|
||||||
"stock_qty": d.stock_qty,
|
"stock_qty": d.stock_qty,
|
||||||
"stock_uom": d.stock_uom,
|
"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,
|
"amount": d.base_net_amount,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -573,7 +573,12 @@ class AccountsController(TransactionBase):
|
|||||||
if bool(uom) != bool(stock_uom): # xor
|
if bool(uom) != bool(stock_uom): # xor
|
||||||
item.stock_uom = item.uom = uom or stock_uom
|
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":
|
if self.doctype == "Purchase Invoice":
|
||||||
self.set_expense_account(for_validate)
|
self.set_expense_account(for_validate)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class SellingController(StockController):
|
|||||||
super(SellingController, self).onload()
|
super(SellingController, self).onload()
|
||||||
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
|
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
|
||||||
for item in self.get("items"):
|
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):
|
def validate(self):
|
||||||
super(SellingController, self).validate()
|
super(SellingController, self).validate()
|
||||||
|
|||||||
@@ -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_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.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.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"],
|
"weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"],
|
||||||
"monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"],
|
"monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"],
|
||||||
|
|||||||
@@ -5,7 +5,16 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
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
|
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))
|
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
||||||
|
|
||||||
def unlink_attendance_from_checkins(self):
|
def unlink_attendance_from_checkins(self):
|
||||||
from frappe.utils import get_link_to_form
|
|
||||||
|
|
||||||
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
|
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
|
||||||
linked_logs = (
|
linked_logs = (
|
||||||
frappe.qb.from_(EmployeeCheckin)
|
frappe.qb.from_(EmployeeCheckin)
|
||||||
@@ -221,75 +228,39 @@ def mark_bulk_attendance(data):
|
|||||||
attendance.submit()
|
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()
|
@frappe.whitelist()
|
||||||
def get_unmarked_days(employee, month, exclude_holidays=0):
|
def get_unmarked_days(employee, from_date, to_date, exclude_holidays=0):
|
||||||
import calendar
|
|
||||||
|
|
||||||
month_map = get_month_map()
|
|
||||||
today = get_datetime()
|
|
||||||
|
|
||||||
joining_date, relieving_date = frappe.get_cached_value(
|
joining_date, relieving_date = frappe.get_cached_value(
|
||||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
"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]:
|
from_date = max(getdate(from_date), joining_date or getdate(from_date))
|
||||||
start_day = joining_date.day
|
to_date = min(getdate(to_date), relieving_date or getdate(to_date))
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
records = frappe.get_all(
|
records = frappe.get_all(
|
||||||
"Attendance",
|
"Attendance",
|
||||||
fields=["attendance_date", "employee"],
|
fields=["attendance_date", "employee"],
|
||||||
filters=[
|
filters=[
|
||||||
["attendance_date", ">=", month_start],
|
["attendance_date", ">=", from_date],
|
||||||
["attendance_date", "<=", month_end],
|
["attendance_date", "<=", to_date],
|
||||||
["employee", "=", employee],
|
["employee", "=", employee],
|
||||||
["docstatus", "!=", 2],
|
["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):
|
if cint(exclude_holidays):
|
||||||
holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
|
holiday_dates = get_holiday_dates_for_employee(employee, from_date, to_date)
|
||||||
holidays = [get_datetime(record) for record in holiday_dates]
|
holidays = [getdate(record) for record in holiday_dates]
|
||||||
marked_days.extend(holidays)
|
marked_days.extend(holidays)
|
||||||
|
|
||||||
unmarked_days = []
|
unmarked_days = []
|
||||||
|
|
||||||
for date in dates_of_month:
|
while from_date <= to_date:
|
||||||
date_time = get_datetime(date)
|
if from_date not in marked_days:
|
||||||
if today.day <= date_time.day and today.month <= date_time.month:
|
unmarked_days.append(from_date)
|
||||||
break
|
|
||||||
if date_time not in marked_days:
|
from_date = add_days(from_date, 1)
|
||||||
unmarked_days.append(date)
|
|
||||||
|
|
||||||
return unmarked_days
|
return unmarked_days
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
frappe.listview_settings['Attendance'] = {
|
frappe.listview_settings["Attendance"] = {
|
||||||
add_fields: ["status", "attendance_date"],
|
add_fields: ["status", "attendance_date"],
|
||||||
|
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
if (["Present", "Work From Home"].includes(doc.status)) {
|
if (["Present", "Work From Home"].includes(doc.status)) {
|
||||||
return [__(doc.status), "green", "status,=," + doc.status];
|
return [__(doc.status), "green", "status,=," + doc.status];
|
||||||
@@ -10,92 +11,74 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function(list_view) {
|
onload: function (list_view) {
|
||||||
let me = this;
|
let me = this;
|
||||||
const months = moment.months();
|
|
||||||
const curMonth = moment().format("MMMM");
|
list_view.page.add_inner_button(__("Mark Attendance"), function () {
|
||||||
months.splice(months.indexOf(curMonth) + 1);
|
let first_day_of_month = moment().startOf('month');
|
||||||
list_view.page.add_inner_button(__("Mark Attendance"), function() {
|
|
||||||
|
if (moment().toDate().getDate() === 1) {
|
||||||
|
first_day_of_month = first_day_of_month.subtract(1, "month");
|
||||||
|
}
|
||||||
|
|
||||||
let dialog = new frappe.ui.Dialog({
|
let dialog = new frappe.ui.Dialog({
|
||||||
title: __("Mark Attendance"),
|
title: __("Mark Attendance"),
|
||||||
fields: [{
|
fields: [
|
||||||
fieldname: 'employee',
|
{
|
||||||
label: __('For Employee'),
|
fieldname: "employee",
|
||||||
fieldtype: 'Link',
|
label: __("For Employee"),
|
||||||
options: 'Employee',
|
fieldtype: "Link",
|
||||||
|
options: "Employee",
|
||||||
get_query: () => {
|
get_query: () => {
|
||||||
return {query: "erpnext.controllers.queries.employee_query"};
|
return {
|
||||||
|
query: "erpnext.controllers.queries.employee_query",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
onchange: function() {
|
onchange: () => me.reset_dialog(dialog),
|
||||||
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: "Section Break",
|
||||||
fieldtype: "Select",
|
fieldname: "time_period_section",
|
||||||
fieldname: "month",
|
hidden: 1,
|
||||||
options: months,
|
},
|
||||||
|
{
|
||||||
|
label: __("Start"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
fieldname: "from_date",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
onchange: function() {
|
default: first_day_of_month.toDate(),
|
||||||
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
onchange: () => me.get_unmarked_days(dialog),
|
||||||
dialog.set_df_property("status", "hidden", 0);
|
},
|
||||||
dialog.set_df_property("exclude_holidays", "hidden", 0);
|
{
|
||||||
dialog.set_df_property("unmarked_days", "options", []);
|
fieldtype: "Column Break",
|
||||||
dialog.no_unmarked_days_left = false;
|
fieldname: "time_period_column",
|
||||||
me.get_multi_select_options(
|
},
|
||||||
dialog.fields_dict.employee.value,
|
{
|
||||||
dialog.fields_dict.month.value,
|
label: __("End"),
|
||||||
dialog.fields_dict.exclude_holidays.get_value()
|
fieldtype: "Date",
|
||||||
).then(options => {
|
fieldname: "to_date",
|
||||||
if (options.length > 0) {
|
reqd: 1,
|
||||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
default: moment().subtract(1, 'days').toDate(),
|
||||||
dialog.set_df_property("unmarked_days", "options", options);
|
onchange: () => me.get_unmarked_days(dialog),
|
||||||
} else {
|
},
|
||||||
dialog.no_unmarked_days_left = true;
|
{
|
||||||
}
|
fieldtype: "Section Break",
|
||||||
});
|
fieldname: "days_section",
|
||||||
}
|
hidden: 1,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("Status"),
|
label: __("Status"),
|
||||||
fieldtype: "Select",
|
fieldtype: "Select",
|
||||||
fieldname: "status",
|
fieldname: "status",
|
||||||
options: ["Present", "Absent", "Half Day", "Work From Home"],
|
options: ["Present", "Absent", "Half Day", "Work From Home"],
|
||||||
hidden: 1,
|
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("Exclude Holidays"),
|
label: __("Exclude Holidays"),
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
fieldname: "exclude_holidays",
|
fieldname: "exclude_holidays",
|
||||||
hidden: 1,
|
onchange: () => me.get_unmarked_days(dialog),
|
||||||
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"),
|
label: __("Unmarked Attendance for days"),
|
||||||
@@ -103,64 +86,110 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
fieldtype: "MultiCheck",
|
fieldtype: "MultiCheck",
|
||||||
options: [],
|
options: [],
|
||||||
columns: 2,
|
columns: 2,
|
||||||
hidden: 1
|
},
|
||||||
}],
|
],
|
||||||
primary_action(data) {
|
primary_action(data) {
|
||||||
if (cur_dialog.no_unmarked_days_left) {
|
if (cur_dialog.no_unmarked_days_left) {
|
||||||
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
|
frappe.msgprint(
|
||||||
[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
|
__(
|
||||||
|
"Attendance from {0} to {1} has already been marked for the Employee {2}",
|
||||||
|
[data.from_date, data.to_date, data.employee]
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
|
frappe.confirm(
|
||||||
|
__("Mark attendance as {0} for {1} on selected dates?", [
|
||||||
|
data.status,
|
||||||
|
data.employee,
|
||||||
|
]),
|
||||||
|
() => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
|
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
|
||||||
args: {
|
args: {
|
||||||
data: data
|
data: data,
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message === 1) {
|
if (r.message === 1) {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
message: __("Attendance Marked"),
|
message: __("Attendance Marked"),
|
||||||
indicator: 'blue'
|
indicator: "blue",
|
||||||
});
|
});
|
||||||
cur_dialog.hide();
|
cur_dialog.hide();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
list_view.refresh();
|
list_view.refresh();
|
||||||
},
|
},
|
||||||
primary_action_label: __('Mark Attendance')
|
primary_action_label: __("Mark Attendance"),
|
||||||
|
|
||||||
});
|
});
|
||||||
dialog.show();
|
dialog.show();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
get_multi_select_options: function(employee, month, exclude_holidays) {
|
reset_dialog: function (dialog) {
|
||||||
return new Promise(resolve => {
|
let fields = dialog.fields_dict;
|
||||||
frappe.call({
|
|
||||||
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
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,
|
async: false,
|
||||||
args: {
|
args: {
|
||||||
employee: employee,
|
employee: fields.employee.value,
|
||||||
month: month,
|
from_date: fields.from_date.value,
|
||||||
exclude_holidays: exclude_holidays
|
to_date: fields.to_date.value,
|
||||||
}
|
exclude_holidays: fields.exclude_holidays.value,
|
||||||
}).then(r => {
|
},
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
var options = [];
|
var options = [];
|
||||||
|
|
||||||
for (var d in r.message) {
|
for (var d in r.message) {
|
||||||
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
|
var momentObj = moment(r.message[d], "YYYY-MM-DD");
|
||||||
var date = momentObj.format('DD-MM-YYYY');
|
var date = momentObj.format("DD-MM-YYYY");
|
||||||
options.push({
|
options.push({
|
||||||
"label": date,
|
label: date,
|
||||||
"value": r.message[d],
|
value: r.message[d],
|
||||||
"checked": 1
|
checked: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resolve(options);
|
|
||||||
});
|
dialog.set_df_property(
|
||||||
|
"unmarked_days",
|
||||||
|
"options",
|
||||||
|
options.length > 0 ? options : []
|
||||||
|
);
|
||||||
|
dialog.no_unmarked_days_left = options.length === 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase
|
|||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
|
get_first_day,
|
||||||
get_last_day,
|
get_last_day,
|
||||||
get_year_ending,
|
get_year_ending,
|
||||||
get_year_start,
|
get_year_start,
|
||||||
@@ -13,11 +14,7 @@ from frappe.utils import (
|
|||||||
nowdate,
|
nowdate,
|
||||||
)
|
)
|
||||||
|
|
||||||
from erpnext.hr.doctype.attendance.attendance import (
|
from erpnext.hr.doctype.attendance.attendance import get_unmarked_days, mark_attendance
|
||||||
get_month_map,
|
|
||||||
get_unmarked_days,
|
|
||||||
mark_attendance,
|
|
||||||
)
|
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.hr.tests.test_utils import get_first_sunday
|
from erpnext.hr.tests.test_utils import get_first_sunday
|
||||||
|
|
||||||
@@ -28,7 +25,7 @@ class TestAttendance(FrappeTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
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())
|
to_date = get_year_ending(getdate())
|
||||||
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
|
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)
|
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||||
|
|
||||||
mark_attendance(employee, attendance_date, "Present")
|
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]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
# attendance already marked for the day
|
# attendance already marked for the day
|
||||||
@@ -81,9 +79,10 @@ class TestAttendance(FrappeTestCase):
|
|||||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||||
|
|
||||||
mark_attendance(employee, attendance_date, "Present")
|
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]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
# attendance already marked for the day
|
# attendance already marked for the day
|
||||||
@@ -110,9 +109,10 @@ class TestAttendance(FrappeTestCase):
|
|||||||
|
|
||||||
attendance_date = add_days(date, 2)
|
attendance_date = add_days(date, 2)
|
||||||
mark_attendance(employee, attendance_date, "Present")
|
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]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
# attendance already marked for the day
|
# attendance already marked for the day
|
||||||
@@ -124,10 +124,3 @@ class TestAttendance(FrappeTestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
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
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from frappe.tests.utils import FrappeTestCase
|
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.attendance.attendance import mark_attendance
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
@@ -14,9 +14,7 @@ class TestMonthlyAttendanceSheet(FrappeTestCase):
|
|||||||
frappe.db.delete("Attendance", {"employee": self.employee})
|
frappe.db.delete("Attendance", {"employee": self.employee})
|
||||||
|
|
||||||
def test_monthly_attendance_sheet_report(self):
|
def test_monthly_attendance_sheet_report(self):
|
||||||
now = now_datetime()
|
previous_month_first = add_months(getdate(), -1).replace(day=1)
|
||||||
previous_month = now.month - 1
|
|
||||||
previous_month_first = now.replace(day=1).replace(month=previous_month).date()
|
|
||||||
|
|
||||||
company = frappe.db.get_value("Employee", self.employee, "company")
|
company = frappe.db.get_value("Employee", self.employee, "company")
|
||||||
|
|
||||||
@@ -27,8 +25,8 @@ class TestMonthlyAttendanceSheet(FrappeTestCase):
|
|||||||
|
|
||||||
filters = frappe._dict(
|
filters = frappe._dict(
|
||||||
{
|
{
|
||||||
"month": previous_month,
|
"month": previous_month_first.month,
|
||||||
"year": now.year,
|
"year": previous_month_first.year,
|
||||||
"company": company,
|
"company": company,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -371,12 +371,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldname: "deposit",
|
fieldname: "deposit",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
label: "Deposit",
|
label: "Deposit",
|
||||||
|
options: "currency",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "withdrawal",
|
fieldname: "withdrawal",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
label: "Withdrawal",
|
label: "Withdrawal",
|
||||||
|
options: "currency",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -394,6 +396,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldname: "allocated_amount",
|
fieldname: "allocated_amount",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
label: "Allocated Amount",
|
label: "Allocated Amount",
|
||||||
|
options: "Currency",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -401,8 +404,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldname: "unallocated_amount",
|
fieldname: "unallocated_amount",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
label: "Unallocated Amount",
|
label: "Unallocated Amount",
|
||||||
|
options: "Currency",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "currency",
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: "Currency",
|
||||||
|
options: "Currency",
|
||||||
|
read_only: 1,
|
||||||
|
hidden: 1,
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
me.dialog = new frappe.ui.Dialog({
|
me.dialog = new frappe.ui.Dialog({
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
|||||||
args: {
|
args: {
|
||||||
item_code: item.item_code,
|
item_code: item.item_code,
|
||||||
warehouse: item.warehouse,
|
warehouse: item.warehouse,
|
||||||
company: doc.company
|
company: doc.company,
|
||||||
|
include_child_warehouses: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"):
|
if from_item and frappe.request.environ.get("HTTP_REFERER"):
|
||||||
# base page after 'Home' will vary on Item page
|
# 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"):
|
if last_page and last_page in ("shop-by-category", "all-products"):
|
||||||
base_nav_page_title = " ".join(last_page.split("-")).title()
|
base_nav_page_title = " ".join(last_page.split("-")).title()
|
||||||
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
|
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class TestItem(FrappeTestCase):
|
|||||||
def test_get_item_details(self):
|
def test_get_item_details(self):
|
||||||
# delete modified item price record and make as per test_records
|
# delete modified item price record and make as per test_records
|
||||||
frappe.db.sql("""delete from `tabItem Price`""")
|
frappe.db.sql("""delete from `tabItem Price`""")
|
||||||
|
frappe.db.sql("""delete from `tabBin`""")
|
||||||
|
|
||||||
to_check = {
|
to_check = {
|
||||||
"item_code": "_Test Item",
|
"item_code": "_Test Item",
|
||||||
@@ -101,9 +102,26 @@ class TestItem(FrappeTestCase):
|
|||||||
"batch_no": None,
|
"batch_no": None,
|
||||||
"uom": "_Test UOM",
|
"uom": "_Test UOM",
|
||||||
"conversion_factor": 1.0,
|
"conversion_factor": 1.0,
|
||||||
|
"reserved_qty": 1,
|
||||||
|
"actual_qty": 5,
|
||||||
|
"ordered_qty": 10,
|
||||||
|
"projected_qty": 14,
|
||||||
}
|
}
|
||||||
|
|
||||||
make_test_objects("Item Price")
|
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"
|
company = "_Test Company"
|
||||||
currency = frappe.get_cached_value("Company", company, "default_currency")
|
currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||||
@@ -127,7 +145,7 @@ class TestItem(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for key, value in to_check.items():
|
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):
|
def test_item_tax_template(self):
|
||||||
expected_item_tax_template = [
|
expected_item_tax_template = [
|
||||||
|
|||||||
@@ -4,24 +4,12 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
|
||||||
add_days,
|
|
||||||
cint,
|
|
||||||
comma_or,
|
|
||||||
cstr,
|
|
||||||
flt,
|
|
||||||
format_time,
|
|
||||||
formatdate,
|
|
||||||
getdate,
|
|
||||||
nowdate,
|
|
||||||
today,
|
|
||||||
)
|
|
||||||
from six import iteritems, itervalues, string_types
|
from six import iteritems, itervalues, string_types
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -2566,62 +2554,3 @@ def get_supplied_items(purchase_order):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return supplied_item_details
|
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
|
|
||||||
|
|||||||
@@ -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.serial_no.serial_no import * # noqa
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry import (
|
from erpnext.stock.doctype.stock_entry.stock_entry import (
|
||||||
FinishedGoodError,
|
FinishedGoodError,
|
||||||
audit_incorrect_valuation_entries,
|
|
||||||
get_incorrect_stock_entries,
|
|
||||||
move_sample_to_retention_warehouse,
|
move_sample_to_retention_warehouse,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
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)
|
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):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -102,9 +102,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
|||||||
elif out.get("warehouse"):
|
elif out.get("warehouse"):
|
||||||
if doc and doc.get("doctype") == "Purchase Order":
|
if doc and doc.get("doctype") == "Purchase Order":
|
||||||
# calculate company_total_stock only for po
|
# 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:
|
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)
|
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)
|
res[fieldname] = pos_profile.get(fieldname)
|
||||||
|
|
||||||
if res.get("warehouse"):
|
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
|
return res
|
||||||
|
|
||||||
@@ -1156,16 +1160,31 @@ def get_projected_qty(item_code, warehouse):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_bin_details(item_code, warehouse, company=None):
|
def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False):
|
||||||
bin_details = frappe.db.get_value(
|
bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0}
|
||||||
"Bin",
|
|
||||||
{"item_code": item_code, "warehouse": warehouse},
|
if warehouse:
|
||||||
["projected_qty", "actual_qty", "reserved_qty"],
|
from frappe.query_builder.functions import Coalesce, Sum
|
||||||
as_dict=True,
|
|
||||||
cache=True,
|
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||||
) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
|
|
||||||
|
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:
|
if company:
|
||||||
bin_details["company_total_stock"] = get_company_total_stock(item_code, company)
|
bin_details["company_total_stock"] = get_company_total_stock(item_code, company)
|
||||||
|
|
||||||
return bin_details
|
return bin_details
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user