Merge branch 'version-13-hotfix' into patch-4

This commit is contained in:
Ankush Menat
2022-03-28 18:06:50 +05:30
committed by GitHub
160 changed files with 2922 additions and 1052 deletions

View File

@@ -5,7 +5,6 @@ on:
paths-ignore:
- '**.js'
- '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
@@ -30,11 +29,6 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone
uses: actions/checkout@v2

View File

@@ -5,7 +5,6 @@ on:
paths-ignore:
- '**.js'
- '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
push:
branches: [ develop ]
@@ -40,12 +39,6 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone
uses: actions/checkout@v2

View File

@@ -232,7 +232,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
}), transaction.currency, company_account)
if total_amount > transaction.unallocated_amount:
frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
frappe.throw(_("The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"))
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers:

View File

@@ -109,8 +109,13 @@ def get_paid_amount(payment_entry, currency, bank_account):
paid_amount_field = "paid_amount"
if payment_entry.payment_document == 'Payment Entry':
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
paid_amount_field = ("base_paid_amount"
if doc.paid_to_account_currency == currency else "paid_amount")
if doc.payment_type == 'Receive':
paid_amount_field = ("received_amount"
if doc.paid_to_account_currency == currency else "base_received_amount")
elif doc.payment_type == 'Pay':
paid_amount_field = ("paid_amount"
if doc.paid_to_account_currency == currency else "base_paid_amount")
return frappe.db.get_value(payment_entry.payment_document,
payment_entry.payment_entry, paid_amount_field)

View File

