Merge branch 'develop' into e-commerce-refactor-develop
This commit is contained in:
commit
780e29b42e
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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({
|
||||
|
@ -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`
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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()
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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"));
|
||||
|
@ -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
|
||||
|
19
erpnext/patches/v13_0/enable_provisional_accounting.py
Normal file
19
erpnext/patches/v13_0/enable_provisional_accounting.py
Normal file
@ -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()
|
@ -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")
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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": []
|
||||
}
|
53
erpnext/tests/test_point_of_sale.py
Normal file
53
erpnext/tests/test_point_of_sale.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user