diff --git a/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json index 4b871ae88d1..7e50962a87f 100644 --- a/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json +++ b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json @@ -1,7 +1,7 @@ { "actions": [], "allow_rename": 1, - "creation": "2022-01-03 18:10:11.697198", + "creation": "2022-01-13 20:07:30.096306", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -20,7 +20,7 @@ }, { "fieldname": "percentage", - "fieldtype": "Int", + "fieldtype": "Percent", "in_list_view": 1, "label": "Percentage (%)", "reqd": 1 @@ -29,7 +29,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-03 18:10:20.029821", + "modified": "2022-02-01 22:22:31.589523", "modified_by": "Administrator", "module": "Accounts", "name": "Cost Center Allocation Percentage", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index f66abdc66bc..97d34e0a714 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -42,7 +42,6 @@ class POSInvoice(SalesInvoice): self.validate_serialised_or_batched_item() self.validate_stock_availablility() self.validate_return_items_qty() - self.validate_non_stock_items() self.set_status() self.set_account_for_mode_of_payment() self.validate_pos() @@ -175,9 +174,11 @@ class POSInvoice(SalesInvoice): def validate_stock_availablility(self): if self.is_return or self.docstatus != 1: return - allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') for d in self.get('items'): + is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item')) + if is_service_item: + return if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) @@ -188,7 +189,7 @@ class POSInvoice(SalesInvoice): if allow_negative_stock: return - available_stock = get_stock_availability(d.item_code, d.warehouse) + available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse) item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) if flt(available_stock) <= 0: @@ -259,14 +260,6 @@ class POSInvoice(SalesInvoice): .format(d.idx, bold_serial_no, bold_return_against) ) - def validate_non_stock_items(self): - for d in self.get("items"): - is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") - if not is_stock_item: - if not frappe.db.exists('Product Bundle', d.item_code): - frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.") - .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) - def validate_mode_of_payment(self): if len(self.payments) == 0: frappe.throw(_("At least one mode of payment is required for POS invoice.")) @@ -506,12 +499,18 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() def get_stock_availability(item_code, warehouse): if frappe.db.get_value('Item', item_code, 'is_stock_item'): + is_stock_item = True bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) - return bin_qty - pos_sales_qty + return bin_qty - pos_sales_qty, is_stock_item else: + is_stock_item = False if frappe.db.exists('Product Bundle', item_code): - return get_bundle_availability(item_code, warehouse) + return get_bundle_availability(item_code, warehouse), is_stock_item + else: + # Is a service item + return 0, is_stock_item + def get_bundle_availability(bundle_item_code, warehouse): product_bundle = frappe.get_doc('Product Bundle', bundle_item_code) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b3642181ac0..279557adc76 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -548,6 +548,10 @@ class PurchaseInvoice(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \ + 'enable_provisional_accounting_for_non_stock_items')) + + purchase_receipt_doc_map = {} for item in self.get("items"): if flt(item.base_net_amount): @@ -643,19 +647,23 @@ class PurchaseInvoice(BuyingController): else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) - auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) - - if auto_accounting_for_non_stock_items: - service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") - + if provisional_accounting_for_non_stock_items: if item.purchase_receipt: + provisional_account = self.get_company_default("default_provisional_account") + purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) + + if not purchase_receipt_doc: + purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt) + purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc + # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0, 'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail, - 'account':service_received_but_not_billed_account}, ['name']) + 'account':provisional_account}, ['name']) if expense_booked_in_pr: - expense_account = service_received_but_not_billed_account + # Intentionally passing purchase invoice item to handle partial billing + purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1) if not self.is_internal_transfer(): gl_entries.append(self.get_gl_dict({ diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 21846bb76c8..d51a008d943 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.exceptions import InvalidCurrency from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as create_purchase_invoice_from_receipt, +) from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_taxes, make_purchase_receipt, @@ -1147,8 +1152,6 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_advance_taxes(self): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice - from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order # create a new supplier to test supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier', @@ -1221,6 +1224,45 @@ class TestPurchaseInvoice(unittest.TestCase): payment_entry.load_from_db() self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def test_provisional_accounting_entry(self): + item = create_item("_Test Non Stock Item", is_stock_item=0) + provisional_account = create_account(account_name="Provision Account", + parent_account="Current Liabilities - _TC", company="_Test Company") + + company = frappe.get_doc('Company', '_Test Company') + company.enable_provisional_accounting_for_non_stock_items = 1 + company.default_provisional_account = provisional_account + company.save() + + pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)) + + pi = create_purchase_invoice_from_receipt(pr.name) + pi.set_posting_time = 1 + pi.posting_date = add_days(pr.posting_date, -1) + pi.items[0].expense_account = 'Cost of Goods Sold - _TC' + pi.save() + pi.submit() + + # Check GLE for Purchase Invoice + expected_gle = [ + ['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)], + ['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)] + ] + + check_gl_entries(self, pi.name, expected_gle, pi.posting_date) + + expected_gle_for_purchase_receipt = [ + ["Provision Account - _TC", 250, 0, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date], + ["Provision Account - _TC", 0, 250, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date] + ] + + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + + company.enable_provisional_accounting_for_non_stock_items = 0 + company.save() + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql("""select account, debit, credit, posting_date from `tabGL Entry` diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 4ff851d7f94..75fcaee3832 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -204,7 +204,7 @@ class SellingController(StockController): valuation_rate_map = {} for item in self.items: - if not item.item_code: + if not item.item_code or item.is_free_item: continue last_purchase_rate, is_stock_item = frappe.get_cached_value( @@ -251,7 +251,7 @@ class SellingController(StockController): valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate for item in self.items: - if not item.item_code: + if not item.item_code or item.is_free_item: continue last_valuation_rate = valuation_rate_map.get( diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2912d3eb0bd..8d17683953e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -40,7 +40,10 @@ class StockController(AccountsController): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - if cint(erpnext.is_perpetual_inventory_enabled(self.company)): + provisional_accounting_for_non_stock_items = \ + cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + + if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items: warehouse_account = get_warehouse_account_map(self.company) if self.docstatus==1: diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py index 559bd393e62..0bb66374d1e 100644 --- a/erpnext/hr/doctype/employee/employee_reminders.py +++ b/erpnext/hr/doctype/employee/employee_reminders.py @@ -20,6 +20,7 @@ def send_reminders_in_advance_weekly(): send_advance_holiday_reminders("Weekly") + def send_reminders_in_advance_monthly(): to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders")) frequency = frappe.db.get_single_value("HR Settings", "frequency") @@ -28,6 +29,7 @@ def send_reminders_in_advance_monthly(): send_advance_holiday_reminders("Monthly") + def send_advance_holiday_reminders(frequency): """Send Holiday Reminders in Advance to Employees `frequency` (str): 'Weekly' or 'Monthly' @@ -42,7 +44,7 @@ def send_advance_holiday_reminders(frequency): else: return - employees = frappe.db.get_all('Employee', pluck='name') + employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name') for employee in employees: holidays = get_holidays_for_employee( employee, @@ -51,10 +53,13 @@ def send_advance_holiday_reminders(frequency): raise_exception=False ) - if not (holidays is None): - send_holidays_reminder_in_advance(employee, holidays) + send_holidays_reminder_in_advance(employee, holidays) + def send_holidays_reminder_in_advance(employee, holidays): + if not holidays: + return + employee_doc = frappe.get_doc('Employee', employee) employee_email = get_employee_email(employee_doc) frequency = frappe.db.get_single_value("HR Settings", "frequency") @@ -101,6 +106,7 @@ def send_birthday_reminders(): reminder_text, message = get_birthday_reminder_text_and_message(others) send_birthday_reminder(person_email, reminder_text, others, message) + def get_birthday_reminder_text_and_message(birthday_persons): if len(birthday_persons) == 1: birthday_person_text = birthday_persons[0]['name'] @@ -116,6 +122,7 @@ def get_birthday_reminder_text_and_message(birthday_persons): return reminder_text, message + def send_birthday_reminder(recipients, reminder_text, birthday_persons, message): frappe.sendmail( recipients=recipients, @@ -129,10 +136,12 @@ def send_birthday_reminder(recipients, reminder_text, birthday_persons, message) header=_("Birthday Reminder 🎂") ) + def get_employees_who_are_born_today(): """Get all employee born today & group them based on their company""" return get_employees_having_an_event_today("birthday") + def get_employees_having_an_event_today(event_type): """Get all employee who have `event_type` today & group them based on their company. `event_type` @@ -210,13 +219,14 @@ def send_work_anniversary_reminders(): reminder_text, message = get_work_anniversary_reminder_text_and_message(others) send_work_anniversary_reminder(person_email, reminder_text, others, message) + def get_work_anniversary_reminder_text_and_message(anniversary_persons): if len(anniversary_persons) == 1: anniversary_person = anniversary_persons[0]['name'] persons_name = anniversary_person # Number of years completed at the company completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year - anniversary_person += f" completed {completed_years} years" + anniversary_person += f" completed {completed_years} year(s)" else: person_names_with_years = [] names = [] @@ -225,7 +235,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): names.append(person_text) # Number of years completed at the company completed_years = getdate().year - person['date_of_joining'].year - person_text += f" completed {completed_years} years" + person_text += f" completed {completed_years} year(s)" person_names_with_years.append(person_text) # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim @@ -239,6 +249,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): return reminder_text, message + def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message): frappe.sendmail( recipients=recipients, @@ -249,5 +260,5 @@ def send_work_anniversary_reminder(recipients, reminder_text, anniversary_person anniversary_persons=anniversary_persons, message=message, ), - header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️") + header=_("Work Anniversary Reminder") ) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 8a2da0866e9..67cbea67e1f 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -36,7 +36,7 @@ class TestEmployee(unittest.TestCase): employee_doc.reload() make_holiday_list() - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value("Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List") frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""") salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly", diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py index 52c00982443..bdb51b008a9 100644 --- a/erpnext/hr/doctype/employee/test_employee_reminders.py +++ b/erpnext/hr/doctype/employee/test_employee_reminders.py @@ -5,10 +5,12 @@ import unittest from datetime import timedelta import frappe -from frappe.utils import getdate +from frappe.utils import add_months, getdate +from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change +from erpnext.hr.utils import get_holidays_for_employee class TestEmployeeReminders(unittest.TestCase): @@ -46,6 +48,24 @@ class TestEmployeeReminders(unittest.TestCase): cls.test_employee = test_employee cls.test_holiday_dates = test_holiday_dates + # Employee without holidays in this month/week + test_employee_2 = make_employee('test@empwithoutholiday.io', company="_Test Company") + test_employee_2 = frappe.get_doc('Employee', test_employee_2) + + test_holiday_list = make_holiday_list( + 'TestHolidayRemindersList2', + holiday_dates=[ + {'holiday_date': add_months(getdate(), 1), 'description': 'test holiday1'}, + ], + from_date=add_months(getdate(), -2), + to_date=add_months(getdate(), 2) + ) + test_employee_2.holiday_list = test_holiday_list.name + test_employee_2.save() + + cls.test_employee_2 = test_employee_2 + cls.holiday_list_2 = test_holiday_list + @classmethod def get_test_holiday_dates(cls): today_date = getdate() @@ -61,6 +81,7 @@ class TestEmployeeReminders(unittest.TestCase): def setUp(self): # Clear Email Queue frappe.db.sql("delete from `tabEmail Queue`") + frappe.db.sql("delete from `tabEmail Queue Recipient`") def test_is_holiday(self): from erpnext.hr.doctype.employee.employee import is_holiday @@ -103,11 +124,10 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) def test_work_anniversary_reminders(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:] - employee.company_email = "test@example.com" - employee.company = "_Test Company" - employee.save() + make_employee("test_work_anniversary@gmail.com", + date_of_joining="1998" + frappe.utils.nowdate()[4:], + company="_Test Company", + ) from erpnext.hr.doctype.employee.employee_reminders import ( get_employees_having_an_event_today, @@ -115,7 +135,12 @@ class TestEmployeeReminders(unittest.TestCase): ) employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') - self.assertTrue(employees_having_work_anniversary.get("_Test Company")) + employees = employees_having_work_anniversary.get("_Test Company") or [] + user_ids = [] + for entry in employees: + user_ids.append(entry.user_id) + + self.assertTrue("test_work_anniversary@gmail.com" in user_ids) hr_settings = frappe.get_doc("HR Settings", "HR Settings") hr_settings.send_work_anniversary_reminders = 1 @@ -126,16 +151,24 @@ class TestEmployeeReminders(unittest.TestCase): email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message) - def test_send_holidays_reminder_in_advance(self): - from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance - from erpnext.hr.utils import get_holidays_for_employee + def test_work_anniversary_reminder_not_sent_for_0_years(self): + make_employee("test_work_anniversary_2@gmail.com", + date_of_joining=getdate(), + company="_Test Company", + ) - # Get HR settings and enable advance holiday reminders - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.send_holiday_reminders = 1 - set_proceed_with_frequency_change() - hr_settings.frequency = 'Weekly' - hr_settings.save() + from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today + + employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') + employees = employees_having_work_anniversary.get("_Test Company") or [] + user_ids = [] + for entry in employees: + user_ids.append(entry.user_id) + + self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids) + + def test_send_holidays_reminder_in_advance(self): + setup_hr_settings('Weekly') holidays = get_holidays_for_employee( self.test_employee.get('name'), @@ -151,32 +184,80 @@ class TestEmployeeReminders(unittest.TestCase): email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertEqual(len(email_queue), 1) + self.assertTrue("Holidays this Week." in email_queue[0].message) def test_advance_holiday_reminders_monthly(self): from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly - # Get HR settings and enable advance holiday reminders - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.send_holiday_reminders = 1 - set_proceed_with_frequency_change() - hr_settings.frequency = 'Monthly' - hr_settings.save() + setup_hr_settings('Monthly') + + # disable emp 2, set same holiday list + frappe.db.set_value('Employee', self.test_employee_2.name, { + 'status': 'Left', + 'holiday_list': self.test_employee.holiday_list + }) send_reminders_in_advance_monthly() - email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue(len(email_queue) > 0) + # even though emp 2 has holiday, non-active employees should not be recipients + recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient') + self.assertTrue(self.test_employee_2.user_id not in recipients) + + # teardown: enable emp 2 + frappe.db.set_value('Employee', self.test_employee_2.name, { + 'status': 'Left', + 'holiday_list': self.holiday_list_2 + }) + def test_advance_holiday_reminders_weekly(self): from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly - # Get HR settings and enable advance holiday reminders - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.send_holiday_reminders = 1 - hr_settings.frequency = 'Weekly' - hr_settings.save() + setup_hr_settings('Weekly') + + # disable emp 2, set same holiday list + frappe.db.set_value('Employee', self.test_employee_2.name, { + 'status': 'Left', + 'holiday_list': self.test_employee.holiday_list + }) send_reminders_in_advance_weekly() - email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue(len(email_queue) > 0) + + # even though emp 2 has holiday, non-active employees should not be recipients + recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient') + self.assertTrue(self.test_employee_2.user_id not in recipients) + + # teardown: enable emp 2 + frappe.db.set_value('Employee', self.test_employee_2.name, { + 'status': 'Left', + 'holiday_list': self.holiday_list_2 + }) + + def test_reminder_not_sent_if_no_holdays(self): + setup_hr_settings('Monthly') + + # reminder not sent if there are no holidays + holidays = get_holidays_for_employee( + self.test_employee_2.get('name'), + getdate(), getdate() + timedelta(days=3), + only_non_weekly=True, + raise_exception=False + ) + send_holidays_reminder_in_advance( + self.test_employee_2.get('name'), + holidays + ) + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertEqual(len(email_queue), 0) + + +def setup_hr_settings(frequency=None): + # Get HR settings and enable advance holiday reminders + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_holiday_reminders = 1 + set_proceed_with_frequency_change() + hr_settings.frequency = frequency or 'Weekly' + hr_settings.save() \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 75e99f8991e..6b85927d3e9 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -75,10 +75,8 @@ class TestLeaveApplication(unittest.TestCase): frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec frappe.set_user("Administrator") - - @classmethod - def setUpClass(cls): set_leave_approver() + frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'") def tearDown(self): @@ -134,10 +132,11 @@ class TestLeaveApplication(unittest.TestCase): make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) holiday_list = make_holiday_list() - frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list) + employee = get_employee() + frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) first_sunday = get_first_sunday(holiday_list) - leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application.reload() self.assertEqual(leave_application.total_leave_days, 4) self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) @@ -157,25 +156,28 @@ class TestLeaveApplication(unittest.TestCase): make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) holiday_list = make_holiday_list() - frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list) + employee = get_employee() + frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) first_sunday = get_first_sunday(holiday_list) # already marked attendance on a holiday should be deleted in this case config = { "doctype": "Attendance", - "employee": "_T-Employee-00001", + "employee": employee.name, "status": "Present" } attendance_on_holiday = frappe.get_doc(config) attendance_on_holiday.attendance_date = first_sunday + attendance_on_holiday.flags.ignore_validate = True attendance_on_holiday.save() # already marked attendance on a non-holiday should be updated attendance = frappe.get_doc(config) attendance.attendance_date = add_days(first_sunday, 3) + attendance.flags.ignore_validate = True attendance.save() - leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application.reload() # holiday should be excluded while marking attendance self.assertEqual(leave_application.total_leave_days, 3) @@ -325,7 +327,7 @@ class TestLeaveApplication(unittest.TestCase): employee = get_employee() default_holiday_list = make_holiday_list() - frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list) + frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list) first_sunday = get_first_sunday(default_holiday_list) optional_leave_date = add_days(first_sunday, 1) diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json index 3d070812152..b7b20d945d6 100644 --- a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json +++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json @@ -70,7 +70,6 @@ { "fieldname": "loan_repayment_entry", "fieldtype": "Link", - "hidden": 1, "label": "Loan Repayment Entry", "no_copy": 1, "options": "Loan Repayment", @@ -88,7 +87,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-03-14 20:47:11.725818", + "modified": "2022-01-31 14:50:14.823213", "modified_by": "Administrator", "module": "Loan Management", "name": "Salary Slip Loan", @@ -97,5 +96,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index fc3b971bcb9..8a7634e24ec 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -93,7 +93,7 @@ frappe.ui.form.on("BOM", { }); } - if(frm.doc.docstatus!=0) { + if(frm.doc.docstatus==1) { frm.add_custom_button(__("Work Order"), function() { frm.trigger("make_work_order"); }, __("Create")); diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7d5486f5fed..d6710ddcdc6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -332,6 +332,7 @@ erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v13_0.update_asset_quantity_field erpnext.patches.v13_0.delete_bank_reconciliation_detail +erpnext.patches.v13_0.enable_provisional_accounting [post_model_sync] erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents diff --git a/erpnext/patches/v13_0/enable_provisional_accounting.py b/erpnext/patches/v13_0/enable_provisional_accounting.py new file mode 100644 index 00000000000..8e222700f86 --- /dev/null +++ b/erpnext/patches/v13_0/enable_provisional_accounting.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + frappe.reload_doc("setup", "doctype", "company") + + company = frappe.qb.DocType("Company") + + frappe.qb.update( + company + ).set( + company.enable_provisional_accounting_for_non_stock_items, company.enable_perpetual_inventory_for_non_stock_items + ).set( + company.default_provisional_account, company.service_received_but_not_billed + ).where( + company.enable_perpetual_inventory_for_non_stock_items == 1 + ).where( + company.service_received_but_not_billed.isnotnull() + ).run() \ No newline at end of file diff --git a/erpnext/patches/v14_0/migrate_cost_center_allocations.py b/erpnext/patches/v14_0/migrate_cost_center_allocations.py index 3d217d89e2f..c4f097fdd92 100644 --- a/erpnext/patches/v14_0/migrate_cost_center_allocations.py +++ b/erpnext/patches/v14_0/migrate_cost_center_allocations.py @@ -27,7 +27,7 @@ def create_new_cost_center_allocation_records(cc_allocations): cca.submit() def get_existing_cost_center_allocations(): - if not frappe.get_meta("Cost Center").has_field("enable_distributed_cost_center"): + if not frappe.db.exists("DocType", "Distributed Cost Center"): return par = frappe.qb.DocType("Cost Center") diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index f33443d0d7d..f727ff4378d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -746,11 +746,12 @@ class SalarySlip(TransactionBase): previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component) # get taxable_earnings for current period (all days) - current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption) + current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption, payroll_period=payroll_period) future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1) # get taxable_earnings, addition_earnings for current actual payment days - current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1) + current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, + based_on_payment_days=1, payroll_period=payroll_period) current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax @@ -876,7 +877,7 @@ class SalarySlip(TransactionBase): return total_tax_paid - def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0): + def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None): joining_date, relieving_date = self.get_joining_and_relieving_dates() taxable_earnings = 0 @@ -903,7 +904,7 @@ class SalarySlip(TransactionBase): # Get additional amount based on future recurring additional salary if additional_amount and earning.is_recurring_additional_salary: additional_income += self.get_future_recurring_additional_amount(earning.additional_salary, - earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month + earning.additional_amount, payroll_period) # Used earning.additional_amount to consider the amount for the full month if earning.deduct_full_tax_on_selected_payroll_date: additional_income_with_full_tax += additional_amount @@ -920,7 +921,7 @@ class SalarySlip(TransactionBase): if additional_amount and ded.is_recurring_additional_salary: additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary, - ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month + ded.additional_amount, payroll_period) # Used ded.additional_amount to consider the amount for the full month return frappe._dict({ "taxable_earnings": taxable_earnings, @@ -929,12 +930,18 @@ class SalarySlip(TransactionBase): "flexi_benefits": flexi_benefits }) - def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount): + def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount, payroll_period): future_recurring_additional_amount = 0 to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date') # future month count excluding current from_date, to_date = getdate(self.start_date), getdate(to_date) + + # If recurring period end date is beyond the payroll period, + # last day of payroll period should be considered for recurring period calculation + if getdate(to_date) > getdate(payroll_period.end_date): + to_date = getdate(payroll_period.end_date) + future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month) if future_recurring_period > 0: diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index bcf981b74dc..597fd5a250c 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -147,7 +147,7 @@ class TestSalarySlip(unittest.TestCase): # Payroll based on attendance frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") - emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company") + emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List") frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"}) # mark attendance diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index db5b20e3e19..993c61d5639 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -24,7 +24,7 @@ def search_by_term(search_term, warehouse, price_list): ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"], as_dict=1) - item_stock_qty = get_stock_availability(item_code, warehouse) + item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) price_list_rate, currency = frappe.db.get_value('Item Price', { 'price_list': price_list, 'item_code': item_code @@ -99,7 +99,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ), {'warehouse': warehouse}, as_dict=1) if items_data: - items_data = filter_service_items(items_data) items = [d.item_code for d in items_data] item_prices_data = frappe.get_all("Item Price", fields = ["item_code", "price_list_rate", "currency"], @@ -112,7 +111,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te for item in items_data: item_code = item.item_code item_price = item_prices.get(item_code) or {} - item_stock_qty = get_stock_availability(item_code, warehouse) + item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) row = {} row.update(item) @@ -144,14 +143,6 @@ def search_for_serial_or_batch_or_barcode_number(search_value): return {} -def filter_service_items(items): - for item in items: - if not item['is_stock_item']: - if not frappe.db.exists('Product Bundle', item['item_code']): - items.remove(item) - - return items - def get_conditions(search_term): condition = "(" condition += """item.name like {search_term} diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ce74f6d0a58..56aa24f6ec2 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -630,18 +630,24 @@ erpnext.PointOfSale.Controller = class { } async check_stock_availability(item_row, qty_needed, warehouse) { - const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message; + const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message; + const available_qty = resp[0]; + const is_stock_item = resp[1]; frappe.dom.unfreeze(); const bold_item_code = item_row.item_code.bold(); const bold_warehouse = warehouse.bold(); const bold_available_qty = available_qty.toString().bold() if (!(available_qty > 0)) { - frappe.model.clear_doc(item_row.doctype, item_row.name); - frappe.throw({ - title: __("Not Available"), - message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) - }) + if (is_stock_item) { + frappe.model.clear_doc(item_row.doctype, item_row.name); + frappe.throw({ + title: __("Not Available"), + message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) + }); + } else { + return; + } } else if (available_qty < qty_needed) { frappe.throw({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), @@ -675,8 +681,8 @@ erpnext.PointOfSale.Controller = class { }, callback(res) { if (!me.item_stock_map[item_code]) - me.item_stock_map[item_code] = {} - me.item_stock_map[item_code][warehouse] = res.message; + me.item_stock_map[item_code] = {}; + me.item_stock_map[item_code][warehouse] = res.message[0]; } }); } diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index a30bcd7cf6d..1177615aee9 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -79,14 +79,20 @@ erpnext.PointOfSale.ItemSelector = class { const me = this; // eslint-disable-next-line no-unused-vars const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item; - const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; - + let indicator_color; let qty_to_display = actual_qty; - if (Math.round(qty_to_display) > 999) { - qty_to_display = Math.round(qty_to_display)/1000; - qty_to_display = qty_to_display.toFixed(1) + 'K'; + if (item.is_stock_item) { + indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"); + + if (Math.round(qty_to_display) > 999) { + qty_to_display = Math.round(qty_to_display)/1000; + qty_to_display = qty_to_display.toFixed(1) + 'K'; + } + } else { + indicator_color = ''; + qty_to_display = ''; } function get_item_image_html() { diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 63d96bf85e7..370a3278a01 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -3,7 +3,7 @@ "allow_import": 1, "allow_rename": 1, "autoname": "field:company_name", - "creation": "2013-04-10 08:35:39", + "creation": "2022-01-25 10:29:55.938239", "description": "Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Organization.", "doctype": "DocType", "document_type": "Setup", @@ -77,13 +77,13 @@ "default_finance_book", "auto_accounting_for_stock_settings", "enable_perpetual_inventory", - "enable_perpetual_inventory_for_non_stock_items", + "enable_provisional_accounting_for_non_stock_items", "default_inventory_account", "stock_adjustment_account", "default_in_transit_warehouse", "column_break_32", "stock_received_but_not_billed", - "service_received_but_not_billed", + "default_provisional_account", "expenses_included_in_valuation", "fixed_asset_defaults", "accumulated_depreciation_account", @@ -684,20 +684,6 @@ "label": "Default Buying Terms", "options": "Terms and Conditions" }, - { - "fieldname": "service_received_but_not_billed", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Service Received But Not Billed", - "no_copy": 1, - "options": "Account" - }, - { - "default": "0", - "fieldname": "enable_perpetual_inventory_for_non_stock_items", - "fieldtype": "Check", - "label": "Enable Perpetual Inventory For Non Stock Items" - }, { "fieldname": "default_in_transit_warehouse", "fieldtype": "Link", @@ -741,6 +727,20 @@ "fieldname": "section_break_28", "fieldtype": "Section Break", "label": "Chart of Accounts" + }, + { + "default": "0", + "fieldname": "enable_provisional_accounting_for_non_stock_items", + "fieldtype": "Check", + "label": "Enable Provisional Accounting For Non Stock Items" + }, + { + "fieldname": "default_provisional_account", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Provisional Account", + "no_copy": 1, + "options": "Account" } ], "icon": "fa fa-building", @@ -748,7 +748,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-10-04 12:09:25.833133", + "modified": "2022-01-25 10:33:16.826067", "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -809,5 +809,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 0a02bcd6cd9..95b1e8b9c63 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -10,6 +10,7 @@ import frappe.defaults from frappe import _ from frappe.cache_manager import clear_defaults_cache from frappe.contacts.address_and_contact import load_address_and_contact +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.utils import cint, formatdate, get_timestamp, today from frappe.utils.nestedset import NestedSet @@ -45,7 +46,7 @@ class Company(NestedSet): self.validate_currency() self.validate_coa_input() self.validate_perpetual_inventory() - self.validate_perpetual_inventory_for_non_stock_items() + self.validate_provisional_account_for_non_stock_items() self.check_country_change() self.check_parent_changed() self.set_chart_of_accounts() @@ -187,11 +188,14 @@ class Company(NestedSet): frappe.msgprint(_("Set default inventory account for perpetual inventory"), alert=True, indicator='orange') - def validate_perpetual_inventory_for_non_stock_items(self): + def validate_provisional_account_for_non_stock_items(self): if not self.get("__islocal"): - if cint(self.enable_perpetual_inventory_for_non_stock_items) == 1 and not self.service_received_but_not_billed: - frappe.throw(_("Set default {0} account for perpetual inventory for non stock items").format( - frappe.bold('Service Received But Not Billed'))) + if cint(self.enable_provisional_accounting_for_non_stock_items) == 1 and not self.default_provisional_account: + frappe.throw(_("Set default {0} account for non stock items").format( + frappe.bold('Provisional Account'))) + + make_property_setter("Purchase Receipt", "provisional_expense_account", "hidden", + not self.enable_provisional_accounting_for_non_stock_items, "Check", validate_fields_for_doctype=False) def check_country_change(self): frappe.flags.country_change = False diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 112ddedac29..b54a90eed35 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -106,6 +106,8 @@ "terms", "bill_no", "bill_date", + "accounting_details_section", + "provisional_expense_account", "more_info", "project", "status", @@ -1144,16 +1146,30 @@ "label": "Represents Company", "options": "Company", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "provisional_expense_account", + "fieldtype": "Link", + "hidden": 1, + "label": "Provisional Expense Account", + "options": "Account" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:11:10.181328", + "modified": "2022-02-01 11:40:52.690984", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -1214,6 +1230,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title", "track_changes": 1 diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c97b306c4e6..1257057ea35 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -8,6 +8,7 @@ from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc from frappe.utils import cint, flt, getdate, nowdate +import erpnext from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account @@ -112,6 +113,7 @@ class PurchaseReceipt(BuyingController): self.validate_uom_is_integer("uom", ["qty", "received_qty"]) self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_cwip_accounts() + self.validate_provisional_expense_account() self.check_on_hold_or_closed_status() @@ -133,6 +135,15 @@ class PurchaseReceipt(BuyingController): company = self.company) break + def validate_provisional_expense_account(self): + provisional_accounting_for_non_stock_items = \ + cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + + if provisional_accounting_for_non_stock_items: + default_provisional_account = self.get_company_default("default_provisional_account") + if not self.provisional_expense_account: + self.provisional_expense_account = default_provisional_account + def validate_with_previous_doc(self): super(PurchaseReceipt, self).validate_with_previous_doc({ "Purchase Order": { @@ -258,13 +269,15 @@ class PurchaseReceipt(BuyingController): get_purchase_document_details, ) - stock_rbnb = self.get_company_default("stock_received_but_not_billed") - landed_cost_entries = get_item_account_wise_additional_cost(self.name) - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) + if erpnext.is_perpetual_inventory_enabled(self.company): + stock_rbnb = self.get_company_default("stock_received_but_not_billed") + landed_cost_entries = get_item_account_wise_additional_cost(self.name) + expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") warehouse_with_no_account = [] stock_items = self.get_stock_items() + provisional_accounting_for_non_stock_items = \ + cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) exchange_rate_map, net_rate_map = get_purchase_document_details(self) @@ -422,43 +435,58 @@ class PurchaseReceipt(BuyingController): elif d.warehouse not in warehouse_with_no_account or \ d.rejected_warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(d.warehouse) - elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items: - service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") - credit_currency = get_account_currency(service_received_but_not_billed_account) - debit_currency = get_account_currency(d.expense_account) - remarks = self.get("remarks") or _("Accounting Entry for Service") - - self.add_gl_entry( - gl_entries=gl_entries, - account=service_received_but_not_billed_account, - cost_center=d.cost_center, - debit=0.0, - credit=d.amount, - remarks=remarks, - against_account=d.expense_account, - account_currency=credit_currency, - project=d.project, - voucher_detail_no=d.name, item=d) - - self.add_gl_entry( - gl_entries=gl_entries, - account=d.expense_account, - cost_center=d.cost_center, - debit=d.amount, - credit=0.0, - remarks=remarks, - against_account=service_received_but_not_billed_account, - account_currency = debit_currency, - project=d.project, - voucher_detail_no=d.name, - item=d) + elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and provisional_accounting_for_non_stock_items: + self.add_provisional_gl_entry(d, gl_entries, self.posting_date) if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + "\n".join(warehouse_with_no_account)) + def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0): + provisional_expense_account = self.get('provisional_expense_account') + credit_currency = get_account_currency(provisional_expense_account) + debit_currency = get_account_currency(item.expense_account) + expense_account = item.expense_account + remarks = self.get("remarks") or _("Accounting Entry for Service") + multiplication_factor = 1 + + if reverse: + multiplication_factor = -1 + expense_account = frappe.db.get_value('Purchase Receipt Item', {'name': item.get('pr_detail')}, ['expense_account']) + + self.add_gl_entry( + gl_entries=gl_entries, + account=provisional_expense_account, + cost_center=item.cost_center, + debit=0.0, + credit=multiplication_factor * item.amount, + remarks=remarks, + against_account=expense_account, + account_currency=credit_currency, + project=item.project, + voucher_detail_no=item.name, + item=item, + posting_date=posting_date) + + self.add_gl_entry( + gl_entries=gl_entries, + account=expense_account, + cost_center=item.cost_center, + debit=multiplication_factor * item.amount, + credit=0.0, + remarks=remarks, + against_account=provisional_expense_account, + account_currency = debit_currency, + project=item.project, + voucher_detail_no=item.name, + item=item, + posting_date=posting_date) + def make_tax_gl_entries(self, gl_entries): - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + + if erpnext.is_perpetual_inventory_enabled(self.company): + expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')]) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} @@ -515,7 +543,8 @@ class PurchaseReceipt(BuyingController): def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account, debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None, - project=None, voucher_detail_no=None, item=None): + project=None, voucher_detail_no=None, item=None, posting_date=None): + gl_entry = { "account": account, "cost_center": cost_center, @@ -534,6 +563,9 @@ class PurchaseReceipt(BuyingController): if credit_in_account_currency: gl_entry.update({"credit_in_account_currency": credit_in_account_currency}) + if posting_date: + gl_entry.update({"posting_date": posting_date}) + gl_entries.append(self.get_gl_dict(gl_entry, item=item)) def get_asset_gl_entry(self, gl_entries): @@ -562,6 +594,7 @@ class PurchaseReceipt(BuyingController): # debit cwip account debit_in_account_currency = (base_asset_amount if cwip_account_currency == self.company_currency else asset_amount) + self.add_gl_entry( gl_entries=gl_entries, account=cwip_account, @@ -577,6 +610,7 @@ class PurchaseReceipt(BuyingController): # credit arbnb account credit_in_account_currency = (base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount) + self.add_gl_entry( gl_entries=gl_entries, account=arbnb_account, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2909a2d2e74..b87d9205e0e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1312,58 +1312,6 @@ class TestPurchaseReceipt(ERPNextTestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) - def test_service_item_purchase_with_perpetual_inventory(self): - company = '_Test Company with perpetual inventory' - service_item = '_Test Non Stock Item' - - before_test_value = frappe.db.get_value( - 'Company', company, 'enable_perpetual_inventory_for_non_stock_items' - ) - frappe.db.set_value( - 'Company', company, - 'enable_perpetual_inventory_for_non_stock_items', 1 - ) - srbnb_account = 'Stock Received But Not Billed - TCP1' - frappe.db.set_value( - 'Company', company, - 'service_received_but_not_billed', srbnb_account - ) - - pr = make_purchase_receipt( - company=company, item=service_item, - warehouse='Finished Goods - TCP1', do_not_save=1 - ) - item_row_with_diff_rate = frappe.copy_doc(pr.items[0]) - item_row_with_diff_rate.rate = 100 - pr.append('items', item_row_with_diff_rate) - - pr.save() - pr.submit() - - item_one_gl_entry = frappe.db.get_all("GL Entry", { - 'voucher_type': pr.doctype, - 'voucher_no': pr.name, - 'account': srbnb_account, - 'voucher_detail_no': pr.items[0].name - }, pluck="name") - - item_two_gl_entry = frappe.db.get_all("GL Entry", { - 'voucher_type': pr.doctype, - 'voucher_no': pr.name, - 'account': srbnb_account, - 'voucher_detail_no': pr.items[1].name - }, pluck="name") - - # check if the entries are not merged into one - # seperate entries should be made since voucher_detail_no is different - self.assertEqual(len(item_one_gl_entry), 1) - self.assertEqual(len(item_two_gl_entry), 1) - - frappe.db.set_value( - 'Company', company, - 'enable_perpetual_inventory_for_non_stock_items', before_test_value - ) - def test_purchase_receipt_with_exchange_rate_difference(self): from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( make_purchase_receipt as create_purchase_receipt, diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 30ea1c3cadc..e5994b2dd48 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -976,7 +976,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-11-15 15:46:10.591600", + "modified": "2022-02-01 11:32:27.980524", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -985,5 +985,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py new file mode 100644 index 00000000000..df2dc8b99a1 --- /dev/null +++ b/erpnext/tests/test_point_of_sale.py @@ -0,0 +1,53 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + + +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile +from erpnext.selling.page.point_of_sale.point_of_sale import get_items +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestCase + + +class TestPointOfSale(ERPNextTestCase): + def test_item_search(self): + """ + Test Stock and Service Item Search. + """ + + pos_profile = make_pos_profile() + item1 = make_item("Test Search Stock Item", {"is_stock_item": 1}) + make_stock_entry( + item_code="Test Search Stock Item", + qty=10, + to_warehouse="_Test Warehouse - _TC", + rate=500, + ) + + result = get_items( + start=0, + page_length=20, + price_list=None, + item_group=item1.item_group, + pos_profile=pos_profile.name, + search_term="Test Search Stock Item", + ) + filtered_items = result.get("items") + + self.assertEqual(len(filtered_items), 1) + self.assertEqual(filtered_items[0]["item_code"], item1.item_code) + self.assertEqual(filtered_items[0]["actual_qty"], 10) + + item2 = make_item("Test Search Service Item", {"is_stock_item": 0}) + result = get_items( + start=0, + page_length=20, + price_list=None, + item_group=item2.item_group, + pos_profile=pos_profile.name, + search_term="Test Search Service Item", + ) + filtered_items = result.get("items") + + self.assertEqual(len(filtered_items), 1) + self.assertEqual(filtered_items[0]["item_code"], item2.item_code)