@@ -33,6 +33,8 @@ class GLEntry(Document):
name will be changed using autoname options (in a scheduled job)
"""
self.name = frappe.generate_hash(txt="", length=10)
if self.meta.autoname == "hash":
self.to_rename = 0
def validate(self):
self.flags.ignore_submit_comment = True
@@ -135,7 +137,7 @@ class GLEntry(Document):
def check_pl_account(self):
if self.is_opening=='Yes' and \
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and not self.is_cancelled:
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account))

View File

@@ -2,6 +2,7 @@
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
@@ -10,11 +11,10 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(ERPNextTestCase):
class TestOpeningInvoiceCreationTool(FrappeTestCase):
@classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):

View File

@@ -376,8 +376,8 @@ def make_payment_request(**args):
if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True
pr.insert(ignore_permissions=True)
if args.submit_doc:
pr.insert(ignore_permissions=True)
pr.submit()
if args.order_type == "Shopping Cart":
@@ -394,7 +394,10 @@ def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
else:
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) / ref_doc.conversion_rate
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency:

View File

@@ -128,6 +128,7 @@ class TestPaymentRequest(unittest.TestCase):
pr1 = make_payment_request(dt="Sales Order", dn=so.name,
recipient_id="nabin@erpnext.com", return_doc=1)
pr1.grand_total = 200
pr1.insert()
pr1.submit()
# Make a 2nd Payment Request

View File

@@ -54,7 +54,7 @@ class POSInvoice(SalesInvoice):
def on_submit(self):
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
if self.loyalty_program:
if not self.is_return and self.loyalty_program:
self.make_loyalty_point_entry()
elif self.is_return and self.return_against and self.loyalty_program:
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
@@ -88,7 +88,7 @@ class POSInvoice(SalesInvoice):
def on_cancel(self):
# run on cancel method of selling controller
super(SalesInvoice, self).on_cancel()
if self.loyalty_program:
if not self.is_return and self.loyalty_program:
self.delete_loyalty_point_entry()
elif self.is_return and self.return_against and self.loyalty_program:
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
@@ -456,6 +456,7 @@ class POSInvoice(SalesInvoice):
pay_req = self.get_existing_payment_request(pay)
if not pay_req:
pay_req = self.get_new_payment_request(pay)
pay_req.insert()
pay_req.submit()
else:
pay_req.request_phone_payment()

View File

@@ -83,7 +83,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn = make_sales_return(pos_inv.name)
pos_inv_cn.set("payments", [])
pos_inv_cn.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -100
})
pos_inv_cn.append('payments', {
'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': -200
})
pos_inv_cn.paid_amount = -300
pos_inv_cn.submit()
@@ -98,7 +101,12 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
consolidated_credit_note = frappe.get_doc("Sales Invoice", pos_inv_cn.consolidated_invoice)
self.assertEqual(consolidated_credit_note.is_return, 1)
self.assertEqual(consolidated_credit_note.payments[0].mode_of_payment, 'Cash')
self.assertEqual(consolidated_credit_note.payments[0].amount, -100)
self.assertEqual(consolidated_credit_note.payments[1].mode_of_payment, 'Bank Draft')
self.assertEqual(consolidated_credit_note.payments[1].amount, -200)
finally:
frappe.set_user("Administrator")

View File

@@ -33,6 +33,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year
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.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
@@ -105,6 +106,7 @@ class PurchaseInvoice(BuyingController):
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.set_expense_account(for_validate=True)
self.validate_expense_account()
self.set_against_expense_account()
self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
@@ -309,6 +311,10 @@ class PurchaseInvoice(BuyingController):
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
def validate_expense_account(self):
for item in self.get('items'):
validate_account_head(item.idx, item.expense_account, self.company, 'Expense')
def set_against_expense_account(self):
against_accounts = []
for item in self.get("items"):

View File

@@ -38,14 +38,18 @@ from erpnext.assets.doctype.asset.depreciation import (
get_gl_entries_on_asset_regain,
make_depreciation_entry,
)
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.selling_controller import SellingController
from erpnext.healthcare.utils import manage_invoice_submit_cancel
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.setup.doctype.company.company import update_company_current_month_sales
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
from erpnext.stock.utils import calculate_mapped_packed_items_return
from erpnext.stock.doctype.serial_no.serial_no import (
get_delivery_note_serial_no,
get_serial_nos,
update_serial_nos_after_submit,
)
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -110,6 +114,8 @@ class SalesInvoice(SellingController):
self.validate_fixed_asset()
self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
self.validate_income_account()
validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference)
if cint(self.is_pos):
@@ -177,6 +183,10 @@ class SalesInvoice(SellingController):
if cost_center_company != self.company:
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
def validate_income_account(self):
for item in self.get('items'):
validate_account_head(item.idx, item.income_account, self.company, 'Income')
def set_tax_withholding(self):
tax_withholding_details = get_party_tax_withholding_details(self)
@@ -228,6 +238,9 @@ class SalesInvoice(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
self.update_stock_ledger()
if self.is_return and self.update_stock:
update_serial_nos_after_submit(self, "items")
# this sequence because outstanding may get -ve
self.make_gl_entries()
@@ -752,11 +765,8 @@ class SalesInvoice(SellingController):
def update_packing_list(self):
if cint(self.update_stock) == 1:
if cint(self.is_return) and self.return_against:
calculate_mapped_packed_items_return(self)
else:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
else:
self.set('packed_items', [])
@@ -1415,12 +1425,19 @@ class SalesInvoice(SellingController):
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
def get_returned_amount(self):
returned_amount = frappe.db.sql("""
select sum(grand_total)
from `tabSales Invoice`
where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
""", self.name)
return abs(flt(returned_amount[0][0])) if returned_amount else 0
from frappe.query_builder.functions import Coalesce, Sum
doc = frappe.qb.DocType(self.doctype)
returned_amount = (
frappe.qb.from_(doc)
.select(Sum(doc.grand_total))
.where(
(doc.docstatus == 1)
& (doc.is_return == 1)
& (Coalesce(doc.return_against, '') == self.name)
)
).run()
return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
# redeem the loyalty points.
def apply_loyalty_points(self):
@@ -1700,7 +1717,6 @@ def make_maintenance_schedule(source_name, target_doc=None):
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None):
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
@@ -1745,6 +1761,7 @@ def make_delivery_note(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
@frappe.whitelist()

View File

@@ -812,12 +812,37 @@ class TestSalesInvoice(unittest.TestCase):
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
pos.change_amount = 5.0
pos.write_off_outstanding_amount_automatically = 1
pos.insert()
pos.submit()
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, -5)
self.assertEqual(pos.write_off_amount, 0)
def test_auto_write_off_amount(self):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
cost_center = "Main - TCP1", do_not_save=True)
pos.is_pos = 1
pos.update_stock = 1
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 40})
pos.write_off_outstanding_amount_automatically = 1
pos.insert()
pos.submit()
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, 10)
def test_pos_with_no_gl_entry_for_change_amount(self):
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)
@@ -2540,6 +2565,12 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def test_standalone_serial_no_return(self):
si = create_sales_invoice(item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1)
si.reload()
self.assertTrue(si.items[0].serial_no)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'

View File

@@ -71,8 +71,7 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
if shipping_amount:
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"):

View File

@@ -267,7 +267,7 @@ def get_loan_amount(filters):
total_amount += flt(amount)
return amount
return total_amount
def get_balance_row(label, amount, account_currency):
if amount > 0:

View File

@@ -4,7 +4,8 @@
import frappe
from frappe import _
from frappe.utils import add_to_date
from frappe.query_builder.functions import Sum
from frappe.utils import add_to_date, flt, get_date_str
from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
@@ -28,15 +29,22 @@ def get_mappers_from_db():
def get_accounts_in_mappers(mapping_names):
return frappe.db.sql('''
select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability,
cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment
from `tabCash Flow Mapping Accounts` cfma
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
where cfma.parent in (%s)
order by cfm.is_working_capital
''', (', '.join('"%s"' % d for d in mapping_names)))
cfm = frappe.qb.DocType('Cash Flow Mapping')
cfma = frappe.qb.DocType('Cash Flow Mapping Accounts')
result = (
frappe.qb
.select(
cfma.name, cfm.label, cfm.is_working_capital,
cfm.is_income_tax_liability, cfm.is_income_tax_expense,
cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account
)
.from_(cfm)
.join(cfma)
.on(cfm.name == cfma.parent)
.where(cfma.parent.isin(mapping_names))
).run()
return result
def setup_mappers(mappers):
cash_flow_accounts = []
@@ -57,31 +65,31 @@ def setup_mappers(mappers):
account_types = [
dict(
name=account[0], label=account[1], is_working_capital=account[2],
name=account[0], account_name=account[7], label=account[1], is_working_capital=account[2],
is_income_tax_liability=account[3], is_income_tax_expense=account[4]
) for account in accounts if not account[3]]
finance_costs_adjustments = [
dict(
name=account[0], label=account[1], is_finance_cost=account[5],
name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5],
is_finance_cost_adjustment=account[6]
) for account in accounts if account[6]]
tax_liabilities = [
dict(
name=account[0], label=account[1], is_income_tax_liability=account[3],
name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3],
is_income_tax_expense=account[4]
) for account in accounts if account[3]]
tax_expenses = [
dict(
name=account[0], label=account[1], is_income_tax_liability=account[3],
name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3],
is_income_tax_expense=account[4]
) for account in accounts if account[4]]
finance_costs = [
dict(
name=account[0], label=account[1], is_finance_cost=account[5])
name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5])
for account in accounts if account[5]]
account_types_labels = sorted(
@@ -124,27 +132,27 @@ def setup_mappers(mappers):
)
for label in account_types_labels:
names = [d['name'] for d in account_types if d['label'] == label[0]]
names = [d['account_name'] for d in account_types if d['label'] == label[0]]
m = dict(label=label[0], names=names, is_working_capital=label[1])
mapping['account_types'].append(m)
for label in fc_adjustment_labels:
names = [d['name'] for d in finance_costs_adjustments if d['label'] == label[0]]
names = [d['account_name'] for d in finance_costs_adjustments if d['label'] == label[0]]
m = dict(label=label[0], names=names)
mapping['finance_costs_adjustments'].append(m)
for label in unique_liability_labels:
names = [d['name'] for d in tax_liabilities if d['label'] == label[0]]
names = [d['account_name'] for d in tax_liabilities if d['label'] == label[0]]
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
mapping['tax_liabilities'].append(m)
for label in unique_expense_labels:
names = [d['name'] for d in tax_expenses if d['label'] == label[0]]
names = [d['account_name'] for d in tax_expenses if d['label'] == label[0]]
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
mapping['tax_expenses'].append(m)
for label in unique_finance_costs_labels:
names = [d['name'] for d in finance_costs if d['label'] == label[0]]
names = [d['account_name'] for d in finance_costs if d['label'] == label[0]]
m = dict(label=label[0], names=names, is_finance_cost=label[1])
mapping['finance_costs'].append(m)
@@ -371,14 +379,30 @@ def execute(filters=None):
def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0):
if not account_names or not account_names[0] or not type(account_names[0]) == str:
# only proceed if account_names is a list of account names
return {}
from erpnext.accounts.report.cash_flow.cash_flow import get_start_date
company = filters.company
data = {}
total = 0
GLEntry = frappe.qb.DocType('GL Entry')
Account = frappe.qb.DocType('Account')
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
accounts = ', '.join('"%s"' % d for d in account_names)
account_subquery = (
frappe.qb.from_(Account)
.where(
(Account.name.isin(account_names)) |
(Account.parent_account.isin(account_names))
)
.select(Account.name)
.as_("account_subquery")
)
if opening_balances:
date_info = dict(date=start_date)
@@ -395,32 +419,31 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
else:
start, end = add_to_date(**date_info), add_to_date(**date_info)
gl_sum = frappe.db.sql_list("""
select sum(credit) - sum(debit)
from `tabGL Entry`
where company=%s and posting_date >= %s and posting_date <= %s
and voucher_type != 'Period Closing Voucher'
and account in ( SELECT name FROM tabAccount WHERE name IN (%s)
OR parent_account IN (%s))
""", (company, start, end, accounts, accounts))
else:
gl_sum = frappe.db.sql_list("""
select sum(credit) - sum(debit)
from `tabGL Entry`
where company=%s and posting_date >= %s and posting_date <= %s
and voucher_type != 'Period Closing Voucher'
and account in ( SELECT name FROM tabAccount WHERE name IN (%s)
OR parent_account IN (%s))
""", (company, start_date if accumulated_values else period['from_date'],
period['to_date'], accounts, accounts))
start, end = get_date_str(start), get_date_str(end)
if gl_sum and gl_sum[0]:
amount = gl_sum[0]
else:
amount = 0
start, end = start_date if accumulated_values else period['from_date'], period['to_date']
start, end = get_date_str(start), get_date_str(end)
total += amount
data.setdefault(period["key"], amount)
result = (
frappe.qb.from_(GLEntry)
.select(Sum(GLEntry.credit) - Sum(GLEntry.debit))
.where(
(GLEntry.company == company) &
(GLEntry.posting_date >= start) &
(GLEntry.posting_date <= end) &
(GLEntry.voucher_type != 'Period Closing Voucher') &
(GLEntry.account.isin(account_subquery))
)
).run()
if result and result[0]:
gl_sum = result[0][0]
else:
gl_sum = 0
total += flt(gl_sum)
data.setdefault(period["key"], flt(gl_sum))
data["total"] = total
return data

View File

@@ -88,10 +88,12 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
do_not_submit=True,
do_not_save=True,
rate=300,
price_list_rate=300,
)
si.items[0].income_account = "Sales - _CD"
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2021-05-01"
si.items[0].service_end_date = "2021-08-01"
@@ -269,11 +271,13 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
do_not_submit=True,
do_not_save=True,
rate=300,
price_list_rate=300,
)
si.items[0].enable_deferred_revenue = 1
si.items[0].income_account = "Sales - _CD"
si.items[0].deferred_revenue_account = deferred_revenue_account
si.items[0].income_account = "Sales - _CD"
si.save()

View File

@@ -39,12 +39,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_start_date"),
"reqd": 1
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_end_date"),
"reqd": 1
},
{
"fieldname": "finance_book",
@@ -56,6 +58,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldname": "dimension",
"label": __("Select Dimension"),
"fieldtype": "Select",
"default": "Cost Center",
"options": get_accounting_dimension_options(),
"reqd": 1,
},
@@ -70,7 +73,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
});
function get_accounting_dimension_options() {
let options =["", "Cost Center", "Project"];
let options =["Cost Center", "Project"];
frappe.db.get_list('Accounting Dimension',
{fields:['document_type']}).then((res) => {
res.forEach((dimension) => {

View File

@@ -16,20 +16,21 @@ from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
def execute(filters=None):
validate_filters(filters)
dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
if not dimension_items_list:
validate_filters(filters)
dimension_list = get_dimensions(filters)
if not dimension_list:
return [], []
dimension_items_list = [''.join(d) for d in dimension_items_list]
columns = get_columns(dimension_items_list)
data = get_data(filters, dimension_items_list)
columns = get_columns(dimension_list)
data = get_data(filters, dimension_list)
return columns, data
def get_data(filters, dimension_items_list):
def get_data(filters, dimension_list):
company_currency = erpnext.get_company_currency(filters.company)
acc = frappe.db.sql("""
select
name, account_number, parent_account, lft, rgt, root_type,
@@ -52,60 +53,54 @@ def get_data(filters, dimension_items_list):
where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
gl_entries_by_account = {}
set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account)
format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list,
frappe.scrub(filters.get('dimension')))
accumulate_values_into_parents(accounts, accounts_by_name, dimension_list)
out = prepare_data(accounts, filters, company_currency, dimension_list)
out = filter_out_zero_value_rows(out, parent_children_map)
return out
def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
for item in dimension_items_list:
condition = get_condition(filters.from_date, item, filters.dimension)
if account:
condition += " and account in ({})"\
.format(", ".join([frappe.db.escape(d) for d in account]))
def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account):
condition = get_condition(filters.get('dimension'))
gl_filters = {
"company": filters.get("company"),
"from_date": filters.get("from_date"),
"to_date": filters.get("to_date"),
"finance_book": cstr(filters.get("finance_book"))
}
if account:
condition += " and account in ({})"\
.format(", ".join([frappe.db.escape(d) for d in account]))
gl_filters['item'] = ''.join(item)
gl_filters = {
"company": filters.get("company"),
"from_date": filters.get("from_date"),
"to_date": filters.get("to_date"),
"finance_book": cstr(filters.get("finance_book"))
}
if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.db.get_value("Company",
filters.company, 'default_finance_book')
gl_filters['dimensions'] = set(dimension_list)
for key, value in filters.items():
if value:
gl_filters.update({
key: value
})
if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.db.get_value("Company",
filters.company, 'default_finance_book')
gl_entries = frappe.db.sql("""
gl_entries = frappe.db.sql("""
select
posting_date, account, debit, credit, is_opening, fiscal_year,
posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year,
debit_in_account_currency, credit_in_account_currency, account_currency
from
`tabGL Entry`
where
company=%(company)s
{condition}
and posting_date >= %(from_date)s
and posting_date <= %(to_date)s
and is_cancelled = 0
order by account, posting_date""".format(
condition=condition),
gl_filters, as_dict=True) #nosec
dimension = frappe.scrub(filters.get('dimension')), condition=condition), gl_filters, as_dict=True) #nosec
for entry in gl_entries:
entry['dimension_item'] = ''.join(item)
gl_entries_by_account.setdefault(entry.account, []).append(entry)
for entry in gl_entries:
gl_entries_by_account.setdefault(entry.account, []).append(entry)
def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type):
for entries in itervalues(gl_entries_by_account):
for entry in entries:
@@ -115,11 +110,12 @@ def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_l
_("Could not retrieve information for {0}.").format(entry.account), title="Error",
raise_exception=1
)
for item in dimension_items_list:
if item == entry.dimension_item:
d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
for dimension in dimension_list:
if dimension == entry.get(dimension_type):
d[frappe.scrub(dimension)] = d.get(frappe.scrub(dimension), 0.0) + flt(entry.debit) - flt(entry.credit)
def prepare_data(accounts, filters, company_currency, dimension_list):
data = []
for d in accounts:
@@ -136,13 +132,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen
if d.account_number else d.account_name)
}
for item in dimension_items_list:
row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
for dimension in dimension_list:
row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
if abs(row[frappe.scrub(item)]) >= 0.005:
if abs(row[frappe.scrub(dimension)]) >= 0.005:
# ignore zero values
has_value = True
total += flt(d.get(frappe.scrub(item), 0.0), 3)
total += flt(d.get(frappe.scrub(dimension), 0.0), 3)
row["has_value"] = has_value
row["total"] = total
@@ -150,62 +146,55 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen
return data
def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
"""accumulate children's values in parent accounts"""
for d in reversed(accounts):
if d.parent_account:
for item in dimension_items_list:
accounts_by_name[d.parent_account][frappe.scrub(item)] = \
accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
for dimension in dimension_list:
accounts_by_name[d.parent_account][frappe.scrub(dimension)] = \
accounts_by_name[d.parent_account].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0)
def get_condition(from_date, item, dimension):
def get_condition(dimension):
conditions = []
if from_date:
conditions.append("posting_date >= %(from_date)s")
if dimension:
if dimension not in ['Cost Center', 'Project']:
if dimension in ['Customer', 'Supplier']:
dimension = 'Party'
else:
dimension = 'Voucher No'
txt = "{0} = %(item)s".format(frappe.scrub(dimension))
conditions.append(txt)
conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension)))
return " and {}".format(" and ".join(conditions)) if conditions else ""
def get_dimension_items_list(dimension, company):
meta = frappe.get_meta(dimension, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
filters = {}
if 'company' in fieldnames:
filters['company'] = company
return frappe.get_all(dimension, filters, as_list=True)
def get_dimensions(filters):
meta = frappe.get_meta(filters.get('dimension'), cached=False)
query_filters = {}
def get_columns(dimension_items_list, accumulated_values=1, company=None):
if meta.has_field('company'):
query_filters = {'company': filters.get('company')}
return frappe.get_all(filters.get('dimension'), filters=query_filters, pluck='name')
def get_columns(dimension_list):
columns = [{
"fieldname": "account",
"label": _("Account"),
"fieldtype": "Link",
"options": "Account",
"width": 300
},
{
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"hidden": 1
}]
if company:
for dimension in dimension_list:
columns.append({
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"hidden": 1
})
for item in dimension_items_list:
columns.append({
"fieldname": frappe.scrub(item),
"label": item,
"fieldname": frappe.scrub(dimension),
"label": dimension,
"fieldtype": "Currency",
"options": "currency",
"width": 150
})
columns.append({
"fieldname": "total",
"label": "Total",

View File

@@ -0,0 +1,134 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
from erpnext.accounts.report.general_ledger.general_ledger import execute
class TestGeneralLedger(FrappeTestCase):
def test_foreign_account_balance_after_exchange_rate_revaluation(self):
"""
Checks the correctness of balance after exchange rate revaluation
"""
# create a new account with USD currency
account_name = "Test USD Account for Revalutation"
company = "_Test Company"
account = frappe.get_doc({
"account_name": account_name,
"is_group": 0,
"company": company,
"root_type": "Asset",
"report_type": "Balance Sheet",
"account_currency": "USD",
"inter_company_account": 0,
"parent_account": "Bank Accounts - _TC",
"account_type": "Bank",
"doctype": "Account"
})
account.insert(ignore_if_duplicate=True)
# create a JV to debit 1000 USD at 75 exchange rate
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = company
jv.multi_currency = 1
jv.cost_center = "_Test Cost Center - _TC"
jv.set("accounts", [
{
"account": account.name,
"debit_in_account_currency": 1000,
"credit_in_account_currency": 0,
"exchange_rate": 75,
"cost_center": "_Test Cost Center - _TC",
},
{
"account": "Cash - _TC",
"debit_in_account_currency": 0,
"credit_in_account_currency": 75000,
"cost_center": "_Test Cost Center - _TC",
},
])
jv.save()
jv.submit()
# create a JV to credit 900 USD at 100 exchange rate
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = company
jv.multi_currency = 1
jv.cost_center = "_Test Cost Center - _TC"
jv.set("accounts", [
{
"account": account.name,
"debit_in_account_currency": 0,
"credit_in_account_currency": 900,
"exchange_rate": 100,
"cost_center": "_Test Cost Center - _TC",
},
{
"account": "Cash - _TC",
"debit_in_account_currency": 90000,
"credit_in_account_currency": 0,
"cost_center": "_Test Cost Center - _TC",
},
])
jv.save()
jv.submit()
# create an exchange rate revaluation entry at 77 exchange rate
revaluation = frappe.new_doc("Exchange Rate Revaluation")
revaluation.posting_date = today()
revaluation.company = company
revaluation.set("accounts", [
{
"account": account.name,
"account_currency": "USD",
"new_exchange_rate": 77,
"new_balance_in_base_currency": 7700,
"balance_in_base_currency": -15000,
"balance_in_account_currency": 100,
"current_exchange_rate": -150
}
])
revaluation.save()
revaluation.submit()
# post journal entry to revaluate
frappe.db.set_value('Company', company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC")
revaluation_jv = revaluation.make_jv_entry()
revaluation_jv = frappe.get_doc(revaluation_jv)
revaluation_jv.cost_center = "_Test Cost Center - _TC"
for acc in revaluation_jv.get("accounts"):
acc.cost_center = "_Test Cost Center - _TC"
revaluation_jv.save()
revaluation_jv.submit()
# check the balance of the account
balance = frappe.db.sql(
"""
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where account = %s
group by account
""", account.name)
self.assertEqual(balance[0][0], 100)
# check if general ledger shows correct balance
columns, data = execute(frappe._dict({
"company": company,
"from_date": today(),
"to_date": today(),
"account": [account.name],
"group_by": "Group by Voucher (Consolidated)",
}))
self.assertEqual(data[1]["account"], account.name)
self.assertEqual(data[1]["debit"], 1000)
self.assertEqual(data[1]["credit"], 0)
self.assertEqual(data[2]["debit"], 0)
self.assertEqual(data[2]["credit"], 900)
self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 100)

View File

@@ -107,6 +107,7 @@ def get_opening_balances(filters):
select party, sum(debit) as opening_debit, sum(credit) as opening_credit
from `tabGL Entry`
where company=%(company)s
and is_cancelled=0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
{account_filter}
@@ -133,6 +134,7 @@ def get_balances_within_period(filters):
select party, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry`
where company=%(company)s
and is_cancelled = 0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and posting_date >= %(from_date)s and posting_date <= %(to_date)s
and ifnull(is_opening, 'No') = 'No'

View File

@@ -93,10 +93,10 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
account_currency = entry['account_currency']
if len(account_currencies) == 1 and account_currency == presentation_currency:
if entry.get('debit'):
if debit_in_account_currency:
entry['debit'] = debit_in_account_currency
if entry.get('credit'):
if credit_in_account_currency:
entry['credit'] = credit_in_account_currency
else:
date = currency_info['report_date']

View File

@@ -402,7 +402,6 @@ def close_or_unclose_purchase_orders(names, status):
frappe.local.message_log = []
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
@@ -443,6 +442,8 @@ def make_purchase_receipt(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doc.set_onload('ignore_price_list', True)
return doc
@frappe.whitelist()
@@ -510,6 +511,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
doc = get_mapped_doc("Purchase Order", source_name, fields,
target_doc, postprocess, ignore_permissions=ignore_permissions)
doc.set_onload('ignore_price_list', True)
return doc

View File

@@ -72,8 +72,8 @@
"section_break_46",
"base_grand_total",
"base_rounding_adjustment",
"base_in_words",
"base_rounded_total",
"base_in_words",
"column_break4",
"grand_total",
"rounding_adjustment",
@@ -635,6 +635,7 @@
"fieldname": "rounded_total",
"fieldtype": "Currency",
"label": "Rounded Total",
"options": "currency",
"read_only": 1
},
{
@@ -810,7 +811,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-12-11 06:43:20.924080",
"modified": "2022-03-14 16:13:20.284572",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
@@ -875,6 +876,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"timeline_field": "supplier",
"title_field": "title"
}

View File

@@ -109,7 +109,6 @@ def get_list_context(context=None):
@frappe.whitelist()
def make_purchase_order(source_name, target_doc=None):
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("get_schedule_dates")
target.run_method("calculate_taxes_and_totals")
@@ -140,6 +139,7 @@ def make_purchase_order(source_name, target_doc=None):
},
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
@frappe.whitelist()

View File

@@ -1567,12 +1567,12 @@ def validate_taxes_and_charges(tax):
tax.rate = None
def validate_account_head(idx, account, company):
def validate_account_head(idx, account, company, context=''):
account_company = frappe.get_cached_value('Account', account, 'company')
if account_company != company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
.format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
frappe.throw(_('Row {0}: {3} Account {1} does not belong to Company {2}')
.format(idx, frappe.bold(account), frappe.bold(company), context), title=_('Invalid Account'))
def validate_cost_center(tax, doc):

View File

@@ -168,7 +168,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
{account_type_condition}
AND is_group = 0
AND company = %(company)s
AND account_currency = %(currency)s
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
AND `{searchfield}` LIKE %(txt)s
{mcond}
ORDER BY idx DESC, name

View File

@@ -208,10 +208,15 @@ def get_already_returned_items(doc):
return items
def get_returned_qty_map_for_row(row_name, doctype):
def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
if doctype in ('Purchase Receipt', 'Purchase Invoice'):
party_type = 'supplier'
else:
party_type = 'customer'
fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
@@ -226,9 +231,12 @@ def get_returned_qty_map_for_row(row_name, doctype):
if doctype == "Purchase Receipt":
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
# Used retrun against and supplier and is_retrun because there is an index added for it
data = frappe.db.get_list(doctype,
fields = fields,
filters = [
[doctype, "return_against", "=", return_against],
[doctype, party_type, "=", party],
[doctype, "docstatus", "=", 1],
[doctype, "is_return", "=", 1],
[child_doctype, reference_field, "=", row_name]
@@ -307,7 +315,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.serial_no = '\n'.join(serial_nos)
if doctype == "Purchase Receipt":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
@@ -321,7 +329,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
@@ -335,7 +343,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
@@ -348,7 +356,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
@@ -391,6 +399,8 @@ def make_return_doc(doctype, source_name, target_doc=None):
}
}, target_doc, set_missing_values)
doclist.set_onload('ignore_price_list', True)
return doclist
def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None,

View File

@@ -37,6 +37,8 @@ class calculate_taxes_and_totals(object):
self.set_discount_amount()
self.apply_discount_amount()
self.calculate_shipping_charges()
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
self.calculate_total_advance()
@@ -50,7 +52,6 @@ class calculate_taxes_and_totals(object):
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
self.calculate_shipping_charges()
self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals()
@@ -113,20 +114,16 @@ class calculate_taxes_and_totals(object):
for item in self.doc.get("items"):
self.doc.round_floats_in(item)
if not item.rate:
item.rate = item.price_list_rate
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
if item.pricing_rules or abs(item.discount_percentage) > 0:
if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
if abs(item.discount_percentage) > 0:
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
elif item.discount_amount or item.pricing_rules:
elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
@@ -276,6 +273,8 @@ class calculate_taxes_and_totals(object):
shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule)
shipping_rule.apply(self.doc)
self._calculate()
def calculate_taxes(self):
rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
if not rounding_adjustment_computed:
@@ -581,7 +580,11 @@ class calculate_taxes_and_totals(object):
.format(self.doc.party_account_currency, invoice_total))
if self.doc.docstatus == 0:
if self.doc.get('write_off_outstanding_amount_automatically'):
self.doc.write_off_amount = 0
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
def is_internal_invoice(self):
"""
@@ -622,7 +625,6 @@ class calculate_taxes_and_totals(object):
change_amount = 0
if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'):
self.calculate_write_off_amount()
self.calculate_change_amount()
change_amount = self.doc.change_amount \
if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount
@@ -633,8 +635,14 @@ class calculate_taxes_and_totals(object):
self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount),
self.doc.precision("outstanding_amount"))
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
self.update_paid_amount_for_return(total_amount_to_pay)
if (
self.doc.doctype == 'Sales Invoice'
and self.doc.get('is_pos')
and self.doc.get('is_return')
and not self.doc.get('is_consolidated')
):
self.set_total_amount_to_default_mop(total_amount_to_pay)
self.calculate_paid_amount()
def calculate_paid_amount(self):
@@ -666,19 +674,20 @@ class calculate_taxes_and_totals(object):
and self.doc.paid_amount > grand_total and not self.doc.is_return \
and any(d.type == "Cash" for d in self.doc.payments):
self.doc.change_amount = flt(self.doc.paid_amount - grand_total +
self.doc.write_off_amount, self.doc.precision("change_amount"))
self.doc.change_amount = flt(self.doc.paid_amount - grand_total,
self.doc.precision("change_amount"))
self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total +
self.doc.base_write_off_amount, self.doc.precision("base_change_amount"))
self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total,
self.doc.precision("base_change_amount"))
def calculate_write_off_amount(self):
if flt(self.doc.change_amount) > 0:
self.doc.write_off_amount = flt(self.doc.grand_total - self.doc.paid_amount
+ self.doc.change_amount, self.doc.precision("write_off_amount"))
if self.doc.get('write_off_outstanding_amount_automatically'):
self.doc.write_off_amount = flt(self.doc.outstanding_amount, self.doc.precision("write_off_amount"))
self.doc.base_write_off_amount = flt(self.doc.write_off_amount * self.doc.conversion_rate,
self.doc.precision("base_write_off_amount"))
self.calculate_outstanding_amount()
def calculate_margin(self, item):
rate_with_margin = 0.0
base_rate_with_margin = 0.0
@@ -714,7 +723,7 @@ class calculate_taxes_and_totals(object):
def set_item_wise_tax_breakup(self):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def update_paid_amount_for_return(self, total_amount_to_pay):
def set_total_amount_to_default_mop(self, total_amount_to_pay):
default_mode_of_payment = frappe.db.get_value('POS Payment Method',
{'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1)
@@ -726,8 +735,6 @@ class calculate_taxes_and_totals(object):
'default': 1
})
self.calculate_paid_amount()
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
return

View File

@@ -189,6 +189,7 @@ class Lead(SellingController):
})
contact.insert(ignore_permissions=True)
contact.reload() # load changes by hooks on contact
return contact

View File

@@ -5,6 +5,12 @@ frappe.ui.form.on('Website Item', {
onload: function(frm) {
// should never check Private
frm.fields_dict["website_image"].df.is_private = 0;
frm.set_query("website_warehouse", () => {
return {
filters: {"is_group": 0}
};
});
},
image: function() {

View File

@@ -424,6 +424,22 @@ erpnext.ProductView = class {
me.change_route_with_filters();
});
// bind filter lookup input box
$('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => {
const $input = $(e.target);
const keyword = ($input.val() || '').toLowerCase();
const $filter_options = $input.next('.filter-options');
$filter_options.find('.filter-lookup-wrapper').show();
$filter_options.find('.filter-lookup-wrapper').each((i, el) => {
const $el = $(el);
const value = $el.data('value').toLowerCase();
if (!value.includes(keyword)) {
$el.hide();
}
});
}, 300));
}
change_route_with_filters() {
@@ -501,7 +517,7 @@ erpnext.ProductView = class {
categories.forEach(category => {
sub_group_html += `
<a href="${ category.route || '#' }" style="text-decoration: none;">
<a href="/${ category.route || '#' }" style="text-decoration: none;">
<div class="category-pill">
${ category.name }
</div>

View File

@@ -17,7 +17,7 @@ from erpnext.e_commerce.shopping_cart.cart import (
request_for_quotation,
update_cart,
)
from erpnext.tests.utils import change_settings, create_test_contact_and_address
from erpnext.tests.utils import create_test_contact_and_address
class TestShoppingCart(unittest.TestCase):

View File

@@ -1,6 +1,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
@@ -9,11 +10,10 @@ from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings imp
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Item"]
class TestVariantSelector(ERPNextTestCase):
class TestVariantSelector(FrappeTestCase):
@classmethod
def setUpClass(cls):
@@ -118,4 +118,4 @@ class TestVariantSelector(ERPNextTestCase):
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(price_info["price_list_rate"], 100.0)
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")

View File

@@ -24,13 +24,12 @@
"expected_discharge",
"references",
"admission_encounter",
"admission_practitioner",
"primary_practitioner",
"medical_department",
"admission_ordered_for",
"expected_length_of_stay",
"admission_service_unit_type",
"cb_admission",
"primary_practitioner",
"secondary_practitioner",
"admission_instruction",
"encounter_details_section",
@@ -134,11 +133,11 @@
"read_only": 1
},
{
"fetch_from": "primary_practitioner.department",
"fieldname": "medical_department",
"fieldtype": "Link",
"label": "Medical Department",
"options": "Medical Department",
"set_only_once": 1
"options": "Medical Department"
},
{
"fieldname": "primary_practitioner",
@@ -211,13 +210,6 @@
"fieldname": "cb_admission",
"fieldtype": "Column Break"
},
{
"fieldname": "admission_practitioner",
"fieldtype": "Link",
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
"read_only": 1
},
{
"fieldname": "admission_encounter",
"fieldtype": "Link",
@@ -412,7 +404,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-09 22:49:07.419692",
"modified": "2022-02-22 12:15:02.843426",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Record",

View File

@@ -72,7 +72,6 @@
"fieldtype": "Link",
"in_filter": 1,
"label": "Patient Appointment",
"no_copy": 1,
"options": "Patient Appointment",
"print_hide": 1,
"read_only": 1
@@ -82,7 +81,6 @@
"fieldtype": "Link",
"in_filter": 1,
"label": "Patient Encounter",
"no_copy": 1,
"options": "Patient Encounter",
"print_hide": 1,
"read_only": 1
@@ -258,7 +256,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2022-01-20 12:30:07.515185",
"modified": "2022-02-19 11:48:16.347334",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Vital Signs",

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, get_first_day, getdate, now_datetime, nowdate
from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
from erpnext.hr.doctype.attendance.attendance import (
get_month_map,
@@ -16,6 +16,13 @@ from erpnext.hr.doctype.leave_application.test_leave_application import get_firs
test_records = frappe.get_test_records('Attendance')
class TestAttendance(FrappeTestCase):
def setUp(self):
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
from_date = get_year_start(getdate())
to_date = get_year_ending(getdate())
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
def test_mark_absent(self):
employee = make_employee("test_mark_absent@example.com")
date = nowdate()
@@ -31,12 +38,9 @@ class TestAttendance(FrappeTestCase):
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee})
frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
first_sunday = get_first_sunday(holiday_list, for_date=first_day)
first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day)
@@ -58,11 +62,9 @@ class TestAttendance(FrappeTestCase):
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee})
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
first_sunday = get_first_sunday(holiday_list, for_date=first_day)
first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day)
@@ -87,9 +89,7 @@ class TestAttendance(FrappeTestCase):
relieving_date=relieving_date)
frappe.db.delete('Attendance', {'employee': employee})
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
attendance_date = add_days(first_day, 2)
mark_attendance(employee, attendance_date, 'Present')

