Merge branch 'develop' into e-commerce-refactor-develop
This commit is contained in:
commit
780e29b42e
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2022-01-03 18:10:11.697198",
|
"creation": "2022-01-13 20:07:30.096306",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
@ -20,7 +20,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "percentage",
|
"fieldname": "percentage",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Percent",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Percentage (%)",
|
"label": "Percentage (%)",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@ -29,7 +29,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-01-03 18:10:20.029821",
|
"modified": "2022-02-01 22:22:31.589523",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Cost Center Allocation Percentage",
|
"name": "Cost Center Allocation Percentage",
|
||||||
|
@ -42,7 +42,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.validate_serialised_or_batched_item()
|
self.validate_serialised_or_batched_item()
|
||||||
self.validate_stock_availablility()
|
self.validate_stock_availablility()
|
||||||
self.validate_return_items_qty()
|
self.validate_return_items_qty()
|
||||||
self.validate_non_stock_items()
|
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_account_for_mode_of_payment()
|
self.set_account_for_mode_of_payment()
|
||||||
self.validate_pos()
|
self.validate_pos()
|
||||||
@ -175,9 +174,11 @@ class POSInvoice(SalesInvoice):
|
|||||||
def validate_stock_availablility(self):
|
def validate_stock_availablility(self):
|
||||||
if self.is_return or self.docstatus != 1:
|
if self.is_return or self.docstatus != 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||||
for d in self.get('items'):
|
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:
|
if d.serial_no:
|
||||||
self.validate_pos_reserved_serial_nos(d)
|
self.validate_pos_reserved_serial_nos(d)
|
||||||
self.validate_delivered_serial_nos(d)
|
self.validate_delivered_serial_nos(d)
|
||||||
@ -188,7 +189,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
if allow_negative_stock:
|
if allow_negative_stock:
|
||||||
return
|
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)
|
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||||
if flt(available_stock) <= 0:
|
if flt(available_stock) <= 0:
|
||||||
@ -259,14 +260,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
.format(d.idx, bold_serial_no, bold_return_against)
|
.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):
|
def validate_mode_of_payment(self):
|
||||||
if len(self.payments) == 0:
|
if len(self.payments) == 0:
|
||||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||||
@ -506,12 +499,18 @@ class POSInvoice(SalesInvoice):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_availability(item_code, warehouse):
|
def get_stock_availability(item_code, warehouse):
|
||||||
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
||||||
|
is_stock_item = True
|
||||||
bin_qty = get_bin_qty(item_code, warehouse)
|
bin_qty = get_bin_qty(item_code, warehouse)
|
||||||
pos_sales_qty = get_pos_reserved_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:
|
else:
|
||||||
|
is_stock_item = False
|
||||||
if frappe.db.exists('Product Bundle', item_code):
|
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):
|
def get_bundle_availability(bundle_item_code, warehouse):
|
||||||
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
|
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)
|
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'))
|
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"):
|
for item in self.get("items"):
|
||||||
if flt(item.base_net_amount):
|
if flt(item.base_net_amount):
|
||||||
@ -643,19 +647,23 @@ class PurchaseInvoice(BuyingController):
|
|||||||
else:
|
else:
|
||||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
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 provisional_accounting_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 item.purchase_receipt:
|
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
|
# 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,
|
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,
|
'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:
|
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():
|
if not self.is_internal_transfer():
|
||||||
gl_entries.append(self.get_gl_dict({
|
gl_entries.append(self.get_gl_dict({
|
||||||
|
@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
|||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
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.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.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||||
from erpnext.exceptions import InvalidCurrency
|
from erpnext.exceptions import InvalidCurrency
|
||||||
from erpnext.projects.doctype.project.test_project import make_project
|
from erpnext.projects.doctype.project.test_project import make_project
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
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 (
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||||
get_taxes,
|
get_taxes,
|
||||||
make_purchase_receipt,
|
make_purchase_receipt,
|
||||||
@ -1147,8 +1152,6 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_purchase_invoice_advance_taxes(self):
|
def test_purchase_invoice_advance_taxes(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.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
|
# create a new supplier to test
|
||||||
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
|
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
|
||||||
@ -1221,6 +1224,45 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
payment_entry.load_from_db()
|
payment_entry.load_from_db()
|
||||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
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):
|
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||||
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
|
@ -204,7 +204,7 @@ class SellingController(StockController):
|
|||||||
valuation_rate_map = {}
|
valuation_rate_map = {}
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.item_code:
|
if not item.item_code or item.is_free_item:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
last_purchase_rate, is_stock_item = frappe.get_cached_value(
|
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
|
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.item_code:
|
if not item.item_code or item.is_free_item:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
last_valuation_rate = valuation_rate_map.get(
|
last_valuation_rate = valuation_rate_map.get(
|
||||||
|
@ -40,7 +40,10 @@ class StockController(AccountsController):
|
|||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
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)
|
warehouse_account = get_warehouse_account_map(self.company)
|
||||||
|
|
||||||
if self.docstatus==1:
|
if self.docstatus==1:
|
||||||
|
@ -20,6 +20,7 @@ def send_reminders_in_advance_weekly():
|
|||||||
|
|
||||||
send_advance_holiday_reminders("Weekly")
|
send_advance_holiday_reminders("Weekly")
|
||||||
|
|
||||||
|
|
||||||
def send_reminders_in_advance_monthly():
|
def send_reminders_in_advance_monthly():
|
||||||
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
|
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
|
||||||
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
||||||
@ -28,6 +29,7 @@ def send_reminders_in_advance_monthly():
|
|||||||
|
|
||||||
send_advance_holiday_reminders("Monthly")
|
send_advance_holiday_reminders("Monthly")
|
||||||
|
|
||||||
|
|
||||||
def send_advance_holiday_reminders(frequency):
|
def send_advance_holiday_reminders(frequency):
|
||||||
"""Send Holiday Reminders in Advance to Employees
|
"""Send Holiday Reminders in Advance to Employees
|
||||||
`frequency` (str): 'Weekly' or 'Monthly'
|
`frequency` (str): 'Weekly' or 'Monthly'
|
||||||
@ -42,7 +44,7 @@ def send_advance_holiday_reminders(frequency):
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
employees = frappe.db.get_all('Employee', pluck='name')
|
employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name')
|
||||||
for employee in employees:
|
for employee in employees:
|
||||||
holidays = get_holidays_for_employee(
|
holidays = get_holidays_for_employee(
|
||||||
employee,
|
employee,
|
||||||
@ -51,10 +53,13 @@ def send_advance_holiday_reminders(frequency):
|
|||||||
raise_exception=False
|
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):
|
def send_holidays_reminder_in_advance(employee, holidays):
|
||||||
|
if not holidays:
|
||||||
|
return
|
||||||
|
|
||||||
employee_doc = frappe.get_doc('Employee', employee)
|
employee_doc = frappe.get_doc('Employee', employee)
|
||||||
employee_email = get_employee_email(employee_doc)
|
employee_email = get_employee_email(employee_doc)
|
||||||
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
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)
|
reminder_text, message = get_birthday_reminder_text_and_message(others)
|
||||||
send_birthday_reminder(person_email, reminder_text, others, message)
|
send_birthday_reminder(person_email, reminder_text, others, message)
|
||||||
|
|
||||||
|
|
||||||
def get_birthday_reminder_text_and_message(birthday_persons):
|
def get_birthday_reminder_text_and_message(birthday_persons):
|
||||||
if len(birthday_persons) == 1:
|
if len(birthday_persons) == 1:
|
||||||
birthday_person_text = birthday_persons[0]['name']
|
birthday_person_text = birthday_persons[0]['name']
|
||||||
@ -116,6 +122,7 @@ def get_birthday_reminder_text_and_message(birthday_persons):
|
|||||||
|
|
||||||
return reminder_text, message
|
return reminder_text, message
|
||||||
|
|
||||||
|
|
||||||
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
|
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
|
||||||
frappe.sendmail(
|
frappe.sendmail(
|
||||||
recipients=recipients,
|
recipients=recipients,
|
||||||
@ -129,10 +136,12 @@ def send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
|
|||||||
header=_("Birthday Reminder 🎂")
|
header=_("Birthday Reminder 🎂")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_employees_who_are_born_today():
|
def get_employees_who_are_born_today():
|
||||||
"""Get all employee born today & group them based on their company"""
|
"""Get all employee born today & group them based on their company"""
|
||||||
return get_employees_having_an_event_today("birthday")
|
return get_employees_having_an_event_today("birthday")
|
||||||
|
|
||||||
|
|
||||||
def get_employees_having_an_event_today(event_type):
|
def get_employees_having_an_event_today(event_type):
|
||||||
"""Get all employee who have `event_type` today
|
"""Get all employee who have `event_type` today
|
||||||
& group them based on their company. `event_type`
|
& 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)
|
reminder_text, message = get_work_anniversary_reminder_text_and_message(others)
|
||||||
send_work_anniversary_reminder(person_email, reminder_text, others, message)
|
send_work_anniversary_reminder(person_email, reminder_text, others, message)
|
||||||
|
|
||||||
|
|
||||||
def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||||
if len(anniversary_persons) == 1:
|
if len(anniversary_persons) == 1:
|
||||||
anniversary_person = anniversary_persons[0]['name']
|
anniversary_person = anniversary_persons[0]['name']
|
||||||
persons_name = anniversary_person
|
persons_name = anniversary_person
|
||||||
# Number of years completed at the company
|
# Number of years completed at the company
|
||||||
completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year
|
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:
|
else:
|
||||||
person_names_with_years = []
|
person_names_with_years = []
|
||||||
names = []
|
names = []
|
||||||
@ -225,7 +235,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
|||||||
names.append(person_text)
|
names.append(person_text)
|
||||||
# Number of years completed at the company
|
# Number of years completed at the company
|
||||||
completed_years = getdate().year - person['date_of_joining'].year
|
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)
|
person_names_with_years.append(person_text)
|
||||||
|
|
||||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
# 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
|
return reminder_text, message
|
||||||
|
|
||||||
|
|
||||||
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
||||||
frappe.sendmail(
|
frappe.sendmail(
|
||||||
recipients=recipients,
|
recipients=recipients,
|
||||||
@ -249,5 +260,5 @@ def send_work_anniversary_reminder(recipients, reminder_text, anniversary_person
|
|||||||
anniversary_persons=anniversary_persons,
|
anniversary_persons=anniversary_persons,
|
||||||
message=message,
|
message=message,
|
||||||
),
|
),
|
||||||
header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️")
|
header=_("Work Anniversary Reminder")
|
||||||
)
|
)
|
||||||
|
@ -36,7 +36,7 @@ class TestEmployee(unittest.TestCase):
|
|||||||
employee_doc.reload()
|
employee_doc.reload()
|
||||||
|
|
||||||
make_holiday_list()
|
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'""")
|
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",
|
salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",
|
||||||
|
@ -5,10 +5,12 @@ import unittest
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import frappe
|
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.employee.test_employee import make_employee
|
||||||
from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change
|
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):
|
class TestEmployeeReminders(unittest.TestCase):
|
||||||
@ -46,6 +48,24 @@ class TestEmployeeReminders(unittest.TestCase):
|
|||||||
cls.test_employee = test_employee
|
cls.test_employee = test_employee
|
||||||
cls.test_holiday_dates = test_holiday_dates
|
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
|
@classmethod
|
||||||
def get_test_holiday_dates(cls):
|
def get_test_holiday_dates(cls):
|
||||||
today_date = getdate()
|
today_date = getdate()
|
||||||
@ -61,6 +81,7 @@ class TestEmployeeReminders(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Clear Email Queue
|
# Clear Email Queue
|
||||||
frappe.db.sql("delete from `tabEmail Queue`")
|
frappe.db.sql("delete from `tabEmail Queue`")
|
||||||
|
frappe.db.sql("delete from `tabEmail Queue Recipient`")
|
||||||
|
|
||||||
def test_is_holiday(self):
|
def test_is_holiday(self):
|
||||||
from erpnext.hr.doctype.employee.employee import is_holiday
|
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)
|
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
|
||||||
|
|
||||||
def test_work_anniversary_reminders(self):
|
def test_work_anniversary_reminders(self):
|
||||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
make_employee("test_work_anniversary@gmail.com",
|
||||||
employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:]
|
date_of_joining="1998" + frappe.utils.nowdate()[4:],
|
||||||
employee.company_email = "test@example.com"
|
company="_Test Company",
|
||||||
employee.company = "_Test Company"
|
)
|
||||||
employee.save()
|
|
||||||
|
|
||||||
from erpnext.hr.doctype.employee.employee_reminders import (
|
from erpnext.hr.doctype.employee.employee_reminders import (
|
||||||
get_employees_having_an_event_today,
|
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')
|
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 = frappe.get_doc("HR Settings", "HR Settings")
|
||||||
hr_settings.send_work_anniversary_reminders = 1
|
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)
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
|
self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
|
||||||
|
|
||||||
def test_send_holidays_reminder_in_advance(self):
|
def test_work_anniversary_reminder_not_sent_for_0_years(self):
|
||||||
from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
|
make_employee("test_work_anniversary_2@gmail.com",
|
||||||
from erpnext.hr.utils import get_holidays_for_employee
|
date_of_joining=getdate(),
|
||||||
|
company="_Test Company",
|
||||||
|
)
|
||||||
|
|
||||||
# Get HR settings and enable advance holiday reminders
|
from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today
|
||||||
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
|
||||||
hr_settings.send_holiday_reminders = 1
|
employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
|
||||||
set_proceed_with_frequency_change()
|
employees = employees_having_work_anniversary.get("_Test Company") or []
|
||||||
hr_settings.frequency = 'Weekly'
|
user_ids = []
|
||||||
hr_settings.save()
|
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(
|
holidays = get_holidays_for_employee(
|
||||||
self.test_employee.get('name'),
|
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)
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
self.assertEqual(len(email_queue), 1)
|
self.assertEqual(len(email_queue), 1)
|
||||||
|
self.assertTrue("Holidays this Week." in email_queue[0].message)
|
||||||
|
|
||||||
def test_advance_holiday_reminders_monthly(self):
|
def test_advance_holiday_reminders_monthly(self):
|
||||||
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
|
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
|
||||||
|
|
||||||
# Get HR settings and enable advance holiday reminders
|
setup_hr_settings('Monthly')
|
||||||
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
|
||||||
hr_settings.send_holiday_reminders = 1
|
# disable emp 2, set same holiday list
|
||||||
set_proceed_with_frequency_change()
|
frappe.db.set_value('Employee', self.test_employee_2.name, {
|
||||||
hr_settings.frequency = 'Monthly'
|
'status': 'Left',
|
||||||
hr_settings.save()
|
'holiday_list': self.test_employee.holiday_list
|
||||||
|
})
|
||||||
|
|
||||||
send_reminders_in_advance_monthly()
|
send_reminders_in_advance_monthly()
|
||||||
|
|
||||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
self.assertTrue(len(email_queue) > 0)
|
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):
|
def test_advance_holiday_reminders_weekly(self):
|
||||||
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
|
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
|
||||||
|
|
||||||
# Get HR settings and enable advance holiday reminders
|
setup_hr_settings('Weekly')
|
||||||
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
|
||||||
hr_settings.send_holiday_reminders = 1
|
# disable emp 2, set same holiday list
|
||||||
hr_settings.frequency = 'Weekly'
|
frappe.db.set_value('Employee', self.test_employee_2.name, {
|
||||||
hr_settings.save()
|
'status': 'Left',
|
||||||
|
'holiday_list': self.test_employee.holiday_list
|
||||||
|
})
|
||||||
|
|
||||||
send_reminders_in_advance_weekly()
|
send_reminders_in_advance_weekly()
|
||||||
|
|
||||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
self.assertTrue(len(email_queue) > 0)
|
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.db.sql("DELETE FROM `tab%s`" % dt) #nosec
|
||||||
|
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
set_leave_approver()
|
set_leave_approver()
|
||||||
|
|
||||||
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
|
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
|
||||||
|
|
||||||
def tearDown(self):
|
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))
|
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()
|
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)
|
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()
|
leave_application.reload()
|
||||||
self.assertEqual(leave_application.total_leave_days, 4)
|
self.assertEqual(leave_application.total_leave_days, 4)
|
||||||
self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 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))
|
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()
|
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)
|
first_sunday = get_first_sunday(holiday_list)
|
||||||
|
|
||||||
# already marked attendance on a holiday should be deleted in this case
|
# already marked attendance on a holiday should be deleted in this case
|
||||||
config = {
|
config = {
|
||||||
"doctype": "Attendance",
|
"doctype": "Attendance",
|
||||||
"employee": "_T-Employee-00001",
|
"employee": employee.name,
|
||||||
"status": "Present"
|
"status": "Present"
|
||||||
}
|
}
|
||||||
attendance_on_holiday = frappe.get_doc(config)
|
attendance_on_holiday = frappe.get_doc(config)
|
||||||
attendance_on_holiday.attendance_date = first_sunday
|
attendance_on_holiday.attendance_date = first_sunday
|
||||||
|
attendance_on_holiday.flags.ignore_validate = True
|
||||||
attendance_on_holiday.save()
|
attendance_on_holiday.save()
|
||||||
|
|
||||||
# already marked attendance on a non-holiday should be updated
|
# already marked attendance on a non-holiday should be updated
|
||||||
attendance = frappe.get_doc(config)
|
attendance = frappe.get_doc(config)
|
||||||
attendance.attendance_date = add_days(first_sunday, 3)
|
attendance.attendance_date = add_days(first_sunday, 3)
|
||||||
|
attendance.flags.ignore_validate = True
|
||||||
attendance.save()
|
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()
|
leave_application.reload()
|
||||||
# holiday should be excluded while marking attendance
|
# holiday should be excluded while marking attendance
|
||||||
self.assertEqual(leave_application.total_leave_days, 3)
|
self.assertEqual(leave_application.total_leave_days, 3)
|
||||||
@ -325,7 +327,7 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
|
|
||||||
default_holiday_list = make_holiday_list()
|
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)
|
first_sunday = get_first_sunday(default_holiday_list)
|
||||||
|
|
||||||
optional_leave_date = add_days(first_sunday, 1)
|
optional_leave_date = add_days(first_sunday, 1)
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "loan_repayment_entry",
|
"fieldname": "loan_repayment_entry",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Loan Repayment Entry",
|
"label": "Loan Repayment Entry",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Loan Repayment",
|
"options": "Loan Repayment",
|
||||||
@ -88,7 +87,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-14 20:47:11.725818",
|
"modified": "2022-01-31 14:50:14.823213",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Salary Slip Loan",
|
"name": "Salary Slip Loan",
|
||||||
@ -97,5 +96,6 @@
|
|||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"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.add_custom_button(__("Work Order"), function() {
|
||||||
frm.trigger("make_work_order");
|
frm.trigger("make_work_order");
|
||||||
}, __("Create"));
|
}, __("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_exchange_rate_settings
|
||||||
erpnext.patches.v13_0.update_asset_quantity_field
|
erpnext.patches.v13_0.update_asset_quantity_field
|
||||||
erpnext.patches.v13_0.delete_bank_reconciliation_detail
|
erpnext.patches.v13_0.delete_bank_reconciliation_detail
|
||||||
|
erpnext.patches.v13_0.enable_provisional_accounting
|
||||||
|
|
||||||
[post_model_sync]
|
[post_model_sync]
|
||||||
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
|
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()
|
cca.submit()
|
||||||
|
|
||||||
def get_existing_cost_center_allocations():
|
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
|
return
|
||||||
|
|
||||||
par = frappe.qb.DocType("Cost Center")
|
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)
|
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)
|
# 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)
|
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
|
# 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_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 = 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
|
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
|
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()
|
joining_date, relieving_date = self.get_joining_and_relieving_dates()
|
||||||
|
|
||||||
taxable_earnings = 0
|
taxable_earnings = 0
|
||||||
@ -903,7 +904,7 @@ class SalarySlip(TransactionBase):
|
|||||||
# Get additional amount based on future recurring additional salary
|
# Get additional amount based on future recurring additional salary
|
||||||
if additional_amount and earning.is_recurring_additional_salary:
|
if additional_amount and earning.is_recurring_additional_salary:
|
||||||
additional_income += self.get_future_recurring_additional_amount(earning.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:
|
if earning.deduct_full_tax_on_selected_payroll_date:
|
||||||
additional_income_with_full_tax += additional_amount
|
additional_income_with_full_tax += additional_amount
|
||||||
@ -920,7 +921,7 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
if additional_amount and ded.is_recurring_additional_salary:
|
if additional_amount and ded.is_recurring_additional_salary:
|
||||||
additional_income -= self.get_future_recurring_additional_amount(ded.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({
|
return frappe._dict({
|
||||||
"taxable_earnings": taxable_earnings,
|
"taxable_earnings": taxable_earnings,
|
||||||
@ -929,12 +930,18 @@ class SalarySlip(TransactionBase):
|
|||||||
"flexi_benefits": flexi_benefits
|
"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
|
future_recurring_additional_amount = 0
|
||||||
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
|
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
|
||||||
|
|
||||||
# future month count excluding current
|
# future month count excluding current
|
||||||
from_date, to_date = getdate(self.start_date), getdate(to_date)
|
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)
|
future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month)
|
||||||
|
|
||||||
if future_recurring_period > 0:
|
if future_recurring_period > 0:
|
||||||
|
@ -147,7 +147,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
# Payroll based on attendance
|
# Payroll based on attendance
|
||||||
frappe.db.set_value("Payroll Settings", None, "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"})
|
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
|
||||||
|
|
||||||
# mark attendance
|
# 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"],
|
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"],
|
||||||
as_dict=1)
|
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_rate, currency = frappe.db.get_value('Item Price', {
|
||||||
'price_list': price_list,
|
'price_list': price_list,
|
||||||
'item_code': item_code
|
'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)
|
), {'warehouse': warehouse}, as_dict=1)
|
||||||
|
|
||||||
if items_data:
|
if items_data:
|
||||||
items_data = filter_service_items(items_data)
|
|
||||||
items = [d.item_code for d in items_data]
|
items = [d.item_code for d in items_data]
|
||||||
item_prices_data = frappe.get_all("Item Price",
|
item_prices_data = frappe.get_all("Item Price",
|
||||||
fields = ["item_code", "price_list_rate", "currency"],
|
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:
|
for item in items_data:
|
||||||
item_code = item.item_code
|
item_code = item.item_code
|
||||||
item_price = item_prices.get(item_code) or {}
|
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 = {}
|
||||||
row.update(item)
|
row.update(item)
|
||||||
@ -144,14 +143,6 @@ def search_for_serial_or_batch_or_barcode_number(search_value):
|
|||||||
|
|
||||||
return {}
|
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):
|
def get_conditions(search_term):
|
||||||
condition = "("
|
condition = "("
|
||||||
condition += """item.name like {search_term}
|
condition += """item.name like {search_term}
|
||||||
|
@ -630,18 +630,24 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async check_stock_availability(item_row, qty_needed, warehouse) {
|
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();
|
frappe.dom.unfreeze();
|
||||||
const bold_item_code = item_row.item_code.bold();
|
const bold_item_code = item_row.item_code.bold();
|
||||||
const bold_warehouse = warehouse.bold();
|
const bold_warehouse = warehouse.bold();
|
||||||
const bold_available_qty = available_qty.toString().bold()
|
const bold_available_qty = available_qty.toString().bold()
|
||||||
if (!(available_qty > 0)) {
|
if (!(available_qty > 0)) {
|
||||||
frappe.model.clear_doc(item_row.doctype, item_row.name);
|
if (is_stock_item) {
|
||||||
frappe.throw({
|
frappe.model.clear_doc(item_row.doctype, item_row.name);
|
||||||
title: __("Not Available"),
|
frappe.throw({
|
||||||
message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
|
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) {
|
} else if (available_qty < qty_needed) {
|
||||||
frappe.throw({
|
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]),
|
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) {
|
callback(res) {
|
||||||
if (!me.item_stock_map[item_code])
|
if (!me.item_stock_map[item_code])
|
||||||
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][warehouse] = res.message[0];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -79,14 +79,20 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
const me = this;
|
const me = this;
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item;
|
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;
|
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
|
||||||
|
let indicator_color;
|
||||||
let qty_to_display = actual_qty;
|
let qty_to_display = actual_qty;
|
||||||
|
|
||||||
if (Math.round(qty_to_display) > 999) {
|
if (item.is_stock_item) {
|
||||||
qty_to_display = Math.round(qty_to_display)/1000;
|
indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange");
|
||||||
qty_to_display = qty_to_display.toFixed(1) + 'K';
|
|
||||||
|
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() {
|
function get_item_image_html() {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "field:company_name",
|
"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.",
|
"description": "Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Organization.",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
@ -77,13 +77,13 @@
|
|||||||
"default_finance_book",
|
"default_finance_book",
|
||||||
"auto_accounting_for_stock_settings",
|
"auto_accounting_for_stock_settings",
|
||||||
"enable_perpetual_inventory",
|
"enable_perpetual_inventory",
|
||||||
"enable_perpetual_inventory_for_non_stock_items",
|
"enable_provisional_accounting_for_non_stock_items",
|
||||||
"default_inventory_account",
|
"default_inventory_account",
|
||||||
"stock_adjustment_account",
|
"stock_adjustment_account",
|
||||||
"default_in_transit_warehouse",
|
"default_in_transit_warehouse",
|
||||||
"column_break_32",
|
"column_break_32",
|
||||||
"stock_received_but_not_billed",
|
"stock_received_but_not_billed",
|
||||||
"service_received_but_not_billed",
|
"default_provisional_account",
|
||||||
"expenses_included_in_valuation",
|
"expenses_included_in_valuation",
|
||||||
"fixed_asset_defaults",
|
"fixed_asset_defaults",
|
||||||
"accumulated_depreciation_account",
|
"accumulated_depreciation_account",
|
||||||
@ -684,20 +684,6 @@
|
|||||||
"label": "Default Buying Terms",
|
"label": "Default Buying Terms",
|
||||||
"options": "Terms and Conditions"
|
"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",
|
"fieldname": "default_in_transit_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -741,6 +727,20 @@
|
|||||||
"fieldname": "section_break_28",
|
"fieldname": "section_break_28",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Chart of Accounts"
|
"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",
|
"icon": "fa fa-building",
|
||||||
@ -748,7 +748,7 @@
|
|||||||
"image_field": "company_logo",
|
"image_field": "company_logo",
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-04 12:09:25.833133",
|
"modified": "2022-01-25 10:33:16.826067",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Company",
|
"name": "Company",
|
||||||
@ -809,5 +809,6 @@
|
|||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -10,6 +10,7 @@ import frappe.defaults
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.cache_manager import clear_defaults_cache
|
from frappe.cache_manager import clear_defaults_cache
|
||||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
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 import cint, formatdate, get_timestamp, today
|
||||||
from frappe.utils.nestedset import NestedSet
|
from frappe.utils.nestedset import NestedSet
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ class Company(NestedSet):
|
|||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
self.validate_coa_input()
|
self.validate_coa_input()
|
||||||
self.validate_perpetual_inventory()
|
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_country_change()
|
||||||
self.check_parent_changed()
|
self.check_parent_changed()
|
||||||
self.set_chart_of_accounts()
|
self.set_chart_of_accounts()
|
||||||
@ -187,11 +188,14 @@ class Company(NestedSet):
|
|||||||
frappe.msgprint(_("Set default inventory account for perpetual inventory"),
|
frappe.msgprint(_("Set default inventory account for perpetual inventory"),
|
||||||
alert=True, indicator='orange')
|
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 not self.get("__islocal"):
|
||||||
if cint(self.enable_perpetual_inventory_for_non_stock_items) == 1 and not self.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 perpetual inventory for non stock items").format(
|
frappe.throw(_("Set default {0} account for non stock items").format(
|
||||||
frappe.bold('Service Received But Not Billed')))
|
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):
|
def check_country_change(self):
|
||||||
frappe.flags.country_change = False
|
frappe.flags.country_change = False
|
||||||
|
@ -106,6 +106,8 @@
|
|||||||
"terms",
|
"terms",
|
||||||
"bill_no",
|
"bill_no",
|
||||||
"bill_date",
|
"bill_date",
|
||||||
|
"accounting_details_section",
|
||||||
|
"provisional_expense_account",
|
||||||
"more_info",
|
"more_info",
|
||||||
"project",
|
"project",
|
||||||
"status",
|
"status",
|
||||||
@ -1144,16 +1146,30 @@
|
|||||||
"label": "Represents Company",
|
"label": "Represents Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"read_only": 1
|
"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",
|
"icon": "fa fa-truck",
|
||||||
"idx": 261,
|
"idx": 261,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-28 13:11:10.181328",
|
"modified": "2022-02-01 11:40:52.690984",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt",
|
"name": "Purchase Receipt",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -1214,6 +1230,7 @@
|
|||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"timeline_field": "supplier",
|
"timeline_field": "supplier",
|
||||||
"title_field": "title",
|
"title_field": "title",
|
||||||
"track_changes": 1
|
"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.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import cint, flt, getdate, nowdate
|
from frappe.utils import cint, flt, getdate, nowdate
|
||||||
|
|
||||||
|
import erpnext
|
||||||
from erpnext.accounts.utils import get_account_currency
|
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.asset import get_asset_account, is_cwip_accounting_enabled
|
||||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
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("uom", ["qty", "received_qty"])
|
||||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
self.validate_cwip_accounts()
|
self.validate_cwip_accounts()
|
||||||
|
self.validate_provisional_expense_account()
|
||||||
|
|
||||||
self.check_on_hold_or_closed_status()
|
self.check_on_hold_or_closed_status()
|
||||||
|
|
||||||
@ -133,6 +135,15 @@ class PurchaseReceipt(BuyingController):
|
|||||||
company = self.company)
|
company = self.company)
|
||||||
break
|
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):
|
def validate_with_previous_doc(self):
|
||||||
super(PurchaseReceipt, self).validate_with_previous_doc({
|
super(PurchaseReceipt, self).validate_with_previous_doc({
|
||||||
"Purchase Order": {
|
"Purchase Order": {
|
||||||
@ -258,13 +269,15 @@ class PurchaseReceipt(BuyingController):
|
|||||||
get_purchase_document_details,
|
get_purchase_document_details,
|
||||||
)
|
)
|
||||||
|
|
||||||
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
if erpnext.is_perpetual_inventory_enabled(self.company):
|
||||||
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
||||||
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
||||||
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
|
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
||||||
|
|
||||||
warehouse_with_no_account = []
|
warehouse_with_no_account = []
|
||||||
stock_items = self.get_stock_items()
|
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)
|
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 \
|
elif d.warehouse not in warehouse_with_no_account or \
|
||||||
d.rejected_warehouse not in warehouse_with_no_account:
|
d.rejected_warehouse not in warehouse_with_no_account:
|
||||||
warehouse_with_no_account.append(d.warehouse)
|
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:
|
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:
|
||||||
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
|
self.add_provisional_gl_entry(d, gl_entries, self.posting_date)
|
||||||
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)
|
|
||||||
|
|
||||||
if warehouse_with_no_account:
|
if warehouse_with_no_account:
|
||||||
frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
|
frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
|
||||||
"\n".join(warehouse_with_no_account))
|
"\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):
|
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')])
|
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
|
# Cost center-wise amount breakup for other charges included for valuation
|
||||||
valuation_tax = {}
|
valuation_tax = {}
|
||||||
@ -515,7 +543,8 @@ class PurchaseReceipt(BuyingController):
|
|||||||
|
|
||||||
def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account,
|
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,
|
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 = {
|
gl_entry = {
|
||||||
"account": account,
|
"account": account,
|
||||||
"cost_center": cost_center,
|
"cost_center": cost_center,
|
||||||
@ -534,6 +563,9 @@ class PurchaseReceipt(BuyingController):
|
|||||||
if credit_in_account_currency:
|
if credit_in_account_currency:
|
||||||
gl_entry.update({"credit_in_account_currency": 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))
|
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
|
||||||
|
|
||||||
def get_asset_gl_entry(self, gl_entries):
|
def get_asset_gl_entry(self, gl_entries):
|
||||||
@ -562,6 +594,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
# debit cwip account
|
# debit cwip account
|
||||||
debit_in_account_currency = (base_asset_amount
|
debit_in_account_currency = (base_asset_amount
|
||||||
if cwip_account_currency == self.company_currency else asset_amount)
|
if cwip_account_currency == self.company_currency else asset_amount)
|
||||||
|
|
||||||
self.add_gl_entry(
|
self.add_gl_entry(
|
||||||
gl_entries=gl_entries,
|
gl_entries=gl_entries,
|
||||||
account=cwip_account,
|
account=cwip_account,
|
||||||
@ -577,6 +610,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
# credit arbnb account
|
# credit arbnb account
|
||||||
credit_in_account_currency = (base_asset_amount
|
credit_in_account_currency = (base_asset_amount
|
||||||
if asset_rbnb_currency == self.company_currency else asset_amount)
|
if asset_rbnb_currency == self.company_currency else asset_amount)
|
||||||
|
|
||||||
self.add_gl_entry(
|
self.add_gl_entry(
|
||||||
gl_entries=gl_entries,
|
gl_entries=gl_entries,
|
||||||
account=arbnb_account,
|
account=arbnb_account,
|
||||||
|
@ -1312,58 +1312,6 @@ class TestPurchaseReceipt(ERPNextTestCase):
|
|||||||
self.assertEqual(pr.status, "To Bill")
|
self.assertEqual(pr.status, "To Bill")
|
||||||
self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
|
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):
|
def test_purchase_receipt_with_exchange_rate_difference(self):
|
||||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||||
make_purchase_receipt as create_purchase_receipt,
|
make_purchase_receipt as create_purchase_receipt,
|
||||||
|
@ -976,7 +976,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-11-15 15:46:10.591600",
|
"modified": "2022-02-01 11:32:27.980524",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt Item",
|
"name": "Purchase Receipt Item",
|
||||||
@ -985,5 +985,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"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