mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-17 00:25:01 +00:00
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:
6
.github/workflows/patch.yml
vendored
6
.github/workflows/patch.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
types: [opened, unlabeled, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -30,11 +29,6 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Check for merge conficts label
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
|
||||
run: |
|
||||
echo "Remove merge conflicts and remove conflict label to run CI"
|
||||
exit 1
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
7
.github/workflows/server-tests.yml
vendored
7
.github/workflows/server-tests.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
types: [opened, unlabeled, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
@@ -40,12 +39,6 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Check for merge conficts label
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
|
||||
run: |
|
||||
echo "Remove merge conflicts and remove conflict label to run CI"
|
||||
exit 1
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext import get_company_currency
|
||||
@@ -231,7 +232,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
}), transaction.currency, company_account)
|
||||
|
||||
if total_amount > transaction.unallocated_amount:
|
||||
frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
|
||||
frappe.throw(_("The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"))
|
||||
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
|
||||
for voucher in vouchers:
|
||||
@@ -275,6 +276,10 @@ def check_matching(bank_account, company, transaction, document_types):
|
||||
}
|
||||
|
||||
matching_vouchers = []
|
||||
|
||||
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
|
||||
document_types, filters))
|
||||
|
||||
for query in subquery:
|
||||
matching_vouchers.extend(
|
||||
frappe.db.sql(query, filters,)
|
||||
@@ -311,6 +316,114 @@ def get_queries(bank_account, company, transaction, document_types):
|
||||
|
||||
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):
|
||||
# get matching payment entries query
|
||||
if transaction.deposit > 0:
|
||||
@@ -348,7 +461,6 @@ def get_je_matching_query(amount_condition, transaction):
|
||||
# We have mapping at the bank level
|
||||
# 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
|
||||
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||
|
||||
return f"""
|
||||
|
||||
@@ -48,7 +48,8 @@ class BankTransaction(StatusUpdater):
|
||||
|
||||
def clear_linked_payment_entries(self, for_cancel=False):
|
||||
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)
|
||||
|
||||
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"
|
||||
if payment_entry.payment_document == 'Payment Entry':
|
||||
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
|
||||
paid_amount_field = ("base_paid_amount"
|
||||
if doc.paid_to_account_currency == currency else "paid_amount")
|
||||
|
||||
if doc.payment_type == 'Receive':
|
||||
paid_amount_field = ("received_amount"
|
||||
if doc.paid_to_account_currency == currency else "base_received_amount")
|
||||
elif doc.payment_type == 'Pay':
|
||||
paid_amount_field = ("paid_amount"
|
||||
if doc.paid_to_account_currency == currency else "base_paid_amount")
|
||||
|
||||
return frappe.db.get_value(payment_entry.payment_document,
|
||||
payment_entry.payment_entry, paid_amount_field)
|
||||
|
||||
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":
|
||||
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:
|
||||
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
create_dimension,
|
||||
@@ -10,11 +11,10 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
|
||||
|
||||
class TestOpeningInvoiceCreationTool(ERPNextTestCase):
|
||||
class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||
|
||||
@@ -54,7 +54,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
def on_submit(self):
|
||||
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
|
||||
if self.loyalty_program:
|
||||
if not self.is_return and self.loyalty_program:
|
||||
self.make_loyalty_point_entry()
|
||||
elif self.is_return and self.return_against and self.loyalty_program:
|
||||
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
|
||||
@@ -88,7 +88,7 @@ class POSInvoice(SalesInvoice):
|
||||
def on_cancel(self):
|
||||
# run on cancel method of selling controller
|
||||
super(SalesInvoice, self).on_cancel()
|
||||
if self.loyalty_program:
|
||||
if not self.is_return and self.loyalty_program:
|
||||
self.delete_loyalty_point_entry()
|
||||
elif self.is_return and self.return_against and self.loyalty_program:
|
||||
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
|
||||
|
||||
@@ -83,7 +83,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
pos_inv_cn.set("payments", [])
|
||||
pos_inv_cn.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -100
|
||||
})
|
||||
pos_inv_cn.append('payments', {
|
||||
'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': -200
|
||||
})
|
||||
pos_inv_cn.paid_amount = -300
|
||||
pos_inv_cn.submit()
|
||||
@@ -98,7 +101,12 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
|
||||
pos_inv_cn.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
|
||||
self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
|
||||
consolidated_credit_note = frappe.get_doc("Sales Invoice", pos_inv_cn.consolidated_invoice)
|
||||
self.assertEqual(consolidated_credit_note.is_return, 1)
|
||||
self.assertEqual(consolidated_credit_note.payments[0].mode_of_payment, 'Cash')
|
||||
self.assertEqual(consolidated_credit_note.payments[0].amount, -100)
|
||||
self.assertEqual(consolidated_credit_note.payments[1].mode_of_payment, 'Bank Draft')
|
||||
self.assertEqual(consolidated_credit_note.payments[1].amount, -200)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -64,10 +64,10 @@
|
||||
<td></td>
|
||||
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b></td>
|
||||
<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 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>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
|
||||
@@ -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){
|
||||
frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
|
||||
frm.set_value('to_date', frappe.datetime.get_today());
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-05-24 19:29:05",
|
||||
"creation": "2022-01-25 10:29:57.771398",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -651,7 +651,6 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 0,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -1974,9 +1973,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
|
||||
"fieldname": "is_debit_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Debit Note"
|
||||
"label": "Is Rate Adjustment Entry (Debit Note)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -2038,7 +2038,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-12-23 20:19:38.667508",
|
||||
"modified": "2022-03-08 16:08:53.517903",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2089,8 +2089,9 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -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.stock.doctype.batch.batch import set_batch_nos
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
|
||||
from erpnext.stock.utils import calculate_mapped_packed_items_return
|
||||
from erpnext.stock.doctype.serial_no.serial_no import (
|
||||
get_delivery_note_serial_no,
|
||||
get_serial_nos,
|
||||
update_serial_nos_after_submit,
|
||||
)
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@@ -228,6 +231,9 @@ class SalesInvoice(SellingController):
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
if self.is_return and self.update_stock:
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
self.make_gl_entries()
|
||||
@@ -752,11 +758,8 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def update_packing_list(self):
|
||||
if cint(self.update_stock) == 1:
|
||||
if cint(self.is_return) and self.return_against:
|
||||
calculate_mapped_packed_items_return(self)
|
||||
else:
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
else:
|
||||
self.set('packed_items', [])
|
||||
|
||||
@@ -1415,12 +1418,19 @@ class SalesInvoice(SellingController):
|
||||
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
|
||||
|
||||
def get_returned_amount(self):
|
||||
returned_amount = frappe.db.sql("""
|
||||
select sum(grand_total)
|
||||
from `tabSales Invoice`
|
||||
where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
|
||||
""", self.name)
|
||||
return abs(flt(returned_amount[0][0])) if returned_amount else 0
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
doc = frappe.qb.DocType(self.doctype)
|
||||
returned_amount = (
|
||||
frappe.qb.from_(doc)
|
||||
.select(Sum(doc.grand_total))
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc.is_return == 1)
|
||||
& (Coalesce(doc.return_against, '') == self.name)
|
||||
)
|
||||
).run()
|
||||
|
||||
return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
|
||||
|
||||
# redeem the loyalty points.
|
||||
def apply_loyalty_points(self):
|
||||
@@ -1700,7 +1710,6 @@ def make_maintenance_schedule(source_name, target_doc=None):
|
||||
@frappe.whitelist()
|
||||
def make_delivery_note(source_name, target_doc=None):
|
||||
def set_missing_values(source, target):
|
||||
target.ignore_pricing_rule = 1
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("set_po_nos")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
@@ -2552,6 +2552,12 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
|
||||
def test_standalone_serial_no_return(self):
|
||||
si = create_sales_invoice(item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1)
|
||||
si.reload()
|
||||
self.assertTrue(si.items[0].serial_no)
|
||||
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
|
||||
@@ -71,8 +71,7 @@ class ShippingRule(Document):
|
||||
if doc.currency != doc.company_currency:
|
||||
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
|
||||
|
||||
if shipping_amount:
|
||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||
|
||||
def get_shipping_amount_from_rules(self, value):
|
||||
for condition in self.get("conditions"):
|
||||
|
||||
@@ -152,7 +152,7 @@ def set_contact_details(party_details, party, party_type):
|
||||
|
||||
def set_other_values(party_details, party, party_type):
|
||||
# copy
|
||||
if party_type=="Customer":
|
||||
if party_type == "Customer":
|
||||
to_copy = ["customer_name", "customer_group", "territory", "language"]
|
||||
else:
|
||||
to_copy = ["supplier_name", "supplier_group", "language"]
|
||||
@@ -171,12 +171,8 @@ def get_default_price_list(party):
|
||||
return party.default_price_list
|
||||
|
||||
if party.doctype == "Customer":
|
||||
price_list = frappe.get_cached_value("Customer Group",
|
||||
party.customer_group, "default_price_list")
|
||||
if price_list:
|
||||
return price_list
|
||||
return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list")
|
||||
|
||||
return None
|
||||
|
||||
def set_price_list(party_details, party, party_type, given_price_list, pos=None):
|
||||
# price list
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
|
||||
import frappe
|
||||
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):
|
||||
@@ -18,7 +23,6 @@ def execute(filters=None):
|
||||
|
||||
data = get_entries(filters)
|
||||
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||
|
||||
total_debit, total_credit = 0,0
|
||||
@@ -118,7 +122,21 @@ def get_columns():
|
||||
]
|
||||
|
||||
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,
|
||||
jv.name as payment_entry, jvd.debit_in_account_currency as debit,
|
||||
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.is_opening, 'No') = 'No'""", filters, as_dict=1)
|
||||
|
||||
payment_entries = frappe.db.sql("""
|
||||
def get_payment_entries(filters):
|
||||
return frappe.db.sql("""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
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
|
||||
""", filters, as_dict=1)
|
||||
|
||||
pos_entries = []
|
||||
if filters.include_pos_transactions:
|
||||
pos_entries = frappe.db.sql("""
|
||||
def get_pos_entries(filters):
|
||||
return frappe.db.sql("""
|
||||
select
|
||||
"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,
|
||||
@@ -161,8 +179,42 @@ def get_entries(filters):
|
||||
si.posting_date ASC, si.name DESC
|
||||
""", filters, as_dict=1)
|
||||
|
||||
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)),
|
||||
key=lambda k: k['posting_date'] or getdate(nowdate()))
|
||||
def get_loan_entries(filters):
|
||||
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):
|
||||
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
|
||||
|
||||
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):
|
||||
if amount > 0:
|
||||
|
||||
134
erpnext/accounts/report/general_ledger/test_general_ledger.py
Normal file
134
erpnext/accounts/report/general_ledger/test_general_ledger.py
Normal 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)
|
||||
@@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = {
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"default": frappe.defaults.get_user_default("Company")
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"label": __("From 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",
|
||||
"label": __("To 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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2013-02-25 17:03:34",
|
||||
"disable_prepared_report": 0,
|
||||
@@ -9,7 +9,7 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-11-13 19:14:23.730198",
|
||||
"modified": "2022-02-11 10:18:36.956558",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Gross Profit",
|
||||
|
||||
@@ -70,43 +70,42 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
data.append(row)
|
||||
|
||||
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 = []
|
||||
for col in group_wise_columns.get(scrub(filters.group_by)):
|
||||
row.append(src.get(col))
|
||||
|
||||
row.append(filters.currency)
|
||||
if idx == len(gross_profit_data.grouped_data)-1:
|
||||
row[0] = "Total"
|
||||
|
||||
data.append(row)
|
||||
|
||||
def get_columns(group_wise_columns, filters):
|
||||
columns = []
|
||||
column_map = frappe._dict({
|
||||
"parent": _("Sales Invoice") + ":Link/Sales Invoice:120",
|
||||
"invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120",
|
||||
"posting_date": _("Posting Date") + ":Date:100",
|
||||
"posting_time": _("Posting Time") + ":Data:100",
|
||||
"item_code": _("Item Code") + ":Link/Item:100",
|
||||
"item_name": _("Item Name") + ":Data:100",
|
||||
"item_group": _("Item Group") + ":Link/Item Group:100",
|
||||
"brand": _("Brand") + ":Link/Brand:100",
|
||||
"description": _("Description") +":Data:100",
|
||||
"warehouse": _("Warehouse") + ":Link/Warehouse:100",
|
||||
"qty": _("Qty") + ":Float:80",
|
||||
"base_rate": _("Avg. Selling Rate") + ":Currency/currency:100",
|
||||
"buying_rate": _("Valuation Rate") + ":Currency/currency:100",
|
||||
"base_amount": _("Selling Amount") + ":Currency/currency:100",
|
||||
"buying_amount": _("Buying Amount") + ":Currency/currency:100",
|
||||
"gross_profit": _("Gross Profit") + ":Currency/currency:100",
|
||||
"gross_profit_percent": _("Gross Profit %") + ":Percent:100",
|
||||
"project": _("Project") + ":Link/Project:100",
|
||||
"sales_person": _("Sales person"),
|
||||
"allocated_amount": _("Allocated Amount") + ":Currency/currency:100",
|
||||
"customer": _("Customer") + ":Link/Customer:100",
|
||||
"customer_group": _("Customer Group") + ":Link/Customer Group:100",
|
||||
"territory": _("Territory") + ":Link/Territory:100"
|
||||
"parent": {"label": _('Sales Invoice'), "fieldname": "parent_invoice", "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
|
||||
"invoice_or_item": {"label": _('Sales Invoice'), "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
|
||||
"posting_date": {"label": _('Posting Date'), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
"posting_time": {"label": _('Posting Time'), "fieldname": "posting_time", "fieldtype": "Data", "width": 100},
|
||||
"item_code": {"label": _('Item Code'), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
|
||||
"item_name": {"label": _('Item Name'), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
|
||||
"item_group": {"label": _('Item Group'), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
|
||||
"brand": {"label": _('Brand'), "fieldtype": "Link", "options": "Brand", "width": 100},
|
||||
"description": {"label": _('Description'), "fieldname": "description", "fieldtype": "Data", "width": 100},
|
||||
"warehouse": {"label": _('Warehouse'), "fieldname": "warehouse", "fieldtype": "Link", "options": "warehouse", "width": 100},
|
||||
"qty": {"label": _('Qty'), "fieldname": "qty", "fieldtype": "Float", "width": 80},
|
||||
"base_rate": {"label": _('Avg. Selling Rate'), "fieldname": "avg._selling_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
|
||||
"buying_rate": {"label": _('Valuation Rate'), "fieldname": "valuation_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
|
||||
"base_amount": {"label": _('Selling Amount'), "fieldname": "selling_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
|
||||
"buying_amount": {"label": _('Buying Amount'), "fieldname": "buying_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
|
||||
"gross_profit": {"label": _('Gross Profit'), "fieldname": "gross_profit", "fieldtype": "Currency", "options": "currency", "width": 100},
|
||||
"gross_profit_percent": {"label": _('Gross Profit Percent'), "fieldname": "gross_profit_%",
|
||||
"fieldtype": "Percent", "width": 100},
|
||||
"project": {"label": _('Project'), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
|
||||
"sales_person": {"label": _('Sales Person'), "fieldname": "sales_person", "fieldtype": "Data","width": 100},
|
||||
"allocated_amount": {"label": _('Allocated Amount'), "fieldname": "allocated_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
|
||||
"customer": {"label": _('Customer'), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 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)):
|
||||
@@ -173,7 +172,7 @@ class GrossProfitGenerator(object):
|
||||
buying_amount = 0
|
||||
|
||||
for row in reversed(self.si_list):
|
||||
if self.skip_row(row, self.product_bundles):
|
||||
if self.skip_row(row):
|
||||
continue
|
||||
|
||||
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()
|
||||
|
||||
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):
|
||||
if self.filters.get("group_by") != "Invoice":
|
||||
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 = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
self.add_to_totals(new_row)
|
||||
else:
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if row.indent == 1.0:
|
||||
@@ -258,17 +246,6 @@ class GrossProfitGenerator(object):
|
||||
if (flt(row.qty) or row.base_amount):
|
||||
row = self.set_average_rate(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):
|
||||
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.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):
|
||||
returned_invoices = frappe.db.sql("""
|
||||
select
|
||||
@@ -308,12 +280,12 @@ class GrossProfitGenerator(object):
|
||||
self.returned_invoices.setdefault(inv.return_against, frappe._dict())\
|
||||
.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 not row.get(scrub(self.filters.get("group_by", ""))):
|
||||
return True
|
||||
elif row.get("is_return") == 1:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_buying_amount_from_product_bundle(self, row, product_bundle):
|
||||
buying_amount = 0.0
|
||||
@@ -371,20 +343,37 @@ class GrossProfitGenerator(object):
|
||||
return self.average_buying_rate[item_code]
|
||||
|
||||
def get_last_purchase_rate(self, item_code, row):
|
||||
condition = ''
|
||||
if row.project:
|
||||
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)
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
|
||||
last_purchase_rate = frappe.db.sql("""
|
||||
select (a.base_rate / a.conversion_factor)
|
||||
from `tabPurchase Invoice Item` a
|
||||
where a.item_code = %s and a.docstatus=1
|
||||
{0}
|
||||
order by a.modified desc limit 1""".format(condition), item_code)
|
||||
query = (frappe.qb.from_(purchase_invoice_item)
|
||||
.inner_join(
|
||||
purchase_invoice
|
||||
).on(
|
||||
purchase_invoice.name == purchase_invoice_item.parent
|
||||
).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
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ def get_opening_balances(filters):
|
||||
select party, sum(debit) as opening_debit, sum(credit) as opening_credit
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s
|
||||
and is_cancelled=0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
|
||||
{account_filter}
|
||||
@@ -133,6 +134,7 @@ def get_balances_within_period(filters):
|
||||
select party, sum(debit) as debit, sum(credit) as credit
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s
|
||||
and is_cancelled = 0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and posting_date >= %(from_date)s and posting_date <= %(to_date)s
|
||||
and ifnull(is_opening, 'No') = 'No'
|
||||
|
||||
@@ -93,10 +93,10 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
||||
account_currency = entry['account_currency']
|
||||
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
if entry.get('debit'):
|
||||
if debit_in_account_currency:
|
||||
entry['debit'] = debit_in_account_currency
|
||||
|
||||
if entry.get('credit'):
|
||||
if credit_in_account_currency:
|
||||
entry['credit'] = credit_in_account_currency
|
||||
else:
|
||||
date = currency_info['report_date']
|
||||
|
||||
16
erpnext/accounts/test_party.py
Normal file
16
erpnext/accounts/test_party.py
Normal 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
|
||||
@@ -1,3 +1,4 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
@@ -7,7 +8,7 @@ def get_data():
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'label': ['Movement'],
|
||||
'label': _('Movement'),
|
||||
'items': ['Asset Movement']
|
||||
}
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@ def post_depreciation_entries(date=None):
|
||||
frappe.db.commit()
|
||||
|
||||
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
|
||||
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')
|
||||
|
||||
@@ -48,7 +48,7 @@ frappe.ui.form.on('Asset Maintenance', {
|
||||
<div class='col-sm-3 small'>
|
||||
<a onclick="frappe.set_route('List', 'Asset Maintenance Log',
|
||||
{'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>
|
||||
</div>
|
||||
</div>`).appendTo(rows);
|
||||
|
||||
@@ -402,7 +402,6 @@ def close_or_unclose_purchase_orders(names, status):
|
||||
frappe.local.message_log = []
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.ignore_pricing_rule = 1
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
"section_break_46",
|
||||
"base_grand_total",
|
||||
"base_rounding_adjustment",
|
||||
"base_in_words",
|
||||
"base_rounded_total",
|
||||
"base_in_words",
|
||||
"column_break4",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
@@ -635,6 +635,7 @@
|
||||
"fieldname": "rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounded Total",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -810,7 +811,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-11 06:43:20.924080",
|
||||
"modified": "2022-03-14 16:13:20.284572",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
@@ -875,6 +876,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title"
|
||||
}
|
||||
@@ -109,7 +109,6 @@ def get_list_context(context=None):
|
||||
@frappe.whitelist()
|
||||
def make_purchase_order(source_name, target_doc=None):
|
||||
def set_missing_values(source, target):
|
||||
target.ignore_pricing_rule = 1
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("get_schedule_dates")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
@@ -6,6 +6,7 @@ import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.utils import date_diff, flt, getdate
|
||||
|
||||
|
||||
@@ -16,12 +17,9 @@ def execute(filters=None):
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
conditions = get_conditions(filters)
|
||||
data = get_data(filters)
|
||||
|
||||
#get queried data
|
||||
data = get_data(filters, conditions)
|
||||
|
||||
#prepare data for report and chart views
|
||||
# prepare data for report and chart views
|
||||
data, chart_data = prepare_data(data, filters)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
@@ -34,53 +32,70 @@ def validate_filters(filters):
|
||||
elif date_diff(to_date, from_date) < 0:
|
||||
frappe.throw(_("To Date cannot be before From Date."))
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ''
|
||||
def get_data(filters):
|
||||
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"):
|
||||
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"):
|
||||
conditions += " and mr.company = '{0}'".format(filters.get("company"))
|
||||
query = query.where(mr.company == filters.get("company"))
|
||||
|
||||
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"):
|
||||
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
|
||||
|
||||
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
|
||||
return query
|
||||
|
||||
def update_qty_columns(row_to_update, data_row):
|
||||
fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -208,10 +208,15 @@ def get_already_returned_items(doc):
|
||||
|
||||
return items
|
||||
|
||||
def get_returned_qty_map_for_row(row_name, doctype):
|
||||
def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
||||
child_doctype = doctype + " Item"
|
||||
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
|
||||
|
||||
if doctype in ('Purchase Receipt', 'Purchase Invoice'):
|
||||
party_type = 'supplier'
|
||||
else:
|
||||
party_type = 'customer'
|
||||
|
||||
fields = [
|
||||
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
|
||||
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
|
||||
@@ -226,9 +231,12 @@ def get_returned_qty_map_for_row(row_name, doctype):
|
||||
if doctype == "Purchase Receipt":
|
||||
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
|
||||
|
||||
# Used retrun against and supplier and is_retrun because there is an index added for it
|
||||
data = frappe.db.get_list(doctype,
|
||||
fields = fields,
|
||||
filters = [
|
||||
[doctype, "return_against", "=", return_against],
|
||||
[doctype, party_type, "=", party],
|
||||
[doctype, "docstatus", "=", 1],
|
||||
[doctype, "is_return", "=", 1],
|
||||
[child_doctype, reference_field, "=", row_name]
|
||||
@@ -307,7 +315,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
target_doc.serial_no = '\n'.join(serial_nos)
|
||||
|
||||
if doctype == "Purchase Receipt":
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
|
||||
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
|
||||
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
|
||||
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
|
||||
@@ -321,7 +329,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
target_doc.purchase_receipt_item = source_doc.name
|
||||
|
||||
elif doctype == "Purchase Invoice":
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
|
||||
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
|
||||
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
|
||||
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
|
||||
@@ -335,7 +343,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
target_doc.purchase_invoice_item = source_doc.name
|
||||
|
||||
elif doctype == "Delivery Note":
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype)
|
||||
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
|
||||
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
|
||||
|
||||
@@ -348,7 +356,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype)
|
||||
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
|
||||
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ class calculate_taxes_and_totals(object):
|
||||
self.set_discount_amount()
|
||||
self.apply_discount_amount()
|
||||
|
||||
self.calculate_shipping_charges()
|
||||
|
||||
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
self.calculate_total_advance()
|
||||
|
||||
@@ -50,7 +52,6 @@ class calculate_taxes_and_totals(object):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
self.calculate_net_total()
|
||||
self.calculate_shipping_charges()
|
||||
self.calculate_taxes()
|
||||
self.manipulate_grand_total_for_inclusive_tax()
|
||||
self.calculate_totals()
|
||||
@@ -113,17 +114,24 @@ class calculate_taxes_and_totals(object):
|
||||
for item in self.doc.get("items"):
|
||||
self.doc.round_floats_in(item)
|
||||
|
||||
if not item.rate:
|
||||
item.rate = item.price_list_rate
|
||||
|
||||
if item.discount_percentage == 100:
|
||||
item.rate = 0.0
|
||||
elif item.price_list_rate:
|
||||
if 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 *
|
||||
(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
|
||||
|
||||
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)
|
||||
if flt(item.rate_with_margin) > 0:
|
||||
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.apply(self.doc)
|
||||
|
||||
self._calculate()
|
||||
|
||||
def calculate_taxes(self):
|
||||
rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
|
||||
if not rounding_adjustment_computed:
|
||||
@@ -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.precision("outstanding_amount"))
|
||||
|
||||
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
|
||||
self.update_paid_amount_for_return(total_amount_to_pay)
|
||||
if (
|
||||
self.doc.doctype == 'Sales Invoice'
|
||||
and self.doc.get('is_pos')
|
||||
and self.doc.get('is_return')
|
||||
and not self.doc.get('is_consolidated')
|
||||
):
|
||||
self.set_total_amount_to_default_mop(total_amount_to_pay)
|
||||
self.calculate_paid_amount()
|
||||
|
||||
def calculate_paid_amount(self):
|
||||
|
||||
@@ -707,7 +723,7 @@ class calculate_taxes_and_totals(object):
|
||||
def set_item_wise_tax_breakup(self):
|
||||
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
|
||||
|
||||
def update_paid_amount_for_return(self, total_amount_to_pay):
|
||||
def set_total_amount_to_default_mop(self, total_amount_to_pay):
|
||||
default_mode_of_payment = frappe.db.get_value('POS Payment Method',
|
||||
{'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1)
|
||||
|
||||
@@ -719,8 +735,6 @@ class calculate_taxes_and_totals(object):
|
||||
'default': 1
|
||||
})
|
||||
|
||||
self.calculate_paid_amount()
|
||||
|
||||
def get_itemised_tax_breakup_html(doc):
|
||||
if not doc.taxes:
|
||||
return
|
||||
|
||||
@@ -47,7 +47,6 @@ def get_product_filter_data(query_args=None):
|
||||
|
||||
sub_categories = []
|
||||
if item_group:
|
||||
field_filters['item_group'] = item_group
|
||||
sub_categories = get_child_groups_for_website(item_group, immediate=True)
|
||||
|
||||
engine = ProductQuery()
|
||||
|
||||
@@ -14,6 +14,8 @@ class ProductFiltersBuilder:
|
||||
self.item_group = item_group
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
@@ -25,18 +27,26 @@ class ProductFiltersBuilder:
|
||||
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
|
||||
|
||||
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)
|
||||
|
||||
if df.fieldtype == "Link":
|
||||
if self.item_group:
|
||||
item_or_filters.extend([
|
||||
["item_group", "=", self.item_group],
|
||||
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
|
||||
])
|
||||
include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants")
|
||||
if include_child:
|
||||
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
|
||||
item_filters['published_in_website'] = 1
|
||||
item_values = frappe.get_all(
|
||||
"Item",
|
||||
fields=[df.fieldname],
|
||||
|
||||
@@ -46,10 +46,10 @@ class ProductQuery:
|
||||
self.filter_with_discount = bool(fields.get("discount"))
|
||||
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:
|
||||
self.build_fields_filters(fields)
|
||||
if item_group:
|
||||
self.build_item_group_filters(item_group)
|
||||
if search_term:
|
||||
self.build_search_filters(search_term)
|
||||
if self.settings.hide_variants:
|
||||
@@ -61,8 +61,6 @@ class ProductQuery:
|
||||
else:
|
||||
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
|
||||
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
|
||||
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):
|
||||
"""Query search term in specified fields
|
||||
|
||||
@@ -191,19 +208,6 @@ class ProductQuery:
|
||||
for field in search_fields:
|
||||
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):
|
||||
"""Add price and availability details in result."""
|
||||
for item in result:
|
||||
@@ -279,16 +283,6 @@ class ProductQuery:
|
||||
|
||||
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):
|
||||
if fields and fields.get("discount"):
|
||||
discount_percent = frappe.utils.flt(fields["discount"][0])
|
||||
|
||||
@@ -13,8 +13,7 @@ test_dependencies = ["Item", "Item Group"]
|
||||
class TestItemGroupProductDataEngine(unittest.TestCase):
|
||||
"Test Products & Sub-Category Querying for Product Listing on Item Group Page."
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def setUp(self):
|
||||
item_codes = [
|
||||
("Test Mobile A", "_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}):
|
||||
create_regular_web_item(item_code, item_args=item_args)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
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", 1)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_product_listing_in_item_group(self):
|
||||
@@ -87,7 +88,6 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
|
||||
|
||||
def test_item_group_with_sub_groups(self):
|
||||
"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)
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -501,7 +501,7 @@ erpnext.ProductView = class {
|
||||
|
||||
categories.forEach(category => {
|
||||
sub_group_html += `
|
||||
<a href="${ category.route || '#' }" style="text-decoration: none;">
|
||||
<a href="/${ category.route || '#' }" style="text-decoration: none;">
|
||||
<div class="category-pill">
|
||||
${ category.name }
|
||||
</div>
|
||||
|
||||
@@ -121,7 +121,11 @@ def place_order():
|
||||
def request_for_quotation():
|
||||
quotation = _get_cart_quotation()
|
||||
quotation.flags.ignore_permissions = True
|
||||
quotation.submit()
|
||||
|
||||
if get_shopping_cart_settings().save_quotations_as_draft:
|
||||
quotation.save()
|
||||
else:
|
||||
quotation.submit()
|
||||
return quotation.name
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import unittest
|
||||
|
||||
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.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_party,
|
||||
request_for_quotation,
|
||||
update_cart,
|
||||
)
|
||||
from erpnext.tests.utils import change_settings, create_test_contact_and_address
|
||||
from erpnext.tests.utils import create_test_contact_and_address
|
||||
|
||||
|
||||
class TestShoppingCart(unittest.TestCase):
|
||||
@@ -23,11 +25,6 @@ class TestShoppingCart(unittest.TestCase):
|
||||
Note:
|
||||
Shopping Cart == Quotation
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
create_test_contact_and_address()
|
||||
@@ -43,6 +40,10 @@ class TestShoppingCart(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
self.disable_shopping_cart()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_get_cart_new_user(self):
|
||||
self.login_as_new_user()
|
||||
|
||||
@@ -177,6 +178,28 @@ class TestShoppingCart(unittest.TestCase):
|
||||
# test if items are rendered without error
|
||||
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):
|
||||
tax_rule = frappe.get_test_records("Tax Rule")[0]
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||
@@ -9,11 +10,10 @@ from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings imp
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
class TestVariantSelector(ERPNextTestCase):
|
||||
class TestVariantSelector(FrappeTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -118,4 +118,4 @@ class TestVariantSelector(ERPNextTestCase):
|
||||
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
|
||||
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
|
||||
self.assertEqual(price_info["price_list_rate"], 100.0)
|
||||
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
|
||||
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
|
||||
|
||||
@@ -174,16 +174,22 @@ def get_month_map():
|
||||
def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||
import calendar
|
||||
month_map = get_month_map()
|
||||
|
||||
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)
|
||||
month_start, month_end = dates_of_month[0], dates_of_month[length-1]
|
||||
if joining_date and joining_date.month == month_map[month]:
|
||||
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_end],
|
||||
["employee", "=", employee],
|
||||
@@ -200,7 +206,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||
|
||||
for date in dates_of_month:
|
||||
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
|
||||
if date_time not in marked_days:
|
||||
unmarked_days.append(date)
|
||||
|
||||
@@ -1,20 +1,116 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
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')
|
||||
|
||||
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):
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
employee = make_employee("test_mark_absent@example.com")
|
||||
date = nowdate()
|
||||
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
attendance = mark_attendance(employee, date, 'Absent')
|
||||
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
|
||||
import frappe
|
||||
@@ -30,3 +31,24 @@ def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getd
|
||||
"holidays" : holiday_dates
|
||||
}).insert()
|
||||
return doc
|
||||
|
||||
|
||||
@contextmanager
|
||||
def set_holiday_list(holiday_list, company_name):
|
||||
"""
|
||||
Context manager for setting holiday list in tests
|
||||
"""
|
||||
try:
|
||||
company = frappe.get_doc('Company', company_name)
|
||||
previous_holiday_list = company.default_holiday_list
|
||||
|
||||
company.default_holiday_list = holiday_list
|
||||
company.save()
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# restore holiday list setup
|
||||
company = frappe.get_doc('Company', company_name)
|
||||
company.default_holiday_list = previous_holiday_list
|
||||
company.save()
|
||||
|
||||
@@ -52,7 +52,7 @@ frappe.ui.form.on("Leave Application", {
|
||||
make_dashboard: function(frm) {
|
||||
var leave_details;
|
||||
let lwps;
|
||||
if (frm.doc.employee) {
|
||||
if (frm.doc.employee && frm.doc.from_date) {
|
||||
frappe.call({
|
||||
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
|
||||
async: false,
|
||||
@@ -146,6 +146,7 @@ frappe.ui.form.on("Leave Application", {
|
||||
},
|
||||
|
||||
to_date: function(frm) {
|
||||
frm.trigger("make_dashboard");
|
||||
frm.trigger("half_day_datepicker");
|
||||
frm.trigger("calculate_total_days");
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Max, Min, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
@@ -34,6 +36,10 @@ class LeaveDayBlockedError(frappe.ValidationError): pass
|
||||
class OverlapError(frappe.ValidationError): pass
|
||||
class AttendanceAlreadyMarkedError(frappe.ValidationError): pass
|
||||
class NotAnOptionalHoliday(frappe.ValidationError): pass
|
||||
class InsufficientLeaveBalanceError(frappe.ValidationError):
|
||||
pass
|
||||
class LeaveAcrossAllocationsError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
@@ -134,21 +140,35 @@ class LeaveApplication(Document):
|
||||
def validate_dates_across_allocation(self):
|
||||
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
|
||||
return
|
||||
def _get_leave_allocation_record(date):
|
||||
allocation = frappe.db.sql("""select name from `tabLeave Allocation`
|
||||
where employee=%s and leave_type=%s and docstatus=1
|
||||
and %s between from_date and to_date""", (self.employee, self.leave_type, date))
|
||||
|
||||
return allocation and allocation[0][0]
|
||||
alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
|
||||
|
||||
if not (alloc_on_from_date or alloc_on_to_date):
|
||||
frappe.throw(_("Application period cannot be outside leave allocation period"))
|
||||
elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
|
||||
frappe.throw(_("Application period cannot be across two allocation records"), exc=LeaveAcrossAllocationsError)
|
||||
|
||||
def get_allocation_based_on_application_dates(self) -> Tuple[Dict, Dict]:
|
||||
"""Returns allocation name, from and to dates for application dates"""
|
||||
def _get_leave_allocation_record(date):
|
||||
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
|
||||
allocation = (
|
||||
frappe.qb.from_(LeaveAllocation)
|
||||
.select(LeaveAllocation.name, LeaveAllocation.from_date, LeaveAllocation.to_date)
|
||||
.where(
|
||||
(LeaveAllocation.employee == self.employee)
|
||||
& (LeaveAllocation.leave_type == self.leave_type)
|
||||
& (LeaveAllocation.docstatus == 1)
|
||||
& ((date >= LeaveAllocation.from_date) & (date <= LeaveAllocation.to_date))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return allocation and allocation[0]
|
||||
|
||||
allocation_based_on_from_date = _get_leave_allocation_record(self.from_date)
|
||||
allocation_based_on_to_date = _get_leave_allocation_record(self.to_date)
|
||||
|
||||
if not (allocation_based_on_from_date or allocation_based_on_to_date):
|
||||
frappe.throw(_("Application period cannot be outside leave allocation period"))
|
||||
|
||||
elif allocation_based_on_from_date != allocation_based_on_to_date:
|
||||
frappe.throw(_("Application period cannot be across two allocation records"))
|
||||
return allocation_based_on_from_date, allocation_based_on_to_date
|
||||
|
||||
def validate_back_dated_application(self):
|
||||
future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation`
|
||||
@@ -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."))
|
||||
|
||||
if not is_lwp(self.leave_type):
|
||||
self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
|
||||
consider_all_leaves_in_the_allocation_period=True)
|
||||
if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance):
|
||||
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
|
||||
frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
|
||||
.format(self.leave_type))
|
||||
else:
|
||||
frappe.throw(_("There is not enough leave balance for Leave Type {0}")
|
||||
.format(self.leave_type))
|
||||
leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
|
||||
consider_all_leaves_in_the_allocation_period=True, for_consumption=True)
|
||||
self.leave_balance = leave_balance.get("leave_balance")
|
||||
leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption")
|
||||
|
||||
if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption):
|
||||
self.show_insufficient_balance_message(leave_balance_for_consumption)
|
||||
|
||||
def show_insufficient_balance_message(self, leave_balance_for_consumption: float) -> None:
|
||||
alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
|
||||
|
||||
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
|
||||
if leave_balance_for_consumption != self.leave_balance:
|
||||
msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(frappe.bold(self.leave_type))
|
||||
msg += "<br><br>"
|
||||
msg += _("Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation.")
|
||||
else:
|
||||
msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(frappe.bold(self.leave_type))
|
||||
|
||||
frappe.msgprint(msg, title=_("Warning"), indicator="orange")
|
||||
else:
|
||||
frappe.throw(_("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)),
|
||||
exc=InsufficientLeaveBalanceError, title=_("Insufficient Balance"))
|
||||
|
||||
def validate_leave_overlap(self):
|
||||
if not self.name:
|
||||
@@ -425,54 +459,111 @@ class LeaveApplication(Document):
|
||||
if self.status != 'Approved' and submit:
|
||||
return
|
||||
|
||||
expiry_date = get_allocation_expiry(self.employee, self.leave_type,
|
||||
expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type,
|
||||
self.to_date, self.from_date)
|
||||
|
||||
lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
|
||||
|
||||
if expiry_date:
|
||||
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
|
||||
else:
|
||||
raise_exception = True
|
||||
if frappe.flags.in_patch:
|
||||
raise_exception=False
|
||||
alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
|
||||
if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
|
||||
# required only if negative balance is allowed for leave type
|
||||
# else will be stopped in validation itself
|
||||
self.create_separate_ledger_entries(alloc_on_from_date, alloc_on_to_date, submit, lwp)
|
||||
else:
|
||||
raise_exception = False if frappe.flags.in_patch else True
|
||||
args = dict(
|
||||
leaves=self.total_leave_days * -1,
|
||||
from_date=self.from_date,
|
||||
to_date=self.to_date,
|
||||
is_lwp=lwp,
|
||||
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
|
||||
)
|
||||
create_leave_ledger_entry(self, args, submit)
|
||||
|
||||
args = dict(
|
||||
leaves=self.total_leave_days * -1,
|
||||
def is_separate_ledger_entry_required(self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None) -> bool:
|
||||
"""Checks if application dates fall in separate allocations"""
|
||||
if ((alloc_on_from_date and not alloc_on_to_date)
|
||||
or (not alloc_on_from_date and alloc_on_to_date)
|
||||
or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp):
|
||||
"""Creates separate ledger entries for application period falling into separate allocations"""
|
||||
# for creating separate ledger entries existing allocation periods should be consecutive
|
||||
if submit and alloc_on_from_date and alloc_on_to_date and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date:
|
||||
frappe.throw(_("Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}.").format(
|
||||
get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date)))
|
||||
|
||||
raise_exception = False if frappe.flags.in_patch else True
|
||||
|
||||
if alloc_on_from_date:
|
||||
first_alloc_end = alloc_on_from_date.to_date
|
||||
second_alloc_start = add_days(alloc_on_from_date.to_date, 1)
|
||||
else:
|
||||
first_alloc_end = add_days(alloc_on_to_date.from_date, -1)
|
||||
second_alloc_start = alloc_on_to_date.from_date
|
||||
|
||||
leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type,
|
||||
self.from_date, first_alloc_end, self.half_day, self.half_day_date)
|
||||
leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type,
|
||||
second_alloc_start, self.to_date, self.half_day, self.half_day_date)
|
||||
|
||||
args = dict(
|
||||
is_lwp=lwp,
|
||||
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
|
||||
)
|
||||
|
||||
if leaves_in_first_alloc:
|
||||
args.update(dict(
|
||||
from_date=self.from_date,
|
||||
to_date=first_alloc_end,
|
||||
leaves=leaves_in_first_alloc * -1
|
||||
))
|
||||
create_leave_ledger_entry(self, args, submit)
|
||||
|
||||
if leaves_in_second_alloc:
|
||||
args.update(dict(
|
||||
from_date=second_alloc_start,
|
||||
to_date=self.to_date,
|
||||
leaves=leaves_in_second_alloc * -1
|
||||
))
|
||||
create_leave_ledger_entry(self, args, submit)
|
||||
|
||||
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
|
||||
"""Splits leave application into two ledger entries to consider expiry of allocation"""
|
||||
raise_exception = False if frappe.flags.in_patch else True
|
||||
|
||||
leaves = get_number_of_leave_days(self.employee, self.leave_type,
|
||||
self.from_date, expiry_date, self.half_day, self.half_day_date)
|
||||
|
||||
if leaves:
|
||||
args = dict(
|
||||
from_date=self.from_date,
|
||||
to_date=expiry_date,
|
||||
leaves=leaves * -1,
|
||||
is_lwp=lwp,
|
||||
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
|
||||
)
|
||||
create_leave_ledger_entry(self, args, submit)
|
||||
|
||||
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
|
||||
''' splits leave application into two ledger entries to consider expiry of allocation '''
|
||||
|
||||
raise_exception = True
|
||||
if frappe.flags.in_patch:
|
||||
raise_exception=False
|
||||
|
||||
args = dict(
|
||||
from_date=self.from_date,
|
||||
to_date=expiry_date,
|
||||
leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
|
||||
is_lwp=lwp,
|
||||
holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
|
||||
)
|
||||
create_leave_ledger_entry(self, args, submit)
|
||||
|
||||
if getdate(expiry_date) != getdate(self.to_date):
|
||||
start_date = add_days(expiry_date, 1)
|
||||
args.update(dict(
|
||||
from_date=start_date,
|
||||
to_date=self.to_date,
|
||||
leaves=date_diff(self.to_date, expiry_date) * -1
|
||||
))
|
||||
create_leave_ledger_entry(self, args, submit)
|
||||
leaves = get_number_of_leave_days(self.employee, self.leave_type,
|
||||
start_date, self.to_date, self.half_day, self.half_day_date)
|
||||
|
||||
if leaves:
|
||||
args.update(dict(
|
||||
from_date=start_date,
|
||||
to_date=self.to_date,
|
||||
leaves=leaves * -1
|
||||
))
|
||||
create_leave_ledger_entry(self, args, submit)
|
||||
|
||||
|
||||
def get_allocation_expiry(employee, leave_type, to_date, from_date):
|
||||
def get_allocation_expiry_for_cf_leaves(employee: str, leave_type: str, to_date: str, from_date: str) -> str:
|
||||
''' Returns expiry of carry forward allocation in leave ledger entry '''
|
||||
expiry = frappe.get_all("Leave Ledger Entry",
|
||||
filters={
|
||||
@@ -480,12 +571,17 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date):
|
||||
'leave_type': leave_type,
|
||||
'is_carry_forward': 1,
|
||||
'transaction_type': 'Leave Allocation',
|
||||
'to_date': ['between', (from_date, to_date)]
|
||||
'to_date': ['between', (from_date, to_date)],
|
||||
'docstatus': 1
|
||||
},fields=['to_date'])
|
||||
return expiry[0]['to_date'] if expiry else None
|
||||
return expiry[0]['to_date'] if expiry else ''
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None):
|
||||
def get_number_of_leave_days(employee: str, leave_type: str, from_date: str, to_date: str, half_day: Optional[int] = None,
|
||||
half_day_date: Optional[str] = None, holiday_list: Optional[str] = None) -> float:
|
||||
"""Returns number of leave days between 2 dates after considering half day and holidays
|
||||
(Based on the include_holiday setting in Leave Type)"""
|
||||
number_of_days = 0
|
||||
if cint(half_day) == 1:
|
||||
if from_date == to_date:
|
||||
@@ -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))
|
||||
return number_of_days
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_leave_details(employee, date):
|
||||
allocation_records = get_leave_allocation_records(employee, date)
|
||||
@@ -514,6 +611,7 @@ def get_leave_details(employee, date):
|
||||
'to_date': ('>=', date),
|
||||
'employee': employee,
|
||||
'leave_type': allocation.leave_type,
|
||||
'docstatus': 1
|
||||
}, 'SUM(total_leaves_allocated)') or 0
|
||||
|
||||
remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
|
||||
@@ -521,29 +619,28 @@ def get_leave_details(employee, date):
|
||||
|
||||
end_date = allocation.to_date
|
||||
leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1
|
||||
leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date)
|
||||
leaves_pending = get_leaves_pending_approval_for_period(employee, d, allocation.from_date, end_date)
|
||||
|
||||
leave_allocation[d] = {
|
||||
"total_leaves": total_allocated_leaves,
|
||||
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
|
||||
"leaves_taken": leaves_taken,
|
||||
"pending_leaves": leaves_pending,
|
||||
"leaves_pending_approval": leaves_pending,
|
||||
"remaining_leaves": remaining_leaves}
|
||||
|
||||
#is used in set query
|
||||
lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1})
|
||||
lwps = [lwp.name for lwp in lwps]
|
||||
lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name")
|
||||
|
||||
ret = {
|
||||
'leave_allocation': leave_allocation,
|
||||
'leave_approver': get_leave_approver(employee),
|
||||
'lwps': lwps
|
||||
return {
|
||||
"leave_allocation": leave_allocation,
|
||||
"leave_approver": get_leave_approver(employee),
|
||||
"lwps": lwp
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False):
|
||||
def get_leave_balance_on(employee: str, leave_type: str, date: str, to_date: str = None,
|
||||
consider_all_leaves_in_the_allocation_period: bool = False, for_consumption: bool = False):
|
||||
'''
|
||||
Returns leave balance till date
|
||||
:param employee: employee name
|
||||
@@ -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 to_date: future date to check for allocation expiry
|
||||
:param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date
|
||||
:param for_consumption: flag to check if leave balance is required for consumption or display
|
||||
eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave
|
||||
in this case leave_balance = 10 but leave_balance_for_consumption = 1
|
||||
if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1}
|
||||
else, returns leave_balance (in this case 10)
|
||||
'''
|
||||
|
||||
if not to_date:
|
||||
@@ -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())
|
||||
|
||||
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
|
||||
expiry = get_allocation_expiry(employee, leave_type, to_date, date)
|
||||
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
|
||||
|
||||
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
|
||||
|
||||
return get_remaining_leaves(allocation, leaves_taken, date, expiry)
|
||||
remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry)
|
||||
|
||||
if for_consumption:
|
||||
return remaining_leaves
|
||||
else:
|
||||
return remaining_leaves.get('leave_balance')
|
||||
|
||||
|
||||
def get_leave_allocation_records(employee, date, leave_type=None):
|
||||
''' returns the total allocated leaves and carry forwarded leaves based on ledger entries '''
|
||||
"""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 ""
|
||||
allocation_details = frappe.db.sql("""
|
||||
SELECT
|
||||
SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves,
|
||||
SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves,
|
||||
MIN(from_date) as from_date,
|
||||
MAX(to_date) as to_date,
|
||||
leave_type
|
||||
FROM `tabLeave Ledger Entry`
|
||||
WHERE
|
||||
from_date <= %(date)s
|
||||
AND to_date >= %(date)s
|
||||
AND docstatus=1
|
||||
AND transaction_type="Leave Allocation"
|
||||
AND employee=%(employee)s
|
||||
AND is_expired=0
|
||||
AND is_lwp=0
|
||||
{0}
|
||||
GROUP BY employee, leave_type
|
||||
""".format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec
|
||||
cf_leave_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0)
|
||||
sum_cf_leaves = Sum(cf_leave_case).as_("cf_leaves")
|
||||
|
||||
new_leaves_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0)
|
||||
sum_new_leaves = Sum(new_leaves_case).as_("new_leaves")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Ledger)
|
||||
.select(
|
||||
sum_cf_leaves,
|
||||
sum_new_leaves,
|
||||
Min(Ledger.from_date).as_("from_date"),
|
||||
Max(Ledger.to_date).as_("to_date"),
|
||||
Ledger.leave_type
|
||||
).where(
|
||||
(Ledger.from_date <= date)
|
||||
& (Ledger.to_date >= date)
|
||||
& (Ledger.docstatus == 1)
|
||||
& (Ledger.transaction_type == "Leave Allocation")
|
||||
& (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()
|
||||
for d in allocation_details:
|
||||
@@ -602,8 +721,9 @@ def get_leave_allocation_records(employee, date, leave_type=None):
|
||||
}))
|
||||
return allocated_leaves
|
||||
|
||||
def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||
''' Returns leaves that are pending approval '''
|
||||
|
||||
def get_leaves_pending_approval_for_period(employee: str, leave_type: str, from_date: str, to_date: str) -> float:
|
||||
''' Returns leaves that are pending for approval '''
|
||||
leaves = frappe.get_all("Leave Application",
|
||||
filters={
|
||||
"employee": employee,
|
||||
@@ -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]
|
||||
return leaves['leaves'] if leaves['leaves'] else 0.0
|
||||
|
||||
def get_remaining_leaves(allocation, leaves_taken, date, expiry):
|
||||
''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
|
||||
def _get_remaining_leaves(remaining_leaves, end_date):
|
||||
|
||||
def get_remaining_leaves(allocation: Dict, leaves_taken: float, date: str, cf_expiry: str) -> Dict[str, float]:
|
||||
'''Returns a dict of leave_balance and leave_balance_for_consumption
|
||||
leave_balance returns the available leave balance
|
||||
leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry
|
||||
'''
|
||||
def _get_remaining_leaves(remaining_leaves, end_date):
|
||||
''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
|
||||
if remaining_leaves > 0:
|
||||
remaining_days = date_diff(end_date, date) + 1
|
||||
remaining_leaves = min(remaining_days, remaining_leaves)
|
||||
|
||||
return remaining_leaves
|
||||
|
||||
total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
|
||||
leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
|
||||
|
||||
if expiry and allocation.unused_leaves:
|
||||
remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
|
||||
remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry)
|
||||
# balance for carry forwarded leaves
|
||||
if cf_expiry and allocation.unused_leaves:
|
||||
cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
|
||||
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
|
||||
|
||||
total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves)
|
||||
leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
|
||||
leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
|
||||
|
||||
return _get_remaining_leaves(total_leaves, allocation.to_date)
|
||||
remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
|
||||
return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
|
||||
|
||||
def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False):
|
||||
|
||||
def get_leaves_for_period(employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True) -> float:
|
||||
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
|
||||
leave_days = 0
|
||||
|
||||
for leave_entry in leave_entries:
|
||||
inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date)
|
||||
|
||||
if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
|
||||
if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
|
||||
leave_days += leave_entry.leaves
|
||||
|
||||
elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \
|
||||
and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)):
|
||||
and not skip_expired_leaves:
|
||||
leave_days += leave_entry.leaves
|
||||
|
||||
elif leave_entry.transaction_type == 'Leave Application':
|
||||
@@ -669,11 +797,6 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_
|
||||
|
||||
return leave_days
|
||||
|
||||
def skip_expiry_leaves(leave_entry, date):
|
||||
''' Checks whether the expired leaves coincide with the to_date of leave balance check.
|
||||
This allows backdated leave entry creation for non carry forwarded allocation '''
|
||||
end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date'])
|
||||
return True if end_date == date and not leave_entry.is_carry_forward else False
|
||||
|
||||
def get_leave_entries(employee, leave_type, from_date, to_date):
|
||||
''' Returns leave entries between from_date and to_date. '''
|
||||
@@ -696,6 +819,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date):
|
||||
"leave_type": leave_type
|
||||
}, as_dict=1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_holidays(employee, from_date, to_date, holiday_list = None):
|
||||
'''get holidays between two dates for the given employee'''
|
||||
@@ -712,6 +836,7 @@ def is_lwp(leave_type):
|
||||
lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", leave_type)
|
||||
return lwp and cint(lwp[0][0]) or 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
from frappe.desk.reportview import get_filters_cond
|
||||
@@ -740,6 +865,7 @@ def get_events(start, end, filters=None):
|
||||
|
||||
return events
|
||||
|
||||
|
||||
def add_department_leaves(events, start, end, employee, company):
|
||||
department = frappe.db.get_value("Employee", employee, "department")
|
||||
|
||||
@@ -820,6 +946,7 @@ def add_block_dates(events, start, end, employee, company):
|
||||
})
|
||||
cnt+=1
|
||||
|
||||
|
||||
def add_holidays(events, start, end, employee, company):
|
||||
applicable_holiday_list = get_holiday_list_for_employee(employee, company)
|
||||
if not applicable_holiday_list:
|
||||
@@ -836,6 +963,7 @@ def add_holidays(events, start, end, employee, company):
|
||||
"name": holiday.name
|
||||
})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_mandatory_approval(doctype):
|
||||
mandatory = ""
|
||||
@@ -848,6 +976,7 @@ def get_mandatory_approval(doctype):
|
||||
|
||||
return mandatory
|
||||
|
||||
|
||||
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||
query = """
|
||||
select employee, leave_type, from_date, to_date, total_leave_days
|
||||
@@ -883,6 +1012,7 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||
|
||||
return leave_days
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_leave_approver(employee):
|
||||
leave_approver, department = frappe.db.get_value("Employee",
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 16%">{{ __("Leave Type") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Total Allocated Leave") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Expired Leave") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Used Leave") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Pending Leave") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Available Leave") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Total Allocated Leave(s)") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Expired Leave(s)") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Used Leave(s)") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Leave(s) Pending Approval") }}</th>
|
||||
<th style="width: 16%" class="text-right">{{ __("Available Leave(s)") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -18,7 +18,7 @@
|
||||
<td class="text-right"> {%= value["total_leaves"] %} </td>
|
||||
<td class="text-right"> {%= value["expired_leaves"] %} </td>
|
||||
<td class="text-right"> {%= value["leaves_taken"] %} </td>
|
||||
<td class="text-right"> {%= value["pending_leaves"] %} </td>
|
||||
<td class="text-right"> {%= value["leaves_pending_approval"] %} </td>
|
||||
<td class="text-right"> {%= value["remaining_leaves"] %} </td>
|
||||
</tr>
|
||||
{% } %}
|
||||
|
||||
@@ -17,12 +17,16 @@ from frappe.utils import (
|
||||
)
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
from erpnext.hr.doctype.leave_application.leave_application import (
|
||||
InsufficientLeaveBalanceError,
|
||||
LeaveAcrossAllocationsError,
|
||||
LeaveDayBlockedError,
|
||||
NotAnOptionalHoliday,
|
||||
OverlapError,
|
||||
get_leave_balance_on,
|
||||
get_leave_details,
|
||||
)
|
||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
create_assignment_for_multiple_employees,
|
||||
@@ -33,7 +37,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_leave_application,
|
||||
)
|
||||
|
||||
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
|
||||
test_dependencies = ["Leave Type", "Leave Allocation", "Leave Block List", "Employee"]
|
||||
|
||||
_test_records = [
|
||||
{
|
||||
@@ -72,15 +76,28 @@ _test_records = [
|
||||
class TestLeaveApplication(unittest.TestCase):
|
||||
def setUp(self):
|
||||
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
|
||||
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
|
||||
frappe.db.delete(dt)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
set_leave_approver()
|
||||
|
||||
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
|
||||
frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"})
|
||||
frappe.db.set_value("Employee", "_T-Employee-00001", "holiday_list", "")
|
||||
|
||||
from_date = get_year_start(getdate())
|
||||
to_date = get_year_ending(getdate())
|
||||
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
|
||||
|
||||
if not frappe.db.exists("Leave Type", "_Test Leave Type"):
|
||||
frappe.get_doc(dict(
|
||||
leave_type_name="_Test Leave Type",
|
||||
doctype="Leave Type",
|
||||
include_holiday=True
|
||||
)).insert()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def _clear_roles(self):
|
||||
frappe.db.sql("""delete from `tabHas Role` where parent in
|
||||
@@ -95,6 +112,132 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
application.to_date = "2013-01-05"
|
||||
return application
|
||||
|
||||
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||
def test_validate_application_across_allocations(self):
|
||||
# Test validation for application dates when negative balance is disabled
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
|
||||
leave_type = frappe.get_doc(dict(
|
||||
leave_type_name="Test Leave Validation",
|
||||
doctype="Leave Type",
|
||||
allow_negative=False
|
||||
)).insert()
|
||||
|
||||
employee = get_employee()
|
||||
date = getdate()
|
||||
first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date))
|
||||
|
||||
leave_application = frappe.get_doc(dict(
|
||||
doctype='Leave Application',
|
||||
employee=employee.name,
|
||||
leave_type=leave_type.name,
|
||||
from_date=add_days(first_sunday, 1),
|
||||
to_date=add_days(first_sunday, 4),
|
||||
company="_Test Company",
|
||||
status="Approved",
|
||||
leave_approver = 'test@example.com'
|
||||
))
|
||||
# Application period cannot be outside leave allocation period
|
||||
self.assertRaises(frappe.ValidationError, leave_application.insert)
|
||||
|
||||
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
|
||||
|
||||
leave_application = frappe.get_doc(dict(
|
||||
doctype='Leave Application',
|
||||
employee=employee.name,
|
||||
leave_type=leave_type.name,
|
||||
from_date=add_days(first_sunday, -10),
|
||||
to_date=add_days(first_sunday, 1),
|
||||
company="_Test Company",
|
||||
status="Approved",
|
||||
leave_approver = 'test@example.com'
|
||||
))
|
||||
|
||||
# Application period cannot be across two allocation records
|
||||
self.assertRaises(LeaveAcrossAllocationsError, leave_application.insert)
|
||||
|
||||
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||
def test_insufficient_leave_balance_validation(self):
|
||||
# CASE 1: Validation when allow negative is disabled
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
|
||||
leave_type = frappe.get_doc(dict(
|
||||
leave_type_name="Test Leave Validation",
|
||||
doctype="Leave Type",
|
||||
allow_negative=False
|
||||
)).insert()
|
||||
|
||||
employee = get_employee()
|
||||
date = getdate()
|
||||
first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date))
|
||||
|
||||
# allocate 2 leaves, apply for more
|
||||
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date), leaves=2)
|
||||
leave_application = frappe.get_doc(dict(
|
||||
doctype='Leave Application',
|
||||
employee=employee.name,
|
||||
leave_type=leave_type.name,
|
||||
from_date=add_days(first_sunday, 1),
|
||||
to_date=add_days(first_sunday, 3),
|
||||
company="_Test Company",
|
||||
status="Approved",
|
||||
leave_approver = 'test@example.com'
|
||||
))
|
||||
self.assertRaises(InsufficientLeaveBalanceError, leave_application.insert)
|
||||
|
||||
# CASE 2: Allows creating application with a warning message when allow negative is enabled
|
||||
frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True)
|
||||
make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name)
|
||||
|
||||
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||
def test_separate_leave_ledger_entry_for_boundary_applications(self):
|
||||
# When application falls in 2 different allocations and Allow Negative is enabled
|
||||
# creates separate leave ledger entries
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
|
||||
leave_type = frappe.get_doc(dict(
|
||||
leave_type_name="Test Leave Validation",
|
||||
doctype="Leave Type",
|
||||
allow_negative=True
|
||||
)).insert()
|
||||
|
||||
employee = get_employee()
|
||||
date = getdate()
|
||||
year_start = getdate(get_year_start(date))
|
||||
year_end = getdate(get_year_ending(date))
|
||||
|
||||
make_allocation_record(leave_type=leave_type.name, from_date=year_start, to_date=year_end)
|
||||
# application across allocations
|
||||
|
||||
# CASE 1: from date has no allocation, to date has an allocation / both dates have allocation
|
||||
application = make_leave_application(employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name)
|
||||
|
||||
# 2 separate leave ledger entries
|
||||
ledgers = frappe.db.get_all("Leave Ledger Entry", {
|
||||
"transaction_type": "Leave Application",
|
||||
"transaction_name": application.name
|
||||
}, ["leaves", "from_date", "to_date"], order_by="from_date")
|
||||
self.assertEqual(len(ledgers), 2)
|
||||
|
||||
self.assertEqual(ledgers[0].from_date, application.from_date)
|
||||
self.assertEqual(ledgers[0].to_date, add_days(year_start, -1))
|
||||
|
||||
self.assertEqual(ledgers[1].from_date, year_start)
|
||||
self.assertEqual(ledgers[1].to_date, application.to_date)
|
||||
|
||||
# CASE 2: from date has an allocation, to date has no allocation
|
||||
application = make_leave_application(employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name)
|
||||
|
||||
# 2 separate leave ledger entries
|
||||
ledgers = frappe.db.get_all("Leave Ledger Entry", {
|
||||
"transaction_type": "Leave Application",
|
||||
"transaction_name": application.name
|
||||
}, ["leaves", "from_date", "to_date"], order_by="from_date")
|
||||
self.assertEqual(len(ledgers), 2)
|
||||
|
||||
self.assertEqual(ledgers[0].from_date, application.from_date)
|
||||
self.assertEqual(ledgers[0].to_date, year_end)
|
||||
|
||||
self.assertEqual(ledgers[1].from_date, add_days(year_end, 1))
|
||||
self.assertEqual(ledgers[1].to_date, application.to_date)
|
||||
|
||||
def test_overwrite_attendance(self):
|
||||
'''check attendance is automatically created on leave approval'''
|
||||
make_allocation_record()
|
||||
@@ -119,6 +262,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
|
||||
self.assertTrue(getdate(d) in dates)
|
||||
|
||||
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||
def test_attendance_for_include_holidays(self):
|
||||
# Case 1: leave type with 'Include holidays within leaves as leaves' enabled
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
|
||||
@@ -131,10 +275,8 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
date = getdate()
|
||||
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
|
||||
|
||||
holiday_list = make_holiday_list()
|
||||
employee = get_employee()
|
||||
frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
|
||||
first_sunday = get_first_sunday(holiday_list)
|
||||
first_sunday = get_first_sunday(self.holiday_list)
|
||||
|
||||
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
|
||||
leave_application.reload()
|
||||
@@ -143,6 +285,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
|
||||
leave_application.cancel()
|
||||
|
||||
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||
def test_attendance_update_for_exclude_holidays(self):
|
||||
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
|
||||
@@ -155,10 +298,8 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
date = getdate()
|
||||
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
|
||||
|
||||
holiday_list = make_holiday_list()
|
||||
employee = get_employee()
|
||||
frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
|
||||
first_sunday = get_first_sunday(holiday_list)
|
||||
first_sunday = get_first_sunday(self.holiday_list)
|
||||
|
||||
# already marked attendance on a holiday should be deleted in this case
|
||||
config = {
|
||||
@@ -177,8 +318,9 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
attendance.flags.ignore_validate = True
|
||||
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()
|
||||
|
||||
# holiday should be excluded while marking attendance
|
||||
self.assertEqual(leave_application.total_leave_days, 3)
|
||||
self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
|
||||
@@ -320,16 +462,14 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
application.half_day_date = "2013-01-05"
|
||||
application.insert()
|
||||
|
||||
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||
def test_optional_leave(self):
|
||||
leave_period = get_leave_period()
|
||||
today = nowdate()
|
||||
holiday_list = 'Test Holiday List for Optional Holiday'
|
||||
employee = get_employee()
|
||||
|
||||
default_holiday_list = make_holiday_list()
|
||||
frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
|
||||
first_sunday = get_first_sunday(default_holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list)
|
||||
optional_leave_date = add_days(first_sunday, 1)
|
||||
|
||||
if not frappe.db.exists('Holiday List', holiday_list):
|
||||
@@ -503,11 +643,13 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
expire_carry_forwarded_leaves_after_days=90)
|
||||
leave_type.submit()
|
||||
leave_type.insert()
|
||||
|
||||
create_carry_forwarded_allocation(employee, leave_type)
|
||||
details = get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True)
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
|
||||
self.assertEqual(details.leave_balance_for_consumption, 21)
|
||||
self.assertEqual(details.leave_balance, 30)
|
||||
|
||||
def test_earned_leaves_creation(self):
|
||||
|
||||
@@ -560,7 +702,14 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
# test to not consider current leave in leave balance while submitting
|
||||
def test_current_leave_on_submit(self):
|
||||
employee = get_employee()
|
||||
leave_type = 'Sick leave'
|
||||
|
||||
leave_type = 'Sick Leave'
|
||||
if not frappe.db.exists('Leave Type', leave_type):
|
||||
frappe.get_doc(dict(
|
||||
leave_type_name=leave_type,
|
||||
doctype='Leave Type'
|
||||
)).insert()
|
||||
|
||||
allocation = frappe.get_doc(dict(
|
||||
doctype = 'Leave Allocation',
|
||||
employee = employee.name,
|
||||
@@ -703,6 +852,35 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
employee.leave_approver = ""
|
||||
employee.save()
|
||||
|
||||
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||
def test_get_leave_details_for_dashboard(self):
|
||||
employee = get_employee()
|
||||
date = getdate()
|
||||
year_start = getdate(get_year_start(date))
|
||||
year_end = getdate(get_year_ending(date))
|
||||
|
||||
# ALLOCATION = 30
|
||||
allocation = make_allocation_record(employee=employee.name, from_date=year_start, to_date=year_end)
|
||||
|
||||
# USED LEAVES = 4
|
||||
first_sunday = get_first_sunday(self.holiday_list)
|
||||
leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type')
|
||||
leave_application.reload()
|
||||
|
||||
# LEAVES PENDING APPROVAL = 1
|
||||
leave_application = make_leave_application(employee.name, add_days(first_sunday, 5), add_days(first_sunday, 5),
|
||||
'_Test Leave Type', submit=False)
|
||||
leave_application.status = 'Open'
|
||||
leave_application.save()
|
||||
|
||||
details = get_leave_details(employee.name, allocation.from_date)
|
||||
leave_allocation = details['leave_allocation']['_Test Leave Type']
|
||||
self.assertEqual(leave_allocation['total_leaves'], 30)
|
||||
self.assertEqual(leave_allocation['leaves_taken'], 4)
|
||||
self.assertEqual(leave_allocation['expired_leaves'], 0)
|
||||
self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
|
||||
self.assertEqual(leave_allocation['remaining_leaves'], 26)
|
||||
|
||||
|
||||
def create_carry_forwarded_allocation(employee, leave_type):
|
||||
# initial leave allocation
|
||||
@@ -724,19 +902,22 @@ def create_carry_forwarded_allocation(employee, leave_type):
|
||||
carry_forward=1)
|
||||
leave_allocation.submit()
|
||||
|
||||
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
|
||||
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
|
||||
allocation = frappe.get_doc({
|
||||
"doctype": "Leave Allocation",
|
||||
"employee": employee or "_T-Employee-00001",
|
||||
"leave_type": leave_type or "_Test Leave Type",
|
||||
"from_date": from_date or "2013-01-01",
|
||||
"to_date": to_date or "2019-12-31",
|
||||
"new_leaves_allocated": 30
|
||||
"new_leaves_allocated": leaves or 30,
|
||||
"carry_forward": carry_forward
|
||||
})
|
||||
|
||||
allocation.insert(ignore_permissions=True)
|
||||
allocation.submit()
|
||||
|
||||
return allocation
|
||||
|
||||
def get_employee():
|
||||
return frappe.get_doc("Employee", "_T-Employee-00001")
|
||||
|
||||
@@ -781,9 +962,10 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
|
||||
allocate_leave.submit()
|
||||
|
||||
|
||||
def get_first_sunday(holiday_list):
|
||||
month_start_date = get_first_day(nowdate())
|
||||
month_end_date = get_last_day(nowdate())
|
||||
def get_first_sunday(holiday_list, for_date=None):
|
||||
date = for_date or getdate()
|
||||
month_start_date = get_first_day(date)
|
||||
month_end_date = get_last_day(date)
|
||||
first_sunday = frappe.db.sql("""
|
||||
select holiday_date from `tabHoliday`
|
||||
where parent = %s
|
||||
@@ -791,4 +973,4 @@ def get_first_sunday(holiday_list):
|
||||
order by holiday_date
|
||||
""", (holiday_list, month_start_date, month_end_date))[0][0]
|
||||
|
||||
return first_sunday
|
||||
return first_sunday
|
||||
|
||||
@@ -171,7 +171,7 @@ def expire_carried_forward_allocation(allocation):
|
||||
''' Expires remaining leaves in the on carried forward allocation '''
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
|
||||
leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type,
|
||||
allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True)
|
||||
allocation.from_date, allocation.to_date, skip_expired_leaves=False)
|
||||
leaves = flt(allocation.leaves) + flt(leaves_taken)
|
||||
|
||||
# allow expired leaves entry to be created
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, get_first_day, get_last_day, getdate
|
||||
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_employee,
|
||||
@@ -94,9 +94,12 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
|
||||
# second last day of the month
|
||||
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
|
||||
frappe.flags.current_date = add_days(get_last_day(getdate()), -1)
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
|
||||
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
|
||||
@@ -3,18 +3,21 @@
|
||||
|
||||
|
||||
from itertools import groupby
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation
|
||||
from erpnext.hr.doctype.leave_application.leave_application import (
|
||||
get_leave_balance_on,
|
||||
get_leaves_for_period,
|
||||
)
|
||||
|
||||
Filters = frappe._dict
|
||||
|
||||
def execute(filters=None):
|
||||
def execute(filters: Optional[Filters] = None) -> Tuple:
|
||||
if filters.to_date <= filters.from_date:
|
||||
frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
|
||||
|
||||
@@ -23,8 +26,9 @@ def execute(filters=None):
|
||||
charts = get_chart_data(data)
|
||||
return columns, data, None, charts
|
||||
|
||||
def get_columns():
|
||||
columns = [{
|
||||
|
||||
def get_columns() -> List[Dict]:
|
||||
return [{
|
||||
'label': _('Leave Type'),
|
||||
'fieldtype': 'Link',
|
||||
'fieldname': 'leave_type',
|
||||
@@ -46,32 +50,31 @@ def get_columns():
|
||||
'label': _('Opening Balance'),
|
||||
'fieldtype': 'float',
|
||||
'fieldname': 'opening_balance',
|
||||
'width': 130,
|
||||
'width': 150,
|
||||
}, {
|
||||
'label': _('Leave Allocated'),
|
||||
'label': _('New Leave(s) Allocated'),
|
||||
'fieldtype': 'float',
|
||||
'fieldname': 'leaves_allocated',
|
||||
'width': 130,
|
||||
'width': 200,
|
||||
}, {
|
||||
'label': _('Leave Taken'),
|
||||
'label': _('Leave(s) Taken'),
|
||||
'fieldtype': 'float',
|
||||
'fieldname': 'leaves_taken',
|
||||
'width': 130,
|
||||
'width': 150,
|
||||
}, {
|
||||
'label': _('Leave Expired'),
|
||||
'label': _('Leave(s) Expired'),
|
||||
'fieldtype': 'float',
|
||||
'fieldname': 'leaves_expired',
|
||||
'width': 130,
|
||||
'width': 150,
|
||||
}, {
|
||||
'label': _('Closing Balance'),
|
||||
'fieldtype': 'float',
|
||||
'fieldname': 'closing_balance',
|
||||
'width': 130,
|
||||
'width': 150,
|
||||
}]
|
||||
|
||||
return columns
|
||||
|
||||
def get_data(filters):
|
||||
def get_data(filters: Filters) -> List:
|
||||
leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name')
|
||||
conditions = get_conditions(filters)
|
||||
|
||||
@@ -102,19 +105,18 @@ def get_data(filters):
|
||||
or ("HR Manager" in frappe.get_roles(user)):
|
||||
if len(active_employees) > 1:
|
||||
row = frappe._dict()
|
||||
row.employee = employee.name,
|
||||
row.employee = employee.name
|
||||
row.employee_name = employee.employee_name
|
||||
|
||||
leaves_taken = get_leaves_for_period(employee.name, leave_type,
|
||||
filters.from_date, filters.to_date) * -1
|
||||
|
||||
new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type)
|
||||
|
||||
|
||||
opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition
|
||||
new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
|
||||
filters.from_date, filters.to_date, employee.name, leave_type)
|
||||
opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
|
||||
|
||||
row.leaves_allocated = new_allocation
|
||||
row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0
|
||||
row.leaves_expired = expired_leaves
|
||||
row.opening_balance = opening
|
||||
row.leaves_taken = leaves_taken
|
||||
|
||||
@@ -125,7 +127,26 @@ def get_data(filters):
|
||||
|
||||
return data
|
||||
|
||||
def get_conditions(filters):
|
||||
|
||||
def get_opening_balance(employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float) -> float:
|
||||
# allocation boundary condition
|
||||
# opening balance is the closing leave balance 1 day before the filter start date
|
||||
opening_balance_date = add_days(filters.from_date, -1)
|
||||
allocation = get_previous_allocation(filters.from_date, leave_type, employee)
|
||||
|
||||
if allocation and allocation.get("to_date") and opening_balance_date and \
|
||||
getdate(allocation.get("to_date")) == getdate(opening_balance_date):
|
||||
# if opening balance date is same as the previous allocation's expiry
|
||||
# then opening balance should only consider carry forwarded leaves
|
||||
opening_balance = carry_forwarded_leaves
|
||||
else:
|
||||
# else directly get leave balance on the previous day
|
||||
opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date)
|
||||
|
||||
return opening_balance
|
||||
|
||||
|
||||
def get_conditions(filters: Filters) -> Dict:
|
||||
conditions={
|
||||
'status': 'Active',
|
||||
}
|
||||
@@ -140,29 +161,26 @@ def get_conditions(filters):
|
||||
|
||||
return conditions
|
||||
|
||||
def get_department_leave_approver_map(department=None):
|
||||
|
||||
def get_department_leave_approver_map(department: Optional[str] = None):
|
||||
# get current department and all its child
|
||||
department_list = frappe.get_list('Department',
|
||||
filters={
|
||||
'disabled': 0
|
||||
},
|
||||
or_filters={
|
||||
'name': department,
|
||||
'parent_department': department
|
||||
},
|
||||
fields=['name'],
|
||||
pluck='name'
|
||||
)
|
||||
filters={'disabled': 0},
|
||||
or_filters={
|
||||
'name': department,
|
||||
'parent_department': department
|
||||
},
|
||||
pluck='name'
|
||||
)
|
||||
# retrieve approvers list from current department and from its subsequent child departments
|
||||
approver_list = frappe.get_all('Department Approver',
|
||||
filters={
|
||||
'parentfield': 'leave_approvers',
|
||||
'parent': ('in', department_list)
|
||||
},
|
||||
fields=['parent', 'approver'],
|
||||
as_list=1
|
||||
)
|
||||
filters={
|
||||
'parentfield': 'leave_approvers',
|
||||
'parent': ('in', department_list)
|
||||
},
|
||||
fields=['parent', 'approver'],
|
||||
as_list=True
|
||||
)
|
||||
|
||||
approvers = {}
|
||||
|
||||
@@ -171,41 +189,61 @@ def get_department_leave_approver_map(department=None):
|
||||
|
||||
return approvers
|
||||
|
||||
def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
|
||||
|
||||
from frappe.utils import getdate
|
||||
|
||||
def get_allocated_and_expired_leaves(from_date: str, to_date: str, employee: str, leave_type: str) -> Tuple[float, float, float]:
|
||||
new_allocation = 0
|
||||
expired_leaves = 0
|
||||
carry_forwarded_leaves = 0
|
||||
|
||||
records= frappe.db.sql("""
|
||||
SELECT
|
||||
employee, leave_type, from_date, to_date, leaves, transaction_name,
|
||||
transaction_type, is_carry_forward, is_expired
|
||||
FROM `tabLeave Ledger Entry`
|
||||
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
|
||||
AND docstatus=1
|
||||
AND transaction_type = 'Leave Allocation'
|
||||
AND (from_date between %(from_date)s AND %(to_date)s
|
||||
OR to_date between %(from_date)s AND %(to_date)s
|
||||
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
|
||||
""", {
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"employee": employee,
|
||||
"leave_type": leave_type
|
||||
}, as_dict=1)
|
||||
records = get_leave_ledger_entries(from_date, to_date, employee, leave_type)
|
||||
|
||||
for record in records:
|
||||
# new allocation records with `is_expired=1` are created when leave expires
|
||||
# these new records should not be considered, else it leads to negative leave balance
|
||||
if record.is_expired:
|
||||
continue
|
||||
|
||||
if record.to_date < getdate(to_date):
|
||||
# leave allocations ending before to_date, reduce leaves taken within that period
|
||||
# since they are already used, they won't expire
|
||||
expired_leaves += record.leaves
|
||||
expired_leaves += get_leaves_for_period(employee, leave_type,
|
||||
record.from_date, record.to_date)
|
||||
|
||||
if record.from_date >= getdate(from_date):
|
||||
new_allocation += record.leaves
|
||||
if record.is_carry_forward:
|
||||
carry_forwarded_leaves += record.leaves
|
||||
else:
|
||||
new_allocation += record.leaves
|
||||
|
||||
return new_allocation, expired_leaves
|
||||
return new_allocation, expired_leaves, carry_forwarded_leaves
|
||||
|
||||
def get_chart_data(data):
|
||||
|
||||
def get_leave_ledger_entries(from_date: str, to_date: str, employee: str, leave_type: str) -> List[Dict]:
|
||||
ledger = frappe.qb.DocType('Leave Ledger Entry')
|
||||
records = (
|
||||
frappe.qb.from_(ledger)
|
||||
.select(
|
||||
ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date,
|
||||
ledger.leaves, ledger.transaction_name, ledger.transaction_type,
|
||||
ledger.is_carry_forward, ledger.is_expired
|
||||
).where(
|
||||
(ledger.docstatus == 1)
|
||||
& (ledger.transaction_type == 'Leave Allocation')
|
||||
& (ledger.employee == employee)
|
||||
& (ledger.leave_type == leave_type)
|
||||
& (
|
||||
(ledger.from_date[from_date: to_date])
|
||||
| (ledger.to_date[from_date: to_date])
|
||||
| ((ledger.from_date < from_date) & (ledger.to_date > to_date))
|
||||
)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def get_chart_data(data: List) -> Dict:
|
||||
labels = []
|
||||
datasets = []
|
||||
employee_data = data
|
||||
@@ -224,7 +262,8 @@ def get_chart_data(data):
|
||||
|
||||
return chart
|
||||
|
||||
def get_dataset_for_chart(employee_data, datasets, labels):
|
||||
|
||||
def get_dataset_for_chart(employee_data: List, datasets: List, labels: List) -> List:
|
||||
leaves = []
|
||||
employee_data = sorted(employee_data, key=lambda k: k['employee_name'])
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -14,11 +14,15 @@
|
||||
"applicant",
|
||||
"section_break_7",
|
||||
"disbursement_date",
|
||||
"clearance_date",
|
||||
"column_break_8",
|
||||
"disbursed_amount",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"customer_details_section",
|
||||
"accounting_details",
|
||||
"disbursement_account",
|
||||
"column_break_16",
|
||||
"loan_account",
|
||||
"bank_account",
|
||||
"disbursement_references_section",
|
||||
"reference_date",
|
||||
@@ -106,11 +110,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Disbursement Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Customer Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.applicant_type",
|
||||
"fieldname": "applicant_type",
|
||||
@@ -149,15 +148,48 @@
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 18:09:32.175355",
|
||||
"modified": "2022-02-17 18:23:44.157598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Disbursement",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -194,5 +226,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -42,9 +42,6 @@ class LoanDisbursement(AccountsController):
|
||||
if not self.posting_date:
|
||||
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):
|
||||
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):
|
||||
gle_map = []
|
||||
loan_details = frappe.get_doc("Loan", self.against_loan)
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.loan_account,
|
||||
"against": loan_details.disbursement_account,
|
||||
"account": self.loan_account,
|
||||
"against": self.disbursement_account,
|
||||
"debit": self.disbursed_amount,
|
||||
"debit_in_account_currency": self.disbursed_amount,
|
||||
"against_voucher_type": "Loan",
|
||||
@@ -137,8 +133,8 @@ class LoanDisbursement(AccountsController):
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.disbursement_account,
|
||||
"against": loan_details.loan_account,
|
||||
"account": self.disbursement_account,
|
||||
"against": self.loan_account,
|
||||
"credit": self.disbursed_amount,
|
||||
"credit_in_account_currency": self.disbursed_amount,
|
||||
"against_voucher_type": "Loan",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "LM-REP-.####",
|
||||
"creation": "2019-09-03 14:44:39.977266",
|
||||
"creation": "2022-01-25 10:30:02.767941",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
@@ -13,6 +13,7 @@
|
||||
"column_break_3",
|
||||
"company",
|
||||
"posting_date",
|
||||
"clearance_date",
|
||||
"rate_of_interest",
|
||||
"payroll_payable_account",
|
||||
"is_term_loan",
|
||||
@@ -37,7 +38,12 @@
|
||||
"total_penalty_paid",
|
||||
"total_interest_paid",
|
||||
"repayment_details",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"accounting_details_section",
|
||||
"payment_account",
|
||||
"penalty_income_account",
|
||||
"column_break_36",
|
||||
"loan_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -260,12 +266,52 @@
|
||||
"fieldname": "repay_from_salary",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-06 01:51:06.707782",
|
||||
"modified": "2022-02-18 19:10:07.742298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Repayment",
|
||||
|
||||
@@ -311,7 +311,6 @@ class LoanRepayment(AccountsController):
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gle_map = []
|
||||
loan_details = frappe.get_doc("Loan", self.against_loan)
|
||||
|
||||
if self.shortfall_amount and self.amount_paid > 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:
|
||||
payment_account = self.payroll_payable_account
|
||||
else:
|
||||
payment_account = loan_details.payment_account
|
||||
payment_account = self.payment_account
|
||||
|
||||
if self.total_penalty_paid:
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.loan_account,
|
||||
"against": loan_details.payment_account,
|
||||
"account": self.loan_account,
|
||||
"against": payment_account,
|
||||
"debit": self.total_penalty_paid,
|
||||
"debit_in_account_currency": self.total_penalty_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
@@ -345,8 +344,8 @@ class LoanRepayment(AccountsController):
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.penalty_income_account,
|
||||
"against": loan_details.loan_account,
|
||||
"account": self.penalty_income_account,
|
||||
"against": self.loan_account,
|
||||
"credit": self.total_penalty_paid,
|
||||
"credit_in_account_currency": self.total_penalty_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
@@ -360,8 +359,7 @@ class LoanRepayment(AccountsController):
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": payment_account,
|
||||
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
|
||||
+ ", " + loan_details.penalty_income_account,
|
||||
"against": self.loan_account + ", " + self.penalty_income_account,
|
||||
"debit": self.amount_paid,
|
||||
"debit_in_account_currency": self.amount_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
@@ -369,16 +367,16 @@ class LoanRepayment(AccountsController):
|
||||
"remarks": remarks,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
"party_type": loan_details.applicant_type if self.repay_from_salary else '',
|
||||
"party": loan_details.applicant if self.repay_from_salary else ''
|
||||
"party_type": self.applicant_type if self.repay_from_salary else '',
|
||||
"party": self.applicant if self.repay_from_salary else ''
|
||||
})
|
||||
)
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.loan_account,
|
||||
"party_type": loan_details.applicant_type,
|
||||
"party": loan_details.applicant,
|
||||
"account": self.loan_account,
|
||||
"party_type": self.applicant_type,
|
||||
"party": self.applicant,
|
||||
"against": payment_account,
|
||||
"credit": self.amount_paid,
|
||||
"credit_in_account_currency": self.amount_paid,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months, today
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
from .blanket_order import make_order
|
||||
|
||||
|
||||
class TestBlanketOrder(ERPNextTestCase):
|
||||
class TestBlanketOrder(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.flags.args = frappe._dict()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import functools
|
||||
import re
|
||||
from collections import deque
|
||||
from operator import itemgetter
|
||||
from typing import List
|
||||
@@ -103,25 +104,33 @@ class BOM(WebsiteGenerator):
|
||||
)
|
||||
|
||||
def autoname(self):
|
||||
names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item)
|
||||
# ignore amended documents while calculating current index
|
||||
existing_boms = frappe.get_all(
|
||||
"BOM",
|
||||
filters={"item": self.item, "amended_from": ["is", "not set"]},
|
||||
pluck="name"
|
||||
)
|
||||
|
||||
if names:
|
||||
# name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1
|
||||
|
||||
# split by item
|
||||
names = [name.split(self.item, 1) for name in names]
|
||||
names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)]
|
||||
|
||||
# split by (-) if cancelled
|
||||
if names:
|
||||
names = [cint(name.split('-')[-1]) for name in names]
|
||||
idx = max(names) + 1
|
||||
else:
|
||||
idx = 1
|
||||
if existing_boms:
|
||||
index = self.get_next_version_index(existing_boms)
|
||||
else:
|
||||
idx = 1
|
||||
index = 1
|
||||
|
||||
prefix = self.doctype
|
||||
suffix = "%.3i" % index # convert index to string (1 -> "001")
|
||||
bom_name = f"{prefix}-{self.item}-{suffix}"
|
||||
|
||||
if len(bom_name) <= 140:
|
||||
name = bom_name
|
||||
else:
|
||||
# since max characters for name is 140, remove enough characters from the
|
||||
# item name to fit the prefix, suffix and the separators
|
||||
truncated_length = 140 - (len(prefix) + len(suffix) + 2)
|
||||
truncated_item_name = self.item[:truncated_length]
|
||||
# if a partial word is found after truncate, remove the extra characters
|
||||
truncated_item_name = truncated_item_name.rsplit(" ", 1)[0]
|
||||
name = f"{prefix}-{truncated_item_name}-{suffix}"
|
||||
|
||||
name = 'BOM-' + self.item + ('-%.3i' % idx)
|
||||
if frappe.db.exists("BOM", name):
|
||||
conflicting_bom = frappe.get_doc("BOM", name)
|
||||
|
||||
@@ -134,6 +143,26 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
self.name = name
|
||||
|
||||
@staticmethod
|
||||
def get_next_version_index(existing_boms: List[str]) -> int:
|
||||
# split by "/" and "-"
|
||||
delimiters = ["/", "-"]
|
||||
pattern = "|".join(map(re.escape, delimiters))
|
||||
bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms]
|
||||
|
||||
# filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001
|
||||
valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
|
||||
|
||||
# extract the current index from the BOM parts
|
||||
if valid_bom_parts:
|
||||
# handle cancelled and submitted documents
|
||||
indexes = [cint(part[-1]) for part in valid_bom_parts]
|
||||
index = max(indexes) + 1
|
||||
else:
|
||||
index = 1
|
||||
|
||||
return index
|
||||
|
||||
def validate(self):
|
||||
self.route = frappe.scrub(self.name).replace('_', '-')
|
||||
|
||||
@@ -141,6 +170,7 @@ class BOM(WebsiteGenerator):
|
||||
frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
|
||||
|
||||
self.clear_operations()
|
||||
self.clear_inspection()
|
||||
self.validate_main_item()
|
||||
self.validate_currency()
|
||||
self.set_conversion_rate()
|
||||
@@ -192,12 +222,13 @@ class BOM(WebsiteGenerator):
|
||||
if self.routing:
|
||||
self.set("operations", [])
|
||||
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,
|
||||
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
|
||||
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):
|
||||
for item in self.get("items"):
|
||||
@@ -386,6 +417,10 @@ class BOM(WebsiteGenerator):
|
||||
if not self.with_operations:
|
||||
self.set('operations', [])
|
||||
|
||||
def clear_inspection(self):
|
||||
if not self.inspection_required:
|
||||
self.quality_inspection_template = None
|
||||
|
||||
def validate_main_item(self):
|
||||
""" Validate main FG item"""
|
||||
item = self.get_item_det(self.item)
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import deque
|
||||
from functools import partial
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -17,15 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_records = frappe.get_test_records('BOM')
|
||||
test_dependencies = ["Item", "Quality Inspection Template"]
|
||||
|
||||
class TestBOM(ERPNextTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.get_value('Item', '_Test Item'):
|
||||
make_test_records('Item')
|
||||
|
||||
class TestBOM(FrappeTestCase):
|
||||
def test_get_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||
items_dict = get_bom_items_as_dict(bom=get_default_bom(),
|
||||
@@ -385,6 +381,87 @@ class TestBOM(ERPNextTestCase):
|
||||
self.assertEqual(bom.transfer_material_against, "Work Order")
|
||||
bom.delete()
|
||||
|
||||
def test_bom_name_length(self):
|
||||
""" test >140 char names"""
|
||||
bom_tree = {
|
||||
"x" * 140 : {
|
||||
" ".join(["abc"] * 35): {}
|
||||
}
|
||||
}
|
||||
create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
def test_version_index(self):
|
||||
|
||||
bom = frappe.new_doc("BOM")
|
||||
|
||||
version_index_test_cases = [
|
||||
(1, []),
|
||||
(1, ["BOM#XYZ"]),
|
||||
(2, ["BOM/ITEM/001"]),
|
||||
(2, ["BOM-ITEM-001"]),
|
||||
(3, ["BOM-ITEM-001", "BOM-ITEM-002"]),
|
||||
(4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]),
|
||||
]
|
||||
|
||||
for expected_index, existing_boms in version_index_test_cases:
|
||||
with self.subTest():
|
||||
self.assertEqual(expected_index, bom.get_next_version_index(existing_boms),
|
||||
msg=f"Incorrect index for {existing_boms}")
|
||||
|
||||
def test_bom_versioning(self):
|
||||
bom_tree = {
|
||||
frappe.generate_hash(length=10) : {
|
||||
frappe.generate_hash(length=10): {}
|
||||
}
|
||||
}
|
||||
bom = create_nested_bom(bom_tree, prefix="")
|
||||
self.assertEqual(int(bom.name.split("-")[-1]), 1)
|
||||
original_bom_name = bom.name
|
||||
|
||||
bom.cancel()
|
||||
bom.reload()
|
||||
self.assertEqual(bom.name, original_bom_name)
|
||||
|
||||
# create a new amendment
|
||||
amendment = frappe.copy_doc(bom)
|
||||
amendment.docstatus = 0
|
||||
amendment.amended_from = bom.name
|
||||
|
||||
amendment.save()
|
||||
amendment.submit()
|
||||
amendment.reload()
|
||||
|
||||
self.assertNotEqual(amendment.name, bom.name)
|
||||
# `origname-001-1` version
|
||||
self.assertEqual(int(amendment.name.split("-")[-1]), 1)
|
||||
self.assertEqual(int(amendment.name.split("-")[-2]), 1)
|
||||
|
||||
# create a new version
|
||||
version = frappe.copy_doc(amendment)
|
||||
version.docstatus = 0
|
||||
version.amended_from = None
|
||||
version.save()
|
||||
self.assertNotEqual(amendment.name, version.name)
|
||||
self.assertEqual(int(version.name.split("-")[-1]), 2)
|
||||
|
||||
def test_clear_inpection_quality(self):
|
||||
|
||||
bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
|
||||
bom.docstatus = 0
|
||||
bom.is_default = 0
|
||||
bom.quality_inspection_template = "_Test Quality Inspection Template"
|
||||
bom.inspection_required = 1
|
||||
bom.save()
|
||||
bom.reload()
|
||||
|
||||
self.assertEqual(bom.quality_inspection_template, '_Test Quality Inspection Template')
|
||||
|
||||
bom.inspection_required = 0
|
||||
bom.save()
|
||||
bom.reload()
|
||||
|
||||
self.assertEqual(bom.quality_inspection_template, None)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -65,7 +65,8 @@
|
||||
"label": "Hour Rate",
|
||||
"oldfieldname": "hour_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"description": "In minutes",
|
||||
@@ -177,7 +178,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-13 16:45:01.092868",
|
||||
"modified": "2022-03-10 06:19:08.462027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operation",
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_records = frappe.get_test_records('BOM')
|
||||
|
||||
class TestBOMUpdateTool(ERPNextTestCase):
|
||||
class TestBOMUpdateTool(FrappeTestCase):
|
||||
def test_replace_bom(self):
|
||||
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class JobCard(Document):
|
||||
self.validate_work_order()
|
||||
|
||||
def set_sub_operations(self):
|
||||
if self.operation:
|
||||
if not self.sub_operations and self.operation:
|
||||
self.sub_operations = []
|
||||
for row in frappe.get_all('Sub Operation',
|
||||
filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
frappe.listview_settings['Job Card'] = {
|
||||
has_indicator_for_draft: true,
|
||||
get_indicator: function(doc) {
|
||||
if (doc.status === "Work In Progress") {
|
||||
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import random_string
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
|
||||
@@ -11,10 +12,9 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestJobCard(ERPNextTestCase):
|
||||
class TestJobCard(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_bom_for_jc_tests()
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
"label": "Select Items to Manufacture"
|
||||
},
|
||||
{
|
||||
"depends_on": "get_items_from",
|
||||
"depends_on": "eval:doc.get_items_from && doc.docstatus == 0",
|
||||
"fieldname": "get_items",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Finished Goods for Manufacture"
|
||||
@@ -197,6 +197,7 @@
|
||||
{
|
||||
"fieldname": "po_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Assembly Items",
|
||||
"no_copy": 1,
|
||||
"options": "Production Plan Item",
|
||||
"reqd": 1
|
||||
@@ -350,6 +351,7 @@
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "get_items_from",
|
||||
"fieldname": "sub_assembly_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Assembly Items",
|
||||
@@ -357,6 +359,7 @@
|
||||
"options": "Production Plan Sub Assembly Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.po_items && doc.po_items.length && doc.docstatus == 0",
|
||||
"fieldname": "get_sub_assembly_items",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Sub Assembly Items"
|
||||
@@ -376,7 +379,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-06 18:35:59.642232",
|
||||
"modified": "2022-03-14 03:56:23.209247",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
@@ -397,5 +400,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_to_date, flt, now_datetime, nowdate
|
||||
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
@@ -16,10 +17,9 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestProductionPlan(ERPNextTestCase):
|
||||
class TestProductionPlan(FrappeTestCase):
|
||||
def setUp(self):
|
||||
for item in ['Test Production Item 1', 'Subassembly Item 1',
|
||||
'Raw Material Item 1', 'Raw Material Item 2']:
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
# See license.txt
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestRouting(ERPNextTestCase):
|
||||
class TestRouting(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.item_code = "Test Routing Item - A"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings, timeout
|
||||
from frappe.utils import add_days, add_months, cint, flt, now, today
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
||||
@@ -21,10 +22,9 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.utils import get_bin
|
||||
from erpnext.tests.utils import ERPNextTestCase, timeout
|
||||
|
||||
|
||||
class TestWorkOrder(ERPNextTestCase):
|
||||
class TestWorkOrder(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.warehouse = '_Test Warehouse 2 - _TC'
|
||||
self.item = '_Test Item'
|
||||
@@ -937,6 +937,28 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
frappe.db.set_value("Manufacturing Settings", None,
|
||||
"backflush_raw_materials_based_on", "BOM")
|
||||
|
||||
@change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
|
||||
def test_auto_batch_creation(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
fg_item = frappe.generate_hash(length=20)
|
||||
child_item = frappe.generate_hash(length=20)
|
||||
|
||||
bom_tree = {fg_item: {child_item: {}}}
|
||||
|
||||
create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
item = frappe.get_doc("Item", fg_item)
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 0
|
||||
item.save()
|
||||
|
||||
try:
|
||||
make_wo_order_test_record(item=fg_item)
|
||||
except frappe.MandatoryError:
|
||||
self.fail("Batch generation causing failing in Work Order")
|
||||
|
||||
|
||||
def update_job_card(job_card, jc_qty=None):
|
||||
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
|
||||
|
||||
|
||||
@@ -333,6 +333,14 @@ class WorkOrder(Document):
|
||||
if not self.batch_size:
|
||||
self.batch_size = total_qty
|
||||
|
||||
batch_auto_creation = frappe.get_cached_value("Item", self.production_item, "create_new_batch")
|
||||
if not batch_auto_creation:
|
||||
frappe.msgprint(
|
||||
_("Batch not created for item {} since it does not have a batch series.")
|
||||
.format(frappe.bold(self.production_item)),
|
||||
alert=True, indicator="orange")
|
||||
return
|
||||
|
||||
while total_qty > 0:
|
||||
qty = self.batch_size
|
||||
if self.batch_size >= total_qty:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# See license.txt
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_bom
|
||||
@@ -10,13 +11,12 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
|
||||
WorkstationHolidayError,
|
||||
check_if_within_operating_hours,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Warehouse"]
|
||||
test_records = frappe.get_test_records('Workstation')
|
||||
make_test_records('Workstation')
|
||||
|
||||
class TestWorkstation(ERPNextTestCase):
|
||||
class TestWorkstation(FrappeTestCase):
|
||||
def test_validate_timings(self):
|
||||
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
|
||||
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"paid",
|
||||
"amount",
|
||||
"mode_of_payment",
|
||||
"razorpay_payment_id",
|
||||
"column_break_12",
|
||||
"payment_id",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
@@ -73,12 +74,6 @@
|
||||
"label": "Mode of Payment",
|
||||
"options": "Mode of Payment"
|
||||
},
|
||||
{
|
||||
"fieldname": "razorpay_payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Razorpay Payment ID",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
@@ -108,12 +103,21 @@
|
||||
"options": "Donation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-11 10:53:11.269005",
|
||||
"modified": "2022-03-16 17:18:45.611741",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Non Profit",
|
||||
"name": "Donation",
|
||||
|
||||
@@ -102,7 +102,7 @@ def capture_razorpay_donations(*args, **kwargs):
|
||||
if not donor:
|
||||
donor = create_donor(payment)
|
||||
|
||||
donation = create_donation(donor, payment)
|
||||
donation = create_razorpay_donation(donor, payment)
|
||||
donation.run_method('create_payment_entry')
|
||||
|
||||
except Exception as e:
|
||||
@@ -114,7 +114,7 @@ def capture_razorpay_donations(*args, **kwargs):
|
||||
return { 'status': 'Success' }
|
||||
|
||||
|
||||
def create_donation(donor, payment):
|
||||
def create_razorpay_donation(donor, payment):
|
||||
if not frappe.db.exists('Mode of Payment', payment.method):
|
||||
create_mode_of_payment(payment.method)
|
||||
|
||||
@@ -128,7 +128,7 @@ def create_donation(donor, payment):
|
||||
'date': getdate(),
|
||||
'amount': flt(payment.amount) / 100, # Convert to rupees from paise
|
||||
'mode_of_payment': payment.method,
|
||||
'razorpay_payment_id': payment.id
|
||||
'payment_id': payment.id
|
||||
}).insert(ignore_mandatory=True)
|
||||
|
||||
donation.submit()
|
||||
|
||||
@@ -5,7 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.non_profit.doctype.donation.donation import create_donation
|
||||
from erpnext.non_profit.doctype.donation.donation import create_razorpay_donation
|
||||
|
||||
|
||||
class TestDonation(unittest.TestCase):
|
||||
@@ -30,7 +30,7 @@ class TestDonation(unittest.TestCase):
|
||||
'method': 'Debit Card',
|
||||
'id': 'pay_MeXAmsgeKOhq7O'
|
||||
})
|
||||
donation = create_donation(donor, payment)
|
||||
donation = create_razorpay_donation(donor, payment)
|
||||
|
||||
self.assertTrue(donation.name)
|
||||
|
||||
|
||||
@@ -100,10 +100,13 @@ def create_customer(user_details, member=None):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = user_details.fullname
|
||||
customer.customer_type = "Individual"
|
||||
customer.customer_group = frappe.db.get_single_value("Selling Settings", "customer_group")
|
||||
customer.territory = frappe.db.get_single_value("Selling Settings", "territory")
|
||||
customer.flags.ignore_mandatory = True
|
||||
customer.insert(ignore_permissions=True)
|
||||
|
||||
try:
|
||||
frappe.db.savepoint("contact_creation")
|
||||
contact = frappe.new_doc("Contact")
|
||||
contact.first_name = user_details.fullname
|
||||
if user_details.mobile:
|
||||
@@ -129,6 +132,7 @@ def create_customer(user_details, member=None):
|
||||
return customer.name
|
||||
|
||||
except Exception as e:
|
||||
frappe.db.rollback(save_point="contact_creation")
|
||||
frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed"))
|
||||
pass
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
"paid",
|
||||
"currency",
|
||||
"amount",
|
||||
"column_break_16",
|
||||
"invoice",
|
||||
"razorpay_details_section",
|
||||
"subscription_id",
|
||||
"column_break_19",
|
||||
"payment_id"
|
||||
],
|
||||
"fields": [
|
||||
@@ -106,20 +108,17 @@
|
||||
{
|
||||
"fieldname": "razorpay_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Razorpay Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subscription ID",
|
||||
"read_only": 1
|
||||
"label": "Subscription ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment ID",
|
||||
"read_only": 1
|
||||
"label": "Payment ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice",
|
||||
@@ -140,11 +139,19 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-19 14:33:44.925122",
|
||||
"modified": "2022-03-16 17:37:28.672916",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Non Profit",
|
||||
"name": "Membership",
|
||||
|
||||
@@ -104,7 +104,7 @@ class Membership(Document):
|
||||
return invoice
|
||||
|
||||
def validate_membership_type_and_settings(self, plan, settings):
|
||||
settings_link = get_link_to_form("Membership Type", self.membership_type)
|
||||
settings_link = get_link_to_form("Non Profit Settings", "Non Profit Settings")
|
||||
|
||||
if not settings.membership_debit_account:
|
||||
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
|
||||
|
||||
@@ -351,3 +351,6 @@ erpnext.patches.v13_0.update_disbursement_account
|
||||
erpnext.patches.v13_0.update_reserved_qty_closed_wo
|
||||
erpnext.patches.v13_0.amazon_mws_deprecation_warning
|
||||
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
|
||||
erpnext.patches.v13_0.update_accounts_in_loan_docs
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
|
||||
erpnext.patches.v13_0.rename_non_profit_fields
|
||||
@@ -6,10 +6,13 @@ def execute():
|
||||
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'):
|
||||
frappe.db.sql("""
|
||||
UPDATE `tabBank` b, `tabBank Account` ba
|
||||
SET b.swift_number = ba.swift_number WHERE b.name = ba.bank
|
||||
""")
|
||||
try:
|
||||
frappe.db.sql("""
|
||||
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', 'payment_request')
|
||||
|
||||
@@ -6,14 +6,14 @@ import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
|
||||
frappe.reload_doc('hr', 'doctype', 'employee_grade')
|
||||
employee_with_assignment = []
|
||||
leave_policy = []
|
||||
|
||||
if "leave_policy" in frappe.db.get_table_columns("Employee"):
|
||||
employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1)
|
||||
|
||||
employee_with_assignment = []
|
||||
leave_policy =[]
|
||||
|
||||
#for employee
|
||||
|
||||
for employee in employees_with_leave_policy:
|
||||
alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1})
|
||||
if not alloc:
|
||||
@@ -22,12 +22,10 @@ def execute():
|
||||
employee_with_assignment.append(employee.name)
|
||||
leave_policy.append(employee.leave_policy)
|
||||
|
||||
|
||||
if "default_leave_policy" in frappe.db.get_table_columns("Employee"):
|
||||
if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
|
||||
employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1)
|
||||
|
||||
#for whole employee Grade
|
||||
|
||||
for grade in employee_grade_with_leave_policy:
|
||||
employees = get_employee_with_grade(grade.name)
|
||||
for employee in employees:
|
||||
@@ -47,13 +45,13 @@ def execute():
|
||||
allocation_exists=True)
|
||||
|
||||
def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False):
|
||||
if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
|
||||
return
|
||||
|
||||
filters = {"employee":employee, "leave_policy": leave_policy}
|
||||
if leave_period:
|
||||
filters["leave_period"] = leave_period
|
||||
|
||||
frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
|
||||
|
||||
if not frappe.db.exists("Leave Policy Assignment" , filters):
|
||||
lpa = frappe.new_doc("Leave Policy Assignment")
|
||||
lpa.employee = employee
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
17
erpnext/patches/v13_0/rename_non_profit_fields.py
Normal file
17
erpnext/patches/v13_0/rename_non_profit_fields.py
Normal 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")
|
||||
42
erpnext/patches/v13_0/update_accounts_in_loan_docs.py
Normal file
42
erpnext/patches/v13_0/update_accounts_in_loan_docs.py
Normal 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()
|
||||
@@ -671,7 +671,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
|
||||
where reference_type="Payroll Entry")
|
||||
order by name limit %(start)s, %(page_len)s"""
|
||||
.format(key=searchfield), {
|
||||
'txt': "%%%s%%" % frappe.db.escape(txt),
|
||||
'txt': "%%%s%%" % txt,
|
||||
'start': start, 'page_len': page_len
|
||||
})
|
||||
|
||||
|
||||
@@ -308,28 +308,59 @@ class SalarySlip(TransactionBase):
|
||||
if payroll_based_on == "Attendance":
|
||||
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"
|
||||
|
||||
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.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:
|
||||
self.payment_days = 0
|
||||
|
||||
def get_unmarked_days(self):
|
||||
marked_days = frappe.get_all("Attendance", filters = {
|
||||
"attendance_date": ["between", [self.start_date, self.end_date]],
|
||||
"employee": self.employee,
|
||||
"docstatus": 1
|
||||
}, fields = ["COUNT(*) as marked_days"])[0].marked_days
|
||||
def get_unmarked_days(self, include_holidays_in_total_working_days):
|
||||
unmarked_days = self.total_working_days
|
||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||
["date_of_joining", "relieving_date"])
|
||||
start_date = self.start_date
|
||||
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):
|
||||
if not joining_date:
|
||||
@@ -968,7 +999,7 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
# apply rounding
|
||||
if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"):
|
||||
amount, additional_amount = rounded(amount), rounded(additional_amount)
|
||||
amount, additional_amount = rounded(amount or 0), rounded(additional_amount or 0)
|
||||
|
||||
return amount, additional_amount
|
||||
|
||||
@@ -1245,9 +1276,9 @@ class SalarySlip(TransactionBase):
|
||||
def set_base_totals(self):
|
||||
self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate)
|
||||
self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate)
|
||||
self.rounded_total = rounded(self.net_pay)
|
||||
self.rounded_total = rounded(self.net_pay or 0)
|
||||
self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate)
|
||||
self.base_rounded_total = rounded(self.base_net_pay)
|
||||
self.base_rounded_total = rounded(self.base_net_pay or 0)
|
||||
self.set_net_total_in_words()
|
||||
|
||||
#calculate total working hours, earnings based on hourly wages and totals
|
||||
@@ -1359,7 +1390,7 @@ class SalarySlip(TransactionBase):
|
||||
'total_allocated_leaves': flt(leave_values.get('total_leaves')),
|
||||
'expired_leaves': flt(leave_values.get('expired_leaves')),
|
||||
'used_leaves': flt(leave_values.get('leaves_taken')),
|
||||
'pending_leaves': flt(leave_values.get('pending_leaves')),
|
||||
'pending_leaves': flt(leave_values.get('leaves_pending_approval')),
|
||||
'available_leaves': flt(leave_values.get('remaining_leaves'))
|
||||
})
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cstr,
|
||||
date_diff,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
@@ -21,6 +23,7 @@ from frappe.utils.make_random import get_random
|
||||
|
||||
import erpnext
|
||||
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.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
@@ -37,17 +40,17 @@ class TestSalarySlip(unittest.TestCase):
|
||||
setup_test()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||
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):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
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")
|
||||
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)
|
||||
|
||||
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):
|
||||
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")
|
||||
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)
|
||||
|
||||
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):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
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,
|
||||
)
|
||||
|
||||
# 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")
|
||||
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))
|
||||
|
||||
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):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
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()
|
||||
# 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()
|
||||
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)
|
||||
|
||||
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):
|
||||
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")
|
||||
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
||||
{"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.gross_pay, 78000)
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"include_holidays_in_total_working_days": 0
|
||||
})
|
||||
def test_salary_slip_with_holidays_excluded(self):
|
||||
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")
|
||||
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
||||
{"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.gross_pay, 78000)
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"include_holidays_in_total_working_days": 1
|
||||
})
|
||||
def test_payment_days(self):
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
create_salary_structure_assignment,
|
||||
)
|
||||
|
||||
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
|
||||
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")
|
||||
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):
|
||||
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")
|
||||
make_employee("test_email_salary_slip@salary.com", company="_Test Company")
|
||||
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
|
||||
ss.company = "_Test Company"
|
||||
ss.save()
|
||||
@@ -994,7 +1062,7 @@ def create_additional_salary(employee, payroll_period, amount):
|
||||
}).submit()
|
||||
return salary_date
|
||||
|
||||
def make_leave_application(employee, from_date, to_date, leave_type, company=None):
|
||||
def make_leave_application(employee, from_date, to_date, leave_type, company=None, submit=True):
|
||||
leave_application = frappe.get_doc(dict(
|
||||
doctype = 'Leave Application',
|
||||
employee = employee,
|
||||
@@ -1002,11 +1070,12 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non
|
||||
from_date = from_date,
|
||||
to_date = to_date,
|
||||
company = company or erpnext.get_default_company() or "_Test Company",
|
||||
docstatus = 1,
|
||||
status = "Approved",
|
||||
leave_approver = 'test@example.com'
|
||||
))
|
||||
leave_application.submit()
|
||||
)).insert()
|
||||
|
||||
if submit:
|
||||
leave_application.submit()
|
||||
|
||||
return leave_application
|
||||
|
||||
@@ -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_approval_notification_template', None)
|
||||
|
||||
def make_holiday_list():
|
||||
def make_holiday_list(list_name=None, from_date=None, to_date=None):
|
||||
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
||||
holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
|
||||
if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"):
|
||||
holiday_list = frappe.get_doc({
|
||||
"doctype": "Holiday List",
|
||||
"holiday_list_name": "Salary Slip Test Holiday List",
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"weekly_off": "Sunday"
|
||||
}).insert()
|
||||
holiday_list.get_weekly_off_dates()
|
||||
holiday_list.save()
|
||||
holiday_list = holiday_list.name
|
||||
name = list_name or "Salary Slip Test Holiday List"
|
||||
|
||||
frappe.delete_doc_if_exists("Holiday List", name, force=True)
|
||||
|
||||
holiday_list = frappe.get_doc({
|
||||
"doctype": "Holiday List",
|
||||
"holiday_list_name": name,
|
||||
"from_date": from_date or fiscal_year[1],
|
||||
"to_date": to_date or fiscal_year[2],
|
||||
"weekly_off": "Sunday"
|
||||
}).insert()
|
||||
holiday_list.get_weekly_off_dates()
|
||||
holiday_list.save()
|
||||
holiday_list = holiday_list.name
|
||||
|
||||
return holiday_list
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"fieldname": "total_allocated_leaves",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Allocated Leave",
|
||||
"label": "Total Allocated Leave(s)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -34,7 +34,7 @@
|
||||
"fieldname": "expired_leaves",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Expired Leave",
|
||||
"label": "Expired Leave(s)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -42,7 +42,7 @@
|
||||
"fieldname": "used_leaves",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Used Leave",
|
||||
"label": "Used Leave(s)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -50,7 +50,7 @@
|
||||
"fieldname": "pending_leaves",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Pending Leave",
|
||||
"label": "Leave(s) Pending Approval",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -58,7 +58,7 @@
|
||||
"fieldname": "available_leaves",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Available Leave",
|
||||
"label": "Available Leave(s)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
@@ -66,7 +66,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-19 10:47:48.546724",
|
||||
"modified": "2022-02-28 14:01:32.327204",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Salary Slip Leave",
|
||||
@@ -74,5 +74,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TestProjectProfitability(unittest.TestCase):
|
||||
class TestProjectProfitability(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql('delete from `tabTimesheet`')
|
||||
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
|
||||
self.assertEqual(fractional_cost, row.fractional_cost)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
@@ -181,6 +181,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldname: "journal_entry",
|
||||
onchange: () => this.update_options(),
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: "Loan Repayment",
|
||||
fieldname: "loan_repayment",
|
||||
onchange: () => this.update_options(),
|
||||
},
|
||||
{
|
||||
fieldname: "column_break_5",
|
||||
fieldtype: "Column Break",
|
||||
@@ -191,13 +197,18 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldname: "sales_invoice",
|
||||
onchange: () => this.update_options(),
|
||||
},
|
||||
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: "Purchase Invoice",
|
||||
fieldname: "purchase_invoice",
|
||||
onchange: () => this.update_options(),
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: "Show Only Exact Amount",
|
||||
fieldname: "exact_match",
|
||||
onchange: () => this.update_options(),
|
||||
},
|
||||
{
|
||||
fieldname: "column_break_5",
|
||||
fieldtype: "Column Break",
|
||||
@@ -210,8 +221,8 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: "Show Only Exact Amount",
|
||||
fieldname: "exact_match",
|
||||
label: "Loan Disbursement",
|
||||
fieldname: "loan_disbursement",
|
||||
onchange: () => this.update_options(),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -39,6 +39,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
this._calculate_taxes_and_totals();
|
||||
this.calculate_discount_amount();
|
||||
|
||||
this.calculate_shipping_charges();
|
||||
|
||||
// Advance calculation applicable to Sales /Purchase Invoice
|
||||
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
|
||||
&& this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) {
|
||||
@@ -81,7 +83,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
this.initialize_taxes();
|
||||
this.determine_exclusive_rate();
|
||||
this.calculate_net_total();
|
||||
this.calculate_shipping_charges();
|
||||
this.calculate_taxes();
|
||||
this.manipulate_grand_total_for_inclusive_tax();
|
||||
this.calculate_totals();
|
||||
@@ -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"]);
|
||||
if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
|
||||
this.shipping_rule();
|
||||
this._calculate_taxes_and_totals();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -569,15 +569,12 @@ body.product-page {
|
||||
}
|
||||
|
||||
.scroll-categories {
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
|
||||
.category-pill {
|
||||
margin: 0px 4px;
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
background-color: #ecf5fe;
|
||||
width: fit-content;
|
||||
padding: 6px 12px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #ecf5fe;
|
||||
font-size: 14px;
|
||||
border-radius: 18px;
|
||||
color: var(--blue-500);
|
||||
|
||||
@@ -128,7 +128,8 @@ class GSTR3BReport(Document):
|
||||
|
||||
def get_inward_nil_exempt(self, state):
|
||||
inward_nil_exempt = frappe.db.sql("""
|
||||
SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst
|
||||
SELECT p.place_of_supply, p.supplier_address,
|
||||
i.base_amount, i.is_nil_exempt, i.is_non_gst
|
||||
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||
WHERE p.docstatus = 1 and p.name = i.parent
|
||||
and p.is_opening = 'No'
|
||||
@@ -136,7 +137,7 @@ class GSTR3BReport(Document):
|
||||
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
|
||||
month(p.posting_date) = %s and year(p.posting_date) = %s
|
||||
and p.company = %s and p.company_gstin = %s
|
||||
GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""",
|
||||
""",
|
||||
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
|
||||
inward_nil_exempt_details = {
|
||||
@@ -150,18 +151,24 @@ class GSTR3BReport(Document):
|
||||
}
|
||||
}
|
||||
|
||||
address_state_map = get_address_state_map()
|
||||
|
||||
for d in inward_nil_exempt:
|
||||
if d.place_of_supply:
|
||||
if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
|
||||
and state == d.place_of_supply.split("-")[1]:
|
||||
inward_nil_exempt_details["gst"]["intra"] += d.base_amount
|
||||
elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
|
||||
and state != d.place_of_supply.split("-")[1]:
|
||||
inward_nil_exempt_details["gst"]["inter"] += d.base_amount
|
||||
elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]:
|
||||
inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
|
||||
elif d.is_non_gst == 1 and state != d.place_of_supply.split("-")[1]:
|
||||
inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount
|
||||
if not d.place_of_supply:
|
||||
d.place_of_supply = "00-" + cstr(state)
|
||||
|
||||
supplier_state = address_state_map.get(d.supplier_address) or state
|
||||
|
||||
if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
|
||||
and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
|
||||
inward_nil_exempt_details["gst"]["intra"] += d.base_amount
|
||||
elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
|
||||
and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
|
||||
inward_nil_exempt_details["gst"]["inter"] += d.base_amount
|
||||
elif d.is_non_gst == 1 and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
|
||||
inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
|
||||
elif d.is_non_gst == 1 and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
|
||||
inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount
|
||||
|
||||
return inward_nil_exempt_details
|
||||
|
||||
@@ -420,6 +427,11 @@ class GSTR3BReport(Document):
|
||||
|
||||
return ",".join(missing_field_invoices)
|
||||
|
||||
def get_address_state_map():
|
||||
return frappe._dict(
|
||||
frappe.get_all('Address', fields=['name', 'gst_state'], as_list=1)
|
||||
)
|
||||
|
||||
def get_json(template):
|
||||
file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template))
|
||||
with open(file_path, 'r') as f:
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.ui.form.on('Tax Exemption 80G Certificate', {
|
||||
'date_of_donation': '',
|
||||
'amount': 0,
|
||||
'mode_of_payment': '',
|
||||
'razorpay_payment_id': ''
|
||||
'payment_id': ''
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"amount",
|
||||
"column_break_27",
|
||||
"mode_of_payment",
|
||||
"razorpay_payment_id"
|
||||
"payment_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -201,13 +201,6 @@
|
||||
"options": "Mode of Payment",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "donation.razorpay_payment_id",
|
||||
"fieldname": "razorpay_payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "RazorPay Payment ID",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "donation.date",
|
||||
"fieldname": "date_of_donation",
|
||||
@@ -266,11 +259,18 @@
|
||||
"hidden": 1,
|
||||
"label": "Title",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "donation.payment_id",
|
||||
"fieldname": "payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment ID",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-22 00:03:34.215633",
|
||||
"modified": "2022-03-16 17:21:39.831059",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "Tax Exemption 80G Certificate",
|
||||
|
||||
@@ -6,29 +6,19 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_company_address
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, get_link_to_form, getdate
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TaxExemption80GCertificate(Document):
|
||||
def validate(self):
|
||||
self.validate_date()
|
||||
self.validate_duplicates()
|
||||
self.validate_company_details()
|
||||
self.set_company_address()
|
||||
self.calculate_total()
|
||||
self.set_title()
|
||||
|
||||
def validate_date(self):
|
||||
if self.recipient == 'Member':
|
||||
if getdate(self.date):
|
||||
fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
|
||||
|
||||
if not (fiscal_year.year_start_date <= getdate(self.date) \
|
||||
<= fiscal_year.year_end_date):
|
||||
frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year)))
|
||||
|
||||
def validate_duplicates(self):
|
||||
if self.recipient == 'Donor':
|
||||
certificate = frappe.db.exists(self.doctype, {
|
||||
@@ -96,7 +86,7 @@ class TaxExemption80GCertificate(Document):
|
||||
'date': doc.from_date,
|
||||
'amount': doc.amount,
|
||||
'invoice_id': doc.invoice,
|
||||
'razorpay_payment_id': doc.payment_id,
|
||||
'payment_id': doc.payment_id,
|
||||
'membership': doc.name
|
||||
})
|
||||
total += flt(doc.amount)
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.non_profit.doctype.donation.donation import create_donation
|
||||
from erpnext.non_profit.doctype.donation.donation import create_razorpay_donation
|
||||
from erpnext.non_profit.doctype.donation.test_donation import (
|
||||
create_donor,
|
||||
create_donor_type,
|
||||
@@ -39,11 +39,11 @@ class TestTaxExemption80GCertificate(unittest.TestCase):
|
||||
donor = create_donor()
|
||||
create_mode_of_payment()
|
||||
payment = frappe._dict({
|
||||
'amount': 100,
|
||||
'amount': 100, # rzp sends data in paise
|
||||
'method': 'Debit Card',
|
||||
'id': 'pay_MeXAmsgeKOhq7O'
|
||||
})
|
||||
donation = create_donation(donor, payment)
|
||||
donation = create_razorpay_donation(donor, payment)
|
||||
|
||||
args = frappe._dict({
|
||||
'recipient': 'Donor',
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"amount",
|
||||
"invoice_id",
|
||||
"column_break_4",
|
||||
"razorpay_payment_id",
|
||||
"payment_id",
|
||||
"membership"
|
||||
],
|
||||
"fields": [
|
||||
@@ -35,26 +35,28 @@
|
||||
"options": "Sales Invoice",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "razorpay_payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Razorpay Payment ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "membership",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Membership",
|
||||
"options": "Membership"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Payment ID"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-15 16:35:10.777587",
|
||||
"modified": "2022-03-17 11:55:24.621708",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "Tax Exemption 80G Certificate Detail",
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Default",
|
||||
"html": "{% if letter_head and not no_letterhead -%}\n <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} 80G Donor Certificate</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n\n This is to confirm that the {{ doc.company }} received an amount of <b>{{doc.get_formatted(\"amount\")}}</b>\n from <b>{{ doc.donor_name }}</b>\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n <br><br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
|
||||
"html": "{% if letter_head and not no_letterhead -%}\n <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} 80G Donor Certificate</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n\n This is to confirm that the {{ doc.company }} received an amount of <b>{{doc.get_formatted(\"amount\")}}</b>\n from <b>{{ doc.donor_name }}</b>\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.payment_id -%}\n bearing Payment ID {{ doc.payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n <br><br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"modified": "2021-02-22 00:20:08.516600",
|
||||
"modified": "2022-03-16 17:25:33.420509",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "80G Certificate for Donation",
|
||||
|
||||
@@ -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 */
|
||||
%(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
|
||||
|
||||
/* disable automatic VAT deduction */
|
||||
'40' as 'BU-Schlüssel',
|
||||
'' as 'BU-Schlüssel',
|
||||
|
||||
gl.posting_date as 'Belegdatum',
|
||||
gl.voucher_no as 'Belegfeld 1',
|
||||
|
||||
@@ -93,7 +93,7 @@ def create_qr_code(doc, method=None):
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# VAT Amount
|
||||
vat_amount = str(doc.total_taxes_and_charges)
|
||||
vat_amount = str(get_vat_amount(doc))
|
||||
|
||||
tag = bytes([5]).hex()
|
||||
length = bytes([len(vat_amount)]).hex()
|
||||
@@ -130,6 +130,22 @@ def create_qr_code(doc, method=None):
|
||||
doc.db_set('ksa_einv_qr', _file.file_url)
|
||||
doc.notify_update()
|
||||
|
||||
def get_vat_amount(doc):
|
||||
vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company})
|
||||
vat_accounts = []
|
||||
vat_amount = 0
|
||||
|
||||
if vat_settings:
|
||||
vat_settings_doc = frappe.get_cached_doc('KSA VAT Setting', vat_settings)
|
||||
|
||||
for row in vat_settings_doc.get('ksa_vat_sales_accounts'):
|
||||
vat_accounts.append(row.account)
|
||||
|
||||
for tax in doc.get('taxes'):
|
||||
if tax.account_head in vat_accounts:
|
||||
vat_amount += tax.tax_amount
|
||||
|
||||
return vat_amount
|
||||
|
||||
def delete_qr_code_file(doc, method=None):
|
||||
region = get_region(doc.company)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user