View File

@@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import nowdate
from frappe.utils import flt, nowdate
import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -13,6 +13,7 @@ from erpnext.hr.doctype.employee_advance.employee_advance import (
create_return_through_additional_salary,
make_bank_entry,
)
from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
@@ -118,3 +119,24 @@ def make_employee_advance(employee_name, args=None):
doc.submit()
return doc
def get_advances_for_claim(claim, advance_name, amount=None):
advances = get_advances(claim.employee, advance_name)
for entry in advances:
if amount:
allocated_amount = amount
else:
allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount)
claim.append("advances", {
"employee_advance": entry.name,
"posting_date": entry.posting_date,
"advance_account": entry.advance_account,
"advance_paid": entry.paid_amount,
"unclaimed_amount": allocated_amount,
"allocated_amount": allocated_amount
})
return claim

View File

@@ -23,10 +23,10 @@ class ExpenseClaim(AccountsController):
def validate(self):
validate_active_employee(self.employee)
self.validate_advances()
set_employee_name(self)
self.validate_sanctioned_amount()
self.calculate_total_amount()
set_employee_name(self)
self.validate_advances()
self.set_expense_account(validate=True)
self.set_payable_account()
self.set_cost_center()
@@ -42,10 +42,18 @@ class ExpenseClaim(AccountsController):
"2": "Cancelled"
}[cstr(self.docstatus or 0)]
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("grand_total")
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1
and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
if (
# set as paid
self.is_paid
or (flt(self.total_sanctioned_amount > 0) and (
# grand total is reimbursed
(self.docstatus == 1 and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision))
# grand total (to be paid) is 0 since linked advances already cover the claimed amount
or (flt(self.grand_total, precision) == 0)
))
) and self.approval_status == "Approved":
status = "Paid"
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
status = "Unpaid"

View File

@@ -72,6 +72,72 @@ class TestExpenseClaim(unittest.TestCase):
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
self.assertEqual(expense_claim.status, "Unpaid")
# expense claim without any sanctioned amount should not have status as Paid
claim = make_expense_claim(payable_account, 1000, 0, "_Test Company", "Travel Expenses - _TC")
self.assertEqual(claim.total_sanctioned_amount, 0)
self.assertEqual(claim.status, "Submitted")
# no gl entries created
gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': claim.name})
self.assertEqual(len(gl_entry), 0)
def test_expense_claim_against_fully_paid_advances(self):
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
get_advances_for_claim,
make_employee_advance,
make_payment_entry,
)
frappe.db.delete("Employee Advance")
payable_account = get_payable_account("_Test Company")
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
advance = make_employee_advance(claim.employee)
pe = make_payment_entry(advance)
pe.submit()
# claim for already paid out advances
claim = get_advances_for_claim(claim, advance.name)
claim.save()
claim.submit()
self.assertEqual(claim.grand_total, 0)
self.assertEqual(claim.status, "Paid")
def test_expense_claim_partially_paid_via_advance(self):
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
get_advances_for_claim,
make_employee_advance,
)
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
make_payment_entry as make_advance_payment,
)
frappe.db.delete("Employee Advance")
payable_account = get_payable_account("_Test Company")
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
# link advance for partial amount
advance = make_employee_advance(claim.employee, {'advance_amount': 500})
pe = make_advance_payment(advance)
pe.submit()
claim = get_advances_for_claim(claim, advance.name)
claim.save()
claim.submit()
self.assertEqual(claim.grand_total, 500)
self.assertEqual(claim.status, "Unpaid")
# reimburse remaning amount
make_payment_entry(claim, payable_account, 500)
claim.reload()
self.assertEqual(claim.total_amount_reimbursed, 500)
self.assertEqual(claim.status, "Paid")
def test_expense_claim_gl_entry(self):
payable_account = get_payable_account(company_name)
taxes = generate_taxes()

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import unittest
from contextlib import contextmanager
from datetime import timedelta
import frappe
@@ -30,3 +31,24 @@ def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getd
"holidays" : holiday_dates
}).insert()
return doc
@contextmanager
def set_holiday_list(holiday_list, company_name):
"""
Context manager for setting holiday list in tests
"""
try:
company = frappe.get_doc('Company', company_name)
previous_holiday_list = company.default_holiday_list
company.default_holiday_list = holiday_list
company.save()
yield
finally:
# restore holiday list setup
company = frappe.get_doc('Company', company_name)
company.default_holiday_list = previous_holiday_list
company.save()

View File

