Merge pull request #30291 from ankush/13_23_pre_release

chore: merge version-13-hotfix to version-13-pre-release
This commit is contained in:
Ankush Menat
2022-03-17 16:31:00 +05:30
committed by GitHub
160 changed files with 2973 additions and 1007 deletions

View File

@@ -5,7 +5,6 @@ on:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.md' - '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch: workflow_dispatch:
@@ -30,11 +29,6 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps: 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 - name: Clone
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -5,7 +5,6 @@ on:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.md' - '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ develop ] branches: [ develop ]
@@ -40,12 +39,6 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps: 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 - name: Clone
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -7,6 +7,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt from frappe.utils import flt
from erpnext import get_company_currency from erpnext import get_company_currency
@@ -231,7 +232,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
}), transaction.currency, company_account) }), transaction.currency, company_account)
if total_amount > transaction.unallocated_amount: 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") account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers: for voucher in vouchers:
@@ -275,6 +276,10 @@ def check_matching(bank_account, company, transaction, document_types):
} }
matching_vouchers = [] matching_vouchers = []
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
document_types, filters))
for query in subquery: for query in subquery:
matching_vouchers.extend( matching_vouchers.extend(
frappe.db.sql(query, filters,) frappe.db.sql(query, filters,)
@@ -311,6 +316,114 @@ def get_queries(bank_account, company, transaction, document_types):
return queries return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters):
vouchers = []
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
return vouchers
def get_ld_matching_query(bank_account, amount_condition, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \
loan_disbursement.applicant == filters.get("party")
rank = (
frappe.qb.terms.Case()
.when(matching_reference, 1)
.else_(0)
)
rank1 = (
frappe.qb.terms.Case()
.when(matching_party, 1)
.else_(0)
)
query = frappe.qb.from_(loan_disbursement).select(
rank + rank1 + 1,
ConstantColumn("Loan Disbursement").as_("doctype"),
loan_disbursement.name,
loan_disbursement.disbursed_amount,
loan_disbursement.reference_number,
loan_disbursement.reference_date,
loan_disbursement.applicant_type,
loan_disbursement.disbursement_date
).where(
loan_disbursement.docstatus == 1
).where(
loan_disbursement.clearance_date.isnull()
).where(
loan_disbursement.disbursement_account == bank_account
)
if amount_condition:
query.where(
loan_disbursement.disbursed_amount == filters.get('amount')
)
else:
query.where(
loan_disbursement.disbursed_amount <= filters.get('amount')
)
vouchers = query.run(as_list=True)
return vouchers
def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get("party_type") and \
loan_repayment.applicant == filters.get("party")
rank = (
frappe.qb.terms.Case()
.when(matching_reference, 1)
.else_(0)
)
rank1 = (
frappe.qb.terms.Case()
.when(matching_party, 1)
.else_(0)
)
query = frappe.qb.from_(loan_repayment).select(
rank + rank1 + 1,
ConstantColumn("Loan Repayment").as_("doctype"),
loan_repayment.name,
loan_repayment.amount_paid,
loan_repayment.reference_number,
loan_repayment.reference_date,
loan_repayment.applicant_type,
loan_repayment.posting_date
).where(
loan_repayment.docstatus == 1
).where(
loan_repayment.clearance_date.isnull()
).where(
loan_repayment.payment_account == bank_account
)
if amount_condition:
query.where(
loan_repayment.amount_paid == filters.get('amount')
)
else:
query.where(
loan_repayment.amount_paid <= filters.get('amount')
)
vouchers = query.run()
return vouchers
def get_pe_matching_query(amount_condition, account_from_to, transaction): def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query # get matching payment entries query
if transaction.deposit > 0: if transaction.deposit > 0:
@@ -348,7 +461,6 @@ def get_je_matching_query(amount_condition, transaction):
# We have mapping at the bank level # We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability # So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f""" return f"""

View File

@@ -48,7 +48,8 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False): def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment",
"Loan Disbursement"]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel) self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice": elif payment_entry.payment_document == "Sales Invoice":
@@ -108,18 +109,30 @@ def get_paid_amount(payment_entry, currency, bank_account):
paid_amount_field = "paid_amount" paid_amount_field = "paid_amount"
if payment_entry.payment_document == 'Payment Entry': if payment_entry.payment_document == 'Payment Entry':
doc = frappe.get_doc("Payment Entry", payment_entry.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, return frappe.db.get_value(payment_entry.payment_document,
payment_entry.payment_entry, paid_amount_field) payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry": elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)") return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account},
"sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim": elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed") return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
elif payment_entry.payment_document == "Loan Disbursement":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount")
elif payment_entry.payment_document == "Loan Repayment":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
else: else:
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry)) frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))

View File

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

View File

@@ -54,7 +54,7 @@ class POSInvoice(SalesInvoice):
def on_submit(self): def on_submit(self):
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program # 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() self.make_loyalty_point_entry()
elif self.is_return and self.return_against and self.loyalty_program: elif self.is_return and self.return_against and self.loyalty_program:
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
@@ -88,7 +88,7 @@ class POSInvoice(SalesInvoice):
def on_cancel(self): def on_cancel(self):
# run on cancel method of selling controller # run on cancel method of selling controller
super(SalesInvoice, self).on_cancel() super(SalesInvoice, self).on_cancel()
if self.loyalty_program: if not self.is_return and self.loyalty_program:
self.delete_loyalty_point_entry() self.delete_loyalty_point_entry()
elif self.is_return and self.return_against and self.loyalty_program: elif self.is_return and self.return_against and self.loyalty_program:
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)

View File

@@ -83,7 +83,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn = make_sales_return(pos_inv.name) pos_inv_cn = make_sales_return(pos_inv.name)
pos_inv_cn.set("payments", []) pos_inv_cn.set("payments", [])
pos_inv_cn.append('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.paid_amount = -300
pos_inv_cn.submit() pos_inv_cn.submit()
@@ -98,7 +101,12 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn.load_from_db() pos_inv_cn.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) 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: finally:
frappe.set_user("Administrator") frappe.set_user("Administrator")

View File

@@ -64,10 +64,10 @@
<td></td> <td></td>
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td> <td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td>
<td style="text-align: right"> <td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
</td> </td>
<td style="text-align: right"> <td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
</td> </td>
{% endif %} {% endif %}
<td style="text-align: right"> <td style="text-align: right">

View File

@@ -51,6 +51,13 @@ frappe.ui.form.on('Process Statement Of Accounts', {
} }
} }
}); });
frm.set_query("account", function() {
return {
filters: {
'company': frm.doc.company
}
};
});
if(frm.doc.__islocal){ if(frm.doc.__islocal){
frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1)); frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value('to_date', frappe.datetime.get_today()); frm.set_value('to_date', frappe.datetime.get_today());

View File

@@ -2,7 +2,7 @@
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-24 19:29:05", "creation": "2022-01-25 10:29:57.771398",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
@@ -651,7 +651,6 @@
"hide_seconds": 1, "hide_seconds": 1,
"label": "Ignore Pricing Rule", "label": "Ignore Pricing Rule",
"no_copy": 1, "no_copy": 1,
"permlevel": 0,
"print_hide": 1 "print_hide": 1
}, },
{ {
@@ -1974,9 +1973,10 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
"fieldname": "is_debit_note", "fieldname": "is_debit_note",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Debit Note" "label": "Is Rate Adjustment Entry (Debit Note)"
}, },
{ {
"default": "0", "default": "0",
@@ -2038,7 +2038,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-12-23 20:19:38.667508", "modified": "2022-03-08 16:08:53.517903",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",
@@ -2089,8 +2089,9 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"timeline_field": "customer", "timeline_field": "customer",
"title_field": "title", "title_field": "title",
"track_changes": 1, "track_changes": 1,
"track_seen": 1 "track_seen": 1
} }

View File

