Merge branch 'develop' into e-commerce-refactor-develop

This commit is contained in:
Marica 2022-02-02 10:45:14 +05:30 committed by GitHub
commit 780e29b42e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 456 additions and 222 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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({

View File

@ -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`

View File

@ -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(

View File

@ -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:

View File

@ -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")
)

View File

@ -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",

View File

@ -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()

View File

@ -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)

View File

@ -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
}

View File

@ -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"));

View File

@ -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

View 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()

View File

@ -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")

View File

@ -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:

View File

@ -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

View File

@ -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}

View File

@ -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];
}
});
}

View File

@ -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() {

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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": []
}

View 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)