@@ -52,7 +52,7 @@ frappe.ui.form.on("Leave Application", {
make_dashboard: function(frm) {
var leave_details;
let lwps;
if (frm.doc.employee) {
if (frm.doc.employee && frm.doc.from_date) {
frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
async: false,
@@ -146,6 +146,7 @@ frappe.ui.form.on("Leave Application", {
},
to_date: function(frm) {
frm.trigger("make_dashboard");
frm.trigger("half_day_datepicker");
frm.trigger("calculate_total_days");
},

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from typing import Dict, Optional, Tuple
import frappe
from frappe import _
@@ -35,6 +36,10 @@ class LeaveDayBlockedError(frappe.ValidationError): pass
class OverlapError(frappe.ValidationError): pass
class AttendanceAlreadyMarkedError(frappe.ValidationError): pass
class NotAnOptionalHoliday(frappe.ValidationError): pass
class InsufficientLeaveBalanceError(frappe.ValidationError):
pass
class LeaveAcrossAllocationsError(frappe.ValidationError):
pass
from frappe.model.document import Document
@@ -135,21 +140,35 @@ class LeaveApplication(Document):
def validate_dates_across_allocation(self):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
return
def _get_leave_allocation_record(date):
allocation = frappe.db.sql("""select name from `tabLeave Allocation`
where employee=%s and leave_type=%s and docstatus=1
and %s between from_date and to_date""", (self.employee, self.leave_type, date))
return allocation and allocation[0][0]
alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
if not (alloc_on_from_date or alloc_on_to_date):
frappe.throw(_("Application period cannot be outside leave allocation period"))
elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
frappe.throw(_("Application period cannot be across two allocation records"), exc=LeaveAcrossAllocationsError)
def get_allocation_based_on_application_dates(self) -> Tuple[Dict, Dict]:
"""Returns allocation name, from and to dates for application dates"""
def _get_leave_allocation_record(date):
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
allocation = (
frappe.qb.from_(LeaveAllocation)
.select(LeaveAllocation.name, LeaveAllocation.from_date, LeaveAllocation.to_date)
.where(
(LeaveAllocation.employee == self.employee)
& (LeaveAllocation.leave_type == self.leave_type)
& (LeaveAllocation.docstatus == 1)
& ((date >= LeaveAllocation.from_date) & (date <= LeaveAllocation.to_date))
)
).run(as_dict=True)
return allocation and allocation[0]
allocation_based_on_from_date = _get_leave_allocation_record(self.from_date)
allocation_based_on_to_date = _get_leave_allocation_record(self.to_date)
if not (allocation_based_on_from_date or allocation_based_on_to_date):
frappe.throw(_("Application period cannot be outside leave allocation period"))
elif allocation_based_on_from_date != allocation_based_on_to_date:
frappe.throw(_("Application period cannot be across two allocation records"))
return allocation_based_on_from_date, allocation_based_on_to_date
def validate_back_dated_application(self):
future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation`
@@ -261,15 +280,29 @@ class LeaveApplication(Document):
frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave."))
if not is_lwp(self.leave_type):
self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
consider_all_leaves_in_the_allocation_period=True)
if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
.format(self.leave_type))
else:
frappe.throw(_("There is not enough leave balance for Leave Type {0}")
.format(self.leave_type))
leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
consider_all_leaves_in_the_allocation_period=True, for_consumption=True)
self.leave_balance = leave_balance.get("leave_balance")
leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption")
if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption):
self.show_insufficient_balance_message(leave_balance_for_consumption)
def show_insufficient_balance_message(self, leave_balance_for_consumption: float) -> None:
alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
if leave_balance_for_consumption != self.leave_balance:
msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(frappe.bold(self.leave_type))
msg += "<br><br>"
msg += _("Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation.")
else:
msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(frappe.bold(self.leave_type))
frappe.msgprint(msg, title=_("Warning"), indicator="orange")
else:
frappe.throw(_("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)),
exc=InsufficientLeaveBalanceError, title=_("Insufficient Balance"))
def validate_leave_overlap(self):
if not self.name:
@@ -426,54 +459,111 @@ class LeaveApplication(Document):
if self.status != 'Approved' and submit:
return
expiry_date = get_allocation_expiry(self.employee, self.leave_type,
expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type,
self.to_date, self.from_date)
lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
if expiry_date:
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
else:
raise_exception = True
if frappe.flags.in_patch:
raise_exception=False
alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
# required only if negative balance is allowed for leave type
# else will be stopped in validation itself
self.create_separate_ledger_entries(alloc_on_from_date, alloc_on_to_date, submit, lwp)
else:
raise_exception = False if frappe.flags.in_patch else True
args = dict(
leaves=self.total_leave_days * -1,
from_date=self.from_date,
to_date=self.to_date,
is_lwp=lwp,
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
args = dict(
leaves=self.total_leave_days * -1,
def is_separate_ledger_entry_required(self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None) -> bool:
"""Checks if application dates fall in separate allocations"""
if ((alloc_on_from_date and not alloc_on_to_date)
or (not alloc_on_from_date and alloc_on_to_date)
or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)):
return True
return False
def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp):
"""Creates separate ledger entries for application period falling into separate allocations"""
# for creating separate ledger entries existing allocation periods should be consecutive
if submit and alloc_on_from_date and alloc_on_to_date and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date:
frappe.throw(_("Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}.").format(
get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date)))
raise_exception = False if frappe.flags.in_patch else True
if alloc_on_from_date:
first_alloc_end = alloc_on_from_date.to_date
second_alloc_start = add_days(alloc_on_from_date.to_date, 1)
else:
first_alloc_end = add_days(alloc_on_to_date.from_date, -1)
second_alloc_start = alloc_on_to_date.from_date
leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type,
self.from_date, first_alloc_end, self.half_day, self.half_day_date)
leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type,
second_alloc_start, self.to_date, self.half_day, self.half_day_date)
args = dict(
is_lwp=lwp,
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
if leaves_in_first_alloc:
args.update(dict(
from_date=self.from_date,
to_date=first_alloc_end,
leaves=leaves_in_first_alloc * -1
))
create_leave_ledger_entry(self, args, submit)
if leaves_in_second_alloc:
args.update(dict(
from_date=second_alloc_start,
to_date=self.to_date,
leaves=leaves_in_second_alloc * -1
))
create_leave_ledger_entry(self, args, submit)
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
"""Splits leave application into two ledger entries to consider expiry of allocation"""
raise_exception = False if frappe.flags.in_patch else True
leaves = get_number_of_leave_days(self.employee, self.leave_type,
self.from_date, expiry_date, self.half_day, self.half_day_date)
if leaves:
args = dict(
from_date=self.from_date,
to_date=expiry_date,
leaves=leaves * -1,
is_lwp=lwp,
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
''' splits leave application into two ledger entries to consider expiry of allocation '''
raise_exception = True
if frappe.flags.in_patch:
raise_exception=False
args = dict(
from_date=self.from_date,
to_date=expiry_date,
leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
is_lwp=lwp,
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
if getdate(expiry_date) != getdate(self.to_date):
start_date = add_days(expiry_date, 1)
args.update(dict(
from_date=start_date,
to_date=self.to_date,
leaves=date_diff(self.to_date, expiry_date) * -1
))
create_leave_ledger_entry(self, args, submit)
leaves = get_number_of_leave_days(self.employee, self.leave_type,
start_date, self.to_date, self.half_day, self.half_day_date)
if leaves:
args.update(dict(
from_date=start_date,
to_date=self.to_date,
leaves=leaves * -1
))
create_leave_ledger_entry(self, args, submit)
def get_allocation_expiry(employee, leave_type, to_date, from_date):
def get_allocation_expiry_for_cf_leaves(employee: str, leave_type: str, to_date: str, from_date: str) -> str:
''' Returns expiry of carry forward allocation in leave ledger entry '''
expiry = frappe.get_all("Leave Ledger Entry",
filters={
@@ -481,12 +571,17 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date):
'leave_type': leave_type,
'is_carry_forward': 1,
'transaction_type': 'Leave Allocation',
'to_date': ['between', (from_date, to_date)]
'to_date': ['between', (from_date, to_date)],
'docstatus': 1
},fields=['to_date'])
return expiry[0]['to_date'] if expiry else None
return expiry[0]['to_date'] if expiry else ''
@frappe.whitelist()
def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None):
def get_number_of_leave_days(employee: str, leave_type: str, from_date: str, to_date: str, half_day: Optional[int] = None,
half_day_date: Optional[str] = None, holiday_list: Optional[str] = None) -> float:
"""Returns number of leave days between 2 dates after considering half day and holidays
(Based on the include_holiday setting in Leave Type)"""
number_of_days = 0
if cint(half_day) == 1:
if from_date == to_date:
@@ -503,6 +598,7 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day
number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list))
return number_of_days
@frappe.whitelist()
def get_leave_details(employee, date):
allocation_records = get_leave_allocation_records(employee, date)
@@ -515,6 +611,7 @@ def get_leave_details(employee, date):
'to_date': ('>=', date),
'employee': employee,
'leave_type': allocation.leave_type,
'docstatus': 1
}, 'SUM(total_leaves_allocated)') or 0
remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
@@ -522,29 +619,28 @@ def get_leave_details(employee, date):
end_date = allocation.to_date
leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1
leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date)
leaves_pending = get_leaves_pending_approval_for_period(employee, d, allocation.from_date, end_date)
leave_allocation[d] = {
"total_leaves": total_allocated_leaves,
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
"leaves_taken": leaves_taken,
"pending_leaves": leaves_pending,
"leaves_pending_approval": leaves_pending,
"remaining_leaves": remaining_leaves}
#is used in set query
lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1})
lwps = [lwp.name for lwp in lwps]
lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name")
ret = {
'leave_allocation': leave_allocation,
'leave_approver': get_leave_approver(employee),
'lwps': lwps
return {
"leave_allocation": leave_allocation,
"leave_approver": get_leave_approver(employee),
"lwps": lwp
}
return ret
@frappe.whitelist()
def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False):
def get_leave_balance_on(employee: str, leave_type: str, date: str, to_date: str = None,
consider_all_leaves_in_the_allocation_period: bool = False, for_consumption: bool = False):
'''
Returns leave balance till date
:param employee: employee name
@@ -552,6 +648,11 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_
:param date: date to check balance on
:param to_date: future date to check for allocation expiry
:param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date
:param for_consumption: flag to check if leave balance is required for consumption or display
eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave
in this case leave_balance = 10 but leave_balance_for_consumption = 1
if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1}
else, returns leave_balance (in this case 10)
'''
if not to_date:
@@ -561,11 +662,17 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_
allocation = allocation_records.get(leave_type, frappe._dict())
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
expiry = get_allocation_expiry(employee, leave_type, to_date, date)
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
return get_remaining_leaves(allocation, leaves_taken, date, expiry)
remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry)
if for_consumption:
return remaining_leaves
else:
return remaining_leaves.get('leave_balance')
def get_leave_allocation_records(employee, date, leave_type=None):
"""Returns the total allocated leaves and carry forwarded leaves based on ledger entries"""
@@ -614,8 +721,9 @@ def get_leave_allocation_records(employee, date, leave_type=None):
}))
return allocated_leaves
def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
''' Returns leaves that are pending approval '''
def get_leaves_pending_approval_for_period(employee: str, leave_type: str, from_date: str, to_date: str) -> float:
''' Returns leaves that are pending for approval '''
leaves = frappe.get_all("Leave Application",
filters={
"employee": employee,
@@ -628,38 +736,46 @@ def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
}, fields=['SUM(total_leave_days) as leaves'])[0]
return leaves['leaves'] if leaves['leaves'] else 0.0
def get_remaining_leaves(allocation, leaves_taken, date, expiry):
''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
def _get_remaining_leaves(remaining_leaves, end_date):
def get_remaining_leaves(allocation: Dict, leaves_taken: float, date: str, cf_expiry: str) -> Dict[str, float]:
'''Returns a dict of leave_balance and leave_balance_for_consumption
leave_balance returns the available leave balance
leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry
'''
def _get_remaining_leaves(remaining_leaves, end_date):
''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
if remaining_leaves > 0:
remaining_days = date_diff(end_date, date) + 1
remaining_leaves = min(remaining_days, remaining_leaves)
return remaining_leaves
total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
if expiry and allocation.unused_leaves:
remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry)
# balance for carry forwarded leaves
if cf_expiry and allocation.unused_leaves:
cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves)
leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
return _get_remaining_leaves(total_leaves, allocation.to_date)
remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False):
def get_leaves_for_period(employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True) -> float:
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
leave_days = 0
for leave_entry in leave_entries:
inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date)
if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
leave_days += leave_entry.leaves
elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \
and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)):
and not skip_expired_leaves:
leave_days += leave_entry.leaves
elif leave_entry.transaction_type == 'Leave Application':
@@ -681,11 +797,6 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_
return leave_days
def skip_expiry_leaves(leave_entry, date):
''' Checks whether the expired leaves coincide with the to_date of leave balance check.
This allows backdated leave entry creation for non carry forwarded allocation '''
end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date'])
return True if end_date == date and not leave_entry.is_carry_forward else False
def get_leave_entries(employee, leave_type, from_date, to_date):
''' Returns leave entries between from_date and to_date. '''
@@ -708,6 +819,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date):
"leave_type": leave_type
}, as_dict=1)
@frappe.whitelist()
def get_holidays(employee, from_date, to_date, holiday_list = None):
'''get holidays between two dates for the given employee'''
@@ -724,6 +836,7 @@ def is_lwp(leave_type):
lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", leave_type)
return lwp and cint(lwp[0][0]) or 0
@frappe.whitelist()
def get_events(start, end, filters=None):
from frappe.desk.reportview import get_filters_cond
@@ -752,6 +865,7 @@ def get_events(start, end, filters=None):
return events
def add_department_leaves(events, start, end, employee, company):
department = frappe.db.get_value("Employee", employee, "department")
@@ -832,6 +946,7 @@ def add_block_dates(events, start, end, employee, company):
})
cnt+=1
def add_holidays(events, start, end, employee, company):
applicable_holiday_list = get_holiday_list_for_employee(employee, company)
if not applicable_holiday_list:
@@ -848,6 +963,7 @@ def add_holidays(events, start, end, employee, company):
"name": holiday.name
})
@frappe.whitelist()
def get_mandatory_approval(doctype):
mandatory = ""
@@ -860,6 +976,7 @@ def get_mandatory_approval(doctype):
return mandatory
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
query = """
select employee, leave_type, from_date, to_date, total_leave_days
@@ -895,6 +1012,7 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
return leave_days
@frappe.whitelist()
def get_leave_approver(employee):
leave_approver, department = frappe.db.get_value("Employee",

View File

@@ -4,11 +4,11 @@
<thead>
<tr>
<th style="width: 16%">{{ __("Leave Type") }}</th>
<th style="width: 16%" class="text-right">{{ __("Total Allocated Leave") }}</th>
<th style="width: 16%" class="text-right">{{ __("Expired Leave") }}</th>
<th style="width: 16%" class="text-right">{{ __("Used Leave") }}</th>
<th style="width: 16%" class="text-right">{{ __("Pending Leave") }}</th>
<th style="width: 16%" class="text-right">{{ __("Available Leave") }}</th>
<th style="width: 16%" class="text-right">{{ __("Total Allocated Leave(s)") }}</th>
<th style="width: 16%" class="text-right">{{ __("Expired Leave(s)") }}</th>
<th style="width: 16%" class="text-right">{{ __("Used Leave(s)") }}</th>
<th style="width: 16%" class="text-right">{{ __("Leave(s) Pending Approval") }}</th>
<th style="width: 16%" class="text-right">{{ __("Available Leave(s)") }}</th>
</tr>
</thead>
<tbody>
@@ -18,7 +18,7 @@
<td class="text-right"> {%= value["total_leaves"] %} </td>
<td class="text-right"> {%= value["expired_leaves"] %} </td>
<td class="text-right"> {%= value["leaves_taken"] %} </td>
<td class="text-right"> {%= value["pending_leaves"] %} </td>
<td class="text-right"> {%= value["leaves_pending_approval"] %} </td>
<td class="text-right"> {%= value["remaining_leaves"] %} </td>
</tr>
{% } %}

View File

@@ -17,12 +17,17 @@ from frappe.utils import (
)
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
InsufficientLeaveBalanceError,
LeaveAcrossAllocationsError,
LeaveDayBlockedError,
NotAnOptionalHoliday,
OverlapError,
get_leave_allocation_records,
get_leave_balance_on,
get_leave_details,
)
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
@@ -33,7 +38,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_leave_application,
)
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
test_dependencies = ["Leave Type", "Leave Allocation", "Leave Block List", "Employee"]
_test_records = [
{
@@ -72,15 +77,28 @@ _test_records = [
class TestLeaveApplication(unittest.TestCase):
def setUp(self):
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
frappe.db.delete(dt)
frappe.set_user("Administrator")
set_leave_approver()
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"})
frappe.db.set_value("Employee", "_T-Employee-00001", "holiday_list", "")
from_date = get_year_start(getdate())
to_date = get_year_ending(getdate())
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
if not frappe.db.exists("Leave Type", "_Test Leave Type"):
frappe.get_doc(dict(
leave_type_name="_Test Leave Type",
doctype="Leave Type",
include_holiday=True
)).insert()
def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator")
def _clear_roles(self):
frappe.db.sql("""delete from `tabHas Role` where parent in
@@ -95,6 +113,132 @@ class TestLeaveApplication(unittest.TestCase):
application.to_date = "2013-01-05"
return application
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_validate_application_across_allocations(self):
# Test validation for application dates when negative balance is disabled
frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
leave_type = frappe.get_doc(dict(
leave_type_name="Test Leave Validation",
doctype="Leave Type",
allow_negative=False
)).insert()
employee = get_employee()
date = getdate()
first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date))
leave_application = frappe.get_doc(dict(
doctype='Leave Application',
employee=employee.name,
leave_type=leave_type.name,
from_date=add_days(first_sunday, 1),
to_date=add_days(first_sunday, 4),
company="_Test Company",
status="Approved",
leave_approver = 'test@example.com'
))
# Application period cannot be outside leave allocation period
self.assertRaises(frappe.ValidationError, leave_application.insert)
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
leave_application = frappe.get_doc(dict(
doctype='Leave Application',
employee=employee.name,
leave_type=leave_type.name,
from_date=add_days(first_sunday, -10),
to_date=add_days(first_sunday, 1),
company="_Test Company",
status="Approved",
leave_approver = 'test@example.com'
))
# Application period cannot be across two allocation records
self.assertRaises(LeaveAcrossAllocationsError, leave_application.insert)
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_insufficient_leave_balance_validation(self):
# CASE 1: Validation when allow negative is disabled
frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
leave_type = frappe.get_doc(dict(
leave_type_name="Test Leave Validation",
doctype="Leave Type",
allow_negative=False
)).insert()
employee = get_employee()
date = getdate()
first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date))
# allocate 2 leaves, apply for more
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date), leaves=2)
leave_application = frappe.get_doc(dict(
doctype='Leave Application',
employee=employee.name,
leave_type=leave_type.name,
from_date=add_days(first_sunday, 1),
to_date=add_days(first_sunday, 3),
company="_Test Company",
status="Approved",
leave_approver = 'test@example.com'
))
self.assertRaises(InsufficientLeaveBalanceError, leave_application.insert)
# CASE 2: Allows creating application with a warning message when allow negative is enabled
frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True)
make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name)
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_separate_leave_ledger_entry_for_boundary_applications(self):
# When application falls in 2 different allocations and Allow Negative is enabled
# creates separate leave ledger entries
frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
leave_type = frappe.get_doc(dict(
leave_type_name="Test Leave Validation",
doctype="Leave Type",
allow_negative=True
)).insert()
employee = get_employee()
date = getdate()
year_start = getdate(get_year_start(date))
year_end = getdate(get_year_ending(date))
make_allocation_record(leave_type=leave_type.name, from_date=year_start, to_date=year_end)
# application across allocations
# CASE 1: from date has no allocation, to date has an allocation / both dates have allocation
application = make_leave_application(employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name)
# 2 separate leave ledger entries
ledgers = frappe.db.get_all("Leave Ledger Entry", {
"transaction_type": "Leave Application",
"transaction_name": application.name
}, ["leaves", "from_date", "to_date"], order_by="from_date")
self.assertEqual(len(ledgers), 2)
self.assertEqual(ledgers[0].from_date, application.from_date)
self.assertEqual(ledgers[0].to_date, add_days(year_start, -1))
self.assertEqual(ledgers[1].from_date, year_start)
self.assertEqual(ledgers[1].to_date, application.to_date)
# CASE 2: from date has an allocation, to date has no allocation
application = make_leave_application(employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name)
# 2 separate leave ledger entries
ledgers = frappe.db.get_all("Leave Ledger Entry", {
"transaction_type": "Leave Application",
"transaction_name": application.name
}, ["leaves", "from_date", "to_date"], order_by="from_date")
self.assertEqual(len(ledgers), 2)
self.assertEqual(ledgers[0].from_date, application.from_date)
self.assertEqual(ledgers[0].to_date, year_end)
self.assertEqual(ledgers[1].from_date, add_days(year_end, 1))
self.assertEqual(ledgers[1].to_date, application.to_date)
def test_overwrite_attendance(self):
'''check attendance is automatically created on leave approval'''
make_allocation_record()
@@ -119,6 +263,7 @@ class TestLeaveApplication(unittest.TestCase):
for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
self.assertTrue(getdate(d) in dates)
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_attendance_for_include_holidays(self):
# Case 1: leave type with 'Include holidays within leaves as leaves' enabled
frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
@@ -131,12 +276,8 @@ class TestLeaveApplication(unittest.TestCase):
date = getdate()
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()
employee = get_employee()
original_holiday_list = employee.holiday_list
frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list)
first_sunday = get_first_sunday(self.holiday_list)
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
leave_application.reload()
@@ -145,8 +286,7 @@ class TestLeaveApplication(unittest.TestCase):
leave_application.cancel()
frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_attendance_update_for_exclude_holidays(self):
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
@@ -159,11 +299,8 @@ class TestLeaveApplication(unittest.TestCase):
date = getdate()
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()
employee = get_employee()
original_holiday_list = employee.holiday_list
frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list)
first_sunday = get_first_sunday(self.holiday_list)
# already marked attendance on a holiday should be deleted in this case
config = {
@@ -184,6 +321,7 @@ class TestLeaveApplication(unittest.TestCase):
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company)
leave_application.reload()
# holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3)
self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
@@ -194,8 +332,6 @@ class TestLeaveApplication(unittest.TestCase):
# attendance on non-holiday updated
self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
def test_block_list(self):
self._clear_roles()
@@ -327,17 +463,14 @@ class TestLeaveApplication(unittest.TestCase):
application.half_day_date = "2013-01-05"
application.insert()
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_optional_leave(self):
leave_period = get_leave_period()
today = nowdate()
holiday_list = 'Test Holiday List for Optional Holiday'
employee = get_employee()
default_holiday_list = make_holiday_list()
original_holiday_list = employee.holiday_list
frappe.db.set_value("Employee", employee.name, "holiday_list", default_holiday_list)
first_sunday = get_first_sunday(default_holiday_list)
first_sunday = get_first_sunday(self.holiday_list)
optional_leave_date = add_days(first_sunday, 1)
if not frappe.db.exists('Holiday List', holiday_list):
@@ -386,8 +519,6 @@ class TestLeaveApplication(unittest.TestCase):
# check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9)
frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
def test_leaves_allowed(self):
employee = get_employee()
leave_period = get_leave_period()
@@ -513,11 +644,13 @@ class TestLeaveApplication(unittest.TestCase):
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
leave_type.submit()
leave_type.insert()
create_carry_forwarded_allocation(employee, leave_type)
details = get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True)
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
self.assertEqual(details.leave_balance_for_consumption, 21)
self.assertEqual(details.leave_balance, 30)
def test_earned_leaves_creation(self):
@@ -570,7 +703,14 @@ class TestLeaveApplication(unittest.TestCase):
# test to not consider current leave in leave balance while submitting
def test_current_leave_on_submit(self):
employee = get_employee()
leave_type = 'Sick leave'
leave_type = 'Sick Leave'
if not frappe.db.exists('Leave Type', leave_type):
frappe.get_doc(dict(
leave_type_name=leave_type,
doctype='Leave Type'
)).insert()
allocation = frappe.get_doc(dict(
doctype = 'Leave Allocation',
employee = employee.name,
@@ -713,6 +853,56 @@ class TestLeaveApplication(unittest.TestCase):
employee.leave_approver = ""
employee.save()
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_get_leave_details_for_dashboard(self):
employee = get_employee()
date = getdate()
year_start = getdate(get_year_start(date))
year_end = getdate(get_year_ending(date))
# ALLOCATION = 30
allocation = make_allocation_record(employee=employee.name, from_date=year_start, to_date=year_end)
# USED LEAVES = 4
first_sunday = get_first_sunday(self.holiday_list)
leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application.reload()
# LEAVES PENDING APPROVAL = 1
leave_application = make_leave_application(employee.name, add_days(first_sunday, 5), add_days(first_sunday, 5),
'_Test Leave Type', submit=False)
leave_application.status = 'Open'
leave_application.save()
details = get_leave_details(employee.name, allocation.from_date)
leave_allocation = details['leave_allocation']['_Test Leave Type']
self.assertEqual(leave_allocation['total_leaves'], 30)
self.assertEqual(leave_allocation['leaves_taken'], 4)
self.assertEqual(leave_allocation['expired_leaves'], 0)
self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
self.assertEqual(leave_allocation['remaining_leaves'], 26)
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_get_leave_allocation_records(self):
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90)
leave_type.insert()
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
details = get_leave_allocation_records(employee.name, getdate(), leave_type.name)
expected_data = {
"from_date": getdate(leave_alloc.from_date),
"to_date": getdate(leave_alloc.to_date),
"total_leaves_allocated": 30.0,
"unused_leaves": 15.0,
"new_leaves_allocated": 15.0,
"leave_type": leave_type.name
}
self.assertEqual(details.get(leave_type.name), expected_data)
def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation
@@ -734,19 +924,24 @@ def create_carry_forwarded_allocation(employee, leave_type):
carry_forward=1)
leave_allocation.submit()
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
return leave_allocation
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001",
"leave_type": leave_type or "_Test Leave Type",
"from_date": from_date or "2013-01-01",
"to_date": to_date or "2019-12-31",
"new_leaves_allocated": 30
"new_leaves_allocated": leaves or 30,
"carry_forward": carry_forward
})
allocation.insert(ignore_permissions=True)
allocation.submit()
return allocation
def get_employee():
return frappe.get_doc("Employee", "_T-Employee-00001")

View File

@@ -171,7 +171,7 @@ def expire_carried_forward_allocation(allocation):
''' Expires remaining leaves in the on carried forward allocation '''
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type,
allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True)
allocation.from_date, allocation.to_date, skip_expired_leaves=False)
leaves = flt(allocation.leaves) + flt(leaves_taken)
# allow expired leaves entry to be created

View File

@@ -8,14 +8,15 @@ from math import ceil
import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate
from frappe.utils import date_diff, flt, formatdate, get_last_day, get_link_to_form, getdate
from six import string_types
class LeavePolicyAssignment(Document):
def validate(self):
self.validate_policy_assignment_overlap()
self.set_dates()
self.validate_policy_assignment_overlap()
self.warn_about_carry_forwarding()
def on_submit(self):
self.grant_leave_alloc_for_employee()
@@ -39,6 +40,20 @@ class LeavePolicyAssignment(Document):
frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
.format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
def warn_about_carry_forwarding(self):
if not self.carry_forward:
return
leave_types = get_leave_type_details()
leave_policy = frappe.get_doc("Leave Policy", self.leave_policy)
for policy in leave_policy.leave_policy_details:
leave_type = leave_types.get(policy.leave_type)
if not leave_type.is_carry_forward:
msg = _("Leaves for the Leave Type {0} won't be carry-forwarded since carry-forwarding is disabled.").format(
frappe.bold(get_link_to_form("Leave Type", leave_type.name)))
frappe.msgprint(msg, indicator="orange", alert=True)
@frappe.whitelist()
def grant_leave_alloc_for_employee(self):
if self.leaves_allocated:

View File

@@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import add_months, get_first_day, get_last_day, getdate
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_employee,
@@ -94,9 +94,12 @@ class TestLeavePolicyAssignment(unittest.TestCase):
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
# second last day of the month
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
frappe.flags.current_date = add_days(get_last_day(getdate()), -1)
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")

View File

@@ -3,18 +3,21 @@
from itertools import groupby
from typing import Dict, List, Optional, Tuple
import frappe
from frappe import _
from frappe.utils import add_days
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
get_leave_balance_on,
get_leaves_for_period,
)
Filters = frappe._dict
def execute(filters=None):
def execute(filters: Optional[Filters] = None) -> Tuple:
if filters.to_date <= filters.from_date:
frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
@@ -23,8 +26,9 @@ def execute(filters=None):
charts = get_chart_data(data)
return columns, data, None, charts
def get_columns():
columns = [{
def get_columns() -> List[Dict]:
return [{
'label': _('Leave Type'),
'fieldtype': 'Link',
'fieldname': 'leave_type',
@@ -46,32 +50,31 @@ def get_columns():
'label': _('Opening Balance'),
'fieldtype': 'float',
'fieldname': 'opening_balance',
'width': 130,
'width': 150,
}, {
'label': _('Leave Allocated'),
'label': _('New Leave(s) Allocated'),
'fieldtype': 'float',
'fieldname': 'leaves_allocated',
'width': 130,
'width': 200,
}, {
'label': _('Leave Taken'),
'label': _('Leave(s) Taken'),
'fieldtype': 'float',
'fieldname': 'leaves_taken',
'width': 130,
'width': 150,
}, {
'label': _('Leave Expired'),
'label': _('Leave(s) Expired'),
'fieldtype': 'float',
'fieldname': 'leaves_expired',
'width': 130,
'width': 150,
}, {
'label': _('Closing Balance'),
'fieldtype': 'float',
'fieldname': 'closing_balance',
'width': 130,
'width': 150,
}]
return columns
def get_data(filters):
def get_data(filters: Filters) -> List:
leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name')
conditions = get_conditions(filters)
@@ -102,19 +105,18 @@ def get_data(filters):
or ("HR Manager" in frappe.get_roles(user)):
if len(active_employees) > 1:
row = frappe._dict()
row.employee = employee.name,
row.employee = employee.name
row.employee_name = employee.employee_name
leaves_taken = get_leaves_for_period(employee.name, leave_type,
filters.from_date, filters.to_date) * -1
new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type)
opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition
new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
filters.from_date, filters.to_date, employee.name, leave_type)
opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
row.leaves_allocated = new_allocation
row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0
row.leaves_expired = expired_leaves
row.opening_balance = opening
row.leaves_taken = leaves_taken
@@ -125,7 +127,26 @@ def get_data(filters):
return data
def get_conditions(filters):
def get_opening_balance(employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float) -> float:
# allocation boundary condition
# opening balance is the closing leave balance 1 day before the filter start date
opening_balance_date = add_days(filters.from_date, -1)
allocation = get_previous_allocation(filters.from_date, leave_type, employee)
if allocation and allocation.get("to_date") and opening_balance_date and \
getdate(allocation.get("to_date")) == getdate(opening_balance_date):
# if opening balance date is same as the previous allocation's expiry
# then opening balance should only consider carry forwarded leaves
opening_balance = carry_forwarded_leaves
else:
# else directly get leave balance on the previous day
opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date)
return opening_balance
def get_conditions(filters: Filters) -> Dict:
conditions={
'status': 'Active',
}
@@ -140,29 +161,26 @@ def get_conditions(filters):
return conditions
def get_department_leave_approver_map(department=None):
def get_department_leave_approver_map(department: Optional[str] = None):
# get current department and all its child
department_list = frappe.get_list('Department',
filters={
'disabled': 0
},
or_filters={
'name': department,
'parent_department': department
},
fields=['name'],
pluck='name'
)
filters={'disabled': 0},
or_filters={
'name': department,
'parent_department': department
},
pluck='name'
)
# retrieve approvers list from current department and from its subsequent child departments
approver_list = frappe.get_all('Department Approver',
filters={
'parentfield': 'leave_approvers',
'parent': ('in', department_list)
},
fields=['parent', 'approver'],
as_list=1
)
filters={
'parentfield': 'leave_approvers',
'parent': ('in', department_list)
},
fields=['parent', 'approver'],
as_list=True
)
approvers = {}
@@ -171,41 +189,61 @@ def get_department_leave_approver_map(department=None):
return approvers
def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
from frappe.utils import getdate
def get_allocated_and_expired_leaves(from_date: str, to_date: str, employee: str, leave_type: str) -> Tuple[float, float, float]:
new_allocation = 0
expired_leaves = 0
carry_forwarded_leaves = 0
records= frappe.db.sql("""
SELECT
employee, leave_type, from_date, to_date, leaves, transaction_name,
transaction_type, is_carry_forward, is_expired
FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1
AND transaction_type = 'Leave Allocation'
AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
""", {
"from_date": from_date,
"to_date": to_date,
"employee": employee,
"leave_type": leave_type
}, as_dict=1)
records = get_leave_ledger_entries(from_date, to_date, employee, leave_type)
for record in records:
# new allocation records with `is_expired=1` are created when leave expires
# these new records should not be considered, else it leads to negative leave balance
if record.is_expired:
continue
if record.to_date < getdate(to_date):
# leave allocations ending before to_date, reduce leaves taken within that period
# since they are already used, they won't expire
expired_leaves += record.leaves
expired_leaves += get_leaves_for_period(employee, leave_type,
record.from_date, record.to_date)
if record.from_date >= getdate(from_date):
new_allocation += record.leaves
if record.is_carry_forward:
carry_forwarded_leaves += record.leaves
else:
new_allocation += record.leaves
return new_allocation, expired_leaves
return new_allocation, expired_leaves, carry_forwarded_leaves
def get_chart_data(data):
def get_leave_ledger_entries(from_date: str, to_date: str, employee: str, leave_type: str) -> List[Dict]:
ledger = frappe.qb.DocType('Leave Ledger Entry')
records = (
frappe.qb.from_(ledger)
.select(
ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date,
ledger.leaves, ledger.transaction_name, ledger.transaction_type,
ledger.is_carry_forward, ledger.is_expired
).where(
(ledger.docstatus == 1)
& (ledger.transaction_type == 'Leave Allocation')
& (ledger.employee == employee)
& (ledger.leave_type == leave_type)
& (
(ledger.from_date[from_date: to_date])
| (ledger.to_date[from_date: to_date])
| ((ledger.from_date < from_date) & (ledger.to_date > to_date))
)
)
).run(as_dict=True)
return records
def get_chart_data(data: List) -> Dict:
labels = []
datasets = []
employee_data = data
@@ -224,7 +262,8 @@ def get_chart_data(data):
return chart
def get_dataset_for_chart(employee_data, datasets, labels):
def get_dataset_for_chart(employee_data: List, datasets: List, labels: List) -> List:
leaves = []
employee_data = sorted(employee_data, key=lambda k: k['employee_name'])

View File

@@ -0,0 +1,161 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.utils import add_days, add_months, flt, get_year_ending, get_year_start, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_first_sunday,
make_allocation_record,
)
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,
)
test_records = frappe.get_test_records('Leave Type')
class TestEmployeeLeaveBalance(unittest.TestCase):
def setUp(self):
for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']:
frappe.db.delete(dt)
frappe.set_user('Administrator')
self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company')
self.date = getdate()
self.year_start = getdate(get_year_start(self.date))
self.mid_year = add_months(self.year_start, 6)
self.year_end = getdate(get_year_ending(self.date))
self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end)
def tearDown(self):
frappe.db.rollback()
@set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
def test_employee_leave_balance(self):
frappe.get_doc(test_records[0]).insert()
# 5 leaves
allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11),
to_date=add_days(self.year_start, -1), leaves=5)
# 30 leaves
allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
# expires 5 leaves
process_expired_allocation()
# 4 days leave
first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application.reload()
filters = frappe._dict({
'from_date': allocation1.from_date,
'to_date': allocation2.to_date,
'employee': self.employee_id
})
report = execute(filters)
expected_data = [{
'leave_type': '_Test Leave Type',
'employee': self.employee_id,
'employee_name': 'test_emp_leave_balance@example.com',
'leaves_allocated': flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated),
'leaves_expired': flt(allocation1.new_leaves_allocated),
'opening_balance': flt(0),
'leaves_taken': flt(leave_application.total_leave_days),
'closing_balance': flt(allocation2.new_leaves_allocated - leave_application.total_leave_days),
'indent': 1
}]
self.assertEqual(report[1], expected_data)
@set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
def test_opening_balance_on_alloc_boundary_dates(self):
frappe.get_doc(test_records[0]).insert()
# 30 leaves allocated
allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
# 4 days leave application in the first allocation
first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application.reload()
# Case 1: opening balance for first alloc boundary
filters = frappe._dict({
'from_date': self.year_start,
'to_date': self.year_end,
'employee': self.employee_id
})
report = execute(filters)
self.assertEqual(report[1][0].opening_balance, 0)
# Case 2: opening balance after leave application date
filters = frappe._dict({
'from_date': add_days(leave_application.to_date, 1),
'to_date': self.year_end,
'employee': self.employee_id
})
report = execute(filters)
self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days))
# Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date
# eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
filters = frappe._dict({
'from_date': add_days(self.year_end, -3),
'to_date': self.year_end,
'employee': self.employee_id
})
report = execute(filters)
self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days))
@set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
def test_opening_balance_considers_carry_forwarded_leaves(self):
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1)
leave_type.insert()
# 30 leaves allocated for first half of the year
allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start,
to_date=self.mid_year, leave_type=leave_type.name)
# 4 days leave application in the first allocation
first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name)
leave_application.reload()
# 30 leaves allocated for second half of the year + carry forward leaves (26) from the previous allocation
allocation2 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.mid_year, 1), to_date=self.year_end,
carry_forward=True, leave_type=leave_type.name)
# Case 1: carry forwarded leaves considered in opening balance for second alloc
filters = frappe._dict({
'from_date': add_days(self.mid_year, 1),
'to_date': self.year_end,
'employee': self.employee_id
})
report = execute(filters)
# available leaves from old alloc
opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days
self.assertEqual(report[1][0].opening_balance, opening_balance)
# Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc
filters = frappe._dict({
'from_date': add_days(self.mid_year, 2),
'to_date': self.year_end,
'employee': self.employee_id
})
report = execute(filters)
# available leaves from old alloc
opening_balance = allocation2.new_leaves_allocated + (allocation1.new_leaves_allocated - leave_application.total_leave_days)
self.assertEqual(report[1][0].opening_balance, opening_balance)

View File

@@ -0,0 +1,117 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.utils import add_days, flt, get_year_ending, get_year_start, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_first_sunday,
make_allocation_record,
)
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,
)
test_records = frappe.get_test_records('Leave Type')
class TestEmployeeLeaveBalance(unittest.TestCase):
def setUp(self):
for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']:
frappe.db.delete(dt)
frappe.set_user('Administrator')
self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company')
self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company')
self.date = getdate()
self.year_start = getdate(get_year_start(self.date))
self.year_end = getdate(get_year_ending(self.date))
self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end)
def tearDown(self):
frappe.db.rollback()
@set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
def test_employee_leave_balance_summary(self):
frappe.get_doc(test_records[0]).insert()
# 5 leaves
allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11),
to_date=add_days(self.year_start, -1), leaves=5)
# 30 leaves
allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
# 2 days leave within the first allocation
leave_application1 = make_leave_application(self.employee_id, add_days(self.year_start, -11), add_days(self.year_start, -10),
'_Test Leave Type')
leave_application1.reload()
# expires 3 leaves
process_expired_allocation()
# 4 days leave within the second allocation
first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
leave_application2 = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application2.reload()
filters = frappe._dict({
'date': add_days(leave_application2.to_date, 1),
'company': '_Test Company',
'employee': self.employee_id
})
report = execute(filters)
expected_data = [[
self.employee_id,
'test_emp_leave_balance@example.com',
frappe.db.get_value('Employee', self.employee_id, 'department'),
flt(
allocation1.new_leaves_allocated # allocated = 5
+ allocation2.new_leaves_allocated # allocated = 30
- leave_application1.total_leave_days # leaves taken in the 1st alloc = 2
- (allocation1.new_leaves_allocated - leave_application1.total_leave_days) # leaves expired from 1st alloc = 3
- leave_application2.total_leave_days # leaves taken in the 2nd alloc = 4
)
]]
self.assertEqual(report[1], expected_data)
@set_holiday_list('_Test Emp Balance Holiday List', '_Test Company')
def test_get_leave_balance_near_alloc_expiry(self):
frappe.get_doc(test_records[0]).insert()
# 30 leaves allocated
allocation = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end)
# 4 days leave application in the first allocation
first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
leave_application.reload()
# Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date
# eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
filters = frappe._dict({
'date': add_days(self.year_end, -3),
'company': '_Test Company',
'employee': self.employee_id
})
report = execute(filters)
expected_data = [[
self.employee_id,
'test_emp_leave_balance@example.com',
frappe.db.get_value('Employee', self.employee_id, 'department'),
flt(allocation.new_leaves_allocated - leave_application.total_leave_days)
]]
self.assertEqual(report[1], expected_data)

View File

@@ -1,15 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, today
from erpnext import get_company_currency
from erpnext.tests.utils import ERPNextTestCase
from .blanket_order import make_order
class TestBlanketOrder(ERPNextTestCase):
class TestBlanketOrder(FrappeTestCase):
def setUp(self):
frappe.flags.args = frappe._dict()

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import functools
import re
from collections import deque
from operator import itemgetter
from typing import List
@@ -103,25 +104,33 @@ class BOM(WebsiteGenerator):
)
def autoname(self):
names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item)
# ignore amended documents while calculating current index
existing_boms = frappe.get_all(
"BOM",
filters={"item": self.item, "amended_from": ["is", "not set"]},
pluck="name"
)
if names:
# name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1
# split by item
names = [name.split(self.item, 1) for name in names]
names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)]
# split by (-) if cancelled
if names:
names = [cint(name.split('-')[-1]) for name in names]
idx = max(names) + 1
else:
idx = 1
if existing_boms:
index = self.get_next_version_index(existing_boms)
else:
idx = 1
index = 1
prefix = self.doctype
suffix = "%.3i" % index # convert index to string (1 -> "001")
bom_name = f"{prefix}-{self.item}-{suffix}"
if len(bom_name) <= 140:
name = bom_name
else:
# since max characters for name is 140, remove enough characters from the
# item name to fit the prefix, suffix and the separators
truncated_length = 140 - (len(prefix) + len(suffix) + 2)
truncated_item_name = self.item[:truncated_length]
# if a partial word is found after truncate, remove the extra characters
truncated_item_name = truncated_item_name.rsplit(" ", 1)[0]
name = f"{prefix}-{truncated_item_name}-{suffix}"
name = 'BOM-' + self.item + ('-%.3i' % idx)
if frappe.db.exists("BOM", name):
conflicting_bom = frappe.get_doc("BOM", name)
@@ -134,6 +143,26 @@ class BOM(WebsiteGenerator):
self.name = name
@staticmethod
def get_next_version_index(existing_boms: List[str]) -> int:
# split by "/" and "-"
delimiters = ["/", "-"]
pattern = "|".join(map(re.escape, delimiters))
bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms]
# filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001
valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
# extract the current index from the BOM parts
if valid_bom_parts:
# handle cancelled and submitted documents
indexes = [cint(part[-1]) for part in valid_bom_parts]
index = max(indexes) + 1
else:
index = 1
return index
def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-')
@@ -141,6 +170,7 @@ class BOM(WebsiteGenerator):
frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
self.clear_operations()
self.clear_inspection()
self.validate_main_item()
self.validate_currency()
self.set_conversion_rate()
@@ -387,6 +417,10 @@ class BOM(WebsiteGenerator):
if not self.with_operations:
self.set('operations', [])
def clear_inspection(self):
if not self.inspection_required:
self.quality_inspection_template = None
def validate_main_item(self):
""" Validate main FG item"""
item = self.get_item_det(self.item)

View File

@@ -6,7 +6,7 @@ from collections import deque
from functools import partial
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -17,15 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.tests.test_subcontracting import set_backflush_based_on
from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM')
test_dependencies = ["Item", "Quality Inspection Template"]
class TestBOM(ERPNextTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
class TestBOM(FrappeTestCase):
def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
items_dict = get_bom_items_as_dict(bom=get_default_bom(),
@@ -385,6 +381,87 @@ class TestBOM(ERPNextTestCase):
self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete()
def test_bom_name_length(self):
""" test >140 char names"""
bom_tree = {
"x" * 140 : {
" ".join(["abc"] * 35): {}
}
}
create_nested_bom(bom_tree, prefix="")
def test_version_index(self):
bom = frappe.new_doc("BOM")
version_index_test_cases = [
(1, []),
(1, ["BOM#XYZ"]),
(2, ["BOM/ITEM/001"]),
(2, ["BOM-ITEM-001"]),
(3, ["BOM-ITEM-001", "BOM-ITEM-002"]),
(4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]),
]
for expected_index, existing_boms in version_index_test_cases:
with self.subTest():
self.assertEqual(expected_index, bom.get_next_version_index(existing_boms),
msg=f"Incorrect index for {existing_boms}")
def test_bom_versioning(self):
bom_tree = {
frappe.generate_hash(length=10) : {
frappe.generate_hash(length=10): {}
}
}
bom = create_nested_bom(bom_tree, prefix="")
self.assertEqual(int(bom.name.split("-")[-1]), 1)
original_bom_name = bom.name
bom.cancel()
bom.reload()
self.assertEqual(bom.name, original_bom_name)
# create a new amendment
amendment = frappe.copy_doc(bom)
amendment.docstatus = 0
amendment.amended_from = bom.name
amendment.save()
amendment.submit()
amendment.reload()
self.assertNotEqual(amendment.name, bom.name)
# `origname-001-1` version
self.assertEqual(int(amendment.name.split("-")[-1]), 1)
self.assertEqual(int(amendment.name.split("-")[-2]), 1)
# create a new version
version = frappe.copy_doc(amendment)
version.docstatus = 0
version.amended_from = None
version.save()
self.assertNotEqual(amendment.name, version.name)
self.assertEqual(int(version.name.split("-")[-1]), 2)
def test_clear_inpection_quality(self):
bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
bom.docstatus = 0
bom.is_default = 0
bom.quality_inspection_template = "_Test Quality Inspection Template"
bom.inspection_required = 1
bom.save()
bom.reload()
self.assertEqual(bom.quality_inspection_template, '_Test Quality Inspection Template')
bom.inspection_required = 0
bom.save()
bom.reload()
self.assertEqual(bom.quality_inspection_template, None)
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@@ -2,15 +2,15 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM')
class TestBOMUpdateTool(ERPNextTestCase):
class TestBOMUpdateTool(FrappeTestCase):
def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"

View File

@@ -48,7 +48,7 @@ class JobCard(Document):
self.validate_work_order()
def set_sub_operations(self):
if self.operation:
if not self.sub_operations and self.operation:
self.sub_operations = []
for row in frappe.get_all('Sub Operation',
filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):
@@ -500,6 +500,9 @@ class JobCard(Document):
2: "Cancelled"
}[self.docstatus or 0]
if self.for_quantity <= self.transferred_qty:
self.status = 'Material Transferred'
if self.time_logs:
self.status = 'Work In Progress'
@@ -507,10 +510,6 @@ class JobCard(Document):
(self.for_quantity <= self.total_completed_qty or not self.items)):
self.status = 'Completed'
if self.status != 'Completed':
if self.for_quantity <= self.transferred_qty:
self.status = 'Material Transferred'
if update_status:
self.db_set('status', self.status)

View File

@@ -1,4 +1,5 @@
frappe.listview_settings['Job Card'] = {
has_indicator_for_draft: true,
get_indicator: function(doc) {
if (doc.status === "Work In Progress") {
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];

View File

@@ -2,6 +2,7 @@
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import random_string
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
@@ -11,10 +12,9 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.tests.utils import ERPNextTestCase
class TestJobCard(ERPNextTestCase):
class TestJobCard(FrappeTestCase):
def setUp(self):
make_bom_for_jc_tests()
@@ -169,6 +169,7 @@ class TestJobCard(ERPNextTestCase):
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
self.assertEqual(job_card.status, "Open")
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)

View File

@@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Production Plan', {
before_save: function(frm) {
// preserve temporary names on production plan item to re-link sub-assembly items
frm.doc.po_items.forEach(item => {
item.temporary_name = item.name;
});
},
setup: function(frm) {
frm.custom_make_buttons = {
'Work Order': 'Work Order / Subcontract PO',

View File

@@ -189,7 +189,7 @@
"label": "Select Items to Manufacture"
},
{
"depends_on": "get_items_from",
"depends_on": "eval:doc.get_items_from && doc.docstatus == 0",
"fieldname": "get_items",
"fieldtype": "Button",
"label": "Get Finished Goods for Manufacture"
@@ -197,6 +197,7 @@
{
"fieldname": "po_items",
"fieldtype": "Table",
"label": "Assembly Items",
"no_copy": 1,
"options": "Production Plan Item",
"reqd": 1
@@ -357,6 +358,7 @@
"options": "Production Plan Sub Assembly Item"
},
{
"depends_on": "eval:doc.po_items && doc.po_items.length && doc.docstatus == 0",
"fieldname": "get_sub_assembly_items",
"fieldtype": "Button",
"label": "Get Sub Assembly Items"
@@ -376,7 +378,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-09-06 18:35:59.642232",
"modified": "2022-03-25 09:15:25.017664",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
@@ -397,5 +399,6 @@
}
],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@@ -32,6 +32,7 @@ class ProductionPlan(Document):
self.set_pending_qty_in_row_without_reference()
self.calculate_total_planned_qty()
self.set_status()
self._rename_temporary_references()
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
@@ -57,6 +58,18 @@ class ProductionPlan(Document):
if not flt(d.planned_qty):
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
def _rename_temporary_references(self):
""" po_items and sub_assembly_items items are both constructed client side without saving.
Attempt to fix linkages by using temporary names to map final row names.
"""
new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name}
actual_names = {d.name for d in self.po_items}
for sub_assy in self.sub_assembly_items:
if sub_assy.production_plan_item not in actual_names:
sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item)
@frappe.whitelist()
def get_open_sales_orders(self):
""" Pull sales orders which are pending to deliver based on criteria selected"""
@@ -982,21 +995,21 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
required_qty = item.get("quantity")
# get available material by transferring to production warehouse
for d in locations:
if required_qty <=0: return
if required_qty <= 0:
return
new_dict = copy.deepcopy(item)
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
if required_qty > 0:
new_dict.update({
"quantity": quantity,
"material_request_type": "Material Transfer",
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
"from_warehouse": d.get("warehouse")
})
new_dict.update({
"quantity": quantity,
"material_request_type": "Material Transfer",
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
"from_warehouse": d.get("warehouse")
})
required_qty -= quantity
new_mr_items.append(new_dict)
required_qty -= quantity
new_mr_items.append(new_dict)
# raise purchase request for remaining qty
if required_qty:

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, flt, now_datetime, nowdate
from erpnext.controllers.item_variant import create_variant
@@ -16,10 +17,9 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.tests.utils import ERPNextTestCase
class TestProductionPlan(ERPNextTestCase):
class TestProductionPlan(FrappeTestCase):
def setUp(self):
for item in ['Test Production Item 1', 'Subassembly Item 1',
'Raw Material Item 1', 'Raw Material Item 2']:
@@ -605,6 +605,50 @@ class TestProductionPlan(ERPNextTestCase):
]
self.assertFalse(pp.all_items_completed())
def test_production_plan_planned_qty(self):
pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55)
pln.make_work_order()
work_order = frappe.db.get_value('Work Order', {'production_plan': pln.name}, 'name')
wo_doc = frappe.get_doc('Work Order', work_order)
wo_doc.update({
'wip_warehouse': 'Work In Progress - _TC',
'fg_warehouse': 'Finished Goods - _TC'
})
wo_doc.submit()
self.assertEqual(wo_doc.qty, 0.55)
def test_temporary_name_relinking(self):
pp = frappe.new_doc("Production Plan")
# this can not be unittested so mocking data that would be expected
# from client side.
for _ in range(10):
po_item = pp.append("po_items", {
"name": frappe.generate_hash(length=10),
"temporary_name": frappe.generate_hash(length=10),
})
pp.append("sub_assembly_items", {
"production_plan_item": po_item.temporary_name
})
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
# bad links should be erased
pp.append("sub_assembly_items", {
"production_plan_item": frappe.generate_hash(length=16)
})
pp._rename_temporary_references()
self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item)
pp.sub_assembly_items.pop()
# reattempting on same doc shouldn't change anything
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
def create_production_plan(**args):
"""

View File

@@ -27,7 +27,8 @@
"material_request",
"material_request_item",
"product_bundle_item",
"item_reference"
"item_reference",
"temporary_name"
],
"fields": [
{
@@ -204,17 +205,25 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Item Reference"
},
{
"fieldname": "temporary_name",
"fieldtype": "Data",
"hidden": 1,
"label": "temporary name"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-06-28 18:31:06.822168",
"modified": "2022-03-24 04:54:09.940224",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@@ -2,14 +2,14 @@
# See license.txt
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase
class TestRouting(ERPNextTestCase):
class TestRouting(FrappeTestCase):
@classmethod
def setUpClass(cls):
cls.item_code = "Test Routing Item - A"

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings, timeout
from frappe.utils import add_days, add_months, cint, flt, now, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
@@ -21,10 +22,9 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin
from erpnext.tests.utils import ERPNextTestCase, timeout
class TestWorkOrder(ERPNextTestCase):
class TestWorkOrder(FrappeTestCase):
def setUp(self):
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
@@ -352,7 +352,6 @@ class TestWorkOrder(ERPNextTestCase):
wo_order = make_wo_order_test_record(planned_start_date=now(),
sales_order=so.name, qty=3)
wo_order.submit()
self.assertEqual(wo_order.docstatus, 1)
allow_overproduction("overproduction_percentage_for_sales_order", 0)
@@ -937,6 +936,28 @@ class TestWorkOrder(ERPNextTestCase):
frappe.db.set_value("Manufacturing Settings", None,
"backflush_raw_materials_based_on", "BOM")
@change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
def test_auto_batch_creation(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
fg_item = frappe.generate_hash(length=20)
child_item = frappe.generate_hash(length=20)
bom_tree = {fg_item: {child_item: {}}}
create_nested_bom(bom_tree, prefix="")
item = frappe.get_doc("Item", fg_item)
item.has_batch_no = 1
item.create_new_batch = 0
item.save()
try:
make_wo_order_test_record(item=fg_item)
except frappe.MandatoryError:
self.fail("Batch generation causing failing in Work Order")
def update_job_card(job_card, jc_qty=None):
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')

View File

@@ -333,6 +333,14 @@ class WorkOrder(Document):
if not self.batch_size:
self.batch_size = total_qty
batch_auto_creation = frappe.get_cached_value("Item", self.production_item, "create_new_batch")
if not batch_auto_creation:
frappe.msgprint(
_("Batch not created for item {} since it does not have a batch series.")
.format(frappe.bold(self.production_item)),
alert=True, indicator="orange")
return
while total_qty > 0:
qty = self.batch_size
if self.batch_size >= total_qty:
@@ -449,7 +457,8 @@ class WorkOrder(Document):
mr_obj.update_requested_qty([self.material_request_item])
def update_ordered_qty(self):
if self.production_plan and self.production_plan_item:
if self.production_plan and self.production_plan_item \
and not self.production_plan_sub_assembly_item:
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
if self.docstatus == 1:
@@ -632,15 +641,19 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
if self.production_plan and self.production_plan_item:
if self.production_plan and self.production_plan_item \
and not self.production_plan_sub_assembly_item:
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings",
if not qty_dict:
return
allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0)
if max_qty < 1:
if not max_qty > 0:
frappe.throw(_("Cannot produce more item for {0}")
.format(self.production_item), OverProductionError)
elif self.qty > max_qty:
@@ -1138,6 +1151,10 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
doc.insert()
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
if enable_capacity_planning:
# automatically added scheduling rows shouldn't change status to WIP
doc.db_set("status", "Open")
return doc
def get_work_order_operation_data(work_order, operation, workstation):

View File

@@ -2,6 +2,7 @@
# See license.txt
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_bom
@@ -10,13 +11,12 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
WorkstationHolidayError,
check_if_within_operating_hours,
)
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation')
class TestWorkstation(ERPNextTestCase):
class TestWorkstation(FrappeTestCase):
def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")

View File

@@ -17,7 +17,8 @@
"paid",
"amount",
"mode_of_payment",
"razorpay_payment_id",
"column_break_12",
"payment_id",
"amended_from"
],
"fields": [
@@ -73,12 +74,6 @@
"label": "Mode of Payment",
"options": "Mode of Payment"
},
{
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "Razorpay Payment ID",
"read_only": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
@@ -108,12 +103,21 @@
"options": "Donation",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "payment_id",
"fieldtype": "Data",
"label": "Payment ID"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-03-11 10:53:11.269005",
"modified": "2022-03-16 17:18:45.611741",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Donation",

View File

@@ -102,7 +102,7 @@ def capture_razorpay_donations(*args, **kwargs):
if not donor:
donor = create_donor(payment)
donation = create_donation(donor, payment)
donation = create_razorpay_donation(donor, payment)
donation.run_method('create_payment_entry')
except Exception as e:
@@ -114,7 +114,7 @@ def capture_razorpay_donations(*args, **kwargs):
return { 'status': 'Success' }
def create_donation(donor, payment):
def create_razorpay_donation(donor, payment):
if not frappe.db.exists('Mode of Payment', payment.method):
create_mode_of_payment(payment.method)
@@ -128,7 +128,7 @@ def create_donation(donor, payment):
'date': getdate(),
'amount': flt(payment.amount) / 100, # Convert to rupees from paise
'mode_of_payment': payment.method,
'razorpay_payment_id': payment.id
'payment_id': payment.id
}).insert(ignore_mandatory=True)
donation.submit()

View File

@@ -5,7 +5,7 @@ import unittest
import frappe
from erpnext.non_profit.doctype.donation.donation import create_donation
from erpnext.non_profit.doctype.donation.donation import create_razorpay_donation
class TestDonation(unittest.TestCase):
@@ -30,7 +30,7 @@ class TestDonation(unittest.TestCase):
'method': 'Debit Card',
'id': 'pay_MeXAmsgeKOhq7O'
})
donation = create_donation(donor, payment)
donation = create_razorpay_donation(donor, payment)
self.assertTrue(donation.name)

View File

@@ -100,10 +100,13 @@ def create_customer(user_details, member=None):
customer = frappe.new_doc("Customer")
customer.customer_name = user_details.fullname
customer.customer_type = "Individual"
customer.customer_group = frappe.db.get_single_value("Selling Settings", "customer_group")
customer.territory = frappe.db.get_single_value("Selling Settings", "territory")
customer.flags.ignore_mandatory = True
customer.insert(ignore_permissions=True)
try:
frappe.db.savepoint("contact_creation")
contact = frappe.new_doc("Contact")
contact.first_name = user_details.fullname
if user_details.mobile:
@@ -129,6 +132,7 @@ def create_customer(user_details, member=None):
return customer.name
except Exception as e:
frappe.db.rollback(save_point="contact_creation")
frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed"))
pass

View File

@@ -21,9 +21,11 @@
"paid",
"currency",
"amount",
"column_break_16",
"invoice",
"razorpay_details_section",
"subscription_id",
"column_break_19",
"payment_id"
],
"fields": [
@@ -106,20 +108,17 @@
{
"fieldname": "razorpay_details_section",
"fieldtype": "Section Break",
"hidden": 1,
"label": "Razorpay Details"
},
{
"fieldname": "subscription_id",
"fieldtype": "Data",
"label": "Subscription ID",
"read_only": 1
"label": "Subscription ID"
},
{
"fieldname": "payment_id",
"fieldtype": "Data",
"label": "Payment ID",
"read_only": 1
"label": "Payment ID"
},
{
"fieldname": "invoice",
@@ -140,11 +139,19 @@
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-19 14:33:44.925122",
"modified": "2022-03-16 17:37:28.672916",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",

View File

@@ -104,7 +104,7 @@ class Membership(Document):
return invoice
def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type)
settings_link = get_link_to_form("Non Profit Settings", "Non Profit Settings")
if not settings.membership_debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))

View File

@@ -1,6 +1,6 @@
erpnext.patches.v12_0.update_is_cancelled_field
erpnext.patches.v13_0.add_bin_unique_constraint
erpnext.patches.v11_0.rename_production_order_to_work_order
erpnext.patches.v13_0.add_bin_unique_constraint
erpnext.patches.v11_0.refactor_naming_series
erpnext.patches.v11_0.refactor_autoname_naming
execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28
@@ -352,3 +352,7 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v13_0.amazon_mws_deprecation_warning
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
erpnext.patches.v13_0.rename_non_profit_fields
erpnext.patches.v13_0.enable_ksa_vat_docs #1
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances

View File

@@ -6,14 +6,14 @@ import frappe
def execute():
frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
frappe.reload_doc('hr', 'doctype', 'employee_grade')
employee_with_assignment = []
leave_policy = []
if "leave_policy" in frappe.db.get_table_columns("Employee"):
employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1)
employee_with_assignment = []
leave_policy =[]
#for employee
for employee in employees_with_leave_policy:
alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1})
if not alloc:
@@ -22,12 +22,10 @@ def execute():
employee_with_assignment.append(employee.name)
leave_policy.append(employee.leave_policy)
if "default_leave_policy" in frappe.db.get_table_columns("Employee"):
if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1)
#for whole employee Grade
for grade in employee_grade_with_leave_policy:
employees = get_employee_with_grade(grade.name)
for employee in employees:
@@ -47,13 +45,13 @@ def execute():
allocation_exists=True)
def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False):
if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
return
filters = {"employee":employee, "leave_policy": leave_policy}
if leave_period:
filters["leave_period"] = leave_period
frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
if not frappe.db.exists("Leave Policy Assignment" , filters):
lpa = frappe.new_doc("Leave Policy Assignment")
lpa.employee = employee

View File

@@ -0,0 +1,12 @@
import frappe
from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_formats
def execute():
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
if not company:
return
add_print_formats()
add_permissions()

View File

@@ -0,0 +1,37 @@
import frappe
def execute():
"""
Remove "production_plan_item" field where linked field doesn't exist in tha table.
"""
frappe.reload_doc("manufacturing", "doctype", "production_plan_item")
work_order = frappe.qb.DocType("Work Order")
pp_item = frappe.qb.DocType("Production Plan Item")
broken_work_orders = (
frappe.qb
.from_(work_order)
.left_join(pp_item).on(work_order.production_plan_item == pp_item.name)
.select(work_order.name)
.where(
(work_order.docstatus == 0)
& (work_order.production_plan_item.notnull())
& (work_order.production_plan_item.like("new-production-plan%"))
& (pp_item.name.isnull())
)
).run()
if not broken_work_orders:
return
broken_work_order_names = [d[0] for d in broken_work_orders]
(frappe.qb
.update(work_order)
.set(work_order.production_plan_item, None)
.where(work_order.name.isin(broken_work_order_names))
).run()

View File

@@ -60,7 +60,7 @@ def execute():
def convert_to_seconds(value, unit):
seconds = 0
if value == 0:
if not value:
return seconds
if unit == 'Hours':
seconds = value * 3600

View File

@@ -0,0 +1,17 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if frappe.db.table_exists("Donation"):
frappe.reload_doc("non_profit", "doctype", "Donation")
rename_field("Donation", "razorpay_payment_id", "payment_id")
if frappe.db.table_exists("Tax Exemption 80G Certificate"):
frappe.reload_doc("regional", "doctype", "Tax Exemption 80G Certificate")
frappe.reload_doc("regional", "doctype", "Tax Exemption 80G Certificate Detail")
rename_field("Tax Exemption 80G Certificate", "razorpay_payment_id", "payment_id")
rename_field("Tax Exemption 80G Certificate Detail", "razorpay_payment_id", "payment_id")

View File

@@ -0,0 +1,22 @@
import frappe
def execute():
"""
Update Expense Claim status to Paid if:
- the entire required amount is already covered via linked advances
- the claim is partially paid via advances and the rest is reimbursed
"""
ExpenseClaim = frappe.qb.DocType('Expense Claim')
(frappe.qb
.update(ExpenseClaim)
.set(ExpenseClaim.status, 'Paid')
.where(
((ExpenseClaim.grand_total == 0) | (ExpenseClaim.grand_total == ExpenseClaim.total_amount_reimbursed))
& (ExpenseClaim.approval_status == 'Approved')
& (ExpenseClaim.docstatus == 1)
& (ExpenseClaim.total_sanctioned_amount > 0)
)
).run()

View File

@@ -660,6 +660,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr
if not_submitted_ss:
frappe.msgprint(_("Could not submit some Salary Slips"))
frappe.flags.via_payroll_entry = False
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters):
@@ -671,7 +673,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
where reference_type="Payroll Entry")
order by name limit %(start)s, %(page_len)s"""
.format(key=searchfield), {
'txt': "%%%s%%" % frappe.db.escape(txt),
'txt': "%%%s%%" % txt,
'start': start, 'page_len': page_len
})

View File

@@ -781,11 +781,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
@@ -911,7 +912,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
@@ -938,7 +939,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
@@ -955,7 +956,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,
@@ -964,12 +965,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:
@@ -999,7 +1006,7 @@ class SalarySlip(TransactionBase):
# apply rounding
if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"):
amount, additional_amount = rounded(amount), rounded(additional_amount)
amount, additional_amount = rounded(amount or 0), rounded(additional_amount or 0)
return amount, additional_amount
@@ -1276,9 +1283,9 @@ class SalarySlip(TransactionBase):
def set_base_totals(self):
self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate)
self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate)
self.rounded_total = rounded(self.net_pay)
self.rounded_total = rounded(self.net_pay or 0)
self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate)
self.base_rounded_total = rounded(self.base_net_pay)
self.base_rounded_total = rounded(self.base_net_pay or 0)
self.set_net_total_in_words()
#calculate total working hours, earnings based on hourly wages and totals
@@ -1390,7 +1397,7 @@ class SalarySlip(TransactionBase):
'total_allocated_leaves': flt(leave_values.get('total_leaves')),
'expired_leaves': flt(leave_values.get('expired_leaves')),
'used_leaves': flt(leave_values.get('leaves_taken')),
'pending_leaves': flt(leave_values.get('pending_leaves')),
'pending_leaves': flt(leave_values.get('leaves_pending_approval')),
'available_leaves': flt(leave_values.get('remaining_leaves'))
})

View File

@@ -38,6 +38,8 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar
class TestSalarySlip(unittest.TestCase):
def setUp(self):
setup_test()
frappe.flags.pop("via_payroll_entry", None)
def tearDown(self):
frappe.db.rollback()
@@ -413,15 +415,17 @@ class TestSalarySlip(unittest.TestCase):
"email_salary_slip_to_employee": 1
})
def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.delete("Email Queue")
make_employee("test_email_salary_slip@salary.com")
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
user_id = "test_email_salary_slip@salary.com"
make_employee(user_id, company="_Test Company")
ss = make_employee_salary_slip(user_id, "Monthly", "Test Salary Slip Email")
ss.company = "_Test Company"
ss.save()
ss.submit()
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
email_queue = frappe.db.a_row_exists("Email Queue")
self.assertTrue(email_queue)
def test_loan_repayment_salary_slip(self):
@@ -1062,7 +1066,7 @@ def create_additional_salary(employee, payroll_period, amount):
}).submit()
return salary_date
def make_leave_application(employee, from_date, to_date, leave_type, company=None):
def make_leave_application(employee, from_date, to_date, leave_type, company=None, submit=True):
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee,
@@ -1070,11 +1074,12 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non
from_date = from_date,
to_date = to_date,
company = company or erpnext.get_default_company() or "_Test Company",
docstatus = 1,
status = "Approved",
leave_approver = 'test@example.com'
))
leave_application.submit()
)).insert()
if submit:
leave_application.submit()
return leave_application
@@ -1092,20 +1097,22 @@ def setup_test():
frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None)
def make_holiday_list():
def make_holiday_list(list_name=None, from_date=None, to_date=None):
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"):
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
"holiday_list_name": "Salary Slip Test Holiday List",
"from_date": fiscal_year[1],
"to_date": fiscal_year[2],
"weekly_off": "Sunday"
}).insert()
holiday_list.get_weekly_off_dates()
holiday_list.save()
holiday_list = holiday_list.name
name = list_name or "Salary Slip Test Holiday List"
frappe.delete_doc_if_exists("Holiday List", name, force=True)
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
"holiday_list_name": name,
"from_date": from_date or fiscal_year[1],
"to_date": to_date or fiscal_year[2],
"weekly_off": "Sunday"
}).insert()
holiday_list.get_weekly_off_dates()
holiday_list.save()
holiday_list = holiday_list.name
return holiday_list

View File

@@ -26,7 +26,7 @@
"fieldname": "total_allocated_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Total Allocated Leave",
"label": "Total Allocated Leave(s)",
"no_copy": 1,
"read_only": 1
},
@@ -34,7 +34,7 @@
"fieldname": "expired_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Expired Leave",
"label": "Expired Leave(s)",
"no_copy": 1,
"read_only": 1
},
@@ -42,7 +42,7 @@
"fieldname": "used_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Used Leave",
"label": "Used Leave(s)",
"no_copy": 1,
"read_only": 1
},
@@ -50,7 +50,7 @@
"fieldname": "pending_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Pending Leave",
"label": "Leave(s) Pending Approval",
"no_copy": 1,
"read_only": 1
},
@@ -58,7 +58,7 @@
"fieldname": "available_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Available Leave",
"label": "Available Leave(s)",
"no_copy": 1,
"read_only": 1
}
@@ -66,7 +66,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-19 10:47:48.546724",
"modified": "2022-02-28 14:01:32.327204",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip Leave",
@@ -74,5 +74,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -34,11 +34,13 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
frappe.model.set_value(item.doctype, item.name, "rate", item_rate);
},
calculate_taxes_and_totals: function(update_paid_amount) {
calculate_taxes_and_totals: async function(update_paid_amount) {
this.discount_amount_applied = false;
this._calculate_taxes_and_totals();
this.calculate_discount_amount();
await this.calculate_shipping_charges();
// Advance calculation applicable to Sales /Purchase Invoice
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
&& this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) {
@@ -81,7 +83,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this.initialize_taxes();
this.determine_exclusive_rate();
this.calculate_net_total();
this.calculate_shipping_charges();
this.calculate_taxes();
this.manipulate_grand_total_for_inclusive_tax();
this.calculate_totals();
@@ -272,27 +273,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
calculate_shipping_charges: function() {
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
this.shipping_rule();
}
},
add_taxes_from_item_tax_template: function(item_tax_map) {
let me = this;
if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
if (typeof (item_tax_map) == "string") {
item_tax_map = JSON.parse(item_tax_map);
}
$.each(item_tax_map, function(tax, rate) {
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
if (!found) {
let child = frappe.model.add_child(me.frm.doc, "taxes");
child.charge_type = "On Net Total";
child.account_head = tax;
child.rate = 0;
}
});
return this.shipping_rule();
}
},
@@ -685,7 +666,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
}));
this.frm.doc.total_advance = flt(total_allocated_amount, precision("total_advance"));
if (this.frm.doc.write_off_outstanding_amount_automatically) {
this.frm.doc.write_off_amount = 0;
}
this.calculate_outstanding_amount(update_paid_amount);
this.calculate_write_off_amount();
},
is_internal_invoice: function() {
@@ -810,7 +796,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this.frm.set_value('base_paid_amount', flt(base_paid_amount, precision("base_paid_amount")));
},
calculate_change_amount: function(){
calculate_change_amount: function() {
this.frm.doc.change_amount = 0.0;
this.frm.doc.base_change_amount = 0.0;
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
@@ -821,26 +807,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
var base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total +
this.frm.doc.write_off_amount, precision("change_amount"));
this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total,
precision("change_amount"));
this.frm.doc.base_change_amount = flt(this.frm.doc.base_paid_amount -
base_grand_total + this.frm.doc.base_write_off_amount,
precision("base_change_amount"));
base_grand_total, precision("base_change_amount"));
}
}
},
calculate_write_off_amount: function(){
if(this.frm.doc.paid_amount > this.frm.doc.grand_total){
this.frm.doc.write_off_amount = flt(this.frm.doc.grand_total - this.frm.doc.paid_amount
+ this.frm.doc.change_amount, precision("write_off_amount"));
calculate_write_off_amount: function() {
if (this.frm.doc.write_off_outstanding_amount_automatically) {
this.frm.doc.write_off_amount = flt(this.frm.doc.outstanding_amount, precision("write_off_amount"));
this.frm.doc.base_write_off_amount = flt(this.frm.doc.write_off_amount * this.frm.doc.conversion_rate,
precision("base_write_off_amount"));
}else{
this.frm.doc.paid_amount = 0.0;
this.calculate_outstanding_amount(false);
}
this.calculate_outstanding_amount(false);
}
});

View File

@@ -736,6 +736,26 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
});
},
add_taxes_from_item_tax_template: function(item_tax_map) {
let me = this;
if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
if (typeof (item_tax_map) == "string") {
item_tax_map = JSON.parse(item_tax_map);
}
$.each(item_tax_map, function(tax, rate) {
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
if (!found) {
let child = frappe.model.add_child(me.frm.doc, "taxes");
child.charge_type = "On Net Total";
child.account_head = tax;
child.rate = 0;
}
});
}
},
serial_no: function(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
@@ -1021,9 +1041,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
var me = this;
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
&& !this.frm.doc.ignore_pricing_rule) {
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency,
function(exchange_rate) {
@@ -1049,7 +1069,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
if(flt(this.frm.doc.conversion_rate)>0.0) {
if(this.frm.doc.ignore_pricing_rule) {
if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
this.calculate_taxes_and_totals();
} else if (!this.in_apply_price_list){
this.apply_price_list();
@@ -1066,6 +1086,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
return this.frm.call({
doc: this.frm.doc,
method: "apply_shipping_rule",
callback: function(r) {
me._calculate_taxes_and_totals();
}
}).fail(() => this.frm.set_value('shipping_rule', ''));
}
},
@@ -1123,8 +1146,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
if(this.frm.doc.price_list_currency !== company_currency && !this.frm.doc.ignore_pricing_rule) {
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
if(this.frm.doc.price_list_currency !== company_currency &&
!(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency,
function(exchange_rate) {
me.frm.set_value("plc_conversion_rate", exchange_rate);
@@ -1863,6 +1887,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
callback: function(r) {
if(!r.exc) {
item.item_tax_rate = r.message;
me.add_taxes_from_item_tax_template(item.item_tax_rate);
me.calculate_taxes_and_totals();
}
}

View File

@@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
fieldtype:'Float',
read_only: me.has_batch && !me.has_serial_no,
label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'),
default: flt(me.item.stock_qty),
default: flt(me.item.stock_qty) || flt(me.item.transfer_qty),
},
...get_pending_qty_fields(me),
{
@@ -94,14 +94,16 @@ erpnext.SerialNoBatchSelector = Class.extend({
description: __('Fetch Serial Numbers based on FIFO'),
click: () => {
let qty = this.dialog.fields_dict.qty.get_value();
let already_selected_serial_nos = get_selected_serial_nos(me);
let numbers = frappe.call({
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
args: {
qty: qty,
item_code: me.item_code,
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
batch_no: me.item.batch_no || null,
posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date
batch_nos: me.item.batch_no || null,
posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date,
exclude_sr_nos: already_selected_serial_nos
}
});
@@ -575,21 +577,40 @@ function get_pending_qty_fields(me) {
return pending_qty_fields;
}
function calc_total_selected_qty(me) {
// get all items with same item code except row for which selector is open.
function get_rows_with_same_item_code(me) {
const { frm: { doc: { items }}, item: { name, item_code }} = me;
const totalSelectedQty = items
.filter( item => ( item.name !== name ) && ( item.item_code === item_code ) )
.map( item => flt(item.qty) )
.reduce( (i, j) => i + j, 0);
return items.filter(item => (item.name !== name) && (item.item_code === item_code))
}
function calc_total_selected_qty(me) {
const totalSelectedQty = get_rows_with_same_item_code(me)
.map(item => flt(item.qty))
.reduce((i, j) => i + j, 0);
return totalSelectedQty;
}
function get_selected_serial_nos(me) {
const selected_serial_nos = get_rows_with_same_item_code(me)
.map(item => item.serial_no)
.filter(serial => serial)
.map(sr_no_string => sr_no_string.split('\n'))
.reduce((acc, arr) => acc.concat(arr), [])
.filter(serial => serial);
return selected_serial_nos;
};
function check_can_calculate_pending_qty(me) {
const { frm: { doc }, item } = me;
const docChecks = doc.bom_no
&& doc.fg_completed_qty
&& erpnext.stock.bom
&& erpnext.stock.bom.name === doc.bom_no;
const itemChecks = !!item && !item.allow_alternative_item;
const itemChecks = !!item
&& !item.allow_alternative_item
&& erpnext.stock.bom && erpnext.stock.items
&& (item.item_code in erpnext.stock.bom.items);
return docChecks && itemChecks;
}
//# sourceURL=serial_no_batch_selector.js

View File

@@ -4,6 +4,7 @@
--green-info: #38A160;
--product-bg-color: white;
--body-bg-color: var(--gray-50);
--text-md: 13px; // variables are in desk folder in frappe for v13, this is a temporary fix
}
body.product-page {
@@ -264,6 +265,15 @@ body.product-page {
font-size: 13px;
}
.filter-lookup-input {
background-color: white;
border: 1px solid var(--gray-300);
&:focus {
border: 1px solid var(--primary);
}
}
.filter-label {
font-size: 11px;
font-weight: 600;
@@ -569,15 +579,12 @@ body.product-page {
}
.scroll-categories {
white-space: nowrap;
overflow-x: auto;
.category-pill {
margin: 0px 4px;
display: inline-block;
padding: 6px 12px;
background-color: #ecf5fe;
width: fit-content;
padding: 6px 12px;
margin-bottom: 8px;
background-color: #ecf5fe;
font-size: 14px;
border-radius: 18px;
color: var(--blue-500);

View File

@@ -128,7 +128,8 @@ class GSTR3BReport(Document):
def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql("""
SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst
SELECT p.place_of_supply, p.supplier_address,
i.base_amount, i.is_nil_exempt, i.is_non_gst
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent
and p.is_opening = 'No'
@@ -136,7 +137,7 @@ class GSTR3BReport(Document):
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
month(p.posting_date) = %s and year(p.posting_date) = %s
and p.company = %s and p.company_gstin = %s
GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""",
""",
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = {
@@ -150,18 +151,24 @@ class GSTR3BReport(Document):
}
}
address_state_map = get_address_state_map()
for d in inward_nil_exempt:
if d.place_of_supply:
if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
and state == d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["gst"]["intra"] += d.base_amount
elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
and state != d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["gst"]["inter"] += d.base_amount
elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
elif d.is_non_gst == 1 and state != d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount
if not d.place_of_supply:
d.place_of_supply = "00-" + cstr(state)
supplier_state = address_state_map.get(d.supplier_address) or state
if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
inward_nil_exempt_details["gst"]["intra"] += d.base_amount
elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
inward_nil_exempt_details["gst"]["inter"] += d.base_amount
elif d.is_non_gst == 1 and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
elif d.is_non_gst == 1 and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount
return inward_nil_exempt_details
@@ -420,6 +427,11 @@ class GSTR3BReport(Document):
return ",".join(missing_field_invoices)
def get_address_state_map():
return frappe._dict(
frappe.get_all('Address', fields=['name', 'gst_state'], as_list=1)
)
def get_json(template):
file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template))
with open(file_path, 'r') as f:

View File

@@ -36,7 +36,7 @@ frappe.ui.form.on('Tax Exemption 80G Certificate', {
'date_of_donation': '',
'amount': 0,
'mode_of_payment': '',
'razorpay_payment_id': ''
'payment_id': ''
});
}
},

View File

@@ -38,7 +38,7 @@
"amount",
"column_break_27",
"mode_of_payment",
"razorpay_payment_id"
"payment_id"
],
"fields": [
{
@@ -201,13 +201,6 @@
"options": "Mode of Payment",
"read_only": 1
},
{
"fetch_from": "donation.razorpay_payment_id",
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "RazorPay Payment ID",
"read_only": 1
},
{
"fetch_from": "donation.date",
"fieldname": "date_of_donation",
@@ -266,11 +259,18 @@
"hidden": 1,
"label": "Title",
"print_hide": 1
},
{
"fetch_from": "donation.payment_id",
"fieldname": "payment_id",
"fieldtype": "Data",
"label": "Payment ID",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-22 00:03:34.215633",
"modified": "2022-03-16 17:21:39.831059",
"modified_by": "Administrator",
"module": "Regional",
"name": "Tax Exemption 80G Certificate",

View File

@@ -6,29 +6,19 @@ import frappe
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
from frappe.model.document import Document
from frappe.utils import flt, get_link_to_form, getdate
from frappe.utils import flt, get_link_to_form
from erpnext.accounts.utils import get_fiscal_year
class TaxExemption80GCertificate(Document):
def validate(self):
self.validate_date()
self.validate_duplicates()
self.validate_company_details()
self.set_company_address()
self.calculate_total()
self.set_title()
def validate_date(self):
if self.recipient == 'Member':
if getdate(self.date):
fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
if not (fiscal_year.year_start_date <= getdate(self.date) \
<= fiscal_year.year_end_date):
frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year)))
def validate_duplicates(self):
if self.recipient == 'Donor':
certificate = frappe.db.exists(self.doctype, {
@@ -96,7 +86,7 @@ class TaxExemption80GCertificate(Document):
'date': doc.from_date,
'amount': doc.amount,
'invoice_id': doc.invoice,
'razorpay_payment_id': doc.payment_id,
'payment_id': doc.payment_id,
'membership': doc.name
})
total += flt(doc.amount)

View File

@@ -7,7 +7,7 @@ import frappe
from frappe.utils import getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.non_profit.doctype.donation.donation import create_donation
from erpnext.non_profit.doctype.donation.donation import create_razorpay_donation
from erpnext.non_profit.doctype.donation.test_donation import (
create_donor,
create_donor_type,
@@ -39,11 +39,11 @@ class TestTaxExemption80GCertificate(unittest.TestCase):
donor = create_donor()
create_mode_of_payment()
payment = frappe._dict({
'amount': 100,
'amount': 100, # rzp sends data in paise
'method': 'Debit Card',
'id': 'pay_MeXAmsgeKOhq7O'
})
donation = create_donation(donor, payment)
donation = create_razorpay_donation(donor, payment)
args = frappe._dict({
'recipient': 'Donor',

View File

@@ -9,7 +9,7 @@
"amount",
"invoice_id",
"column_break_4",
"razorpay_payment_id",
"payment_id",
"membership"
],
"fields": [
@@ -35,26 +35,28 @@
"options": "Sales Invoice",
"reqd": 1
},
{
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "Razorpay Payment ID"
},
{
"fieldname": "membership",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Membership",
"options": "Membership"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "payment_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Payment ID"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-15 16:35:10.777587",
"modified": "2022-03-17 11:55:24.621708",
"modified_by": "Administrator",
"module": "Regional",
"name": "Tax Exemption 80G Certificate Detail",

View File

@@ -21,8 +21,9 @@ PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state:
doc.gst_state_number = state_numbers[doc.gst_state]
if hasattr(doc, 'gst_state'):
set_gst_state_and_state_number(doc)
if not hasattr(doc, 'gstin') or not doc.gstin:
return
@@ -52,7 +53,6 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc)
if not doc.gst_state:
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
@@ -84,17 +84,14 @@ def update_gst_category(doc, method):
frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
def set_gst_state_and_state_number(doc):
if not doc.gst_state:
if not doc.state:
return
if not doc.gst_state and doc.state:
state = doc.state.lower()
states_lowercase = {s.lower():s for s in states}
if state in states_lowercase:
doc.gst_state = states_lowercase[state]
else:
return
doc.gst_state_number = state_numbers[doc.gst_state]
doc.gst_state_number = state_numbers.get(doc.gst_state)
def validate_gstin_check_digit(gstin, label='GSTIN'):
''' Function to validate the check digit of the GSTIN.'''

View File

@@ -10,10 +10,10 @@
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "{% if letter_head and not no_letterhead -%}\n <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} 80G Donor Certificate</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n\n This is to confirm that the {{ doc.company }} received an amount of <b>{{doc.get_formatted(\"amount\")}}</b>\n from <b>{{ doc.donor_name }}</b>\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n <br><br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
"html": "{% if letter_head and not no_letterhead -%}\n <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} 80G Donor Certificate</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n\n This is to confirm that the {{ doc.company }} received an amount of <b>{{doc.get_formatted(\"amount\")}}</b>\n from <b>{{ doc.donor_name }}</b>\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.payment_id -%}\n bearing Payment ID {{ doc.payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n <br><br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
"idx": 0,
"line_breaks": 0,
"modified": "2021-02-22 00:20:08.516600",
"modified": "2022-03-16 17:25:33.420509",
"modified_by": "Administrator",
"module": "Regional",
"name": "80G Certificate for Donation",

View File

@@ -93,7 +93,7 @@ def create_qr_code(doc, method=None):
tlv_array.append(''.join([tag, length, value]))
# VAT Amount
vat_amount = str(doc.total_taxes_and_charges)
vat_amount = str(get_vat_amount(doc))
tag = bytes([5]).hex()
length = bytes([len(vat_amount)]).hex()
@@ -130,6 +130,22 @@ def create_qr_code(doc, method=None):
doc.db_set('ksa_einv_qr', _file.file_url)
doc.notify_update()
def get_vat_amount(doc):
vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company})
vat_accounts = []
vat_amount = 0
if vat_settings:
vat_settings_doc = frappe.get_cached_doc('KSA VAT Setting', vat_settings)
for row in vat_settings_doc.get('ksa_vat_sales_accounts'):
vat_accounts.append(row.account)
for tax in doc.get('taxes'):
if tax.account_head in vat_accounts:
vat_amount += tax.tax_amount
return vat_amount
def delete_qr_code_file(doc, method=None):
region = get_region(doc.company)

View File

@@ -28,9 +28,12 @@ def update_itemised_tax_data(doc):
elif row.item_code and itemised_tax.get(row.item_code):
tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()])
row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
meta = frappe.get_meta(row.doctype)
if meta.has_field('tax_rate'):
row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
def get_account_currency(account):
"""Helper function to get account currency."""

View File

@@ -4,12 +4,13 @@
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt
from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
from erpnext.tests.utils import create_test_contact_and_address
test_ignore = ["Price List"]
test_dependencies = ['Payment Term', 'Payment Terms Template']
@@ -18,7 +19,7 @@ test_records = frappe.get_test_records('Customer')
from six import iteritems
class TestCustomer(ERPNextTestCase):
class TestCustomer(FrappeTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')

View File

@@ -1,12 +1,10 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.queries import item_query
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ['Item', 'Customer', 'Supplier']
@@ -18,7 +16,7 @@ def create_party_specific_item(**args):
psi.based_on_value = args.get('based_on_value')
psi.insert()
class TestPartySpecificItem(ERPNextTestCase):
class TestPartySpecificItem(FrappeTestCase):
def setUp(self):
self.customer = frappe.get_last_doc("Customer")
self.supplier = frappe.get_last_doc("Supplier")

Some files were not shown because too many files have changed in this diff Show More