@@ -44,8 +44,11 @@ from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timeshe
from erpnext.setup.doctype.company.company import update_company_current_month_sales 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.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.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.doctype.serial_no.serial_no import (
from erpnext.stock.utils import calculate_mapped_packed_items_return get_delivery_note_serial_no,
get_serial_nos,
update_serial_nos_after_submit,
)
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
@@ -228,6 +231,9 @@ class SalesInvoice(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO # because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() 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 # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
@@ -752,11 +758,8 @@ class SalesInvoice(SellingController):
def update_packing_list(self): def update_packing_list(self):
if cint(self.update_stock) == 1: if cint(self.update_stock) == 1:
if cint(self.is_return) and self.return_against: from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
calculate_mapped_packed_items_return(self) make_packing_list(self)
else:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
else: else:
self.set('packed_items', []) self.set('packed_items', [])
@@ -1415,12 +1418,19 @@ class SalesInvoice(SellingController):
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name) frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
def get_returned_amount(self): def get_returned_amount(self):
returned_amount = frappe.db.sql(""" from frappe.query_builder.functions import Coalesce, Sum
select sum(grand_total) doc = frappe.qb.DocType(self.doctype)
from `tabSales Invoice` returned_amount = (
where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s frappe.qb.from_(doc)
""", self.name) .select(Sum(doc.grand_total))
return abs(flt(returned_amount[0][0])) if returned_amount else 0 .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. # redeem the loyalty points.
def apply_loyalty_points(self): def apply_loyalty_points(self):
@@ -1700,7 +1710,6 @@ def make_maintenance_schedule(source_name, target_doc=None):
@frappe.whitelist() @frappe.whitelist()
def make_delivery_note(source_name, target_doc=None): def make_delivery_note(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values") target.run_method("set_missing_values")
target.run_method("set_po_nos") target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals") target.run_method("calculate_taxes_and_totals")

View File

@@ -2552,6 +2552,12 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) 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(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'

View File

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

View File

@@ -152,7 +152,7 @@ def set_contact_details(party_details, party, party_type):
def set_other_values(party_details, party, party_type): def set_other_values(party_details, party, party_type):
# copy # copy
if party_type=="Customer": if party_type == "Customer":
to_copy = ["customer_name", "customer_group", "territory", "language"] to_copy = ["customer_name", "customer_group", "territory", "language"]
else: else:
to_copy = ["supplier_name", "supplier_group", "language"] to_copy = ["supplier_name", "supplier_group", "language"]
@@ -171,12 +171,8 @@ def get_default_price_list(party):
return party.default_price_list return party.default_price_list
if party.doctype == "Customer": if party.doctype == "Customer":
price_list = frappe.get_cached_value("Customer Group", return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list")
party.customer_group, "default_price_list")
if price_list:
return price_list
return None
def set_price_list(party_details, party, party_type, given_price_list, pos=None): def set_price_list(party_details, party, party_type, given_price_list, pos=None):
# price list # price list

View File

@@ -4,7 +4,12 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt, getdate, nowdate from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from pypika import CustomFunction
from erpnext.accounts.utils import get_balance_on
def execute(filters=None): def execute(filters=None):
@@ -18,7 +23,6 @@ def execute(filters=None):
data = get_entries(filters) data = get_entries(filters)
from erpnext.accounts.utils import get_balance_on
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0,0 total_debit, total_credit = 0,0
@@ -118,7 +122,21 @@ def get_columns():
] ]
def get_entries(filters): def get_entries(filters):
journal_entries = frappe.db.sql(""" journal_entries = get_journal_entries(filters)
payment_entries = get_payment_entries(filters)
loan_entries = get_loan_entries(filters)
pos_entries = []
if filters.include_pos_transactions:
pos_entries = get_pos_entries(filters)
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)),
key=lambda k: getdate(k['posting_date']))
def get_journal_entries(filters):
return frappe.db.sql("""
select "Journal Entry" as payment_document, jv.posting_date, select "Journal Entry" as payment_document, jv.posting_date,
jv.name as payment_entry, jvd.debit_in_account_currency as debit, jv.name as payment_entry, jvd.debit_in_account_currency as debit,
jvd.credit_in_account_currency as credit, jvd.against_account, jvd.credit_in_account_currency as credit, jvd.against_account,
@@ -130,7 +148,8 @@ def get_entries(filters):
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1) and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1)
payment_entries = frappe.db.sql(""" def get_payment_entries(filters):
return frappe.db.sql("""
select select
"Payment Entry" as payment_document, name as payment_entry, "Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date, reference_no, reference_date as ref_date,
@@ -145,9 +164,8 @@ def get_entries(filters):
and ifnull(clearance_date, '4000-01-01') > %(report_date)s and ifnull(clearance_date, '4000-01-01') > %(report_date)s
""", filters, as_dict=1) """, filters, as_dict=1)
pos_entries = [] def get_pos_entries(filters):
if filters.include_pos_transactions: return frappe.db.sql("""
pos_entries = frappe.db.sql("""
select select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.debit_to as against_account, sip.clearance_date, si.posting_date, si.debit_to as against_account, sip.clearance_date,
@@ -161,8 +179,42 @@ def get_entries(filters):
si.posting_date ASC, si.name DESC si.posting_date ASC, si.name DESC
""", filters, as_dict=1) """, filters, as_dict=1)
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), def get_loan_entries(filters):
key=lambda k: k['posting_date'] or getdate(nowdate())) loan_docs = []
for doctype in ["Loan Disbursement", "Loan Repayment"]:
loan_doc = frappe.qb.DocType(doctype)
ifnull = CustomFunction('IFNULL', ['value', 'default'])
if doctype == "Loan Disbursement":
amount_field = (loan_doc.disbursed_amount).as_("credit")
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
else:
amount_field = (loan_doc.amount_paid).as_("debit")
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
entries = frappe.qb.from_(loan_doc).select(
ConstantColumn(doctype).as_("payment_document"),
(loan_doc.name).as_("payment_entry"),
(loan_doc.reference_number).as_("reference_no"),
(loan_doc.reference_date).as_("ref_date"),
amount_field,
posting_date,
).where(
loan_doc.docstatus == 1
).where(
account == filters.get('account')
).where(
posting_date <= getdate(filters.get('report_date'))
).where(
ifnull(loan_doc.clearance_date, '4000-01-01') > getdate(filters.get('report_date'))
).run(as_dict=1)
loan_docs.extend(entries)
return loan_docs
def get_amounts_not_reflected_in_system(filters): def get_amounts_not_reflected_in_system(filters):
je_amount = frappe.db.sql(""" je_amount = frappe.db.sql("""
@@ -182,7 +234,40 @@ def get_amounts_not_reflected_in_system(filters):
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0 pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
return je_amount + pe_amount loan_amount = get_loan_amount(filters)
return je_amount + pe_amount + loan_amount
def get_loan_amount(filters):
total_amount = 0
for doctype in ["Loan Disbursement", "Loan Repayment"]:
loan_doc = frappe.qb.DocType(doctype)
ifnull = CustomFunction('IFNULL', ['value', 'default'])
if doctype == "Loan Disbursement":
amount_field = Sum(loan_doc.disbursed_amount)
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
else:
amount_field = Sum(loan_doc.amount_paid)
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
amount = frappe.qb.from_(loan_doc).select(
amount_field
).where(
loan_doc.docstatus == 1
).where(
account == filters.get('account')
).where(
posting_date > getdate(filters.get('report_date'))
).where(
ifnull(loan_doc.clearance_date, '4000-01-01') <= getdate(filters.get('report_date'))
).run()[0][0]
total_amount += flt(amount)
return amount
def get_balance_row(label, amount, account_currency): def get_balance_row(label, amount, account_currency):
if amount > 0: if amount > 0:

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

@@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = {
"label": __("Company"), "label": __("Company"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Company", "options": "Company",
"reqd": 1, "default": frappe.defaults.get_user_default("Company"),
"default": frappe.defaults.get_user_default("Company") "reqd": 1
}, },
{ {
"fieldname":"from_date", "fieldname":"from_date",
"label": __("From Date"), "label": __("From Date"),
"fieldtype": "Date", "fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_start_date") "default": frappe.defaults.get_user_default("year_start_date"),
"reqd": 1
}, },
{ {
"fieldname":"to_date", "fieldname":"to_date",
"label": __("To Date"), "label": __("To Date"),
"fieldtype": "Date", "fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_end_date") "default": frappe.defaults.get_user_default("year_end_date"),
"reqd": 1
}, },
{ {
"fieldname":"sales_invoice", "fieldname":"sales_invoice",

View File

@@ -1,5 +1,5 @@
{ {
"add_total_row": 0, "add_total_row": 1,
"columns": [], "columns": [],
"creation": "2013-02-25 17:03:34", "creation": "2013-02-25 17:03:34",
"disable_prepared_report": 0, "disable_prepared_report": 0,
@@ -9,7 +9,7 @@
"filters": [], "filters": [],
"idx": 3, "idx": 3,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2021-11-13 19:14:23.730198", "modified": "2022-02-11 10:18:36.956558",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Gross Profit", "name": "Gross Profit",

View File

@@ -70,43 +70,42 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
data.append(row) data.append(row)
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data): def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
for idx, src in enumerate(gross_profit_data.grouped_data): for src in gross_profit_data.grouped_data:
row = [] row = []
for col in group_wise_columns.get(scrub(filters.group_by)): for col in group_wise_columns.get(scrub(filters.group_by)):
row.append(src.get(col)) row.append(src.get(col))
row.append(filters.currency) row.append(filters.currency)
if idx == len(gross_profit_data.grouped_data)-1:
row[0] = "Total"
data.append(row) data.append(row)
def get_columns(group_wise_columns, filters): def get_columns(group_wise_columns, filters):
columns = [] columns = []
column_map = frappe._dict({ column_map = frappe._dict({
"parent": _("Sales Invoice") + ":Link/Sales Invoice:120", "parent": {"label": _('Sales Invoice'), "fieldname": "parent_invoice", "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
"invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120", "invoice_or_item": {"label": _('Sales Invoice'), "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
"posting_date": _("Posting Date") + ":Date:100", "posting_date": {"label": _('Posting Date'), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
"posting_time": _("Posting Time") + ":Data:100", "posting_time": {"label": _('Posting Time'), "fieldname": "posting_time", "fieldtype": "Data", "width": 100},
"item_code": _("Item Code") + ":Link/Item:100", "item_code": {"label": _('Item Code'), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
"item_name": _("Item Name") + ":Data:100", "item_name": {"label": _('Item Name'), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
"item_group": _("Item Group") + ":Link/Item Group:100", "item_group": {"label": _('Item Group'), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
"brand": _("Brand") + ":Link/Brand:100", "brand": {"label": _('Brand'), "fieldtype": "Link", "options": "Brand", "width": 100},
"description": _("Description") +":Data:100", "description": {"label": _('Description'), "fieldname": "description", "fieldtype": "Data", "width": 100},
"warehouse": _("Warehouse") + ":Link/Warehouse:100", "warehouse": {"label": _('Warehouse'), "fieldname": "warehouse", "fieldtype": "Link", "options": "warehouse", "width": 100},
"qty": _("Qty") + ":Float:80", "qty": {"label": _('Qty'), "fieldname": "qty", "fieldtype": "Float", "width": 80},
"base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", "base_rate": {"label": _('Avg. Selling Rate'), "fieldname": "avg._selling_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
"buying_rate": _("Valuation Rate") + ":Currency/currency:100", "buying_rate": {"label": _('Valuation Rate'), "fieldname": "valuation_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
"base_amount": _("Selling Amount") + ":Currency/currency:100", "base_amount": {"label": _('Selling Amount'), "fieldname": "selling_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
"buying_amount": _("Buying Amount") + ":Currency/currency:100", "buying_amount": {"label": _('Buying Amount'), "fieldname": "buying_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
"gross_profit": _("Gross Profit") + ":Currency/currency:100", "gross_profit": {"label": _('Gross Profit'), "fieldname": "gross_profit", "fieldtype": "Currency", "options": "currency", "width": 100},
"gross_profit_percent": _("Gross Profit %") + ":Percent:100", "gross_profit_percent": {"label": _('Gross Profit Percent'), "fieldname": "gross_profit_%",
"project": _("Project") + ":Link/Project:100", "fieldtype": "Percent", "width": 100},
"sales_person": _("Sales person"), "project": {"label": _('Project'), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
"allocated_amount": _("Allocated Amount") + ":Currency/currency:100", "sales_person": {"label": _('Sales Person'), "fieldname": "sales_person", "fieldtype": "Data","width": 100},
"customer": _("Customer") + ":Link/Customer:100", "allocated_amount": {"label": _('Allocated Amount'), "fieldname": "allocated_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
"customer_group": _("Customer Group") + ":Link/Customer Group:100", "customer": {"label": _('Customer'), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 100},
"territory": _("Territory") + ":Link/Territory:100" "customer_group": {"label": _('Customer Group'), "fieldname": "customer_group", "fieldtype": "Link", "options": "customer", "width": 100},
"territory": {"label": _('Territory'), "fieldname": "territory", "fieldtype": "Link", "options": "territory", "width": 100},
}) })
for col in group_wise_columns.get(scrub(filters.group_by)): for col in group_wise_columns.get(scrub(filters.group_by)):
@@ -173,7 +172,7 @@ class GrossProfitGenerator(object):
buying_amount = 0 buying_amount = 0
for row in reversed(self.si_list): for row in reversed(self.si_list):
if self.skip_row(row, self.product_bundles): if self.skip_row(row):
continue continue
row.base_amount = flt(row.base_net_amount, self.currency_precision) row.base_amount = flt(row.base_net_amount, self.currency_precision)
@@ -223,16 +222,6 @@ class GrossProfitGenerator(object):
self.get_average_rate_based_on_group_by() self.get_average_rate_based_on_group_by()
def get_average_rate_based_on_group_by(self): def get_average_rate_based_on_group_by(self):
# sum buying / selling totals for group
self.totals = frappe._dict(
qty=0,
base_amount=0,
buying_amount=0,
gross_profit=0,
gross_profit_percent=0,
base_rate=0,
buying_rate=0
)
for key in list(self.grouped): for key in list(self.grouped):
if self.filters.get("group_by") != "Invoice": if self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]): for i, row in enumerate(self.grouped[key]):
@@ -244,7 +233,6 @@ class GrossProfitGenerator(object):
new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row = self.set_average_rate(new_row) new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row) self.grouped_data.append(new_row)
self.add_to_totals(new_row)
else: else:
for i, row in enumerate(self.grouped[key]): for i, row in enumerate(self.grouped[key]):
if row.indent == 1.0: if row.indent == 1.0:
@@ -258,17 +246,6 @@ class GrossProfitGenerator(object):
if (flt(row.qty) or row.base_amount): if (flt(row.qty) or row.base_amount):
row = self.set_average_rate(row) row = self.set_average_rate(row)
self.grouped_data.append(row) self.grouped_data.append(row)
self.add_to_totals(row)
self.set_average_gross_profit(self.totals)
if self.filters.get("group_by") == "Invoice":
self.totals.indent = 0.0
self.totals.parent_invoice = ""
self.totals.invoice_or_item = "Total"
self.si_list.append(self.totals)
else:
self.grouped_data.append(self.totals)
def is_not_invoice_row(self, row): def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice" return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
@@ -286,11 +263,6 @@ class GrossProfitGenerator(object):
new_row.buying_rate = flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 new_row.buying_rate = flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0
new_row.base_rate = flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 new_row.base_rate = flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0
def add_to_totals(self, new_row):
for key in self.totals:
if new_row.get(key):
self.totals[key] += new_row[key]
def get_returned_invoice_items(self): def get_returned_invoice_items(self):
returned_invoices = frappe.db.sql(""" returned_invoices = frappe.db.sql("""
select select
@@ -308,12 +280,12 @@ class GrossProfitGenerator(object):
self.returned_invoices.setdefault(inv.return_against, frappe._dict())\ self.returned_invoices.setdefault(inv.return_against, frappe._dict())\
.setdefault(inv.item_code, []).append(inv) .setdefault(inv.item_code, []).append(inv)
def skip_row(self, row, product_bundles): def skip_row(self, row):
if self.filters.get("group_by") != "Invoice": if self.filters.get("group_by") != "Invoice":
if not row.get(scrub(self.filters.get("group_by", ""))): if not row.get(scrub(self.filters.get("group_by", ""))):
return True return True
elif row.get("is_return") == 1:
return True return False
def get_buying_amount_from_product_bundle(self, row, product_bundle): def get_buying_amount_from_product_bundle(self, row, product_bundle):
buying_amount = 0.0 buying_amount = 0.0
@@ -371,20 +343,37 @@ class GrossProfitGenerator(object):
return self.average_buying_rate[item_code] return self.average_buying_rate[item_code]
def get_last_purchase_rate(self, item_code, row): def get_last_purchase_rate(self, item_code, row):
condition = '' purchase_invoice = frappe.qb.DocType("Purchase Invoice")
if row.project: purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
condition += " AND a.project=%s" % (frappe.db.escape(row.project))
elif row.cost_center:
condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center))
if self.filters.to_date:
condition += " AND modified='%s'" % (self.filters.to_date)
last_purchase_rate = frappe.db.sql(""" query = (frappe.qb.from_(purchase_invoice_item)
select (a.base_rate / a.conversion_factor) .inner_join(
from `tabPurchase Invoice Item` a purchase_invoice
where a.item_code = %s and a.docstatus=1 ).on(
{0} purchase_invoice.name == purchase_invoice_item.parent
order by a.modified desc limit 1""".format(condition), item_code) ).select(
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor
).where(
purchase_invoice.docstatus == 1
).where(
purchase_invoice.posting_date <= self.filters.to_date
).where(
purchase_invoice_item.item_code == item_code
))
if row.project:
query.where(
purchase_invoice_item.project == row.project
)
if row.cost_center:
query.where(
purchase_invoice_item.cost_center == row.cost_center
)
query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
query.limit(1)
last_purchase_rate = query.run()
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0

View File

@@ -107,6 +107,7 @@ def get_opening_balances(filters):
select party, sum(debit) as opening_debit, sum(credit) as opening_credit select party, sum(debit) as opening_debit, sum(credit) as opening_credit
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s where company=%(company)s
and is_cancelled=0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
{account_filter} {account_filter}
@@ -133,6 +134,7 @@ def get_balances_within_period(filters):
select party, sum(debit) as debit, sum(credit) as credit select party, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s where company=%(company)s
and is_cancelled = 0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and posting_date >= %(from_date)s and posting_date <= %(to_date)s and posting_date >= %(from_date)s and posting_date <= %(to_date)s
and ifnull(is_opening, 'No') = 'No' 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'] account_currency = entry['account_currency']
if len(account_currencies) == 1 and account_currency == presentation_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 entry['debit'] = debit_in_account_currency
if entry.get('credit'): if credit_in_account_currency:
entry['credit'] = credit_in_account_currency entry['credit'] = credit_in_account_currency
else: else:
date = currency_info['report_date'] date = currency_info['report_date']

View File

@@ -0,0 +1,16 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.party import get_default_price_list
class PartyTestCase(FrappeTestCase):
def test_get_default_price_list_should_return_none_for_invalid_group(self):
customer = frappe.get_doc({
'doctype': 'Customer',
'customer_name': 'test customer',
}).insert(ignore_permissions=True, ignore_mandatory=True)
customer.customer_group = None
customer.save()
price_list = get_default_price_list(customer)
assert price_list is None

View File

@@ -1,3 +1,4 @@
from frappe import _
def get_data(): def get_data():
@@ -7,7 +8,7 @@ def get_data():
}, },
'transactions': [ 'transactions': [
{ {
'label': ['Movement'], 'label': _('Movement'),
'items': ['Asset Movement'] 'items': ['Asset Movement']
} }
] ]

View File

@@ -23,7 +23,7 @@ def post_depreciation_entries(date=None):
frappe.db.commit() frappe.db.commit()
def get_depreciable_assets(date): def get_depreciable_assets(date):
return frappe.db.sql_list("""select a.name return frappe.db.sql_list("""select distinct a.name
from tabAsset a, `tabDepreciation Schedule` ds from tabAsset a, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
and a.status in ('Submitted', 'Partially Depreciated') and a.status in ('Submitted', 'Partially Depreciated')

View File

@@ -48,7 +48,7 @@ frappe.ui.form.on('Asset Maintenance', {
<div class='col-sm-3 small'> <div class='col-sm-3 small'>
<a onclick="frappe.set_route('List', 'Asset Maintenance Log', <a onclick="frappe.set_route('List', 'Asset Maintenance Log',
{'asset_name': '${d.asset_name}','maintenance_status': '${d.maintenance_status}' });"> {'asset_name': '${d.asset_name}','maintenance_status': '${d.maintenance_status}' });">
${d.maintenance_status} <span class="badge">${d.count}</span> ${__(d.maintenance_status)} <span class="badge">${d.count}</span>
</a> </a>
</div> </div>
</div>`).appendTo(rows); </div>`).appendTo(rows);

View File

@@ -402,7 +402,6 @@ def close_or_unclose_purchase_orders(names, status):
frappe.local.message_log = [] frappe.local.message_log = []
def set_missing_values(source, target): def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values") target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals") target.run_method("calculate_taxes_and_totals")

View File

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

View File

@@ -109,7 +109,6 @@ def get_list_context(context=None):
@frappe.whitelist() @frappe.whitelist()
def make_purchase_order(source_name, target_doc=None): def make_purchase_order(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values") target.run_method("set_missing_values")
target.run_method("get_schedule_dates") target.run_method("get_schedule_dates")
target.run_method("calculate_taxes_and_totals") target.run_method("calculate_taxes_and_totals")

View File

@@ -6,6 +6,7 @@ import copy
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import date_diff, flt, getdate from frappe.utils import date_diff, flt, getdate
@@ -16,12 +17,9 @@ def execute(filters=None):
validate_filters(filters) validate_filters(filters)
columns = get_columns(filters) columns = get_columns(filters)
conditions = get_conditions(filters) data = get_data(filters)
#get queried data # prepare data for report and chart views
data = get_data(filters, conditions)
#prepare data for report and chart views
data, chart_data = prepare_data(data, filters) data, chart_data = prepare_data(data, filters)
return columns, data, None, chart_data return columns, data, None, chart_data
@@ -34,53 +32,70 @@ def validate_filters(filters):
elif date_diff(to_date, from_date) < 0: elif date_diff(to_date, from_date) < 0:
frappe.throw(_("To Date cannot be before From Date.")) frappe.throw(_("To Date cannot be before From Date."))
def get_conditions(filters): def get_data(filters):
conditions = '' mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")
query = (
frappe.qb.from_(mr)
.join(mr_item).on(mr_item.parent == mr.name)
.select(
mr.name.as_("material_request"),
mr.transaction_date.as_("date"),
mr_item.schedule_date.as_("required_date"),
mr_item.item_code.as_("item_code"),
Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
Coalesce(mr_item.stock_uom, '').as_("uom"),
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(
Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))
).as_("qty_to_receive"),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(
Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))
).as_("qty_to_order"),
mr_item.item_name,
mr_item.description,
mr.company
).where(
(mr.material_request_type == "Purchase")
& (mr.docstatus == 1)
& (mr.status != "Stopped")
& (mr.per_received < 100)
)
)
query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
query = (
query.groupby(
mr.name, mr_item.item_code
).orderby(
mr.transaction_date, mr.schedule_date
)
)
data = query.run(as_dict=True)
return data
def get_conditions(filters, query, mr, mr_item):
if filters.get("from_date") and filters.get("to_date"): if filters.get("from_date") and filters.get("to_date"):
conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date")) query = (
query.where(
(mr.transaction_date >= filters.get("from_date"))
& (mr.transaction_date <= filters.get("to_date"))
)
)
if filters.get("company"): if filters.get("company"):
conditions += " and mr.company = '{0}'".format(filters.get("company")) query = query.where(mr.company == filters.get("company"))
if filters.get("material_request"): if filters.get("material_request"):
conditions += " and mr.name = '{0}'".format(filters.get("material_request")) query = query.where(mr.name == filters.get("material_request"))
if filters.get("item_code"): if filters.get("item_code"):
conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code")) query = query.where(mr_item.item_code == filters.get("item_code"))
return conditions return query
def get_data(filters, conditions):
data = frappe.db.sql("""
select
mr.name as material_request,
mr.transaction_date as date,
mr_item.schedule_date as required_date,
mr_item.item_code as item_code,
sum(ifnull(mr_item.stock_qty, 0)) as qty,
ifnull(mr_item.stock_uom, '') as uom,
sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty,
sum(ifnull(mr_item.received_qty, 0)) as received_qty,
(sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive,
(sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order,
mr_item.item_name as item_name,
mr_item.description as "description",
mr.company as company
from
`tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where
mr_item.parent = mr.name
and mr.material_request_type = "Purchase"
and mr.docstatus = 1
and mr.status != "Stopped"
{conditions}
group by mr.name, mr_item.item_code
having
sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0))
order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1)
return data
def update_qty_columns(row_to_update, data_row): def update_qty_columns(row_to_update, data_row):
fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]

View File

@@ -0,0 +1,68 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import (
get_data,
)
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
def setUp(self) -> None:
create_item("Test MR Report Item")
self.setup_material_request() # to order and receive
self.setup_material_request(order=True, days=1) # to receive (ordered)
self.setup_material_request(order=True, receive=True, days=2) # complete (ordered & received)
self.filters = frappe._dict(
company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
item_code="Test MR Report Item"
)
def tearDown(self) -> None:
frappe.db.rollback()
def test_date_range(self):
data = get_data(self.filters)
self.assertEqual(len(data), 2) # MRs today should be fetched
data = get_data(self.filters.update({"from_date": add_days(today(), 10)}))
self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is in future
def test_ordered_received_material_requests(self):
data = get_data(self.filters)
# from the 3 MRs made, only 2 (to receive) should be fetched
self.assertEqual(len(data), 2)
self.assertEqual(data[0].ordered_qty, 0.0)
self.assertEqual(data[1].ordered_qty, 57.0)
def setup_material_request(self, order=False, receive=False, days=0):
po = None
test_records = frappe.get_test_records('Material Request')
mr = frappe.copy_doc(test_records[0])
mr.transaction_date = add_days(today(), days)
mr.schedule_date = add_days(mr.transaction_date, 1)
for row in mr.items:
row.item_code = "Test MR Report Item"
row.item_name = "Test MR Report Item"
row.description = "Test MR Report Item"
row.uom = "Nos"
row.schedule_date = mr.schedule_date
mr.submit()
if order or receive:
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
po.submit()
if receive:
pr = make_purchase_receipt(po.name)
pr.submit()

View File

@@ -208,10 +208,15 @@ def get_already_returned_items(doc):
return items 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" child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) 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 = [ fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
"sum(abs(`tab{0}`.stock_qty)) as stock_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": if doctype == "Purchase Receipt":
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] 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, data = frappe.db.get_list(doctype,
fields = fields, fields = fields,
filters = [ filters = [
[doctype, "return_against", "=", return_against],
[doctype, party_type, "=", party],
[doctype, "docstatus", "=", 1], [doctype, "docstatus", "=", 1],
[doctype, "is_return", "=", 1], [doctype, "is_return", "=", 1],
[child_doctype, reference_field, "=", row_name] [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) target_doc.serial_no = '\n'.join(serial_nos)
if doctype == "Purchase Receipt": 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.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.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)) 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 target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice": 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.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.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)) 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 target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note": 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.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)) 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: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return
elif doctype == "Sales Invoice" or doctype == "POS Invoice": 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.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)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))

View File

@@ -37,6 +37,8 @@ class calculate_taxes_and_totals(object):
self.set_discount_amount() self.set_discount_amount()
self.apply_discount_amount() self.apply_discount_amount()
self.calculate_shipping_charges()
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
self.calculate_total_advance() self.calculate_total_advance()
@@ -50,7 +52,6 @@ class calculate_taxes_and_totals(object):
self.initialize_taxes() self.initialize_taxes()
self.determine_exclusive_rate() self.determine_exclusive_rate()
self.calculate_net_total() self.calculate_net_total()
self.calculate_shipping_charges()
self.calculate_taxes() self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax() self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals() self.calculate_totals()
@@ -113,17 +114,24 @@ class calculate_taxes_and_totals(object):
for item in self.doc.get("items"): for item in self.doc.get("items"):
self.doc.round_floats_in(item) self.doc.round_floats_in(item)
if not item.rate:
item.rate = item.price_list_rate
if item.discount_percentage == 100: if item.discount_percentage == 100:
item.rate = 0.0 item.rate = 0.0
elif item.price_list_rate: elif item.price_list_rate:
if not item.rate or (item.pricing_rules and item.discount_percentage > 0): if item.pricing_rules or abs(item.discount_percentage) > 0:
item.rate = flt(item.price_list_rate * item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
elif item.discount_amount and item.pricing_rules: if abs(item.discount_percentage) > 0:
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
elif item.discount_amount or item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount item.rate = item.price_list_rate - item.discount_amount
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']: if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0: if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
@@ -269,6 +277,8 @@ class calculate_taxes_and_totals(object):
shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule)
shipping_rule.apply(self.doc) shipping_rule.apply(self.doc)
self._calculate()
def calculate_taxes(self): def calculate_taxes(self):
rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment') rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
if not rounding_adjustment_computed: if not rounding_adjustment_computed:
@@ -626,8 +636,14 @@ class calculate_taxes_and_totals(object):
self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount),
self.doc.precision("outstanding_amount")) self.doc.precision("outstanding_amount"))
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): if (
self.update_paid_amount_for_return(total_amount_to_pay) 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): def calculate_paid_amount(self):
@@ -707,7 +723,7 @@ class calculate_taxes_and_totals(object):
def set_item_wise_tax_breakup(self): def set_item_wise_tax_breakup(self):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) 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', default_mode_of_payment = frappe.db.get_value('POS Payment Method',
{'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1) {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1)
@@ -719,8 +735,6 @@ class calculate_taxes_and_totals(object):
'default': 1 'default': 1
}) })
self.calculate_paid_amount()
def get_itemised_tax_breakup_html(doc): def get_itemised_tax_breakup_html(doc):
if not doc.taxes: if not doc.taxes:
return return

View File

@@ -47,7 +47,6 @@ def get_product_filter_data(query_args=None):
sub_categories = [] sub_categories = []
if item_group: if item_group:
field_filters['item_group'] = item_group
sub_categories = get_child_groups_for_website(item_group, immediate=True) sub_categories = get_child_groups_for_website(item_group, immediate=True)
engine = ProductQuery() engine = ProductQuery()

View File

@@ -14,6 +14,8 @@ class ProductFiltersBuilder:
self.item_group = item_group self.item_group = item_group
def get_field_filters(self): def get_field_filters(self):
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
if not self.item_group and not self.doc.enable_field_filters: if not self.item_group and not self.doc.enable_field_filters:
return return
@@ -25,18 +27,26 @@ class ProductFiltersBuilder:
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
for df in fields: for df in fields:
item_filters, item_or_filters = {}, [] item_filters, item_or_filters = {"published_in_website": 1}, []
link_doctype_values = self.get_filtered_link_doctype_records(df) link_doctype_values = self.get_filtered_link_doctype_records(df)
if df.fieldtype == "Link": if df.fieldtype == "Link":
if self.item_group: if self.item_group:
item_or_filters.extend([ include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants")
["item_group", "=", self.item_group], if include_child:
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups include_groups = get_child_groups_for_website(self.item_group, include_self=True)
]) include_groups = [x.name for x in include_groups]
item_or_filters.extend([
["item_group", "in", include_groups],
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
])
else:
item_or_filters.extend([
["item_group", "=", self.item_group],
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
])
# Get link field values attached to published items # Get link field values attached to published items
item_filters['published_in_website'] = 1
item_values = frappe.get_all( item_values = frappe.get_all(
"Item", "Item",
fields=[df.fieldname], fields=[df.fieldname],

View File

@@ -46,10 +46,10 @@ class ProductQuery:
self.filter_with_discount = bool(fields.get("discount")) self.filter_with_discount = bool(fields.get("discount"))
result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
website_item_groups = self.get_website_item_group_results(item_group, website_item_groups)
if fields: if fields:
self.build_fields_filters(fields) self.build_fields_filters(fields)
if item_group:
self.build_item_group_filters(item_group)
if search_term: if search_term:
self.build_search_filters(search_term) self.build_search_filters(search_term)
if self.settings.hide_variants: if self.settings.hide_variants:
@@ -61,8 +61,6 @@ class ProductQuery:
else: else:
result, count = self.query_items(start=start) result, count = self.query_items(start=start)
result = self.combine_web_item_group_results(item_group, result, website_item_groups)
# sort combined results by ranking # sort combined results by ranking
result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
@@ -168,6 +166,25 @@ class ProductQuery:
# `=` will be faster than `IN` for most cases # `=` will be faster than `IN` for most cases
self.filters.append([field, "=", values]) self.filters.append([field, "=", values])
def build_item_group_filters(self, item_group):
"Add filters for Item group page and include Website Item Groups."
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
item_group_filters = []
item_group_filters.append(["Website Item", "item_group", "=", item_group])
# Consider Website Item Groups
item_group_filters.append(["Website Item Group", "item_group", "=", item_group])
if frappe.db.get_value("Item Group", item_group, "include_descendants"):
# include child item group's items as well
# eg. Group Node A, will show items of child 1 and child 2 as well
# on it's web page
include_groups = get_child_groups_for_website(item_group, include_self=True)
include_groups = [x.name for x in include_groups]
item_group_filters.append(["Website Item", "item_group", "in", include_groups])
self.or_filters.extend(item_group_filters)
def build_search_filters(self, search_term): def build_search_filters(self, search_term):
"""Query search term in specified fields """Query search term in specified fields
@@ -191,19 +208,6 @@ class ProductQuery:
for field in search_fields: for field in search_fields:
self.or_filters.append([field, "like", search]) self.or_filters.append([field, "like", search])
def get_website_item_group_results(self, item_group, website_item_groups):
"""Get Web Items for Item Group Page via Website Item Groups."""
if item_group:
website_item_groups = frappe.db.get_all(
"Website Item",
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
filters=[
["Website Item Group", "item_group", "=", item_group],
["published", "=", 1]
]
)
return website_item_groups
def add_display_details(self, result, discount_list, cart_items): def add_display_details(self, result, discount_list, cart_items):
"""Add price and availability details in result.""" """Add price and availability details in result."""
for item in result: for item in result:
@@ -279,16 +283,6 @@ class ProductQuery:
return [] return []
def combine_web_item_group_results(self, item_group, result, website_item_groups):
"""Combine results with context of website item groups into item results."""
if item_group and website_item_groups:
items_list = {row.name for row in result}
for row in website_item_groups:
if row.wig_parent not in items_list:
result.append(row)
return result
def filter_results_by_discount(self, fields, result): def filter_results_by_discount(self, fields, result):
if fields and fields.get("discount"): if fields and fields.get("discount"):
discount_percent = frappe.utils.flt(fields["discount"][0]) discount_percent = frappe.utils.flt(fields["discount"][0])

View File

@@ -13,8 +13,7 @@ test_dependencies = ["Item", "Item Group"]
class TestItemGroupProductDataEngine(unittest.TestCase): class TestItemGroupProductDataEngine(unittest.TestCase):
"Test Products & Sub-Category Querying for Product Listing on Item Group Page." "Test Products & Sub-Category Querying for Product Listing on Item Group Page."
@classmethod def setUp(self):
def setUpClass(cls):
item_codes = [ item_codes = [
("Test Mobile A", "_Test Item Group B"), ("Test Mobile A", "_Test Item Group B"),
("Test Mobile B", "_Test Item Group B"), ("Test Mobile B", "_Test Item Group B"),
@@ -28,8 +27,10 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
if not frappe.db.exists("Website Item", {"item_code": item_code}): if not frappe.db.exists("Website Item", {"item_code": item_code}):
create_regular_web_item(item_code, item_args=item_args) create_regular_web_item(item_code, item_args=item_args)
@classmethod frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
def tearDownClass(cls): frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def test_product_listing_in_item_group(self): def test_product_listing_in_item_group(self):
@@ -87,7 +88,6 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
def test_item_group_with_sub_groups(self): def test_item_group_with_sub_groups(self):
"Test Valid Sub Item Groups in Item Group Page." "Test Valid Sub Item Groups in Item Group Page."
frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0) frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
result = get_product_filter_data(query_args={ result = get_product_filter_data(query_args={
@@ -114,4 +114,45 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
# check if child group is fetched if shown in website # check if child group is fetched if shown in website
self.assertIn("_Test Item Group B - 1", child_groups) self.assertIn("_Test Item Group B - 1", child_groups)
self.assertIn("_Test Item Group B - 2", child_groups) self.assertIn("_Test Item Group B - 2", child_groups)
def test_item_group_page_with_descendants_included(self):
"""
Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3).
> _Test Item Group B [Level 1]
> _Test Item Group B - 1 [Level 2]
> _Test Item Group B - 1 - 1 [Level 3]
"""
frappe.get_doc({ # create Level 3 nested child group
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B - 1 - 1",
"parent_item_group": "_Test Item Group B - 1"
}).insert()
create_regular_web_item( # create an item belonging to level 3 item group
"Test Mobile F",
item_args={"item_group": "_Test Item Group B - 1 - 1"}
)
frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1)
# enable 'include descendants' in Level 1
frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1)
result = get_product_filter_data(query_args={
"field_filters": {},
"attribute_filters": {},
"start": 0,
"item_group": "_Test Item Group B"
})
items = result.get("items")
item_codes = [item.get("item_code") for item in items]
# check if all sub groups' items are pulled
self.assertEqual(len(items), 6)
self.assertIn("Test Mobile A", item_codes)
self.assertIn("Test Mobile C", item_codes)
self.assertIn("Test Mobile E", item_codes)
self.assertIn("Test Mobile F", item_codes)

View File

@@ -501,7 +501,7 @@ erpnext.ProductView = class {
categories.forEach(category => { categories.forEach(category => {
sub_group_html += ` sub_group_html += `
<a href="${ category.route || '#' }" style="text-decoration: none;"> <a href="/${ category.route || '#' }" style="text-decoration: none;">
<div class="category-pill"> <div class="category-pill">
${ category.name } ${ category.name }
</div> </div>

View File

@@ -121,7 +121,11 @@ def place_order():
def request_for_quotation(): def request_for_quotation():
quotation = _get_cart_quotation() quotation = _get_cart_quotation()
quotation.flags.ignore_permissions = True quotation.flags.ignore_permissions = True
quotation.submit()
if get_shopping_cart_settings().save_quotations_as_draft:
quotation.save()
else:
quotation.submit()
return quotation.name return quotation.name
@frappe.whitelist() @frappe.whitelist()

View File

@@ -5,7 +5,8 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import add_months, nowdate from frappe.tests.utils import change_settings
from frappe.utils import add_months, cint, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
@@ -13,9 +14,10 @@ from erpnext.e_commerce.shopping_cart.cart import (
_get_cart_quotation, _get_cart_quotation,
get_cart_quotation, get_cart_quotation,
get_party, get_party,
request_for_quotation,
update_cart, 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): class TestShoppingCart(unittest.TestCase):
@@ -23,11 +25,6 @@ class TestShoppingCart(unittest.TestCase):
Note: Note:
Shopping Cart == Quotation Shopping Cart == Quotation
""" """
@classmethod
def tearDownClass(cls):
frappe.db.sql("delete from `tabTax Rule`")
def setUp(self): def setUp(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
create_test_contact_and_address() create_test_contact_and_address()
@@ -43,6 +40,10 @@ class TestShoppingCart(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
self.disable_shopping_cart() self.disable_shopping_cart()
@classmethod
def tearDownClass(cls):
frappe.db.sql("delete from `tabTax Rule`")
def test_get_cart_new_user(self): def test_get_cart_new_user(self):
self.login_as_new_user() self.login_as_new_user()
@@ -177,6 +178,28 @@ class TestShoppingCart(unittest.TestCase):
# test if items are rendered without error # test if items are rendered without error
frappe.render_template("templates/includes/cart/cart_items.html", cart) frappe.render_template("templates/includes/cart/cart_items.html", cart)
@change_settings("E Commerce Settings",{
"save_quotations_as_draft": 1
})
def test_cart_without_checkout_and_draft_quotation(self):
"Test impact of 'save_quotations_as_draft' checkbox."
frappe.local.shopping_cart_settings = None
# add item to cart
update_cart("_Test Item", 1)
quote_name = request_for_quotation() # Request for Quote
quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
self.assertEqual(quote_doctstatus, 0)
frappe.db.set_value("E Commerce Settings", None, "save_quotations_as_draft", 0)
frappe.local.shopping_cart_settings = None
update_cart("_Test Item", 1)
quote_name = request_for_quotation() # Request for Quote
quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
self.assertEqual(quote_doctstatus, 1)
def create_tax_rule(self): def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0] tax_rule = frappe.get_test_records("Tax Rule")[0]
try: try:

View File

@@ -1,6 +1,7 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.item_variant import create_variant from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( 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.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values 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.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Item"] test_dependencies = ["Item"]
class TestVariantSelector(ERPNextTestCase): class TestVariantSelector(FrappeTestCase):
@classmethod @classmethod
def setUpClass(cls): 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(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["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

@@ -174,16 +174,22 @@ def get_month_map():
def get_unmarked_days(employee, month, exclude_holidays=0): def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar import calendar
month_map = get_month_map() month_map = get_month_map()
today = get_datetime() today = get_datetime()
dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)] joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"])
start_day = 1
end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
length = len(dates_of_month) if joining_date and joining_date.month == month_map[month]:
month_start, month_end = dates_of_month[0], dates_of_month[length-1] start_day = joining_date.day
if relieving_date and relieving_date.month == month_map[month]:
end_day = relieving_date.day + 1
records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [ dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(start_day, end_day)]
month_start, month_end = dates_of_month[0], dates_of_month[-1]
records = frappe.get_all("Attendance", fields=['attendance_date', 'employee'], filters=[
["attendance_date", ">=", month_start], ["attendance_date", ">=", month_start],
["attendance_date", "<=", month_end], ["attendance_date", "<=", month_end],
["employee", "=", employee], ["employee", "=", employee],
@@ -200,7 +206,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
for date in dates_of_month: for date in dates_of_month:
date_time = get_datetime(date) date_time = get_datetime(date)
if today.day == date_time.day and today.month == date_time.month: if today.day <= date_time.day and today.month <= date_time.month:
break break
if date_time not in marked_days: if date_time not in marked_days:
unmarked_days.append(date) unmarked_days.append(date)

View File

@@ -1,20 +1,116 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.utils import nowdate from frappe.tests.utils import FrappeTestCase
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,
get_unmarked_days,
mark_attendance,
)
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
test_records = frappe.get_test_records('Attendance') test_records = frappe.get_test_records('Attendance')
class TestAttendance(unittest.TestCase): 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): def test_mark_absent(self):
from erpnext.hr.doctype.employee.test_employee import make_employee
employee = make_employee("test_mark_absent@example.com") employee = make_employee("test_mark_absent@example.com")
date = nowdate() date = nowdate()
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date}) frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
from erpnext.hr.doctype.attendance.attendance import mark_attendance
attendance = mark_attendance(employee, date, 'Absent') attendance = mark_attendance(employee, date, 'Absent')
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'}) fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
self.assertEqual(attendance, fetch_attendance) self.assertEqual(attendance, fetch_attendance)
def test_unmarked_days(self):
now = now_datetime()
previous_month = now.month - 1
first_day = now.replace(day=1).replace(month=previous_month).date()
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)
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)
unmarked_days = get_unmarked_days(employee, month_name)
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
self.assertNotIn(first_day, unmarked_days)
# attendance unmarked
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
# holiday considered in unmarked days
self.assertIn(first_sunday, unmarked_days)
def test_unmarked_days_excluding_holidays(self):
now = now_datetime()
previous_month = now.month - 1
first_day = now.replace(day=1).replace(month=previous_month).date()
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)
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)
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
self.assertNotIn(first_day, unmarked_days)
# attendance unmarked
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
# holidays not considered in unmarked days
self.assertNotIn(first_sunday, unmarked_days)
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
now = now_datetime()
previous_month = now.month - 1
first_day = now.replace(day=1).replace(month=previous_month).date()
doj = add_days(first_day, 1)
relieving_date = add_days(first_day, 5)
employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj,
relieving_date=relieving_date)
frappe.db.delete('Attendance', {'employee': employee})
frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list)
attendance_date = add_days(first_day, 2)
mark_attendance(employee, attendance_date, 'Present')
month_name = get_month_name(first_day)
unmarked_days = get_unmarked_days(employee, month_name)
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
self.assertNotIn(attendance_date, unmarked_days)
# date before doj not in unmarked days
self.assertNotIn(add_days(doj, -1), unmarked_days)
# date after relieving not in unmarked days
self.assertNotIn(add_days(relieving_date, 1), unmarked_days)
def tearDown(self):
frappe.db.rollback()
def get_month_name(date):
month_number = date.month
for month, number in get_month_map().items():
if number == month_number:
return month

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest import unittest
from contextlib import contextmanager
from datetime import timedelta from datetime import timedelta
import frappe import frappe
@@ -30,3 +31,24 @@ def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getd
"holidays" : holiday_dates "holidays" : holiday_dates
}).insert() }).insert()
return doc 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) { make_dashboard: function(frm) {
var leave_details; var leave_details;
let lwps; let lwps;
if (frm.doc.employee) { if (frm.doc.employee && frm.doc.from_date) {
frappe.call({ frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details", method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
async: false, async: false,
@@ -146,6 +146,7 @@ frappe.ui.form.on("Leave Application", {
}, },
to_date: function(frm) { to_date: function(frm) {
frm.trigger("make_dashboard");
frm.trigger("half_day_datepicker"); frm.trigger("half_day_datepicker");
frm.trigger("calculate_total_days"); frm.trigger("calculate_total_days");
}, },

View File

@@ -1,9 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from typing import Dict, Optional, Tuple
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
cint, cint,
@@ -34,6 +36,10 @@ class LeaveDayBlockedError(frappe.ValidationError): pass
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class AttendanceAlreadyMarkedError(frappe.ValidationError): pass class AttendanceAlreadyMarkedError(frappe.ValidationError): pass
class NotAnOptionalHoliday(frappe.ValidationError): pass class NotAnOptionalHoliday(frappe.ValidationError): pass
class InsufficientLeaveBalanceError(frappe.ValidationError):
pass
class LeaveAcrossAllocationsError(frappe.ValidationError):
pass
from frappe.model.document import Document from frappe.model.document import Document
@@ -134,21 +140,35 @@ class LeaveApplication(Document):
def validate_dates_across_allocation(self): def validate_dates_across_allocation(self):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
return 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_from_date = _get_leave_allocation_record(self.from_date)
allocation_based_on_to_date = _get_leave_allocation_record(self.to_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): return allocation_based_on_from_date, 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"))
def validate_back_dated_application(self): def validate_back_dated_application(self):
future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation`
@@ -260,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.")) 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): 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, 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) consider_all_leaves_in_the_allocation_period=True, for_consumption=True)
if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance): self.leave_balance = leave_balance.get("leave_balance")
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption")
frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
.format(self.leave_type)) if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption):
else: self.show_insufficient_balance_message(leave_balance_for_consumption)
frappe.throw(_("There is not enough leave balance for Leave Type {0}")
.format(self.leave_type)) 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): def validate_leave_overlap(self):
if not self.name: if not self.name:
@@ -425,54 +459,111 @@ class LeaveApplication(Document):
if self.status != 'Approved' and submit: if self.status != 'Approved' and submit:
return 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) self.to_date, self.from_date)
lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp") lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
if expiry_date: if expiry_date:
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp) self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
else: else:
raise_exception = True alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
if frappe.flags.in_patch: if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
raise_exception=False # 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( def is_separate_ledger_entry_required(self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None) -> bool:
leaves=self.total_leave_days * -1, """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, 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, 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, is_lwp=lwp,
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
) )
create_leave_ledger_entry(self, args, submit) 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): if getdate(expiry_date) != getdate(self.to_date):
start_date = add_days(expiry_date, 1) start_date = add_days(expiry_date, 1)
args.update(dict( leaves = get_number_of_leave_days(self.employee, self.leave_type,
from_date=start_date, start_date, self.to_date, self.half_day, self.half_day_date)
to_date=self.to_date,
leaves=date_diff(self.to_date, expiry_date) * -1 if leaves:
)) args.update(dict(
create_leave_ledger_entry(self, args, submit) 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 ''' ''' Returns expiry of carry forward allocation in leave ledger entry '''
expiry = frappe.get_all("Leave Ledger Entry", expiry = frappe.get_all("Leave Ledger Entry",
filters={ filters={
@@ -480,12 +571,17 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date):
'leave_type': leave_type, 'leave_type': leave_type,
'is_carry_forward': 1, 'is_carry_forward': 1,
'transaction_type': 'Leave Allocation', 'transaction_type': 'Leave Allocation',
'to_date': ['between', (from_date, to_date)] 'to_date': ['between', (from_date, to_date)],
'docstatus': 1
},fields=['to_date']) },fields=['to_date'])
return expiry[0]['to_date'] if expiry else None return expiry[0]['to_date'] if expiry else ''
@frappe.whitelist() @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 number_of_days = 0
if cint(half_day) == 1: if cint(half_day) == 1:
if from_date == to_date: if from_date == to_date:
@@ -502,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)) number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list))
return number_of_days return number_of_days
@frappe.whitelist() @frappe.whitelist()
def get_leave_details(employee, date): def get_leave_details(employee, date):
allocation_records = get_leave_allocation_records(employee, date) allocation_records = get_leave_allocation_records(employee, date)
@@ -514,6 +611,7 @@ def get_leave_details(employee, date):
'to_date': ('>=', date), 'to_date': ('>=', date),
'employee': employee, 'employee': employee,
'leave_type': allocation.leave_type, 'leave_type': allocation.leave_type,
'docstatus': 1
}, 'SUM(total_leaves_allocated)') or 0 }, 'SUM(total_leaves_allocated)') or 0
remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
@@ -521,29 +619,28 @@ def get_leave_details(employee, date):
end_date = allocation.to_date end_date = allocation.to_date
leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1 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] = { leave_allocation[d] = {
"total_leaves": total_allocated_leaves, "total_leaves": total_allocated_leaves,
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken), "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
"leaves_taken": leaves_taken, "leaves_taken": leaves_taken,
"pending_leaves": leaves_pending, "leaves_pending_approval": leaves_pending,
"remaining_leaves": remaining_leaves} "remaining_leaves": remaining_leaves}
#is used in set query #is used in set query
lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1}) lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name")
lwps = [lwp.name for lwp in lwps]
ret = { return {
'leave_allocation': leave_allocation, "leave_allocation": leave_allocation,
'leave_approver': get_leave_approver(employee), "leave_approver": get_leave_approver(employee),
'lwps': lwps "lwps": lwp
} }
return ret
@frappe.whitelist() @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 Returns leave balance till date
:param employee: employee name :param employee: employee name
@@ -551,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 date: date to check balance on
:param to_date: future date to check for allocation expiry :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 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: if not to_date:
@@ -560,35 +662,52 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_
allocation = allocation_records.get(leave_type, frappe._dict()) allocation = allocation_records.get(leave_type, frappe._dict())
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date 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) 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): def get_leave_allocation_records(employee, date, leave_type=None):
''' returns the total allocated leaves and carry forwarded leaves based on ledger entries ''' """Returns the total allocated leaves and carry forwarded leaves based on ledger entries"""
Ledger = frappe.qb.DocType("Leave Ledger Entry")
conditions = ("and leave_type='%s'" % leave_type) if leave_type else "" cf_leave_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0)
allocation_details = frappe.db.sql(""" sum_cf_leaves = Sum(cf_leave_case).as_("cf_leaves")
SELECT
SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves, new_leaves_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0)
SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves, sum_new_leaves = Sum(new_leaves_case).as_("new_leaves")
MIN(from_date) as from_date,
MAX(to_date) as to_date, query = (
leave_type frappe.qb.from_(Ledger)
FROM `tabLeave Ledger Entry` .select(
WHERE sum_cf_leaves,
from_date <= %(date)s sum_new_leaves,
AND to_date >= %(date)s Min(Ledger.from_date).as_("from_date"),
AND docstatus=1 Max(Ledger.to_date).as_("to_date"),
AND transaction_type="Leave Allocation" Ledger.leave_type
AND employee=%(employee)s ).where(
AND is_expired=0 (Ledger.from_date <= date)
AND is_lwp=0 & (Ledger.to_date >= date)
{0} & (Ledger.docstatus == 1)
GROUP BY employee, leave_type & (Ledger.transaction_type == "Leave Allocation")
""".format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec & (Ledger.employee == employee)
& (Ledger.is_expired == 0)
& (Ledger.is_lwp == 0)
)
)
if leave_type:
query = query.where((Ledger.leave_type == leave_type))
query = query.groupby(Ledger.employee, Ledger.leave_type)
allocation_details = query.run(as_dict=True)
allocated_leaves = frappe._dict() allocated_leaves = frappe._dict()
for d in allocation_details: for d in allocation_details:
@@ -602,8 +721,9 @@ def get_leave_allocation_records(employee, date, leave_type=None):
})) }))
return allocated_leaves 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", leaves = frappe.get_all("Leave Application",
filters={ filters={
"employee": employee, "employee": employee,
@@ -616,38 +736,46 @@ def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
}, fields=['SUM(total_leave_days) as leaves'])[0] }, fields=['SUM(total_leave_days) as leaves'])[0]
return leaves['leaves'] if leaves['leaves'] else 0.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: if remaining_leaves > 0:
remaining_days = date_diff(end_date, date) + 1 remaining_days = date_diff(end_date, date) + 1
remaining_leaves = min(remaining_days, remaining_leaves) remaining_leaves = min(remaining_days, remaining_leaves)
return 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: # balance for carry forwarded leaves
remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) if cf_expiry and allocation.unused_leaves:
remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry) 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_entries = get_leave_entries(employee, leave_type, from_date, to_date)
leave_days = 0 leave_days = 0
for leave_entry in leave_entries: for leave_entry in leave_entries:
inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date) 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 leave_days += leave_entry.leaves
elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \ 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 leave_days += leave_entry.leaves
elif leave_entry.transaction_type == 'Leave Application': elif leave_entry.transaction_type == 'Leave Application':
@@ -669,11 +797,6 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_
return leave_days 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): def get_leave_entries(employee, leave_type, from_date, to_date):
''' Returns leave entries between from_date and to_date. ''' ''' Returns leave entries between from_date and to_date. '''
@@ -696,6 +819,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date):
"leave_type": leave_type "leave_type": leave_type
}, as_dict=1) }, as_dict=1)
@frappe.whitelist() @frappe.whitelist()
def get_holidays(employee, from_date, to_date, holiday_list = None): def get_holidays(employee, from_date, to_date, holiday_list = None):
'''get holidays between two dates for the given employee''' '''get holidays between two dates for the given employee'''
@@ -712,6 +836,7 @@ def is_lwp(leave_type):
lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", 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 return lwp and cint(lwp[0][0]) or 0
@frappe.whitelist() @frappe.whitelist()
def get_events(start, end, filters=None): def get_events(start, end, filters=None):
from frappe.desk.reportview import get_filters_cond from frappe.desk.reportview import get_filters_cond
@@ -740,6 +865,7 @@ def get_events(start, end, filters=None):
return events return events
def add_department_leaves(events, start, end, employee, company): def add_department_leaves(events, start, end, employee, company):
department = frappe.db.get_value("Employee", employee, "department") department = frappe.db.get_value("Employee", employee, "department")
@@ -820,6 +946,7 @@ def add_block_dates(events, start, end, employee, company):
}) })
cnt+=1 cnt+=1
def add_holidays(events, start, end, employee, company): def add_holidays(events, start, end, employee, company):
applicable_holiday_list = get_holiday_list_for_employee(employee, company) applicable_holiday_list = get_holiday_list_for_employee(employee, company)
if not applicable_holiday_list: if not applicable_holiday_list:
@@ -836,6 +963,7 @@ def add_holidays(events, start, end, employee, company):
"name": holiday.name "name": holiday.name
}) })
@frappe.whitelist() @frappe.whitelist()
def get_mandatory_approval(doctype): def get_mandatory_approval(doctype):
mandatory = "" mandatory = ""
@@ -848,6 +976,7 @@ def get_mandatory_approval(doctype):
return mandatory return mandatory
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
query = """ query = """
select employee, leave_type, from_date, to_date, total_leave_days select employee, leave_type, from_date, to_date, total_leave_days
@@ -883,6 +1012,7 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
return leave_days return leave_days
@frappe.whitelist() @frappe.whitelist()
def get_leave_approver(employee): def get_leave_approver(employee):
leave_approver, department = frappe.db.get_value("Employee", leave_approver, department = frappe.db.get_value("Employee",

View File

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

View File

@@ -17,12 +17,16 @@ from frappe.utils import (
) )
from erpnext.hr.doctype.employee.test_employee import make_employee 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_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_application.leave_application import ( from erpnext.hr.doctype.leave_application.leave_application import (
InsufficientLeaveBalanceError,
LeaveAcrossAllocationsError,
LeaveDayBlockedError, LeaveDayBlockedError,
NotAnOptionalHoliday, NotAnOptionalHoliday,
OverlapError, OverlapError,
get_leave_balance_on, get_leave_balance_on,
get_leave_details,
) )
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees, create_assignment_for_multiple_employees,
@@ -33,7 +37,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_leave_application, make_leave_application,
) )
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"] test_dependencies = ["Leave Type", "Leave Allocation", "Leave Block List", "Employee"]
_test_records = [ _test_records = [
{ {
@@ -72,15 +76,28 @@ _test_records = [
class TestLeaveApplication(unittest.TestCase): class TestLeaveApplication(unittest.TestCase):
def setUp(self): def setUp(self):
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]: 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") frappe.set_user("Administrator")
set_leave_approver() 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): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
frappe.set_user("Administrator")
def _clear_roles(self): def _clear_roles(self):
frappe.db.sql("""delete from `tabHas Role` where parent in frappe.db.sql("""delete from `tabHas Role` where parent in
@@ -95,6 +112,132 @@ class TestLeaveApplication(unittest.TestCase):
application.to_date = "2013-01-05" application.to_date = "2013-01-05"
return application 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): def test_overwrite_attendance(self):
'''check attendance is automatically created on leave approval''' '''check attendance is automatically created on leave approval'''
make_allocation_record() make_allocation_record()
@@ -119,6 +262,7 @@ class TestLeaveApplication(unittest.TestCase):
for d in ('2018-01-01', '2018-01-02', '2018-01-03'): for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
self.assertTrue(getdate(d) in dates) self.assertTrue(getdate(d) in dates)
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_attendance_for_include_holidays(self): def test_attendance_for_include_holidays(self):
# Case 1: leave type with 'Include holidays within leaves as leaves' enabled # Case 1: leave type with 'Include holidays within leaves as leaves' enabled
frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1) frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
@@ -131,10 +275,8 @@ class TestLeaveApplication(unittest.TestCase):
date = getdate() date = getdate()
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
holiday_list = make_holiday_list()
employee = get_employee() employee = get_employee()
frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) first_sunday = get_first_sunday(self.holiday_list)
first_sunday = get_first_sunday(holiday_list)
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
leave_application.reload() leave_application.reload()
@@ -143,6 +285,7 @@ class TestLeaveApplication(unittest.TestCase):
leave_application.cancel() leave_application.cancel()
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_attendance_update_for_exclude_holidays(self): def test_attendance_update_for_exclude_holidays(self):
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled # 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) frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
@@ -155,10 +298,8 @@ class TestLeaveApplication(unittest.TestCase):
date = getdate() date = getdate()
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
holiday_list = make_holiday_list()
employee = get_employee() employee = get_employee()
frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) first_sunday = get_first_sunday(self.holiday_list)
first_sunday = get_first_sunday(holiday_list)
# already marked attendance on a holiday should be deleted in this case # already marked attendance on a holiday should be deleted in this case
config = { config = {
@@ -177,8 +318,9 @@ class TestLeaveApplication(unittest.TestCase):
attendance.flags.ignore_validate = True attendance.flags.ignore_validate = True
attendance.save() attendance.save()
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company)
leave_application.reload() leave_application.reload()
# holiday should be excluded while marking attendance # holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3) self.assertEqual(leave_application.total_leave_days, 3)
self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3) self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
@@ -320,16 +462,14 @@ class TestLeaveApplication(unittest.TestCase):
application.half_day_date = "2013-01-05" application.half_day_date = "2013-01-05"
application.insert() application.insert()
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
def test_optional_leave(self): def test_optional_leave(self):
leave_period = get_leave_period() leave_period = get_leave_period()
today = nowdate() today = nowdate()
holiday_list = 'Test Holiday List for Optional Holiday' holiday_list = 'Test Holiday List for Optional Holiday'
employee = get_employee() employee = get_employee()
default_holiday_list = make_holiday_list() first_sunday = get_first_sunday(self.holiday_list)
frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
first_sunday = get_first_sunday(default_holiday_list)
optional_leave_date = add_days(first_sunday, 1) optional_leave_date = add_days(first_sunday, 1)
if not frappe.db.exists('Holiday List', holiday_list): if not frappe.db.exists('Holiday List', holiday_list):
@@ -503,11 +643,13 @@ class TestLeaveApplication(unittest.TestCase):
leave_type_name="_Test_CF_leave_expiry", leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1, is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90) expire_carry_forwarded_leaves_after_days=90)
leave_type.submit() leave_type.insert()
create_carry_forwarded_allocation(employee, leave_type) 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): def test_earned_leaves_creation(self):
@@ -560,7 +702,14 @@ class TestLeaveApplication(unittest.TestCase):
# test to not consider current leave in leave balance while submitting # test to not consider current leave in leave balance while submitting
def test_current_leave_on_submit(self): def test_current_leave_on_submit(self):
employee = get_employee() 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( allocation = frappe.get_doc(dict(
doctype = 'Leave Allocation', doctype = 'Leave Allocation',
employee = employee.name, employee = employee.name,
@@ -703,6 +852,35 @@ class TestLeaveApplication(unittest.TestCase):
employee.leave_approver = "" employee.leave_approver = ""
employee.save() 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)
def create_carry_forwarded_allocation(employee, leave_type): def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation # initial leave allocation
@@ -724,19 +902,22 @@ def create_carry_forwarded_allocation(employee, leave_type):
carry_forward=1) carry_forward=1)
leave_allocation.submit() leave_allocation.submit()
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None): def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
allocation = frappe.get_doc({ allocation = frappe.get_doc({
"doctype": "Leave Allocation", "doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001", "employee": employee or "_T-Employee-00001",
"leave_type": leave_type or "_Test Leave Type", "leave_type": leave_type or "_Test Leave Type",
"from_date": from_date or "2013-01-01", "from_date": from_date or "2013-01-01",
"to_date": to_date or "2019-12-31", "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.insert(ignore_permissions=True)
allocation.submit() allocation.submit()
return allocation
def get_employee(): def get_employee():
return frappe.get_doc("Employee", "_T-Employee-00001") return frappe.get_doc("Employee", "_T-Employee-00001")
@@ -781,9 +962,10 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
allocate_leave.submit() allocate_leave.submit()
def get_first_sunday(holiday_list): def get_first_sunday(holiday_list, for_date=None):
month_start_date = get_first_day(nowdate()) date = for_date or getdate()
month_end_date = get_last_day(nowdate()) month_start_date = get_first_day(date)
month_end_date = get_last_day(date)
first_sunday = frappe.db.sql(""" first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday` select holiday_date from `tabHoliday`
where parent = %s where parent = %s
@@ -791,4 +973,4 @@ def get_first_sunday(holiday_list):
order by holiday_date order by holiday_date
""", (holiday_list, month_start_date, month_end_date))[0][0] """, (holiday_list, month_start_date, month_end_date))[0][0]
return first_sunday return first_sunday

View File

@@ -171,7 +171,7 @@ def expire_carried_forward_allocation(allocation):
''' Expires remaining leaves in the on carried forward allocation ''' ''' Expires remaining leaves in the on carried forward allocation '''
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, 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) leaves = flt(allocation.leaves) + flt(leaves_taken)
# allow expired leaves entry to be created # allow expired leaves entry to be created

View File

@@ -4,7 +4,7 @@
import unittest import unittest
import frappe 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 ( from erpnext.hr.doctype.leave_application.test_leave_application import (
get_employee, get_employee,
@@ -94,9 +94,12 @@ class TestLeavePolicyAssignment(unittest.TestCase):
"leave_policy": leave_policy.name, "leave_policy": leave_policy.name,
"leave_period": leave_period.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)) 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", { leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0] "leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated") }, "total_leaves_allocated")

View File

@@ -3,18 +3,21 @@
from itertools import groupby from itertools import groupby
from typing import Dict, List, Optional, Tuple
import frappe import frappe
from frappe import _ 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 ( from erpnext.hr.doctype.leave_application.leave_application import (
get_leave_balance_on, get_leave_balance_on,
get_leaves_for_period, 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: if filters.to_date <= filters.from_date:
frappe.throw(_('"From Date" can not be greater than or equal to "To 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) charts = get_chart_data(data)
return columns, data, None, charts return columns, data, None, charts
def get_columns():
columns = [{ def get_columns() -> List[Dict]:
return [{
'label': _('Leave Type'), 'label': _('Leave Type'),
'fieldtype': 'Link', 'fieldtype': 'Link',
'fieldname': 'leave_type', 'fieldname': 'leave_type',
@@ -46,32 +50,31 @@ def get_columns():
'label': _('Opening Balance'), 'label': _('Opening Balance'),
'fieldtype': 'float', 'fieldtype': 'float',
'fieldname': 'opening_balance', 'fieldname': 'opening_balance',
'width': 130, 'width': 150,
}, { }, {
'label': _('Leave Allocated'), 'label': _('New Leave(s) Allocated'),
'fieldtype': 'float', 'fieldtype': 'float',
'fieldname': 'leaves_allocated', 'fieldname': 'leaves_allocated',
'width': 130, 'width': 200,
}, { }, {
'label': _('Leave Taken'), 'label': _('Leave(s) Taken'),
'fieldtype': 'float', 'fieldtype': 'float',
'fieldname': 'leaves_taken', 'fieldname': 'leaves_taken',
'width': 130, 'width': 150,
}, { }, {
'label': _('Leave Expired'), 'label': _('Leave(s) Expired'),
'fieldtype': 'float', 'fieldtype': 'float',
'fieldname': 'leaves_expired', 'fieldname': 'leaves_expired',
'width': 130, 'width': 150,
}, { }, {
'label': _('Closing Balance'), 'label': _('Closing Balance'),
'fieldtype': 'float', 'fieldtype': 'float',
'fieldname': 'closing_balance', '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') leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name')
conditions = get_conditions(filters) conditions = get_conditions(filters)
@@ -102,19 +105,18 @@ def get_data(filters):
or ("HR Manager" in frappe.get_roles(user)): or ("HR Manager" in frappe.get_roles(user)):
if len(active_employees) > 1: if len(active_employees) > 1:
row = frappe._dict() row = frappe._dict()
row.employee = employee.name, row.employee = employee.name
row.employee_name = employee.employee_name row.employee_name = employee.employee_name
leaves_taken = get_leaves_for_period(employee.name, leave_type, leaves_taken = get_leaves_for_period(employee.name, leave_type,
filters.from_date, filters.to_date) * -1 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) 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)
opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition
row.leaves_allocated = new_allocation 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.opening_balance = opening
row.leaves_taken = leaves_taken row.leaves_taken = leaves_taken
@@ -125,7 +127,26 @@ def get_data(filters):
return data 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={ conditions={
'status': 'Active', 'status': 'Active',
} }
@@ -140,29 +161,26 @@ def get_conditions(filters):
return conditions 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 # get current department and all its child
department_list = frappe.get_list('Department', department_list = frappe.get_list('Department',
filters={ filters={'disabled': 0},
'disabled': 0 or_filters={
}, 'name': department,
or_filters={ 'parent_department': department
'name': department, },
'parent_department': department pluck='name'
}, )
fields=['name'],
pluck='name'
)
# retrieve approvers list from current department and from its subsequent child departments # retrieve approvers list from current department and from its subsequent child departments
approver_list = frappe.get_all('Department Approver', approver_list = frappe.get_all('Department Approver',
filters={ filters={
'parentfield': 'leave_approvers', 'parentfield': 'leave_approvers',
'parent': ('in', department_list) 'parent': ('in', department_list)
}, },
fields=['parent', 'approver'], fields=['parent', 'approver'],
as_list=1 as_list=True
) )
approvers = {} approvers = {}
@@ -171,41 +189,61 @@ def get_department_leave_approver_map(department=None):
return approvers 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 new_allocation = 0
expired_leaves = 0 expired_leaves = 0
carry_forwarded_leaves = 0
records= frappe.db.sql(""" records = get_leave_ledger_entries(from_date, to_date, employee, leave_type)
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)
for record in records: 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): 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 += record.leaves
expired_leaves += get_leaves_for_period(employee, leave_type,
record.from_date, record.to_date)
if record.from_date >= getdate(from_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 = [] labels = []
datasets = [] datasets = []
employee_data = data employee_data = data
@@ -224,7 +262,8 @@ def get_chart_data(data):
return chart 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 = [] leaves = []
employee_data = sorted(employee_data, key=lambda k: k['employee_name']) 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

@@ -0,0 +1,44 @@
import frappe
from dateutil.relativedelta import relativedelta
from frappe.tests.utils import FrappeTestCase
from frappe.utils import now_datetime
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute
class TestMonthlyAttendanceSheet(FrappeTestCase):
def setUp(self):
self.employee = make_employee("test_employee@example.com")
frappe.db.delete('Attendance', {'employee': self.employee})
def test_monthly_attendance_sheet_report(self):
now = now_datetime()
previous_month = now.month - 1
previous_month_first = now.replace(day=1).replace(month=previous_month).date()
company = frappe.db.get_value('Employee', self.employee, 'company')
# mark different attendance status on first 3 days of previous month
mark_attendance(self.employee, previous_month_first, 'Absent')
mark_attendance(self.employee, previous_month_first + relativedelta(days=1), 'Present')
mark_attendance(self.employee, previous_month_first + relativedelta(days=2), 'On Leave')
filters = frappe._dict({
'month': previous_month,
'year': now.year,
'company': company,
})
report = execute(filters=filters)
employees = report[1][0]
datasets = report[3]['data']['datasets']
absent = datasets[0]['values']
present = datasets[1]['values']
leaves = datasets[2]['values']
# ensure correct attendance is reflect on the report
self.assertIn(self.employee, employees)
self.assertEqual(absent[0], 1)
self.assertEqual(present[1], 1)
self.assertEqual(leaves[2], 1)

View File

@@ -14,11 +14,15 @@
"applicant", "applicant",
"section_break_7", "section_break_7",
"disbursement_date", "disbursement_date",
"clearance_date",
"column_break_8", "column_break_8",
"disbursed_amount", "disbursed_amount",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"customer_details_section", "accounting_details",
"disbursement_account",
"column_break_16",
"loan_account",
"bank_account", "bank_account",
"disbursement_references_section", "disbursement_references_section",
"reference_date", "reference_date",
@@ -106,11 +110,6 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Disbursement Details" "label": "Disbursement Details"
}, },
{
"fieldname": "customer_details_section",
"fieldtype": "Section Break",
"label": "Customer Details"
},
{ {
"fetch_from": "against_loan.applicant_type", "fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type", "fieldname": "applicant_type",
@@ -149,15 +148,48 @@
"fieldname": "reference_number", "fieldname": "reference_number",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Reference Number" "label": "Reference Number"
},
{
"fieldname": "clearance_date",
"fieldtype": "Date",
"label": "Clearance Date",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "accounting_details",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "against_loan.disbursement_account",
"fieldname": "disbursement_account",
"fieldtype": "Link",
"label": "Disbursement Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fetch_from": "against_loan.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 18:09:32.175355", "modified": "2022-02-17 18:23:44.157598",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Disbursement", "name": "Loan Disbursement",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -194,5 +226,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -42,9 +42,6 @@ class LoanDisbursement(AccountsController):
if not self.posting_date: if not self.posting_date:
self.posting_date = self.disbursement_date or nowdate() self.posting_date = self.disbursement_date or nowdate()
if not self.bank_account and self.applicant_type == "Customer":
self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
def validate_disbursal_amount(self): def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan) possible_disbursal_amount = get_disbursal_amount(self.against_loan)
@@ -117,12 +114,11 @@ class LoanDisbursement(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = [] gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": self.loan_account,
"against": loan_details.disbursement_account, "against": self.disbursement_account,
"debit": self.disbursed_amount, "debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@@ -137,8 +133,8 @@ class LoanDisbursement(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.disbursement_account, "account": self.disbursement_account,
"against": loan_details.loan_account, "against": self.loan_account,
"credit": self.disbursed_amount, "credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",

View File

@@ -1,7 +1,7 @@
{ {
"actions": [], "actions": [],
"autoname": "LM-REP-.####", "autoname": "LM-REP-.####",
"creation": "2019-09-03 14:44:39.977266", "creation": "2022-01-25 10:30:02.767941",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
@@ -13,6 +13,7 @@
"column_break_3", "column_break_3",
"company", "company",
"posting_date", "posting_date",
"clearance_date",
"rate_of_interest", "rate_of_interest",
"payroll_payable_account", "payroll_payable_account",
"is_term_loan", "is_term_loan",
@@ -37,7 +38,12 @@
"total_penalty_paid", "total_penalty_paid",
"total_interest_paid", "total_interest_paid",
"repayment_details", "repayment_details",
"amended_from" "amended_from",
"accounting_details_section",
"payment_account",
"penalty_income_account",
"column_break_36",
"loan_account"
], ],
"fields": [ "fields": [
{ {
@@ -260,12 +266,52 @@
"fieldname": "repay_from_salary", "fieldname": "repay_from_salary",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Repay From Salary" "label": "Repay From Salary"
},
{
"fieldname": "clearance_date",
"fieldtype": "Date",
"label": "Clearance Date",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "against_loan.payment_account",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Repayment Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "column_break_36",
"fieldtype": "Column Break"
},
{
"fetch_from": "against_loan.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1
},
{
"fetch_from": "against_loan.penalty_income_account",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Penalty Income Account",
"options": "Account"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-01-06 01:51:06.707782", "modified": "2022-02-18 19:10:07.742298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Repayment", "name": "Loan Repayment",

View File

@@ -311,7 +311,6 @@ class LoanRepayment(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = [] gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
if self.shortfall_amount and self.amount_paid > self.shortfall_amount: if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount, remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
@@ -324,13 +323,13 @@ class LoanRepayment(AccountsController):
if self.repay_from_salary: if self.repay_from_salary:
payment_account = self.payroll_payable_account payment_account = self.payroll_payable_account
else: else:
payment_account = loan_details.payment_account payment_account = self.payment_account
if self.total_penalty_paid: if self.total_penalty_paid:
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": self.loan_account,
"against": loan_details.payment_account, "against": payment_account,
"debit": self.total_penalty_paid, "debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid, "debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@@ -345,8 +344,8 @@ class LoanRepayment(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.penalty_income_account, "account": self.penalty_income_account,
"against": loan_details.loan_account, "against": self.loan_account,
"credit": self.total_penalty_paid, "credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid, "credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@@ -360,8 +359,7 @@ class LoanRepayment(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": payment_account, "account": payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account "against": self.loan_account + ", " + self.penalty_income_account,
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid, "debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid, "debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@@ -369,16 +367,16 @@ class LoanRepayment(AccountsController):
"remarks": remarks, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date), "posting_date": getdate(self.posting_date),
"party_type": loan_details.applicant_type if self.repay_from_salary else '', "party_type": self.applicant_type if self.repay_from_salary else '',
"party": loan_details.applicant if self.repay_from_salary else '' "party": self.applicant if self.repay_from_salary else ''
}) })
) )
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": self.loan_account,
"party_type": loan_details.applicant_type, "party_type": self.applicant_type,
"party": loan_details.applicant, "party": self.applicant,
"against": payment_account, "against": payment_account,
"credit": self.amount_paid, "credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid, "credit_in_account_currency": self.amount_paid,

View File

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

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import functools import functools
import re
from collections import deque from collections import deque
from operator import itemgetter from operator import itemgetter
from typing import List from typing import List
@@ -103,25 +104,33 @@ class BOM(WebsiteGenerator):
) )
def autoname(self): 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: if existing_boms:
# name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1 index = self.get_next_version_index(existing_boms)
# 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
else: 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): if frappe.db.exists("BOM", name):
conflicting_bom = frappe.get_doc("BOM", name) conflicting_bom = frappe.get_doc("BOM", name)
@@ -134,6 +143,26 @@ class BOM(WebsiteGenerator):
self.name = name 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): def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-') self.route = frappe.scrub(self.name).replace('_', '-')
@@ -141,6 +170,7 @@ class BOM(WebsiteGenerator):
frappe.throw(_("Please select a Company first."), title=_("Mandatory")) frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
self.clear_operations() self.clear_operations()
self.clear_inspection()
self.validate_main_item() self.validate_main_item()
self.validate_currency() self.validate_currency()
self.set_conversion_rate() self.set_conversion_rate()
@@ -192,12 +222,13 @@ class BOM(WebsiteGenerator):
if self.routing: if self.routing:
self.set("operations", []) self.set("operations", [])
fields = ["sequence_id", "operation", "workstation", "description", fields = ["sequence_id", "operation", "workstation", "description",
"time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"] "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate",
"set_cost_based_on_bom_qty"]
for row in frappe.get_all("BOM Operation", fields = fields, for row in frappe.get_all("BOM Operation", fields = fields,
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"): filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
child = self.append('operations', row) child = self.append('operations', row)
child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2) child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate"))
def set_bom_material_details(self): def set_bom_material_details(self):
for item in self.get("items"): for item in self.get("items"):
@@ -386,6 +417,10 @@ class BOM(WebsiteGenerator):
if not self.with_operations: if not self.with_operations:
self.set('operations', []) self.set('operations', [])
def clear_inspection(self):
if not self.inspection_required:
self.quality_inspection_template = None
def validate_main_item(self): def validate_main_item(self):
""" Validate main FG item""" """ Validate main FG item"""
item = self.get_item_det(self.item) item = self.get_item_det(self.item)

View File

@@ -6,7 +6,7 @@ from collections import deque
from functools import partial from functools import partial
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order 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, create_stock_reconciliation,
) )
from erpnext.tests.test_subcontracting import set_backflush_based_on from erpnext.tests.test_subcontracting import set_backflush_based_on
from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM') test_records = frappe.get_test_records('BOM')
test_dependencies = ["Item", "Quality Inspection Template"]
class TestBOM(ERPNextTestCase): class TestBOM(FrappeTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
def test_get_items(self): def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
items_dict = get_bom_items_as_dict(bom=get_default_bom(), 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") self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete() 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"): 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}) return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@@ -65,7 +65,8 @@
"label": "Hour Rate", "label": "Hour Rate",
"oldfieldname": "hour_rate", "oldfieldname": "hour_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency" "options": "currency",
"precision": "2"
}, },
{ {
"description": "In minutes", "description": "In minutes",
@@ -177,7 +178,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-09-13 16:45:01.092868", "modified": "2022-03-10 06:19:08.462027",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operation", "name": "BOM Operation",

View File

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

View File

@@ -48,7 +48,7 @@ class JobCard(Document):
self.validate_work_order() self.validate_work_order()
def set_sub_operations(self): def set_sub_operations(self):
if self.operation: if not self.sub_operations and self.operation:
self.sub_operations = [] self.sub_operations = []
for row in frappe.get_all('Sub Operation', for row in frappe.get_all('Sub Operation',
filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'): filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):

View File

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

View File

@@ -2,6 +2,7 @@
# See license.txt # See license.txt
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import random_string from frappe.utils import random_string
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError 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.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation 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.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): def setUp(self):
make_bom_for_jc_tests() make_bom_for_jc_tests()

View File

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

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, flt, now_datetime, nowdate from frappe.utils import add_to_date, flt, now_datetime, nowdate
from erpnext.controllers.item_variant import create_variant 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 ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.tests.utils import ERPNextTestCase
class TestProductionPlan(ERPNextTestCase): class TestProductionPlan(FrappeTestCase):
def setUp(self): def setUp(self):
for item in ['Test Production Item 1', 'Subassembly Item 1', for item in ['Test Production Item 1', 'Subassembly Item 1',
'Raw Material Item 1', 'Raw Material Item 2']: 'Raw Material Item 1', 'Raw Material Item 2']:

View File

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

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe import frappe
from frappe.tests.utils import FrappeTestCase, change_settings, timeout
from frappe.utils import add_days, add_months, cint, flt, now, today from frappe.utils import add_days, add_months, cint, flt, now, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError 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.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.tests.utils import ERPNextTestCase, timeout
class TestWorkOrder(ERPNextTestCase): class TestWorkOrder(FrappeTestCase):
def setUp(self): def setUp(self):
self.warehouse = '_Test Warehouse 2 - _TC' self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item' self.item = '_Test Item'
@@ -937,6 +937,28 @@ class TestWorkOrder(ERPNextTestCase):
frappe.db.set_value("Manufacturing Settings", None, frappe.db.set_value("Manufacturing Settings", None,
"backflush_raw_materials_based_on", "BOM") "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): def update_job_card(job_card, jc_qty=None):
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')

View File

@@ -333,6 +333,14 @@ class WorkOrder(Document):
if not self.batch_size: if not self.batch_size:
self.batch_size = total_qty 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: while total_qty > 0:
qty = self.batch_size qty = self.batch_size
if self.batch_size >= total_qty: if self.batch_size >= total_qty:

View File

@@ -2,6 +2,7 @@
# See license.txt # See license.txt
import frappe import frappe
from frappe.test_runner import make_test_records 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.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_bom from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_bom
@@ -10,13 +11,12 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
WorkstationHolidayError, WorkstationHolidayError,
check_if_within_operating_hours, check_if_within_operating_hours,
) )
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Warehouse"] test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation') test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation') make_test_records('Workstation')
class TestWorkstation(ERPNextTestCase): class TestWorkstation(FrappeTestCase):
def test_validate_timings(self): 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 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") 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", "paid",
"amount", "amount",
"mode_of_payment", "mode_of_payment",
"razorpay_payment_id", "column_break_12",
"payment_id",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@@ -73,12 +74,6 @@
"label": "Mode of Payment", "label": "Mode of Payment",
"options": "Mode of Payment" "options": "Mode of Payment"
}, },
{
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "Razorpay Payment ID",
"read_only": 1
},
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
@@ -108,12 +103,21 @@
"options": "Donation", "options": "Donation",
"print_hide": 1, "print_hide": 1,
"read_only": 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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-11 10:53:11.269005", "modified": "2022-03-16 17:18:45.611741",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Donation", "name": "Donation",

View File

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

View File

@@ -5,7 +5,7 @@ import unittest
import frappe 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): class TestDonation(unittest.TestCase):
@@ -30,7 +30,7 @@ class TestDonation(unittest.TestCase):
'method': 'Debit Card', 'method': 'Debit Card',
'id': 'pay_MeXAmsgeKOhq7O' 'id': 'pay_MeXAmsgeKOhq7O'
}) })
donation = create_donation(donor, payment) donation = create_razorpay_donation(donor, payment)
self.assertTrue(donation.name) self.assertTrue(donation.name)

View File

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

View File

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

View File

@@ -104,7 +104,7 @@ class Membership(Document):
return invoice return invoice
def validate_membership_type_and_settings(self, plan, settings): 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: if not settings.membership_debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link)) frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))

View File

@@ -351,3 +351,6 @@ erpnext.patches.v13_0.update_disbursement_account
erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.amazon_mws_deprecation_warning
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr 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
erpnext.patches.v13_0.rename_non_profit_fields

View File

@@ -6,10 +6,13 @@ def execute():
frappe.reload_doc('accounts', 'doctype', 'bank', force=1) frappe.reload_doc('accounts', 'doctype', 'bank', force=1)
if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'): if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'):
frappe.db.sql(""" try:
UPDATE `tabBank` b, `tabBank Account` ba frappe.db.sql("""
SET b.swift_number = ba.swift_number WHERE b.name = ba.bank UPDATE `tabBank` b, `tabBank Account` ba
""") SET b.swift_number = ba.swift_number WHERE b.name = ba.bank
""")
except Exception as e:
frappe.log_error(e, title="Patch Migration Failed")
frappe.reload_doc('accounts', 'doctype', 'bank_account') frappe.reload_doc('accounts', 'doctype', 'bank_account')
frappe.reload_doc('accounts', 'doctype', 'payment_request') frappe.reload_doc('accounts', 'doctype', 'payment_request')

View File

@@ -6,14 +6,14 @@ import frappe
def execute(): 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"): 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) 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: for employee in employees_with_leave_policy:
alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}) alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1})
if not alloc: if not alloc:
@@ -22,12 +22,10 @@ def execute():
employee_with_assignment.append(employee.name) employee_with_assignment.append(employee.name)
leave_policy.append(employee.leave_policy) leave_policy.append(employee.leave_policy)
if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
if "default_leave_policy" in frappe.db.get_table_columns("Employee"):
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) 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 whole employee Grade
for grade in employee_grade_with_leave_policy: for grade in employee_grade_with_leave_policy:
employees = get_employee_with_grade(grade.name) employees = get_employee_with_grade(grade.name)
for employee in employees: for employee in employees:
@@ -47,13 +45,13 @@ def execute():
allocation_exists=True) allocation_exists=True)
def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False): 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} filters = {"employee":employee, "leave_policy": leave_policy}
if leave_period: if leave_period:
filters["leave_period"] = leave_period filters["leave_period"] = leave_period
frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
if not frappe.db.exists("Leave Policy Assignment" , filters): if not frappe.db.exists("Leave Policy Assignment" , filters):
lpa = frappe.new_doc("Leave Policy Assignment") lpa = frappe.new_doc("Leave Policy Assignment")
lpa.employee = employee lpa.employee = employee

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

@@ -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,42 @@
import frappe
def execute():
frappe.reload_doc('loan_management', 'doctype', 'loan')
frappe.reload_doc('loan_management', 'doctype', 'loan_disbursement')
frappe.reload_doc('loan_management', 'doctype', 'loan_repayment')
ld = frappe.qb.DocType("Loan Disbursement").as_("ld")
lr = frappe.qb.DocType("Loan Repayment").as_("lr")
loan = frappe.qb.DocType("Loan")
frappe.qb.update(
ld
).inner_join(
loan
).on(
loan.name == ld.against_loan
).set(
ld.disbursement_account, loan.disbursement_account
).set(
ld.loan_account, loan.loan_account
).where(
ld.docstatus < 2
).run()
frappe.qb.update(
lr
).inner_join(
loan
).on(
loan.name == lr.against_loan
).set(
lr.payment_account, loan.payment_account
).set(
lr.loan_account, loan.loan_account
).set(
lr.penalty_income_account, loan.penalty_income_account
).where(
lr.docstatus < 2
).run()

View File

@@ -671,7 +671,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
where reference_type="Payroll Entry") where reference_type="Payroll Entry")
order by name limit %(start)s, %(page_len)s""" order by name limit %(start)s, %(page_len)s"""
.format(key=searchfield), { .format(key=searchfield), {
'txt': "%%%s%%" % frappe.db.escape(txt), 'txt': "%%%s%%" % txt,
'start': start, 'page_len': page_len 'start': start, 'page_len': page_len
}) })

View File

@@ -308,28 +308,59 @@ class SalarySlip(TransactionBase):
if payroll_based_on == "Attendance": if payroll_based_on == "Attendance":
self.payment_days -= flt(absent) self.payment_days -= flt(absent)
unmarked_days = self.get_unmarked_days()
consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent": if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent":
unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days)
self.absent_days += unmarked_days #will be treated as absent self.absent_days += unmarked_days #will be treated as absent
self.payment_days -= unmarked_days self.payment_days -= unmarked_days
if include_holidays_in_total_working_days:
for holiday in holidays:
if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }):
self.payment_days += 1
else: else:
self.payment_days = 0 self.payment_days = 0
def get_unmarked_days(self): def get_unmarked_days(self, include_holidays_in_total_working_days):
marked_days = frappe.get_all("Attendance", filters = { unmarked_days = self.total_working_days
"attendance_date": ["between", [self.start_date, self.end_date]], joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
"employee": self.employee, ["date_of_joining", "relieving_date"])
"docstatus": 1 start_date = self.start_date
}, fields = ["COUNT(*) as marked_days"])[0].marked_days end_date = self.end_date
return self.total_working_days - marked_days if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)):
start_date = joining_date
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
include_holidays_in_total_working_days, self.start_date, joining_date)
if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)):
end_date = relieving_date
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
include_holidays_in_total_working_days, relieving_date, self.end_date)
# exclude days for which attendance has been marked
unmarked_days -= frappe.get_all("Attendance", filters = {
"attendance_date": ["between", [start_date, end_date]],
"employee": self.employee,
"docstatus": 1
}, fields = ["COUNT(*) as marked_days"])[0].marked_days
return unmarked_days
def get_unmarked_days_based_on_doj_or_relieving(self, unmarked_days,
include_holidays_in_total_working_days, start_date, end_date):
"""
Exclude days before DOJ or after
Relieving Date from unmarked days
"""
from erpnext.hr.doctype.employee.employee import is_holiday
if include_holidays_in_total_working_days:
unmarked_days -= date_diff(end_date, start_date)
else:
# exclude only if not holidays
for days in range(date_diff(end_date, start_date)):
date = add_days(end_date, -days)
if not is_holiday(self.employee, date):
unmarked_days -= 1
return unmarked_days
def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days): def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
if not joining_date: if not joining_date:
@@ -968,7 +999,7 @@ class SalarySlip(TransactionBase):
# apply rounding # apply rounding
if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"): 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 return amount, additional_amount
@@ -1245,9 +1276,9 @@ class SalarySlip(TransactionBase):
def set_base_totals(self): def set_base_totals(self):
self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate) 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.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_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() self.set_net_total_in_words()
#calculate total working hours, earnings based on hourly wages and totals #calculate total working hours, earnings based on hourly wages and totals
@@ -1359,7 +1390,7 @@ class SalarySlip(TransactionBase):
'total_allocated_leaves': flt(leave_values.get('total_leaves')), 'total_allocated_leaves': flt(leave_values.get('total_leaves')),
'expired_leaves': flt(leave_values.get('expired_leaves')), 'expired_leaves': flt(leave_values.get('expired_leaves')),
'used_leaves': flt(leave_values.get('leaves_taken')), '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')) 'available_leaves': flt(leave_values.get('remaining_leaves'))
}) })

View File

@@ -7,10 +7,12 @@ import unittest
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.tests.utils import change_settings
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
add_months, add_months,
cstr, cstr,
date_diff,
flt, flt,
get_first_day, get_first_day,
get_last_day, get_last_day,
@@ -21,6 +23,7 @@ from frappe.utils.make_random import get_random
import erpnext import erpnext
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
@@ -37,17 +40,17 @@ class TestSalarySlip(unittest.TestCase):
setup_test() setup_test()
def tearDown(self): def tearDown(self):
frappe.db.rollback()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator") frappe.set_user("Administrator")
@change_settings("Payroll Settings", {
"payroll_based_on": "Attendance",
"daily_wages_fraction_for_half_day": 0.75
})
def test_payment_days_based_on_attendance(self): def test_payment_days_based_on_attendance(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
no_of_days = self.get_no_of_days() no_of_days = self.get_no_of_days()
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75)
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com") emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -85,14 +88,78 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.gross_pay, gross_pay) self.assertEqual(ss.gross_pay, gross_pay)
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") @change_settings("Payroll Settings", {
"payroll_based_on": "Attendance",
"consider_unmarked_attendance_as": "Absent",
"include_holidays_in_total_working_days": True
})
def test_payment_days_for_mid_joinee_including_holidays(self):
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
no_of_days = self.get_no_of_days()
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
frappe.db.set_value("Employee", new_emp_id, {
"date_of_joining": joining_date,
"relieving_date": relieving_date,
"status": "Left"
})
holidays = 0
for days in range(date_diff(relieving_date, joining_date) + 1):
date = add_days(joining_date, days)
if not is_holiday("Salary Slip Test Holiday List", date):
mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
else:
holidays += 1
new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
self.assertEqual(new_ss.total_working_days, no_of_days[0])
self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
@change_settings("Payroll Settings", {
"payroll_based_on": "Attendance",
"consider_unmarked_attendance_as": "Absent",
"include_holidays_in_total_working_days": False
})
def test_payment_days_for_mid_joinee_excluding_holidays(self):
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
no_of_days = self.get_no_of_days()
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
frappe.db.set_value("Employee", new_emp_id, {
"date_of_joining": joining_date,
"relieving_date": relieving_date,
"status": "Left"
})
holidays = 0
for days in range(date_diff(relieving_date, joining_date) + 1):
date = add_days(joining_date, days)
if not is_holiday("Salary Slip Test Holiday List", date):
mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
else:
holidays += 1
new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
self.assertEqual(new_ss.total_working_days, no_of_days[0] - no_of_days[1])
self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
@change_settings("Payroll Settings", {
"payroll_based_on": "Leave"
})
def test_payment_days_based_on_leave_application(self): def test_payment_days_based_on_leave_application(self):
no_of_days = self.get_no_of_days() no_of_days = self.get_no_of_days()
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com") emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -133,8 +200,9 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") @change_settings("Payroll Settings", {
"payroll_based_on": "Attendance"
})
def test_payment_days_in_salary_slip_based_on_timesheet(self): def test_payment_days_in_salary_slip_based_on_timesheet(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.projects.doctype.timesheet.test_timesheet import ( from erpnext.projects.doctype.timesheet.test_timesheet import (
@@ -145,9 +213,6 @@ class TestSalarySlip(unittest.TestCase):
make_salary_slip as make_salary_slip_for_timesheet, make_salary_slip as make_salary_slip_for_timesheet,
) )
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List") emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
@@ -185,8 +250,9 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2)) self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") @change_settings("Payroll Settings", {
"payroll_based_on": "Attendance"
})
def test_component_amount_dependent_on_another_payment_days_based_component(self): def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
@@ -194,9 +260,6 @@ class TestSalarySlip(unittest.TestCase):
) )
no_of_days = self.get_no_of_days() no_of_days = self.get_no_of_days()
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
salary_structure = make_salary_structure_for_payment_days_based_component_dependency() salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company") employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
@@ -242,11 +305,12 @@ class TestSalarySlip(unittest.TestCase):
expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision) expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision)
self.assertEqual(actual_amount, expected_amount) self.assertEqual(actual_amount, expected_amount)
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
@change_settings("Payroll Settings", {
"include_holidays_in_total_working_days": 1
})
def test_salary_slip_with_holidays_included(self): def test_salary_slip_with_holidays_included(self):
no_of_days = self.get_no_of_days() no_of_days = self.get_no_of_days()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
make_employee("test_salary_slip_with_holidays_included@salary.com") make_employee("test_salary_slip_with_holidays_included@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee", frappe.db.set_value("Employee", frappe.get_value("Employee",
{"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None) {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None)
@@ -260,9 +324,11 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.earnings[1].amount, 3000) self.assertEqual(ss.earnings[1].amount, 3000)
self.assertEqual(ss.gross_pay, 78000) self.assertEqual(ss.gross_pay, 78000)
@change_settings("Payroll Settings", {
"include_holidays_in_total_working_days": 0
})
def test_salary_slip_with_holidays_excluded(self): def test_salary_slip_with_holidays_excluded(self):
no_of_days = self.get_no_of_days() no_of_days = self.get_no_of_days()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
make_employee("test_salary_slip_with_holidays_excluded@salary.com") make_employee("test_salary_slip_with_holidays_excluded@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee", frappe.db.set_value("Employee", frappe.get_value("Employee",
{"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None) {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None)
@@ -277,14 +343,15 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.earnings[1].amount, 3000) self.assertEqual(ss.earnings[1].amount, 3000)
self.assertEqual(ss.gross_pay, 78000) self.assertEqual(ss.gross_pay, 78000)
@change_settings("Payroll Settings", {
"include_holidays_in_total_working_days": 1
})
def test_payment_days(self): def test_payment_days(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment, create_salary_structure_assignment,
) )
no_of_days = self.get_no_of_days() no_of_days = self.get_no_of_days()
# Holidays not included in working days
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
# set joinng date in the same month # set joinng date in the same month
employee = make_employee("test_payment_days@salary.com") employee = make_employee("test_payment_days@salary.com")
@@ -342,12 +409,13 @@ class TestSalarySlip(unittest.TestCase):
frappe.set_user("test_employee_salary_slip_read_permission@salary.com") frappe.set_user("test_employee_salary_slip_read_permission@salary.com")
self.assertTrue(salary_slip_test_employee.has_permission("read")) self.assertTrue(salary_slip_test_employee.has_permission("read"))
@change_settings("Payroll Settings", {
"email_salary_slip_to_employee": 1
})
def test_email_salary_slip(self): def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`") frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1) make_employee("test_email_salary_slip@salary.com", company="_Test Company")
make_employee("test_email_salary_slip@salary.com")
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email") ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
ss.company = "_Test Company" ss.company = "_Test Company"
ss.save() ss.save()
@@ -994,7 +1062,7 @@ def create_additional_salary(employee, payroll_period, amount):
}).submit() }).submit()
return salary_date 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( leave_application = frappe.get_doc(dict(
doctype = 'Leave Application', doctype = 'Leave Application',
employee = employee, employee = employee,
@@ -1002,11 +1070,12 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non
from_date = from_date, from_date = from_date,
to_date = to_date, to_date = to_date,
company = company or erpnext.get_default_company() or "_Test Company", company = company or erpnext.get_default_company() or "_Test Company",
docstatus = 1,
status = "Approved", status = "Approved",
leave_approver = 'test@example.com' leave_approver = 'test@example.com'
)) )).insert()
leave_application.submit()
if submit:
leave_application.submit()
return leave_application return leave_application
@@ -1024,20 +1093,22 @@ def setup_test():
frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
frappe.db.set_value('HR Settings', None, 'leave_approval_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()) fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List") name = list_name or "Salary Slip Test Holiday List"
if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"):
holiday_list = frappe.get_doc({ frappe.delete_doc_if_exists("Holiday List", name, force=True)
"doctype": "Holiday List",
"holiday_list_name": "Salary Slip Test Holiday List", holiday_list = frappe.get_doc({
"from_date": fiscal_year[1], "doctype": "Holiday List",
"to_date": fiscal_year[2], "holiday_list_name": name,
"weekly_off": "Sunday" "from_date": from_date or fiscal_year[1],
}).insert() "to_date": to_date or fiscal_year[2],
holiday_list.get_weekly_off_dates() "weekly_off": "Sunday"
holiday_list.save() }).insert()
holiday_list = holiday_list.name holiday_list.get_weekly_off_dates()
holiday_list.save()
holiday_list = holiday_list.name
return holiday_list return holiday_list

View File

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

View File

@@ -2,6 +2,7 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -13,7 +14,7 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_
from erpnext.projects.report.project_profitability.project_profitability import execute from erpnext.projects.report.project_profitability.project_profitability import execute
class TestProjectProfitability(unittest.TestCase): class TestProjectProfitability(FrappeTestCase):
def setUp(self): def setUp(self):
frappe.db.sql('delete from `tabTimesheet`') frappe.db.sql('delete from `tabTimesheet`')
emp = make_employee('test_employee_9@salary.com', company='_Test Company') emp = make_employee('test_employee_9@salary.com', company='_Test Company')
@@ -68,6 +69,3 @@ class TestProjectProfitability(unittest.TestCase):
fractional_cost = self.salary_slip.base_gross_pay * utilization fractional_cost = self.salary_slip.base_gross_pay * utilization
self.assertEqual(fractional_cost, row.fractional_cost) self.assertEqual(fractional_cost, row.fractional_cost)
def tearDown(self):
frappe.db.rollback()

View File

@@ -181,6 +181,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "journal_entry", fieldname: "journal_entry",
onchange: () => this.update_options(), onchange: () => this.update_options(),
}, },
{
fieldtype: "Check",
label: "Loan Repayment",
fieldname: "loan_repayment",
onchange: () => this.update_options(),
},
{ {
fieldname: "column_break_5", fieldname: "column_break_5",
fieldtype: "Column Break", fieldtype: "Column Break",
@@ -191,13 +197,18 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "sales_invoice", fieldname: "sales_invoice",
onchange: () => this.update_options(), onchange: () => this.update_options(),
}, },
{ {
fieldtype: "Check", fieldtype: "Check",
label: "Purchase Invoice", label: "Purchase Invoice",
fieldname: "purchase_invoice", fieldname: "purchase_invoice",
onchange: () => this.update_options(), onchange: () => this.update_options(),
}, },
{
fieldtype: "Check",
label: "Show Only Exact Amount",
fieldname: "exact_match",
onchange: () => this.update_options(),
},
{ {
fieldname: "column_break_5", fieldname: "column_break_5",
fieldtype: "Column Break", fieldtype: "Column Break",
@@ -210,8 +221,8 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
}, },
{ {
fieldtype: "Check", fieldtype: "Check",
label: "Show Only Exact Amount", label: "Loan Disbursement",
fieldname: "exact_match", fieldname: "loan_disbursement",
onchange: () => this.update_options(), onchange: () => this.update_options(),
}, },
{ {

View File

@@ -39,6 +39,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this._calculate_taxes_and_totals(); this._calculate_taxes_and_totals();
this.calculate_discount_amount(); this.calculate_discount_amount();
this.calculate_shipping_charges();
// Advance calculation applicable to Sales /Purchase Invoice // Advance calculation applicable to Sales /Purchase Invoice
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype) if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
&& this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) { && 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.initialize_taxes();
this.determine_exclusive_rate(); this.determine_exclusive_rate();
this.calculate_net_total(); this.calculate_net_total();
this.calculate_shipping_charges();
this.calculate_taxes(); this.calculate_taxes();
this.manipulate_grand_total_for_inclusive_tax(); this.manipulate_grand_total_for_inclusive_tax();
this.calculate_totals(); this.calculate_totals();
@@ -273,6 +274,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); 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)) { if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
this.shipping_rule(); this.shipping_rule();
this._calculate_taxes_and_totals();
} }
}, },

View File

@@ -569,15 +569,12 @@ body.product-page {
} }
.scroll-categories { .scroll-categories {
white-space: nowrap;
overflow-x: auto;
.category-pill { .category-pill {
margin: 0px 4px;
display: inline-block; display: inline-block;
padding: 6px 12px;
background-color: #ecf5fe;
width: fit-content; width: fit-content;
padding: 6px 12px;
margin-bottom: 8px;
background-color: #ecf5fe;
font-size: 14px; font-size: 14px;
border-radius: 18px; border-radius: 18px;
color: var(--blue-500); color: var(--blue-500);

View File

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

View File

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

View File

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

View File

@@ -6,29 +6,19 @@ import frappe
from frappe import _ from frappe import _
from frappe.contacts.doctype.address.address import get_company_address from frappe.contacts.doctype.address.address import get_company_address
from frappe.model.document import Document 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 from erpnext.accounts.utils import get_fiscal_year
class TaxExemption80GCertificate(Document): class TaxExemption80GCertificate(Document):
def validate(self): def validate(self):
self.validate_date()
self.validate_duplicates() self.validate_duplicates()
self.validate_company_details() self.validate_company_details()
self.set_company_address() self.set_company_address()
self.calculate_total() self.calculate_total()
self.set_title() 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): def validate_duplicates(self):
if self.recipient == 'Donor': if self.recipient == 'Donor':
certificate = frappe.db.exists(self.doctype, { certificate = frappe.db.exists(self.doctype, {
@@ -96,7 +86,7 @@ class TaxExemption80GCertificate(Document):
'date': doc.from_date, 'date': doc.from_date,
'amount': doc.amount, 'amount': doc.amount,
'invoice_id': doc.invoice, 'invoice_id': doc.invoice,
'razorpay_payment_id': doc.payment_id, 'payment_id': doc.payment_id,
'membership': doc.name 'membership': doc.name
}) })
total += flt(doc.amount) total += flt(doc.amount)

View File

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

View File

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

View File

@@ -10,10 +10,10 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Print Format", "doctype": "Print Format",
"font": "Default", "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, "idx": 0,
"line_breaks": 0, "line_breaks": 0,
"modified": "2021-02-22 00:20:08.516600", "modified": "2022-03-16 17:25:33.420509",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "80G Certificate for Donation", "name": "80G Certificate for Donation",

View File

@@ -345,8 +345,7 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1):
/* against number or, if empty, party against number */ /* against number or, if empty, party against number */
%(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
/* disable automatic VAT deduction */ '' as 'BU-Schlüssel',
'40' as 'BU-Schlüssel',
gl.posting_date as 'Belegdatum', gl.posting_date as 'Belegdatum',
gl.voucher_no as 'Belegfeld 1', gl.voucher_no as 'Belegfeld 1',

View File

@@ -93,7 +93,7 @@ def create_qr_code(doc, method=None):
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# VAT Amount # VAT Amount
vat_amount = str(doc.total_taxes_and_charges) vat_amount = str(get_vat_amount(doc))
tag = bytes([5]).hex() tag = bytes([5]).hex()
length = bytes([len(vat_amount)]).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.db_set('ksa_einv_qr', _file.file_url)
doc.notify_update() 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): def delete_qr_code_file(doc, method=None):
region = get_region(doc.company) region = get_region(doc.company)

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