mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-28 01:14:46 +00:00
Merge branch 'version-13-hotfix' into v13-perf-bom-update-tool
This commit is contained in:
1
.flake8
1
.flake8
@@ -31,6 +31,7 @@ ignore =
|
|||||||
E124, # closing bracket, irritating while writing QB code
|
E124, # closing bracket, irritating while writing QB code
|
||||||
E131, # continuation line unaligned for hanging indent
|
E131, # continuation line unaligned for hanging indent
|
||||||
E123, # closing bracket does not match indentation of opening bracket's line
|
E123, # closing bracket does not match indentation of opening bracket's line
|
||||||
|
E101, # ensured by use of black
|
||||||
|
|
||||||
max-line-length = 200
|
max-line-length = 200
|
||||||
exclude=.github/helper/semgrep_rules
|
exclude=.github/helper/semgrep_rules
|
||||||
|
|||||||
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
@@ -11,7 +11,7 @@ fi
|
|||||||
|
|
||||||
cd ~ || exit
|
cd ~ || exit
|
||||||
|
|
||||||
sudo apt-get install redis-server libcups2-dev
|
sudo apt update && sudo apt install redis-server libcups2-dev
|
||||||
|
|
||||||
pip install frappe-bench
|
pip install frappe-bench
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ def allow_regional(fn):
|
|||||||
return caller
|
return caller
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def get_last_membership(member):
|
def get_last_membership(member):
|
||||||
"""Returns last membership if exists"""
|
"""Returns last membership if exists"""
|
||||||
last_membership = frappe.get_all(
|
last_membership = frappe.get_all(
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
expense_account="Cost of Goods Sold - TPC",
|
expense_account="Cost of Goods Sold - TPC",
|
||||||
rate=400,
|
rate=400,
|
||||||
debit_to="Debtors - TPC",
|
debit_to="Debtors - TPC",
|
||||||
|
currency="USD",
|
||||||
|
customer="_Test Customer USD",
|
||||||
)
|
)
|
||||||
|
|
||||||
create_sales_invoice(
|
create_sales_invoice(
|
||||||
company=company,
|
company=company,
|
||||||
cost_center=cost_center2,
|
cost_center=cost_center2,
|
||||||
@@ -86,6 +89,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
expense_account="Cost of Goods Sold - TPC",
|
expense_account="Cost of Goods Sold - TPC",
|
||||||
rate=200,
|
rate=200,
|
||||||
debit_to="Debtors - TPC",
|
debit_to="Debtors - TPC",
|
||||||
|
currency="USD",
|
||||||
|
customer="_Test Customer USD",
|
||||||
)
|
)
|
||||||
|
|
||||||
pcv = self.make_period_closing_voucher(submit=False)
|
pcv = self.make_period_closing_voucher(submit=False)
|
||||||
@@ -119,14 +124,17 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
surplus_account = create_account()
|
surplus_account = create_account()
|
||||||
cost_center = create_cost_center("Test Cost Center 1")
|
cost_center = create_cost_center("Test Cost Center 1")
|
||||||
|
|
||||||
create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
company=company,
|
company=company,
|
||||||
income_account="Sales - TPC",
|
income_account="Sales - TPC",
|
||||||
expense_account="Cost of Goods Sold - TPC",
|
expense_account="Cost of Goods Sold - TPC",
|
||||||
cost_center=cost_center,
|
cost_center=cost_center,
|
||||||
rate=400,
|
rate=400,
|
||||||
debit_to="Debtors - TPC",
|
debit_to="Debtors - TPC",
|
||||||
|
currency="USD",
|
||||||
|
customer="_Test Customer USD",
|
||||||
)
|
)
|
||||||
|
|
||||||
jv = make_journal_entry(
|
jv = make_journal_entry(
|
||||||
account1="Cash - TPC",
|
account1="Cash - TPC",
|
||||||
account2="Sales - TPC",
|
account2="Sales - TPC",
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ frappe.ui.form.on('POS Closing Entry', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
before_save: function(frm) {
|
before_save: async function(frm) {
|
||||||
|
frappe.dom.freeze(__('Processing Sales! Please Wait...'));
|
||||||
|
|
||||||
frm.set_value("grand_total", 0);
|
frm.set_value("grand_total", 0);
|
||||||
frm.set_value("net_total", 0);
|
frm.set_value("net_total", 0);
|
||||||
frm.set_value("total_quantity", 0);
|
frm.set_value("total_quantity", 0);
|
||||||
@@ -112,17 +114,23 @@ frappe.ui.form.on('POS Closing Entry', {
|
|||||||
row.expected_amount = row.opening_amount;
|
row.expected_amount = row.opening_amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let row of frm.doc.pos_transactions) {
|
const pos_inv_promises = frm.doc.pos_transactions.map(
|
||||||
frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => {
|
row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
|
||||||
frm.doc.grand_total += flt(doc.grand_total);
|
);
|
||||||
frm.doc.net_total += flt(doc.net_total);
|
|
||||||
frm.doc.total_quantity += flt(doc.total_qty);
|
const pos_invoices = await Promise.all(pos_inv_promises);
|
||||||
refresh_payments(doc, frm);
|
|
||||||
refresh_taxes(doc, frm);
|
for (let doc of pos_invoices) {
|
||||||
refresh_fields(frm);
|
frm.doc.grand_total += flt(doc.grand_total);
|
||||||
set_html_data(frm);
|
frm.doc.net_total += flt(doc.net_total);
|
||||||
});
|
frm.doc.total_quantity += flt(doc.total_qty);
|
||||||
|
refresh_payments(doc, frm);
|
||||||
|
refresh_taxes(doc, frm);
|
||||||
|
refresh_fields(frm);
|
||||||
|
set_html_data(frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frappe.dom.unfreeze();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -712,7 +712,7 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
title="_Test Pricing Rule with Min Qty - 2",
|
title="_Test Pricing Rule with Min Qty - 2",
|
||||||
)
|
)
|
||||||
|
|
||||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
|
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
|
||||||
item = si.items[0]
|
item = si.items[0]
|
||||||
item.stock_qty = 1
|
item.stock_qty = 1
|
||||||
si.save()
|
si.save()
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
|||||||
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
|
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
|
||||||
this.frm.trigger('supplier');
|
this.frm.trigger('supplier');
|
||||||
}
|
}
|
||||||
|
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(doc) {
|
refresh: function(doc) {
|
||||||
|
|||||||
@@ -540,7 +540,16 @@ class PurchaseInvoice(BuyingController):
|
|||||||
from_repost=from_repost,
|
from_repost=from_repost,
|
||||||
)
|
)
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
|
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
if provisional_entries:
|
||||||
|
for entry in provisional_entries:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"GL Entry",
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
|
||||||
|
"is_cancelled",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
if update_outstanding == "No":
|
if update_outstanding == "No":
|
||||||
update_outstanding_amt(
|
update_outstanding_amt(
|
||||||
@@ -1078,7 +1087,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# Stock ledger value is not matching with the warehouse amount
|
# Stock ledger value is not matching with the warehouse amount
|
||||||
if (
|
if (
|
||||||
self.update_stock
|
self.update_stock
|
||||||
and voucher_wise_stock_value.get(item.name)
|
and voucher_wise_stock_value.get((item.name, item.warehouse))
|
||||||
and warehouse_debit_amount
|
and warehouse_debit_amount
|
||||||
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
|||||||
make_purchase_receipt,
|
make_purchase_receipt,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
|
||||||
|
from erpnext.stock.tests.test_utils import StockTestMixin
|
||||||
|
|
||||||
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
|
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
|
||||||
test_ignore = ["Serial No"]
|
test_ignore = ["Serial No"]
|
||||||
|
|
||||||
|
|
||||||
class TestPurchaseInvoice(unittest.TestCase):
|
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
unlink_payment_on_cancel_of_invoice()
|
unlink_payment_on_cancel_of_invoice()
|
||||||
@@ -659,6 +660,80 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||||
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
||||||
|
|
||||||
|
def test_standalone_return_using_pi(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
item = self.make_item().name
|
||||||
|
company = "_Test Company with perpetual inventory"
|
||||||
|
warehouse = "Stores - TCP1"
|
||||||
|
|
||||||
|
make_stock_entry(item_code=item, target=warehouse, qty=50, rate=120)
|
||||||
|
|
||||||
|
return_pi = make_purchase_invoice(
|
||||||
|
is_return=1,
|
||||||
|
item=item,
|
||||||
|
qty=-10,
|
||||||
|
update_stock=1,
|
||||||
|
rate=100,
|
||||||
|
company=company,
|
||||||
|
warehouse=warehouse,
|
||||||
|
cost_center="Main - TCP1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert that stock consumption is with actual rate
|
||||||
|
self.assertGLEs(
|
||||||
|
return_pi,
|
||||||
|
[{"credit": 1200, "debit": 0}],
|
||||||
|
gle_filters={"account": "Stock In Hand - TCP1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert loss booked in COGS
|
||||||
|
self.assertGLEs(
|
||||||
|
return_pi,
|
||||||
|
[{"credit": 0, "debit": 200}],
|
||||||
|
gle_filters={"account": "Cost of Goods Sold - TCP1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_return_with_lcv(self):
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||||
|
create_landed_cost_voucher,
|
||||||
|
)
|
||||||
|
|
||||||
|
item = self.make_item().name
|
||||||
|
company = "_Test Company with perpetual inventory"
|
||||||
|
warehouse = "Stores - TCP1"
|
||||||
|
cost_center = "Main - TCP1"
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
item=item,
|
||||||
|
company=company,
|
||||||
|
warehouse=warehouse,
|
||||||
|
cost_center=cost_center,
|
||||||
|
update_stock=1,
|
||||||
|
qty=10,
|
||||||
|
rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create landed cost voucher - will increase valuation of received item by 10
|
||||||
|
create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company, charges=100)
|
||||||
|
return_pi = make_return_doc(pi.doctype, pi.name)
|
||||||
|
return_pi.save().submit()
|
||||||
|
|
||||||
|
# assert that stock consumption is with actual in rate
|
||||||
|
self.assertGLEs(
|
||||||
|
return_pi,
|
||||||
|
[{"credit": 1100, "debit": 0}],
|
||||||
|
gle_filters={"account": "Stock In Hand - TCP1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert loss booked in COGS
|
||||||
|
self.assertGLEs(
|
||||||
|
return_pi,
|
||||||
|
[{"credit": 0, "debit": 100}],
|
||||||
|
gle_filters={"account": "Cost of Goods Sold - TCP1"},
|
||||||
|
)
|
||||||
|
|
||||||
def test_multi_currency_gle(self):
|
def test_multi_currency_gle(self):
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
supplier="_Test Supplier USD",
|
supplier="_Test Supplier USD",
|
||||||
@@ -1492,6 +1567,18 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||||
|
|
||||||
|
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||||
|
pi.cancel()
|
||||||
|
|
||||||
|
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||||
|
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||||
|
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
|
||||||
|
]
|
||||||
|
|
||||||
|
check_gl_entries(
|
||||||
|
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
|
||||||
|
)
|
||||||
|
|
||||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||||
company.save()
|
company.save()
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
|||||||
me.frm.refresh_fields();
|
me.frm.refresh_fields();
|
||||||
}
|
}
|
||||||
erpnext.queries.setup_warehouse_query(this.frm);
|
erpnext.queries.setup_warehouse_query(this.frm);
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(doc, dt, dn) {
|
refresh: function(doc, dt, dn) {
|
||||||
@@ -475,7 +474,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
|||||||
let row = frappe.get_doc(d.doctype, d.name)
|
let row = frappe.get_doc(d.doctype, d.name)
|
||||||
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
|
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
|
||||||
});
|
});
|
||||||
frm.trigger("calculate_timesheet_totals");
|
this.frm.trigger("calculate_timesheet_totals");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -885,27 +884,44 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
|
|
||||||
set_timesheet_data: function(frm, timesheets) {
|
set_timesheet_data: function(frm, timesheets) {
|
||||||
frm.clear_table("timesheets")
|
frm.clear_table("timesheets")
|
||||||
timesheets.forEach(timesheet => {
|
timesheets.forEach(async (timesheet) => {
|
||||||
if (frm.doc.currency != timesheet.currency) {
|
if (frm.doc.currency != timesheet.currency) {
|
||||||
frappe.call({
|
const exchange_rate = await frm.events.get_exchange_rate(
|
||||||
method: "erpnext.setup.utils.get_exchange_rate",
|
frm, timesheet.currency, frm.doc.currency
|
||||||
args: {
|
)
|
||||||
from_currency: timesheet.currency,
|
frm.events.append_time_log(frm, timesheet, exchange_rate)
|
||||||
to_currency: frm.doc.currency
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
exchange_rate = r.message;
|
|
||||||
frm.events.append_time_log(frm, timesheet, exchange_rate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
frm.events.append_time_log(frm, timesheet, 1.0);
|
frm.events.append_time_log(frm, timesheet, 1.0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async get_exchange_rate(frm, from_currency, to_currency) {
|
||||||
|
if (
|
||||||
|
frm.exchange_rates
|
||||||
|
&& frm.exchange_rates[from_currency]
|
||||||
|
&& frm.exchange_rates[from_currency][to_currency]
|
||||||
|
) {
|
||||||
|
return frm.exchange_rates[from_currency][to_currency];
|
||||||
|
}
|
||||||
|
|
||||||
|
return frappe.call({
|
||||||
|
method: "erpnext.setup.utils.get_exchange_rate",
|
||||||
|
args: {
|
||||||
|
from_currency,
|
||||||
|
to_currency
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
// cache exchange rates
|
||||||
|
frm.exchange_rates = frm.exchange_rates || {};
|
||||||
|
frm.exchange_rates[from_currency] = frm.exchange_rates[from_currency] || {};
|
||||||
|
frm.exchange_rates[from_currency][to_currency] = r.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
append_time_log: function(frm, time_log, exchange_rate) {
|
append_time_log: function(frm, time_log, exchange_rate) {
|
||||||
const row = frm.add_child("timesheets");
|
const row = frm.add_child("timesheets");
|
||||||
row.activity_type = time_log.activity_type;
|
row.activity_type = time_log.activity_type;
|
||||||
@@ -916,7 +932,7 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
row.billing_hours = time_log.billing_hours;
|
row.billing_hours = time_log.billing_hours;
|
||||||
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
|
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
|
||||||
row.timesheet_detail = time_log.name;
|
row.timesheet_detail = time_log.name;
|
||||||
row.project_name = time_log.project_name;
|
row.project_name = time_log.project_name;
|
||||||
|
|
||||||
frm.refresh_field("timesheets");
|
frm.refresh_field("timesheets");
|
||||||
frm.trigger("calculate_timesheet_totals");
|
frm.trigger("calculate_timesheet_totals");
|
||||||
|
|||||||
@@ -1113,24 +1113,24 @@ class SalesInvoice(SellingController):
|
|||||||
asset = self.get_asset(item)
|
asset = self.get_asset(item)
|
||||||
|
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
|
if asset.calculate_depreciation:
|
||||||
|
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||||
|
self.reset_depreciation_schedule(asset)
|
||||||
|
|
||||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||||
asset, item.base_net_amount, item.finance_book
|
asset, item.base_net_amount, item.finance_book
|
||||||
)
|
)
|
||||||
asset.db_set("disposal_date", None)
|
asset.db_set("disposal_date", None)
|
||||||
|
|
||||||
if asset.calculate_depreciation:
|
|
||||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
|
||||||
self.reset_depreciation_schedule(asset)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
if asset.calculate_depreciation:
|
||||||
|
self.depreciate_asset(asset)
|
||||||
|
|
||||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||||
asset, item.base_net_amount, item.finance_book
|
asset, item.base_net_amount, item.finance_book
|
||||||
)
|
)
|
||||||
asset.db_set("disposal_date", self.posting_date)
|
asset.db_set("disposal_date", self.posting_date)
|
||||||
|
|
||||||
if asset.calculate_depreciation:
|
|
||||||
self.depreciate_asset(asset)
|
|
||||||
|
|
||||||
for gle in fixed_asset_gl_entries:
|
for gle in fixed_asset_gl_entries:
|
||||||
gle["against"] = self.customer
|
gle["against"] = self.customer
|
||||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||||
@@ -1198,6 +1198,7 @@ class SalesInvoice(SellingController):
|
|||||||
asset.save()
|
asset.save()
|
||||||
|
|
||||||
make_depreciation_entry(asset.name, self.posting_date)
|
make_depreciation_entry(asset.name, self.posting_date)
|
||||||
|
asset.load_from_db()
|
||||||
|
|
||||||
def reset_depreciation_schedule(self, asset):
|
def reset_depreciation_schedule(self, asset):
|
||||||
asset.flags.ignore_validate_update_after_submit = True
|
asset.flags.ignore_validate_update_after_submit = True
|
||||||
@@ -1207,6 +1208,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
self.modify_depreciation_schedule_for_asset_repairs(asset)
|
self.modify_depreciation_schedule_for_asset_repairs(asset)
|
||||||
asset.save()
|
asset.save()
|
||||||
|
asset.load_from_db()
|
||||||
|
|
||||||
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
||||||
asset_repairs = frappe.get_all(
|
asset_repairs = frappe.get_all(
|
||||||
|
|||||||
@@ -898,3 +898,18 @@ def get_default_contact(doctype, name):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_party_account(party_type, party, company, account):
|
||||||
|
doc = frappe.get_doc(party_type, party)
|
||||||
|
account_exists = False
|
||||||
|
for d in doc.get("accounts"):
|
||||||
|
if d.account == account:
|
||||||
|
account_exists = True
|
||||||
|
|
||||||
|
if not account_exists:
|
||||||
|
accounts = {"company": company, "account": account}
|
||||||
|
|
||||||
|
doc.append("accounts", accounts)
|
||||||
|
|
||||||
|
doc.save()
|
||||||
|
|||||||
@@ -198,10 +198,12 @@ def get_loan_entries(filters):
|
|||||||
amount_field = (loan_doc.disbursed_amount).as_("credit")
|
amount_field = (loan_doc.disbursed_amount).as_("credit")
|
||||||
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
||||||
account = loan_doc.disbursement_account
|
account = loan_doc.disbursement_account
|
||||||
|
salary_condition = loan_doc.docstatus == 1
|
||||||
else:
|
else:
|
||||||
amount_field = (loan_doc.amount_paid).as_("debit")
|
amount_field = (loan_doc.amount_paid).as_("debit")
|
||||||
posting_date = (loan_doc.posting_date).as_("posting_date")
|
posting_date = (loan_doc.posting_date).as_("posting_date")
|
||||||
account = loan_doc.payment_account
|
account = loan_doc.payment_account
|
||||||
|
salary_condition = loan_doc.repay_from_salary == 0
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(loan_doc)
|
frappe.qb.from_(loan_doc)
|
||||||
@@ -214,14 +216,12 @@ def get_loan_entries(filters):
|
|||||||
posting_date,
|
posting_date,
|
||||||
)
|
)
|
||||||
.where(loan_doc.docstatus == 1)
|
.where(loan_doc.docstatus == 1)
|
||||||
|
.where(salary_condition)
|
||||||
.where(account == filters.get("account"))
|
.where(account == filters.get("account"))
|
||||||
.where(posting_date <= getdate(filters.get("report_date")))
|
.where(posting_date <= getdate(filters.get("report_date")))
|
||||||
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
|
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
|
||||||
)
|
)
|
||||||
|
|
||||||
if doctype == "Loan Repayment":
|
|
||||||
query.where(loan_doc.repay_from_salary == 0)
|
|
||||||
|
|
||||||
entries = query.run(as_dict=1)
|
entries = query.run(as_dict=1)
|
||||||
loan_docs.extend(entries)
|
loan_docs.extend(entries)
|
||||||
|
|
||||||
@@ -267,15 +267,17 @@ def get_loan_amount(filters):
|
|||||||
amount_field = Sum(loan_doc.disbursed_amount)
|
amount_field = Sum(loan_doc.disbursed_amount)
|
||||||
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
||||||
account = loan_doc.disbursement_account
|
account = loan_doc.disbursement_account
|
||||||
|
salary_condition = loan_doc.docstatus == 1
|
||||||
else:
|
else:
|
||||||
amount_field = Sum(loan_doc.amount_paid)
|
amount_field = Sum(loan_doc.amount_paid)
|
||||||
posting_date = (loan_doc.posting_date).as_("posting_date")
|
posting_date = (loan_doc.posting_date).as_("posting_date")
|
||||||
account = loan_doc.payment_account
|
account = loan_doc.payment_account
|
||||||
|
salary_condition = loan_doc.repay_from_salary == 0
|
||||||
amount = (
|
amount = (
|
||||||
frappe.qb.from_(loan_doc)
|
frappe.qb.from_(loan_doc)
|
||||||
.select(amount_field)
|
.select(amount_field)
|
||||||
.where(loan_doc.docstatus == 1)
|
.where(loan_doc.docstatus == 1)
|
||||||
|
.where(salary_condition)
|
||||||
.where(account == filters.get("account"))
|
.where(account == filters.get("account"))
|
||||||
.where(posting_date > getdate(filters.get("report_date")))
|
.where(posting_date > getdate(filters.get("report_date")))
|
||||||
.where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date")))
|
.where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date")))
|
||||||
|
|||||||
@@ -263,7 +263,10 @@ def get_report_summary(summary_data, currency):
|
|||||||
def get_chart_data(columns, data):
|
def get_chart_data(columns, data):
|
||||||
labels = [d.get("label") for d in columns[2:]]
|
labels = [d.get("label") for d in columns[2:]]
|
||||||
datasets = [
|
datasets = [
|
||||||
{"name": account.get("account").replace("'", ""), "values": [account.get("total")]}
|
{
|
||||||
|
"name": account.get("account").replace("'", ""),
|
||||||
|
"values": [account.get(d.get("fieldname")) for d in columns[2:]],
|
||||||
|
}
|
||||||
for account in data
|
for account in data
|
||||||
if account.get("parent_account") == None and account.get("currency")
|
if account.get("parent_account") == None and account.get("currency")
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -443,12 +443,6 @@ def get_grand_total(filters, doctype):
|
|||||||
] # nosec
|
] # nosec
|
||||||
|
|
||||||
|
|
||||||
def get_deducted_taxes():
|
|
||||||
return frappe.db.sql_list(
|
|
||||||
"select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_accounts(
|
def get_tax_accounts(
|
||||||
item_list,
|
item_list,
|
||||||
columns,
|
columns,
|
||||||
@@ -462,6 +456,7 @@ def get_tax_accounts(
|
|||||||
tax_columns = []
|
tax_columns = []
|
||||||
invoice_item_row = {}
|
invoice_item_row = {}
|
||||||
itemised_tax = {}
|
itemised_tax = {}
|
||||||
|
add_deduct_tax = "charge_type"
|
||||||
|
|
||||||
tax_amount_precision = (
|
tax_amount_precision = (
|
||||||
get_field_precision(
|
get_field_precision(
|
||||||
@@ -477,13 +472,13 @@ def get_tax_accounts(
|
|||||||
conditions = ""
|
conditions = ""
|
||||||
if doctype == "Purchase Invoice":
|
if doctype == "Purchase Invoice":
|
||||||
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
|
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
|
||||||
|
add_deduct_tax = "add_deduct_tax"
|
||||||
|
|
||||||
deducted_tax = get_deducted_taxes()
|
|
||||||
tax_details = frappe.db.sql(
|
tax_details = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select
|
select
|
||||||
name, parent, description, item_wise_tax_detail,
|
name, parent, description, item_wise_tax_detail,
|
||||||
charge_type, base_tax_amount_after_discount_amount
|
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
|
||||||
from `tab%s`
|
from `tab%s`
|
||||||
where
|
where
|
||||||
parenttype = %s and docstatus = 1
|
parenttype = %s and docstatus = 1
|
||||||
@@ -491,12 +486,22 @@ def get_tax_accounts(
|
|||||||
and parent in (%s)
|
and parent in (%s)
|
||||||
%s
|
%s
|
||||||
order by description
|
order by description
|
||||||
"""
|
""".format(
|
||||||
|
add_deduct_tax=add_deduct_tax
|
||||||
|
)
|
||||||
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
|
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
|
||||||
tuple([doctype] + list(invoice_item_row)),
|
tuple([doctype] + list(invoice_item_row)),
|
||||||
)
|
)
|
||||||
|
|
||||||
for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details:
|
for (
|
||||||
|
name,
|
||||||
|
parent,
|
||||||
|
description,
|
||||||
|
item_wise_tax_detail,
|
||||||
|
charge_type,
|
||||||
|
add_deduct_tax,
|
||||||
|
tax_amount,
|
||||||
|
) in tax_details:
|
||||||
description = handle_html(description)
|
description = handle_html(description)
|
||||||
if description not in tax_columns and tax_amount:
|
if description not in tax_columns and tax_amount:
|
||||||
# as description is text editor earlier and markup can break the column convention in reports
|
# as description is text editor earlier and markup can break the column convention in reports
|
||||||
@@ -529,7 +534,9 @@ def get_tax_accounts(
|
|||||||
if item_tax_amount:
|
if item_tax_amount:
|
||||||
tax_value = flt(item_tax_amount, tax_amount_precision)
|
tax_value = flt(item_tax_amount, tax_amount_precision)
|
||||||
tax_value = (
|
tax_value = (
|
||||||
tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value
|
tax_value * -1
|
||||||
|
if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct")
|
||||||
|
else tax_value
|
||||||
)
|
)
|
||||||
|
|
||||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ def set_gl_entries_by_account(
|
|||||||
{additional_conditions}
|
{additional_conditions}
|
||||||
and posting_date <= %(to_date)s
|
and posting_date <= %(to_date)s
|
||||||
and {based_on} is not null
|
and {based_on} is not null
|
||||||
|
and is_cancelled = 0
|
||||||
order by {based_on}, posting_date""".format(
|
order by {based_on}, posting_date""".format(
|
||||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -346,9 +346,13 @@ def get_columns(invoice_list, additional_table_columns):
|
|||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
conditions = ""
|
conditions = ""
|
||||||
|
|
||||||
|
accounting_dimensions = get_accounting_dimensions(as_list=False) or []
|
||||||
|
accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
|
||||||
|
|
||||||
if filters.get("company"):
|
if filters.get("company"):
|
||||||
conditions += " and company=%(company)s"
|
conditions += " and company=%(company)s"
|
||||||
if filters.get("customer"):
|
|
||||||
|
if filters.get("customer") and "customer" not in accounting_dimensions_list:
|
||||||
conditions += " and customer = %(customer)s"
|
conditions += " and customer = %(customer)s"
|
||||||
|
|
||||||
if filters.get("from_date"):
|
if filters.get("from_date"):
|
||||||
@@ -359,32 +363,18 @@ def get_conditions(filters):
|
|||||||
if filters.get("owner"):
|
if filters.get("owner"):
|
||||||
conditions += " and owner = %(owner)s"
|
conditions += " and owner = %(owner)s"
|
||||||
|
|
||||||
if filters.get("mode_of_payment"):
|
def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
if not filters.get(field) or field in accounting_dimensions_list:
|
||||||
where parent=`tabSales Invoice`.name
|
return ""
|
||||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
return f""" and exists(select name from `tab{table}`
|
||||||
|
where parent=`tabSales Invoice`.name
|
||||||
|
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
|
||||||
|
|
||||||
if filters.get("cost_center"):
|
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
conditions += get_sales_invoice_item_field_condition("cost_center")
|
||||||
where parent=`tabSales Invoice`.name
|
conditions += get_sales_invoice_item_field_condition("warehouse")
|
||||||
and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)"""
|
conditions += get_sales_invoice_item_field_condition("brand")
|
||||||
|
conditions += get_sales_invoice_item_field_condition("item_group")
|
||||||
if filters.get("warehouse"):
|
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
|
||||||
where parent=`tabSales Invoice`.name
|
|
||||||
and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)"""
|
|
||||||
|
|
||||||
if filters.get("brand"):
|
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
|
||||||
where parent=`tabSales Invoice`.name
|
|
||||||
and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)"""
|
|
||||||
|
|
||||||
if filters.get("item_group"):
|
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
|
||||||
where parent=`tabSales Invoice`.name
|
|
||||||
and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)"""
|
|
||||||
|
|
||||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
|
||||||
|
|
||||||
if accounting_dimensions:
|
if accounting_dimensions:
|
||||||
common_condition = """
|
common_condition = """
|
||||||
|
|||||||
49
erpnext/accounts/test/test_reports.py
Normal file
49
erpnext/accounts/test/test_reports.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import unittest
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
|
||||||
|
|
||||||
|
DEFAULT_FILTERS = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2010-01-01",
|
||||||
|
"to_date": "2030-01-01",
|
||||||
|
"period_start_date": "2010-01-01",
|
||||||
|
"period_end_date": "2030-01-01",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
||||||
|
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}),
|
||||||
|
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}),
|
||||||
|
("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
|
||||||
|
("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
|
||||||
|
("Consolidated Financial Statement", {"report": "Balance Sheet"}),
|
||||||
|
("Consolidated Financial Statement", {"report": "Profit and Loss Statement"}),
|
||||||
|
("Consolidated Financial Statement", {"report": "Cash Flow"}),
|
||||||
|
("Gross Profit", {"group_by": "Invoice"}),
|
||||||
|
("Gross Profit", {"group_by": "Item Code"}),
|
||||||
|
("Gross Profit", {"group_by": "Item Group"}),
|
||||||
|
("Gross Profit", {"group_by": "Customer"}),
|
||||||
|
("Gross Profit", {"group_by": "Customer Group"}),
|
||||||
|
("Item-wise Sales Register", {}),
|
||||||
|
("Item-wise Purchase Register", {}),
|
||||||
|
("Sales Register", {}),
|
||||||
|
("Sales Register", {"item_group": "All Item Groups"}),
|
||||||
|
("Purchase Register", {}),
|
||||||
|
]
|
||||||
|
|
||||||
|
OPTIONAL_FILTERS = {}
|
||||||
|
|
||||||
|
|
||||||
|
class TestReports(unittest.TestCase):
|
||||||
|
def test_execute_all_accounts_reports(self):
|
||||||
|
"""Test that all script report in stock modules are executable with supported filters"""
|
||||||
|
for report, filter in REPORT_FILTER_TEST_CASES:
|
||||||
|
with self.subTest(report=report):
|
||||||
|
execute_script_report(
|
||||||
|
report_name=report,
|
||||||
|
module="Accounts",
|
||||||
|
filters=filter,
|
||||||
|
default_filters=DEFAULT_FILTERS,
|
||||||
|
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||||
|
)
|
||||||
@@ -193,31 +193,31 @@ class TestAsset(AssetSetup):
|
|||||||
def test_gle_made_by_asset_sale(self):
|
def test_gle_made_by_asset_sale(self):
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
available_for_use_date="2020-06-06",
|
available_for_use_date="2021-06-06",
|
||||||
purchase_date="2020-01-01",
|
purchase_date="2021-01-01",
|
||||||
expected_value_after_useful_life=10000,
|
expected_value_after_useful_life=10000,
|
||||||
total_number_of_depreciations=3,
|
total_number_of_depreciations=3,
|
||||||
frequency_of_depreciation=10,
|
frequency_of_depreciation=10,
|
||||||
depreciation_start_date="2020-12-31",
|
depreciation_start_date="2021-12-31",
|
||||||
submit=1,
|
submit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
post_depreciation_entries(date="2021-01-01")
|
post_depreciation_entries(date="2022-01-01")
|
||||||
|
|
||||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||||
si.customer = "_Test Customer"
|
si.customer = "_Test Customer"
|
||||||
si.due_date = nowdate()
|
si.posting_date = getdate("2022-04-22")
|
||||||
si.get("items")[0].rate = 25000
|
si.due_date = getdate("2022-04-22")
|
||||||
si.insert()
|
si.get("items")[0].rate = 75000
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||||
|
|
||||||
expected_gle = (
|
expected_gle = (
|
||||||
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
|
("_Test Accumulated Depreciations - _TC", 36082.31, 0.0),
|
||||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||||
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
|
("_Test Gain/Loss on Asset Disposal - _TC", 0.0, 11082.31),
|
||||||
("Debtors - _TC", 25000.0, 0.0),
|
("Debtors - _TC", 75000.0, 0.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
gle = frappe.db.sql(
|
gle = frappe.db.sql(
|
||||||
@@ -229,7 +229,9 @@ class TestAsset(AssetSetup):
|
|||||||
|
|
||||||
self.assertEqual(gle, expected_gle)
|
self.assertEqual(gle, expected_gle)
|
||||||
|
|
||||||
|
si.load_from_db()
|
||||||
si.cancel()
|
si.cancel()
|
||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||||
|
|
||||||
def test_expense_head(self):
|
def test_expense_head(self):
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ frappe.ui.form.on("Purchase Order", {
|
|||||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||||
return erpnext.queries.warehouse(frm.doc);
|
return erpnext.queries.warehouse(frm.doc);
|
||||||
});
|
});
|
||||||
|
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
apply_tds: function(frm) {
|
apply_tds: function(frm) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
|
|||||||
from erpnext.accounts.party import (
|
from erpnext.accounts.party import (
|
||||||
get_party_account,
|
get_party_account,
|
||||||
get_party_account_currency,
|
get_party_account_currency,
|
||||||
|
get_party_gle_currency,
|
||||||
validate_party_frozen_disabled,
|
validate_party_frozen_disabled,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
|
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
|
||||||
@@ -169,6 +170,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
self.validate_party()
|
self.validate_party()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
|
self.validate_party_account_currency()
|
||||||
|
|
||||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||||
@@ -1445,6 +1447,27 @@ class AccountsController(TransactionBase):
|
|||||||
# at quotation / sales order level and we shouldn't stop someone
|
# at quotation / sales order level and we shouldn't stop someone
|
||||||
# from creating a sales invoice if sales order is already created
|
# from creating a sales invoice if sales order is already created
|
||||||
|
|
||||||
|
def validate_party_account_currency(self):
|
||||||
|
if self.doctype not in ("Sales Invoice", "Purchase Invoice"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_opening == "Yes":
|
||||||
|
return
|
||||||
|
|
||||||
|
party_type, party = self.get_party()
|
||||||
|
party_gle_currency = get_party_gle_currency(party_type, party, self.company)
|
||||||
|
party_account = (
|
||||||
|
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
|
||||||
|
)
|
||||||
|
party_account_currency = get_account_currency(party_account)
|
||||||
|
|
||||||
|
if not party_gle_currency and (party_account_currency != self.currency):
|
||||||
|
frappe.throw(
|
||||||
|
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
||||||
|
frappe.bold(party_account), party_account_currency, self.currency
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def delink_advance_entries(self, linked_doc_name):
|
def delink_advance_entries(self, linked_doc_name):
|
||||||
total_allocated_amount = 0
|
total_allocated_amount = 0
|
||||||
for adv in self.advances:
|
for adv in self.advances:
|
||||||
@@ -2636,7 +2659,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.update_reserved_qty_for_subcontract()
|
parent.update_reserved_qty_for_subcontract()
|
||||||
parent.create_raw_materials_supplied("supplied_items")
|
parent.create_raw_materials_supplied("supplied_items")
|
||||||
parent.save()
|
parent.save()
|
||||||
else:
|
else: # Sales Order
|
||||||
|
parent.validate_warehouse()
|
||||||
parent.update_reserved_qty()
|
parent.update_reserved_qty()
|
||||||
parent.update_project()
|
parent.update_project()
|
||||||
parent.update_prevdoc_status("submit")
|
parent.update_prevdoc_status("submit")
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
|||||||
return data[0]
|
return data[0]
|
||||||
|
|
||||||
|
|
||||||
def make_return_doc(doctype, source_name, target_doc=None):
|
def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, cstr, flt, get_datetime, get_request_session, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, get_datetime, get_request_session, getdate, nowdate
|
||||||
|
|
||||||
|
from erpnext import get_company_currency
|
||||||
from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import (
|
from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import (
|
||||||
dump_request_data,
|
dump_request_data,
|
||||||
make_shopify_log,
|
make_shopify_log,
|
||||||
@@ -143,6 +144,10 @@ def create_sales_order(shopify_order, shopify_settings, company=None):
|
|||||||
"taxes": get_order_taxes(shopify_order, shopify_settings),
|
"taxes": get_order_taxes(shopify_order, shopify_settings),
|
||||||
"apply_discount_on": "Grand Total",
|
"apply_discount_on": "Grand Total",
|
||||||
"discount_amount": get_discounted_amount(shopify_order),
|
"discount_amount": get_discounted_amount(shopify_order),
|
||||||
|
"currency": frappe.get_value(
|
||||||
|
"Customer", customer or shopify_settings.default_customer, "default_currency"
|
||||||
|
)
|
||||||
|
or get_company_currency(shopify_settings.company),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -178,6 +183,7 @@ def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=Fal
|
|||||||
si.set_posting_time = 1
|
si.set_posting_time = 1
|
||||||
si.posting_date = posting_date
|
si.posting_date = posting_date
|
||||||
si.due_date = posting_date
|
si.due_date = posting_date
|
||||||
|
si.currency = so.currency
|
||||||
si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-"
|
si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-"
|
||||||
si.flags.ignore_mandatory = True
|
si.flags.ignore_mandatory = True
|
||||||
set_cost_center(si.items, shopify_settings.cost_center)
|
set_cost_center(si.items, shopify_settings.cost_center)
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class ShopifySettings(unittest.TestCase):
|
|||||||
"warehouse": "_Test Warehouse - _TC",
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
"cash_bank_account": "Cash - _TC",
|
"cash_bank_account": "Cash - _TC",
|
||||||
"account": "Cash - _TC",
|
"account": "Cash - _TC",
|
||||||
|
"company": "_Test Company",
|
||||||
"customer_group": "_Test Customer Group",
|
"customer_group": "_Test Customer Group",
|
||||||
"cost_center": "Main - _TC",
|
"cost_center": "Main - _TC",
|
||||||
"taxes": [{"shopify_tax": "International Shipping", "tax_account": "Legal Expenses - _TC"}],
|
"taxes": [{"shopify_tax": "International Shipping", "tax_account": "Legal Expenses - _TC"}],
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ def create_sales_invoice():
|
|||||||
sales_invoice.customer = frappe.db.get_value("Patient", patient, "customer")
|
sales_invoice.customer = frappe.db.get_value("Patient", patient, "customer")
|
||||||
sales_invoice.due_date = getdate()
|
sales_invoice.due_date = getdate()
|
||||||
sales_invoice.company = "_Test Company"
|
sales_invoice.company = "_Test Company"
|
||||||
|
sales_invoice.currency = "INR"
|
||||||
sales_invoice.debit_to = get_receivable_account("_Test Company")
|
sales_invoice.debit_to = get_receivable_account("_Test Company")
|
||||||
|
|
||||||
tests = [insulin_resistance_template, blood_test_template]
|
tests = [insulin_resistance_template, blood_test_template]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from frappe.model.document import Document
|
|||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import flt, get_link_to_form, get_time, getdate
|
from frappe.utils import flt, get_link_to_form, get_time, getdate
|
||||||
|
|
||||||
|
from erpnext import get_company_currency
|
||||||
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import (
|
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import (
|
||||||
get_income_account,
|
get_income_account,
|
||||||
get_receivable_account,
|
get_receivable_account,
|
||||||
@@ -252,6 +253,10 @@ def create_sales_invoice(appointment_doc):
|
|||||||
sales_invoice = frappe.new_doc("Sales Invoice")
|
sales_invoice = frappe.new_doc("Sales Invoice")
|
||||||
sales_invoice.patient = appointment_doc.patient
|
sales_invoice.patient = appointment_doc.patient
|
||||||
sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer")
|
sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer")
|
||||||
|
sales_invoice.currency = frappe.get_value(
|
||||||
|
"Customer", sales_invoice.customer, "default_currency"
|
||||||
|
) or get_company_currency(appointment_doc.company)
|
||||||
|
|
||||||
sales_invoice.appointment = appointment_doc.name
|
sales_invoice.appointment = appointment_doc.name
|
||||||
sales_invoice.due_date = getdate()
|
sales_invoice.due_date = getdate()
|
||||||
sales_invoice.company = appointment_doc.company
|
sales_invoice.company = appointment_doc.company
|
||||||
|
|||||||
@@ -379,6 +379,7 @@ def create_patient(
|
|||||||
patient.mobile = mobile
|
patient.mobile = mobile
|
||||||
patient.email = email
|
patient.email = email
|
||||||
patient.customer = customer
|
patient.customer = customer
|
||||||
|
patient.default_currency = "INR"
|
||||||
patient.invite_user = create_user
|
patient.invite_user = create_user
|
||||||
patient.save(ignore_permissions=True)
|
patient.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import frappe
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
from erpnext import get_company_currency
|
||||||
|
|
||||||
|
|
||||||
class TherapyPlan(Document):
|
class TherapyPlan(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@@ -72,6 +74,9 @@ def make_sales_invoice(reference_name, patient, company, therapy_plan_template):
|
|||||||
si.company = company
|
si.company = company
|
||||||
si.patient = patient
|
si.patient = patient
|
||||||
si.customer = frappe.db.get_value("Patient", patient, "customer")
|
si.customer = frappe.db.get_value("Patient", patient, "customer")
|
||||||
|
si.currency = frappe.get_value(
|
||||||
|
"Customer", si.customer, "default_currency"
|
||||||
|
) or get_company_currency(si.company)
|
||||||
|
|
||||||
item = frappe.db.get_value("Therapy Plan Template", therapy_plan_template, "linked_item")
|
item = frappe.db.get_value("Therapy Plan Template", therapy_plan_template, "linked_item")
|
||||||
price_list, price_list_currency = frappe.db.get_values(
|
price_list, price_list_currency = frappe.db.get_values(
|
||||||
|
|||||||
@@ -3,7 +3,15 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
|
from frappe.utils import (
|
||||||
|
add_days,
|
||||||
|
add_months,
|
||||||
|
get_last_day,
|
||||||
|
get_year_ending,
|
||||||
|
get_year_start,
|
||||||
|
getdate,
|
||||||
|
nowdate,
|
||||||
|
)
|
||||||
|
|
||||||
from erpnext.hr.doctype.attendance.attendance import (
|
from erpnext.hr.doctype.attendance.attendance import (
|
||||||
get_month_map,
|
get_month_map,
|
||||||
@@ -35,63 +43,64 @@ class TestAttendance(FrappeTestCase):
|
|||||||
self.assertEqual(attendance, fetch_attendance)
|
self.assertEqual(attendance, fetch_attendance)
|
||||||
|
|
||||||
def test_unmarked_days(self):
|
def test_unmarked_days(self):
|
||||||
now = now_datetime()
|
first_sunday = get_first_sunday(
|
||||||
previous_month = now.month - 1
|
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
)
|
||||||
|
attendance_date = add_days(first_sunday, 1)
|
||||||
|
|
||||||
employee = make_employee(
|
employee = make_employee(
|
||||||
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
|
"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
|
||||||
)
|
)
|
||||||
frappe.db.delete("Attendance", {"employee": employee})
|
frappe.db.delete("Attendance", {"employee": employee})
|
||||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
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, attendance_date, "Present")
|
||||||
mark_attendance(employee, first_day, "Present")
|
month_name = get_month_name(attendance_date)
|
||||||
month_name = get_month_name(first_day)
|
|
||||||
|
|
||||||
unmarked_days = get_unmarked_days(employee, month_name)
|
unmarked_days = get_unmarked_days(employee, month_name)
|
||||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
# attendance already marked for the day
|
# attendance already marked for the day
|
||||||
self.assertNotIn(first_day, unmarked_days)
|
self.assertNotIn(attendance_date, unmarked_days)
|
||||||
# attendance unmarked
|
# attendance unmarked
|
||||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
|
||||||
# holiday considered in unmarked days
|
# holiday considered in unmarked days
|
||||||
self.assertIn(first_sunday, unmarked_days)
|
self.assertIn(first_sunday, unmarked_days)
|
||||||
|
|
||||||
def test_unmarked_days_excluding_holidays(self):
|
def test_unmarked_days_excluding_holidays(self):
|
||||||
now = now_datetime()
|
first_sunday = get_first_sunday(
|
||||||
previous_month = now.month - 1
|
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
)
|
||||||
|
attendance_date = add_days(first_sunday, 1)
|
||||||
|
|
||||||
employee = make_employee(
|
employee = make_employee(
|
||||||
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
|
"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
|
||||||
)
|
)
|
||||||
frappe.db.delete("Attendance", {"employee": employee})
|
frappe.db.delete("Attendance", {"employee": employee})
|
||||||
|
|
||||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
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, attendance_date, "Present")
|
||||||
mark_attendance(employee, first_day, "Present")
|
month_name = get_month_name(attendance_date)
|
||||||
month_name = get_month_name(first_day)
|
|
||||||
|
|
||||||
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
||||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
# attendance already marked for the day
|
# attendance already marked for the day
|
||||||
self.assertNotIn(first_day, unmarked_days)
|
self.assertNotIn(attendance_date, unmarked_days)
|
||||||
# attendance unmarked
|
# attendance unmarked
|
||||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
|
||||||
# holidays not considered in unmarked days
|
# holidays not considered in unmarked days
|
||||||
self.assertNotIn(first_sunday, unmarked_days)
|
self.assertNotIn(first_sunday, unmarked_days)
|
||||||
|
|
||||||
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
||||||
now = now_datetime()
|
first_sunday = get_first_sunday(
|
||||||
previous_month = now.month - 1
|
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
)
|
||||||
|
date = add_days(first_sunday, 1)
|
||||||
|
|
||||||
doj = add_days(first_day, 1)
|
doj = add_days(date, 1)
|
||||||
relieving_date = add_days(first_day, 5)
|
relieving_date = add_days(date, 5)
|
||||||
employee = make_employee(
|
employee = make_employee(
|
||||||
"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
|
"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
|
||||||
)
|
)
|
||||||
@@ -99,9 +108,9 @@ class TestAttendance(FrappeTestCase):
|
|||||||
|
|
||||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||||
|
|
||||||
attendance_date = add_days(first_day, 2)
|
attendance_date = add_days(date, 2)
|
||||||
mark_attendance(employee, attendance_date, "Present")
|
mark_attendance(employee, attendance_date, "Present")
|
||||||
month_name = get_month_name(first_day)
|
month_name = get_month_name(attendance_date)
|
||||||
|
|
||||||
unmarked_days = get_unmarked_days(employee, month_name)
|
unmarked_days = get_unmarked_days(employee, month_name)
|
||||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
|||||||
persons_name = anniversary_person
|
persons_name = anniversary_person
|
||||||
# Number of years completed at the company
|
# Number of years completed at the company
|
||||||
completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
|
completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
|
||||||
anniversary_person += f" completed {completed_years} year(s)"
|
anniversary_person += f" completed {get_pluralized_years(completed_years)}"
|
||||||
else:
|
else:
|
||||||
person_names_with_years = []
|
person_names_with_years = []
|
||||||
names = []
|
names = []
|
||||||
@@ -239,7 +239,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
|||||||
names.append(person_text)
|
names.append(person_text)
|
||||||
# Number of years completed at the company
|
# Number of years completed at the company
|
||||||
completed_years = getdate().year - person["date_of_joining"].year
|
completed_years = getdate().year - person["date_of_joining"].year
|
||||||
person_text += f" completed {completed_years} year(s)"
|
person_text += f" completed {get_pluralized_years(completed_years)}"
|
||||||
person_names_with_years.append(person_text)
|
person_names_with_years.append(person_text)
|
||||||
|
|
||||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
||||||
@@ -254,6 +254,12 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
|||||||
return reminder_text, message
|
return reminder_text, message
|
||||||
|
|
||||||
|
|
||||||
|
def get_pluralized_years(years):
|
||||||
|
if years == 1:
|
||||||
|
return "1 year"
|
||||||
|
return f"{years} years"
|
||||||
|
|
||||||
|
|
||||||
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
||||||
frappe.sendmail(
|
frappe.sendmail(
|
||||||
recipients=recipients,
|
recipients=recipients,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2017-10-09 14:26:29.612365",
|
"creation": "2022-01-17 18:36:51.450395",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled",
|
"options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-11 18:38:38.617478",
|
"modified": "2022-05-23 19:33:52.345823",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee Advance",
|
"name": "Employee Advance",
|
||||||
@@ -236,5 +236,6 @@
|
|||||||
"search_fields": "employee,employee_name",
|
"search_fields": "employee,employee_name",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"title_field": "employee_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -29,19 +29,43 @@ class EmployeeAdvance(Document):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.ignore_linked_doctypes = "GL Entry"
|
self.ignore_linked_doctypes = "GL Entry"
|
||||||
|
self.set_status(update=True)
|
||||||
|
|
||||||
|
def set_status(self, update=False):
|
||||||
|
precision = self.precision("paid_amount")
|
||||||
|
total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision)
|
||||||
|
status = None
|
||||||
|
|
||||||
def set_status(self):
|
|
||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
self.status = "Draft"
|
status = "Draft"
|
||||||
if self.docstatus == 1:
|
elif self.docstatus == 1:
|
||||||
if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount):
|
if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt(
|
||||||
self.status = "Claimed"
|
self.paid_amount, precision
|
||||||
elif self.paid_amount and self.advance_amount == flt(self.paid_amount):
|
):
|
||||||
self.status = "Paid"
|
status = "Claimed"
|
||||||
|
elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt(
|
||||||
|
self.paid_amount, precision
|
||||||
|
):
|
||||||
|
status = "Returned"
|
||||||
|
elif (
|
||||||
|
flt(self.claimed_amount) > 0
|
||||||
|
and (flt(self.return_amount) > 0)
|
||||||
|
and total_amount == flt(self.paid_amount, precision)
|
||||||
|
):
|
||||||
|
status = "Partly Claimed and Returned"
|
||||||
|
elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt(
|
||||||
|
self.paid_amount, precision
|
||||||
|
):
|
||||||
|
status = "Paid"
|
||||||
else:
|
else:
|
||||||
self.status = "Unpaid"
|
status = "Unpaid"
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
self.status = "Cancelled"
|
status = "Cancelled"
|
||||||
|
|
||||||
|
if update:
|
||||||
|
self.db_set("status", status)
|
||||||
|
else:
|
||||||
|
self.status = status
|
||||||
|
|
||||||
def set_total_advance_paid(self):
|
def set_total_advance_paid(self):
|
||||||
gle = frappe.qb.DocType("GL Entry")
|
gle = frappe.qb.DocType("GL Entry")
|
||||||
@@ -89,8 +113,7 @@ class EmployeeAdvance(Document):
|
|||||||
|
|
||||||
self.db_set("paid_amount", paid_amount)
|
self.db_set("paid_amount", paid_amount)
|
||||||
self.db_set("return_amount", return_amount)
|
self.db_set("return_amount", return_amount)
|
||||||
self.set_status()
|
self.set_status(update=True)
|
||||||
frappe.db.set_value("Employee Advance", self.name, "status", self.status)
|
|
||||||
|
|
||||||
def update_claimed_amount(self):
|
def update_claimed_amount(self):
|
||||||
claimed_amount = (
|
claimed_amount = (
|
||||||
@@ -112,8 +135,7 @@ class EmployeeAdvance(Document):
|
|||||||
|
|
||||||
frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount))
|
frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount))
|
||||||
self.reload()
|
self.reload()
|
||||||
self.set_status()
|
self.set_status(update=True)
|
||||||
frappe.db.set_value("Employee Advance", self.name, "status", self.status)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -265,6 +287,7 @@ def make_return_entry(
|
|||||||
"party_type": "Employee",
|
"party_type": "Employee",
|
||||||
"party": employee,
|
"party": employee,
|
||||||
"is_advance": "Yes",
|
"is_advance": "Yes",
|
||||||
|
"cost_center": erpnext.get_default_cost_center(company),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -282,6 +305,7 @@ def make_return_entry(
|
|||||||
"account_currency": bank_cash_account.account_currency,
|
"account_currency": bank_cash_account.account_currency,
|
||||||
"account_type": bank_cash_account.account_type,
|
"account_type": bank_cash_account.account_type,
|
||||||
"exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1,
|
"exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1,
|
||||||
|
"cost_center": erpnext.get_default_cost_center(company),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
15
erpnext/hr/doctype/employee_advance/employee_advance_list.js
Normal file
15
erpnext/hr/doctype/employee_advance/employee_advance_list.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
frappe.listview_settings["Employee Advance"] = {
|
||||||
|
get_indicator: function(doc) {
|
||||||
|
let status_color = {
|
||||||
|
"Draft": "red",
|
||||||
|
"Submitted": "blue",
|
||||||
|
"Cancelled": "red",
|
||||||
|
"Paid": "green",
|
||||||
|
"Unpaid": "orange",
|
||||||
|
"Claimed": "blue",
|
||||||
|
"Returned": "gray",
|
||||||
|
"Partly Claimed and Returned": "yellow"
|
||||||
|
};
|
||||||
|
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,13 +12,21 @@ from erpnext.hr.doctype.employee_advance.employee_advance import (
|
|||||||
EmployeeAdvanceOverPayment,
|
EmployeeAdvanceOverPayment,
|
||||||
create_return_through_additional_salary,
|
create_return_through_additional_salary,
|
||||||
make_bank_entry,
|
make_bank_entry,
|
||||||
|
make_return_entry,
|
||||||
)
|
)
|
||||||
from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
|
from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
|
||||||
|
from erpnext.hr.doctype.expense_claim.test_expense_claim import (
|
||||||
|
get_payable_account,
|
||||||
|
make_expense_claim,
|
||||||
|
)
|
||||||
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
|
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
|
||||||
class TestEmployeeAdvance(unittest.TestCase):
|
class TestEmployeeAdvance(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
frappe.db.delete("Employee Advance")
|
||||||
|
|
||||||
def test_paid_amount_and_status(self):
|
def test_paid_amount_and_status(self):
|
||||||
employee_name = make_employee("_T@employe.advance")
|
employee_name = make_employee("_T@employe.advance")
|
||||||
advance = make_employee_advance(employee_name)
|
advance = make_employee_advance(employee_name)
|
||||||
@@ -53,9 +61,108 @@ class TestEmployeeAdvance(unittest.TestCase):
|
|||||||
self.assertEqual(advance.paid_amount, 0)
|
self.assertEqual(advance.paid_amount, 0)
|
||||||
self.assertEqual(advance.status, "Unpaid")
|
self.assertEqual(advance.status, "Unpaid")
|
||||||
|
|
||||||
|
advance.cancel()
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.status, "Cancelled")
|
||||||
|
|
||||||
|
def test_claimed_status(self):
|
||||||
|
# CLAIMED Status check, full amount claimed
|
||||||
|
payable_account = get_payable_account("_Test Company")
|
||||||
|
claim = make_expense_claim(
|
||||||
|
payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True
|
||||||
|
)
|
||||||
|
|
||||||
|
advance = make_employee_advance(claim.employee)
|
||||||
|
pe = make_payment_entry(advance)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
claim = get_advances_for_claim(claim, advance.name)
|
||||||
|
claim.save()
|
||||||
|
claim.submit()
|
||||||
|
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.claimed_amount, 1000)
|
||||||
|
self.assertEqual(advance.status, "Claimed")
|
||||||
|
|
||||||
|
# advance should not be shown in claims
|
||||||
|
advances = get_advances(claim.employee)
|
||||||
|
advances = [entry.name for entry in advances]
|
||||||
|
self.assertTrue(advance.name not in advances)
|
||||||
|
|
||||||
|
# cancel claim; status should be Paid
|
||||||
|
claim.cancel()
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.claimed_amount, 0)
|
||||||
|
self.assertEqual(advance.status, "Paid")
|
||||||
|
|
||||||
|
def test_partly_claimed_and_returned_status(self):
|
||||||
|
payable_account = get_payable_account("_Test Company")
|
||||||
|
claim = make_expense_claim(
|
||||||
|
payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True
|
||||||
|
)
|
||||||
|
|
||||||
|
advance = make_employee_advance(claim.employee)
|
||||||
|
pe = make_payment_entry(advance)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
# PARTLY CLAIMED AND RETURNED status check
|
||||||
|
# 500 Claimed, 500 Returned
|
||||||
|
claim = make_expense_claim(
|
||||||
|
payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True
|
||||||
|
)
|
||||||
|
|
||||||
|
advance = make_employee_advance(claim.employee)
|
||||||
|
pe = make_payment_entry(advance)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
claim = get_advances_for_claim(claim, advance.name, amount=500)
|
||||||
|
claim.save()
|
||||||
|
claim.submit()
|
||||||
|
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.claimed_amount, 500)
|
||||||
|
self.assertEqual(advance.status, "Paid")
|
||||||
|
|
||||||
|
entry = make_return_entry(
|
||||||
|
employee=advance.employee,
|
||||||
|
company=advance.company,
|
||||||
|
employee_advance_name=advance.name,
|
||||||
|
return_amount=flt(advance.paid_amount - advance.claimed_amount),
|
||||||
|
advance_account=advance.advance_account,
|
||||||
|
mode_of_payment=advance.mode_of_payment,
|
||||||
|
currency=advance.currency,
|
||||||
|
exchange_rate=advance.exchange_rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = frappe.get_doc(entry)
|
||||||
|
entry.insert()
|
||||||
|
entry.submit()
|
||||||
|
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.return_amount, 500)
|
||||||
|
self.assertEqual(advance.status, "Partly Claimed and Returned")
|
||||||
|
|
||||||
|
# advance should not be shown in claims
|
||||||
|
advances = get_advances(claim.employee)
|
||||||
|
advances = [entry.name for entry in advances]
|
||||||
|
self.assertTrue(advance.name not in advances)
|
||||||
|
|
||||||
|
# Cancel return entry; status should change to PAID
|
||||||
|
entry.cancel()
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.return_amount, 0)
|
||||||
|
self.assertEqual(advance.status, "Paid")
|
||||||
|
|
||||||
|
# advance should be shown in claims
|
||||||
|
advances = get_advances(claim.employee)
|
||||||
|
advances = [entry.name for entry in advances]
|
||||||
|
self.assertTrue(advance.name in advances)
|
||||||
|
|
||||||
def test_repay_unclaimed_amount_from_salary(self):
|
def test_repay_unclaimed_amount_from_salary(self):
|
||||||
employee_name = make_employee("_T@employe.advance")
|
employee_name = make_employee("_T@employe.advance")
|
||||||
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
|
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
|
||||||
|
pe = make_payment_entry(advance)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
args = {"type": "Deduction"}
|
args = {"type": "Deduction"}
|
||||||
create_salary_component("Advance Salary - Deduction", **args)
|
create_salary_component("Advance Salary - Deduction", **args)
|
||||||
@@ -85,11 +192,13 @@ class TestEmployeeAdvance(unittest.TestCase):
|
|||||||
|
|
||||||
advance.reload()
|
advance.reload()
|
||||||
self.assertEqual(advance.return_amount, 1000)
|
self.assertEqual(advance.return_amount, 1000)
|
||||||
|
self.assertEqual(advance.status, "Returned")
|
||||||
|
|
||||||
# update advance return amount on additional salary cancellation
|
# update advance return amount on additional salary cancellation
|
||||||
additional_salary.cancel()
|
additional_salary.cancel()
|
||||||
advance.reload()
|
advance.reload()
|
||||||
self.assertEqual(advance.return_amount, 700)
|
self.assertEqual(advance.return_amount, 700)
|
||||||
|
self.assertEqual(advance.status, "Paid")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", {
|
|||||||
['docstatus', '=', 1],
|
['docstatus', '=', 1],
|
||||||
['employee', '=', frm.doc.employee],
|
['employee', '=', frm.doc.employee],
|
||||||
['paid_amount', '>', 0],
|
['paid_amount', '>', 0],
|
||||||
['status', '!=', 'Claimed']
|
['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -414,25 +414,27 @@ def get_expense_claim_account(expense_claim_type, company):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_advances(employee, advance_id=None):
|
def get_advances(employee, advance_id=None):
|
||||||
|
advance = frappe.qb.DocType("Employee Advance")
|
||||||
|
|
||||||
|
query = frappe.qb.from_(advance).select(
|
||||||
|
advance.name,
|
||||||
|
advance.posting_date,
|
||||||
|
advance.paid_amount,
|
||||||
|
advance.claimed_amount,
|
||||||
|
advance.advance_account,
|
||||||
|
)
|
||||||
|
|
||||||
if not advance_id:
|
if not advance_id:
|
||||||
condition = "docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount".format(
|
query = query.where(
|
||||||
frappe.db.escape(employee)
|
(advance.docstatus == 1)
|
||||||
|
& (advance.employee == employee)
|
||||||
|
& (advance.paid_amount > 0)
|
||||||
|
& (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"]))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
condition = "name={0}".format(frappe.db.escape(advance_id))
|
query = query.where(advance.name == advance_id)
|
||||||
|
|
||||||
return frappe.db.sql(
|
return query.run(as_dict=True)
|
||||||
"""
|
|
||||||
select
|
|
||||||
name, posting_date, paid_amount, claimed_amount, advance_account
|
|
||||||
from
|
|
||||||
`tabEmployee Advance`
|
|
||||||
where {0}
|
|
||||||
""".format(
|
|
||||||
condition
|
|
||||||
),
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.utils import get_link_to_form
|
||||||
from frappe.website.website_generator import WebsiteGenerator
|
from frappe.website.website_generator import WebsiteGenerator
|
||||||
|
|
||||||
from erpnext.hr.doctype.staffing_plan.staffing_plan import (
|
from erpnext.hr.doctype.staffing_plan.staffing_plan import (
|
||||||
@@ -33,26 +34,32 @@ class JobOpening(WebsiteGenerator):
|
|||||||
self.staffing_plan = staffing_plan[0].name
|
self.staffing_plan = staffing_plan[0].name
|
||||||
self.planned_vacancies = staffing_plan[0].vacancies
|
self.planned_vacancies = staffing_plan[0].vacancies
|
||||||
elif not self.planned_vacancies:
|
elif not self.planned_vacancies:
|
||||||
planned_vacancies = frappe.db.sql(
|
self.planned_vacancies = frappe.db.get_value(
|
||||||
"""
|
"Staffing Plan Detail",
|
||||||
select vacancies from `tabStaffing Plan Detail`
|
{"parent": self.staffing_plan, "designation": self.designation},
|
||||||
where parent=%s and designation=%s""",
|
"vacancies",
|
||||||
(self.staffing_plan, self.designation),
|
|
||||||
)
|
)
|
||||||
self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None
|
|
||||||
|
|
||||||
if self.staffing_plan and self.planned_vacancies:
|
if self.staffing_plan and self.planned_vacancies:
|
||||||
staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company")
|
staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company")
|
||||||
lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"])
|
|
||||||
|
|
||||||
designation_counts = get_designation_counts(self.designation, self.company)
|
designation_counts = get_designation_counts(self.designation, self.company, self.name)
|
||||||
current_count = designation_counts["employee_count"] + designation_counts["job_openings"]
|
current_count = designation_counts["employee_count"] + designation_counts["job_openings"]
|
||||||
|
|
||||||
if self.planned_vacancies <= current_count:
|
number_of_positions = frappe.db.get_value(
|
||||||
|
"Staffing Plan Detail",
|
||||||
|
{"parent": self.staffing_plan, "designation": self.designation},
|
||||||
|
"number_of_positions",
|
||||||
|
)
|
||||||
|
|
||||||
|
if number_of_positions <= current_count:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}"
|
"Job Openings for the designation {0} are already open or the hiring is complete as per the Staffing Plan {1}"
|
||||||
).format(self.designation, self.staffing_plan)
|
).format(
|
||||||
|
frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan)
|
||||||
|
),
|
||||||
|
title=_("Vacancies fulfilled"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context(self, context):
|
def get_context(self, context):
|
||||||
|
|||||||
@@ -3,8 +3,77 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# test_records = frappe.get_test_records('Job Opening')
|
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
|
||||||
|
from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company
|
||||||
|
|
||||||
|
|
||||||
class TestJobOpening(unittest.TestCase):
|
class TestJobOpening(FrappeTestCase):
|
||||||
pass
|
def setUp(self):
|
||||||
|
frappe.db.delete("Staffing Plan")
|
||||||
|
frappe.db.delete("Staffing Plan Detail")
|
||||||
|
frappe.db.delete("Job Opening")
|
||||||
|
|
||||||
|
make_company("_Test Opening Company", "_TOC")
|
||||||
|
frappe.db.delete("Employee", {"company": "_Test Opening Company"})
|
||||||
|
|
||||||
|
def test_vacancies_fulfilled(self):
|
||||||
|
make_employee(
|
||||||
|
"test_job_opening@example.com", company="_Test Opening Company", designation="Designer"
|
||||||
|
)
|
||||||
|
|
||||||
|
staffing_plan = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Staffing Plan",
|
||||||
|
"company": "_Test Opening Company",
|
||||||
|
"name": "Test",
|
||||||
|
"from_date": getdate(),
|
||||||
|
"to_date": add_days(getdate(), 10),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
staffing_plan.append(
|
||||||
|
"staffing_details",
|
||||||
|
{"designation": "Designer", "vacancies": 1, "estimated_cost_per_position": 50000},
|
||||||
|
)
|
||||||
|
staffing_plan.insert()
|
||||||
|
staffing_plan.submit()
|
||||||
|
|
||||||
|
self.assertEqual(staffing_plan.staffing_details[0].number_of_positions, 2)
|
||||||
|
|
||||||
|
# allows creating 1 job opening as per vacancy
|
||||||
|
opening_1 = get_job_opening()
|
||||||
|
opening_1.insert()
|
||||||
|
|
||||||
|
# vacancies as per staffing plan already fulfilled via job opening and existing employee count
|
||||||
|
opening_2 = get_job_opening(job_title="Designer New")
|
||||||
|
self.assertRaises(frappe.ValidationError, opening_2.insert)
|
||||||
|
|
||||||
|
# allows updating existing job opening
|
||||||
|
opening_1.status = "Closed"
|
||||||
|
opening_1.save()
|
||||||
|
|
||||||
|
|
||||||
|
def get_job_opening(**args):
|
||||||
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
opening = frappe.db.exists("Job Opening", {"job_title": args.job_title or "Designer"})
|
||||||
|
if opening:
|
||||||
|
return frappe.get_doc("Job Opening", opening)
|
||||||
|
|
||||||
|
opening = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Job Opening",
|
||||||
|
"job_title": "Designer",
|
||||||
|
"designation": "Designer",
|
||||||
|
"company": "_Test Opening Company",
|
||||||
|
"status": "Open",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
opening.update(args)
|
||||||
|
|
||||||
|
return opening
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
date: frm.doc.from_date,
|
date: frm.doc.from_date,
|
||||||
to_date: frm.doc.to_date,
|
to_date: frm.doc.to_date,
|
||||||
leave_type: frm.doc.leave_type,
|
leave_type: frm.doc.leave_type,
|
||||||
consider_all_leaves_in_the_allocation_period: true
|
consider_all_leaves_in_the_allocation_period: 1
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class LeaveApplication(Document):
|
|||||||
share_doc_with_approver(self, self.leave_approver)
|
share_doc_with_approver(self, self.leave_approver)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.status == "Open":
|
if self.status in ["Open", "Cancelled"]:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
|
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
|
||||||
)
|
)
|
||||||
@@ -758,22 +758,6 @@ def get_leave_details(employee, date):
|
|||||||
leave_allocation = {}
|
leave_allocation = {}
|
||||||
for d in allocation_records:
|
for d in allocation_records:
|
||||||
allocation = allocation_records.get(d, frappe._dict())
|
allocation = allocation_records.get(d, frappe._dict())
|
||||||
|
|
||||||
total_allocated_leaves = (
|
|
||||||
frappe.db.get_value(
|
|
||||||
"Leave Allocation",
|
|
||||||
{
|
|
||||||
"from_date": ("<=", 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(
|
remaining_leaves = get_leave_balance_on(
|
||||||
employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
|
employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
|
||||||
)
|
)
|
||||||
@@ -783,10 +767,11 @@ def get_leave_details(employee, date):
|
|||||||
leaves_pending = get_leaves_pending_approval_for_period(
|
leaves_pending = get_leaves_pending_approval_for_period(
|
||||||
employee, d, allocation.from_date, end_date
|
employee, d, allocation.from_date, end_date
|
||||||
)
|
)
|
||||||
|
expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken)
|
||||||
|
|
||||||
leave_allocation[d] = {
|
leave_allocation[d] = {
|
||||||
"total_leaves": total_allocated_leaves,
|
"total_leaves": allocation.total_leaves_allocated,
|
||||||
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
|
"expired_leaves": expired_leaves if expired_leaves > 0 else 0,
|
||||||
"leaves_taken": leaves_taken,
|
"leaves_taken": leaves_taken,
|
||||||
"leaves_pending_approval": leaves_pending,
|
"leaves_pending_approval": leaves_pending,
|
||||||
"remaining_leaves": remaining_leaves,
|
"remaining_leaves": remaining_leaves,
|
||||||
@@ -831,7 +816,7 @@ def get_leave_balance_on(
|
|||||||
allocation_records = get_leave_allocation_records(employee, date, leave_type)
|
allocation_records = get_leave_allocation_records(employee, date, leave_type)
|
||||||
allocation = allocation_records.get(leave_type, frappe._dict())
|
allocation = allocation_records.get(leave_type, frappe._dict())
|
||||||
|
|
||||||
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
|
end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
|
||||||
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
|
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
|
||||||
|
|
||||||
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
|
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
|
||||||
@@ -1118,7 +1103,7 @@ def add_leaves(events, start, end, filter_conditions=None):
|
|||||||
WHERE
|
WHERE
|
||||||
from_date <= %(end)s AND to_date >= %(start)s <= to_date
|
from_date <= %(end)s AND to_date >= %(start)s <= to_date
|
||||||
AND docstatus < 2
|
AND docstatus < 2
|
||||||
AND status != 'Rejected'
|
AND status in ('Approved', 'Open')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if conditions:
|
if conditions:
|
||||||
@@ -1202,24 +1187,32 @@ def get_mandatory_approval(doctype):
|
|||||||
|
|
||||||
|
|
||||||
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||||
query = """
|
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||||
select employee, leave_type, from_date, to_date, total_leave_days
|
query = (
|
||||||
from `tabLeave Application`
|
frappe.qb.from_(LeaveApplication)
|
||||||
where employee=%(employee)s
|
.select(
|
||||||
and docstatus=1
|
LeaveApplication.employee,
|
||||||
and (from_date between %(from_date)s and %(to_date)s
|
LeaveApplication.leave_type,
|
||||||
or to_date between %(from_date)s and %(to_date)s
|
LeaveApplication.from_date,
|
||||||
or (from_date < %(from_date)s and to_date > %(to_date)s))
|
LeaveApplication.to_date,
|
||||||
"""
|
LeaveApplication.total_leave_days,
|
||||||
if leave_type:
|
)
|
||||||
query += "and leave_type=%(leave_type)s"
|
.where(
|
||||||
|
(LeaveApplication.employee == employee)
|
||||||
leave_applications = frappe.db.sql(
|
& (LeaveApplication.docstatus == 1)
|
||||||
query,
|
& (LeaveApplication.status == "Approved")
|
||||||
{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
|
& (
|
||||||
as_dict=1,
|
(LeaveApplication.from_date.between(from_date, to_date))
|
||||||
|
| (LeaveApplication.to_date.between(from_date, to_date))
|
||||||
|
| ((LeaveApplication.from_date < from_date) & (LeaveApplication.to_date > to_date))
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if leave_type:
|
||||||
|
query = query.where(LeaveApplication.leave_type == leave_type)
|
||||||
|
leave_applications = query.run(as_dict=True)
|
||||||
|
|
||||||
leave_days = 0
|
leave_days = 0
|
||||||
for leave_app in leave_applications:
|
for leave_app in leave_applications:
|
||||||
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
|
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
frappe.listview_settings['Leave Application'] = {
|
frappe.listview_settings["Leave Application"] = {
|
||||||
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
|
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
|
||||||
has_indicator_for_draft: 1,
|
has_indicator_for_draft: 1,
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
if (doc.status === "Approved") {
|
let status_color = {
|
||||||
return [__("Approved"), "green", "status,=,Approved"];
|
"Approved": "green",
|
||||||
} else if (doc.status === "Rejected") {
|
"Rejected": "red",
|
||||||
return [__("Rejected"), "red", "status,=,Rejected"];
|
"Open": "orange",
|
||||||
} else {
|
"Cancelled": "red",
|
||||||
return [__("Open"), "red", "status,=,Open"];
|
"Submitted": "blue"
|
||||||
}
|
};
|
||||||
|
return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,14 @@ _test_records = [
|
|||||||
|
|
||||||
class TestLeaveApplication(unittest.TestCase):
|
class TestLeaveApplication(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
|
for dt in [
|
||||||
|
"Leave Application",
|
||||||
|
"Leave Allocation",
|
||||||
|
"Salary Slip",
|
||||||
|
"Leave Ledger Entry",
|
||||||
|
"Leave Period",
|
||||||
|
"Leave Policy Assignment",
|
||||||
|
]:
|
||||||
frappe.db.delete(dt)
|
frappe.db.delete(dt)
|
||||||
|
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
@@ -702,58 +709,24 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
self.assertEqual(details.leave_balance, 30)
|
self.assertEqual(details.leave_balance, 30)
|
||||||
|
|
||||||
def test_earned_leaves_creation(self):
|
def test_earned_leaves_creation(self):
|
||||||
|
from erpnext.hr.utils import allocate_earned_leaves
|
||||||
frappe.db.sql("""delete from `tabLeave Period`""")
|
|
||||||
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
|
|
||||||
frappe.db.sql("""delete from `tabLeave Allocation`""")
|
|
||||||
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
|
|
||||||
|
|
||||||
leave_period = get_leave_period()
|
leave_period = get_leave_period()
|
||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
leave_type = "Test Earned Leave Type"
|
leave_type = "Test Earned Leave Type"
|
||||||
frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
|
make_policy_assignment(employee, leave_type, leave_period)
|
||||||
frappe.get_doc(
|
|
||||||
dict(
|
|
||||||
leave_type_name=leave_type,
|
|
||||||
doctype="Leave Type",
|
|
||||||
is_earned_leave=1,
|
|
||||||
earned_leave_frequency="Monthly",
|
|
||||||
rounding=0.5,
|
|
||||||
max_leaves_allowed=6,
|
|
||||||
)
|
|
||||||
).insert()
|
|
||||||
|
|
||||||
leave_policy = frappe.get_doc(
|
for i in range(0, 14):
|
||||||
{
|
|
||||||
"doctype": "Leave Policy",
|
|
||||||
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
|
|
||||||
}
|
|
||||||
).insert()
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"assignment_based_on": "Leave Period",
|
|
||||||
"leave_policy": leave_policy.name,
|
|
||||||
"leave_period": leave_period.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
leave_policy_assignments = create_assignment_for_multiple_employees(
|
|
||||||
[employee.name], frappe._dict(data)
|
|
||||||
)
|
|
||||||
|
|
||||||
from erpnext.hr.utils import allocate_earned_leaves
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < 14:
|
|
||||||
allocate_earned_leaves()
|
allocate_earned_leaves()
|
||||||
i += 1
|
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||||
|
|
||||||
# validate earned leaves creation without maximum leaves
|
# validate earned leaves creation without maximum leaves
|
||||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||||
i = 0
|
|
||||||
while i < 6:
|
for i in range(0, 6):
|
||||||
allocate_earned_leaves()
|
allocate_earned_leaves()
|
||||||
i += 1
|
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||||
|
|
||||||
# test to not consider current leave in leave balance while submitting
|
# test to not consider current leave in leave balance while submitting
|
||||||
@@ -969,6 +942,54 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
self.assertEqual(leave_allocation["leaves_pending_approval"], 1)
|
self.assertEqual(leave_allocation["leaves_pending_approval"], 1)
|
||||||
self.assertEqual(leave_allocation["remaining_leaves"], 26)
|
self.assertEqual(leave_allocation["remaining_leaves"], 26)
|
||||||
|
|
||||||
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
|
def test_get_earned_leave_details_for_dashboard(self):
|
||||||
|
from erpnext.hr.utils import allocate_earned_leaves
|
||||||
|
|
||||||
|
leave_period = get_leave_period()
|
||||||
|
employee = get_employee()
|
||||||
|
leave_type = "Test Earned Leave Type"
|
||||||
|
leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period)
|
||||||
|
allocation = frappe.db.get_value(
|
||||||
|
"Leave Allocation",
|
||||||
|
{"leave_policy_assignment": leave_policy_assignments[0]},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
allocation = frappe.get_doc("Leave Allocation", allocation)
|
||||||
|
allocation.new_leaves_allocated = 2
|
||||||
|
allocation.save()
|
||||||
|
|
||||||
|
for i in range(0, 6):
|
||||||
|
allocate_earned_leaves()
|
||||||
|
|
||||||
|
first_sunday = get_first_sunday(self.holiday_list)
|
||||||
|
make_leave_application(
|
||||||
|
employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type
|
||||||
|
)
|
||||||
|
|
||||||
|
details = get_leave_details(employee.name, allocation.from_date)
|
||||||
|
leave_allocation = details["leave_allocation"][leave_type]
|
||||||
|
expected = {
|
||||||
|
"total_leaves": 2.0,
|
||||||
|
"expired_leaves": 0.0,
|
||||||
|
"leaves_taken": 1.0,
|
||||||
|
"leaves_pending_approval": 0.0,
|
||||||
|
"remaining_leaves": 1.0,
|
||||||
|
}
|
||||||
|
self.assertEqual(leave_allocation, expected)
|
||||||
|
|
||||||
|
details = get_leave_details(employee.name, getdate())
|
||||||
|
leave_allocation = details["leave_allocation"][leave_type]
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"total_leaves": 5.0,
|
||||||
|
"expired_leaves": 0.0,
|
||||||
|
"leaves_taken": 1.0,
|
||||||
|
"leaves_pending_approval": 0.0,
|
||||||
|
"remaining_leaves": 4.0,
|
||||||
|
}
|
||||||
|
self.assertEqual(leave_allocation, expected)
|
||||||
|
|
||||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
def test_get_leave_allocation_records(self):
|
def test_get_leave_allocation_records(self):
|
||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
@@ -1099,3 +1120,36 @@ def get_first_sunday(holiday_list, for_date=None):
|
|||||||
)[0][0]
|
)[0][0]
|
||||||
|
|
||||||
return first_sunday
|
return first_sunday
|
||||||
|
|
||||||
|
|
||||||
|
def make_policy_assignment(employee, leave_type, leave_period):
|
||||||
|
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
|
||||||
|
frappe.get_doc(
|
||||||
|
dict(
|
||||||
|
leave_type_name=leave_type,
|
||||||
|
doctype="Leave Type",
|
||||||
|
is_earned_leave=1,
|
||||||
|
earned_leave_frequency="Monthly",
|
||||||
|
rounding=0.5,
|
||||||
|
max_leaves_allowed=6,
|
||||||
|
)
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
leave_policy = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Leave Policy",
|
||||||
|
"title": "Test Leave Policy",
|
||||||
|
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"assignment_based_on": "Leave Period",
|
||||||
|
"leave_policy": leave_policy.name,
|
||||||
|
"leave_period": leave_period.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
leave_policy_assignments = create_assignment_for_multiple_employees(
|
||||||
|
[employee.name], frappe._dict(data)
|
||||||
|
)
|
||||||
|
return leave_policy_assignments
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import getdate, nowdate
|
from frappe.utils import getdate, nowdate
|
||||||
|
|
||||||
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
|
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
|
||||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
|
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
|
||||||
from erpnext.hr.utils import set_employee_name, validate_active_employee
|
from erpnext.hr.utils import set_employee_name, validate_active_employee
|
||||||
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
|
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
|
||||||
@@ -107,7 +107,10 @@ class LeaveEncashment(Document):
|
|||||||
self.leave_balance = (
|
self.leave_balance = (
|
||||||
allocation.total_leaves_allocated
|
allocation.total_leaves_allocated
|
||||||
- allocation.carry_forwarded_leaves_count
|
- allocation.carry_forwarded_leaves_count
|
||||||
- get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
|
# adding this because the function returns a -ve number
|
||||||
|
+ get_leaves_for_period(
|
||||||
|
self.employee, self.leave_type, allocation.from_date, self.encashment_date
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
encashable_days = self.leave_balance - frappe.db.get_value(
|
encashable_days = self.leave_balance - frappe.db.get_value(
|
||||||
@@ -126,14 +129,25 @@ class LeaveEncashment(Document):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_leave_allocation(self):
|
def get_leave_allocation(self):
|
||||||
leave_allocation = frappe.db.sql(
|
date = self.encashment_date or getdate()
|
||||||
"""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
|
|
||||||
between from_date and to_date and docstatus=1 and leave_type='{1}'
|
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
|
||||||
and employee= '{2}'""".format(
|
leave_allocation = (
|
||||||
self.encashment_date or getdate(nowdate()), self.leave_type, self.employee
|
frappe.qb.from_(LeaveAllocation)
|
||||||
),
|
.select(
|
||||||
as_dict=1,
|
LeaveAllocation.name,
|
||||||
) # nosec
|
LeaveAllocation.from_date,
|
||||||
|
LeaveAllocation.to_date,
|
||||||
|
LeaveAllocation.total_leaves_allocated,
|
||||||
|
LeaveAllocation.carry_forwarded_leaves_count,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
((LeaveAllocation.from_date <= date) & (date <= LeaveAllocation.to_date))
|
||||||
|
& (LeaveAllocation.docstatus == 1)
|
||||||
|
& (LeaveAllocation.leave_type == self.leave_type)
|
||||||
|
& (LeaveAllocation.employee == self.employee)
|
||||||
|
)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
return leave_allocation[0] if leave_allocation else None
|
return leave_allocation[0] if leave_allocation else None
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,42 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_months, today
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import add_days, get_year_ending, get_year_start, getdate
|
||||||
|
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||||
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
|
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
|
||||||
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
|
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
|
||||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||||
create_assignment_for_multiple_employees,
|
create_assignment_for_multiple_employees,
|
||||||
)
|
)
|
||||||
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||||
|
make_holiday_list,
|
||||||
|
make_leave_application,
|
||||||
|
)
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
test_dependencies = ["Leave Type"]
|
test_records = frappe.get_test_records("Leave Type")
|
||||||
|
|
||||||
|
|
||||||
class TestLeaveEncashment(unittest.TestCase):
|
class TestLeaveEncashment(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.sql("""delete from `tabLeave Period`""")
|
frappe.db.delete("Leave Period")
|
||||||
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
|
frappe.db.delete("Leave Policy Assignment")
|
||||||
frappe.db.sql("""delete from `tabLeave Allocation`""")
|
frappe.db.delete("Leave Allocation")
|
||||||
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
|
frappe.db.delete("Leave Ledger Entry")
|
||||||
frappe.db.sql("""delete from `tabAdditional Salary`""")
|
frappe.db.delete("Additional Salary")
|
||||||
|
frappe.db.delete("Leave Encashment")
|
||||||
|
|
||||||
|
if not frappe.db.exists("Leave Type", "_Test Leave Type Encashment"):
|
||||||
|
frappe.get_doc(test_records[2]).insert()
|
||||||
|
|
||||||
|
date = getdate()
|
||||||
|
year_start = getdate(get_year_start(date))
|
||||||
|
year_end = getdate(get_year_ending(date))
|
||||||
|
|
||||||
|
make_holiday_list("_Test Leave Encashment", year_start, year_end)
|
||||||
|
|
||||||
# create the leave policy
|
# create the leave policy
|
||||||
leave_policy = create_leave_policy(
|
leave_policy = create_leave_policy(
|
||||||
@@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase):
|
|||||||
leave_policy.submit()
|
leave_policy.submit()
|
||||||
|
|
||||||
# create employee, salary structure and assignment
|
# create employee, salary structure and assignment
|
||||||
self.employee = make_employee("test_employee_encashment@example.com")
|
self.employee = make_employee("test_employee_encashment@example.com", company="_Test Company")
|
||||||
|
|
||||||
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
|
self.leave_period = create_leave_period(year_start, year_end, "_Test Company")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"assignment_based_on": "Leave Period",
|
"assignment_based_on": "Leave Period",
|
||||||
@@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase):
|
|||||||
other_details={"leave_encashment_amount_per_day": 50},
|
other_details={"leave_encashment_amount_per_day": 50},
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||||
for dt in [
|
|
||||||
"Leave Period",
|
|
||||||
"Leave Allocation",
|
|
||||||
"Leave Ledger Entry",
|
|
||||||
"Additional Salary",
|
|
||||||
"Leave Encashment",
|
|
||||||
"Salary Structure",
|
|
||||||
"Leave Policy",
|
|
||||||
]:
|
|
||||||
frappe.db.sql("delete from `tab%s`" % dt)
|
|
||||||
|
|
||||||
def test_leave_balance_value_and_amount(self):
|
def test_leave_balance_value_and_amount(self):
|
||||||
frappe.db.sql("""delete from `tabLeave Encashment`""")
|
|
||||||
leave_encashment = frappe.get_doc(
|
leave_encashment = frappe.get_doc(
|
||||||
dict(
|
dict(
|
||||||
doctype="Leave Encashment",
|
doctype="Leave Encashment",
|
||||||
employee=self.employee,
|
employee=self.employee,
|
||||||
leave_type="_Test Leave Type Encashment",
|
leave_type="_Test Leave Type Encashment",
|
||||||
leave_period=self.leave_period.name,
|
leave_period=self.leave_period.name,
|
||||||
payroll_date=today(),
|
encashment_date=self.leave_period.to_date,
|
||||||
currency="INR",
|
currency="INR",
|
||||||
)
|
)
|
||||||
).insert()
|
).insert()
|
||||||
@@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase):
|
|||||||
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
||||||
self.assertTrue(add_sal)
|
self.assertTrue(add_sal)
|
||||||
|
|
||||||
def test_creation_of_leave_ledger_entry_on_submit(self):
|
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||||
frappe.db.sql("""delete from `tabLeave Encashment`""")
|
def test_leave_balance_value_with_leaves_and_amount(self):
|
||||||
|
date = self.leave_period.from_date
|
||||||
|
leave_application = make_leave_application(
|
||||||
|
self.employee, date, add_days(date, 3), "_Test Leave Type Encashment"
|
||||||
|
)
|
||||||
|
leave_application.reload()
|
||||||
|
|
||||||
leave_encashment = frappe.get_doc(
|
leave_encashment = frappe.get_doc(
|
||||||
dict(
|
dict(
|
||||||
doctype="Leave Encashment",
|
doctype="Leave Encashment",
|
||||||
employee=self.employee,
|
employee=self.employee,
|
||||||
leave_type="_Test Leave Type Encashment",
|
leave_type="_Test Leave Type Encashment",
|
||||||
leave_period=self.leave_period.name,
|
leave_period=self.leave_period.name,
|
||||||
payroll_date=today(),
|
encashment_date=self.leave_period.to_date,
|
||||||
|
currency="INR",
|
||||||
|
)
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
self.assertEqual(leave_encashment.leave_balance, 10 - leave_application.total_leave_days)
|
||||||
|
# encashable days threshold is 5, total leaves are 6, so encashable days = 6-5 = 1
|
||||||
|
# with charge of 50 per day
|
||||||
|
self.assertEqual(leave_encashment.encashable_days, leave_encashment.leave_balance - 5)
|
||||||
|
self.assertEqual(leave_encashment.encashment_amount, 50)
|
||||||
|
|
||||||
|
leave_encashment.submit()
|
||||||
|
|
||||||
|
# assert links
|
||||||
|
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
||||||
|
self.assertTrue(add_sal)
|
||||||
|
|
||||||
|
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||||
|
def test_creation_of_leave_ledger_entry_on_submit(self):
|
||||||
|
leave_encashment = frappe.get_doc(
|
||||||
|
dict(
|
||||||
|
doctype="Leave Encashment",
|
||||||
|
employee=self.employee,
|
||||||
|
leave_type="_Test Leave Type Encashment",
|
||||||
|
leave_period=self.leave_period.name,
|
||||||
|
encashment_date=self.leave_period.to_date,
|
||||||
currency="INR",
|
currency="INR",
|
||||||
)
|
)
|
||||||
).insert()
|
).insert()
|
||||||
|
|||||||
@@ -175,27 +175,24 @@ class StaffingPlan(Document):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_designation_counts(designation, company):
|
def get_designation_counts(designation, company, job_opening=None):
|
||||||
if not designation:
|
if not designation:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
employee_counts = {}
|
|
||||||
company_set = get_descendants_of("Company", company)
|
company_set = get_descendants_of("Company", company)
|
||||||
company_set.append(company)
|
company_set.append(company)
|
||||||
|
|
||||||
employee_counts["employee_count"] = frappe.db.get_value(
|
employee_count = frappe.db.count(
|
||||||
"Employee",
|
"Employee", {"designation": designation, "status": "Active", "company": ("in", company_set)}
|
||||||
filters={"designation": designation, "status": "Active", "company": ("in", company_set)},
|
|
||||||
fieldname=["count(name)"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
employee_counts["job_openings"] = frappe.db.get_value(
|
filters = {"designation": designation, "status": "Open", "company": ("in", company_set)}
|
||||||
"Job Opening",
|
if job_opening:
|
||||||
filters={"designation": designation, "status": "Open", "company": ("in", company_set)},
|
filters["name"] = ("!=", job_opening)
|
||||||
fieldname=["count(name)"],
|
|
||||||
)
|
|
||||||
|
|
||||||
return employee_counts
|
job_openings = frappe.db.count("Job Opening", filters)
|
||||||
|
|
||||||
|
return {"employee_count": employee_count, "job_openings": job_openings}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -85,13 +85,16 @@ def _set_up():
|
|||||||
make_company()
|
make_company()
|
||||||
|
|
||||||
|
|
||||||
def make_company():
|
def make_company(name=None, abbr=None):
|
||||||
if frappe.db.exists("Company", "_Test Company 10"):
|
if not name:
|
||||||
|
name = "_Test Company 10"
|
||||||
|
|
||||||
|
if frappe.db.exists("Company", name):
|
||||||
return
|
return
|
||||||
|
|
||||||
company = frappe.new_doc("Company")
|
company = frappe.new_doc("Company")
|
||||||
company.company_name = "_Test Company 10"
|
company.company_name = name
|
||||||
company.abbr = "_TC10"
|
company.abbr = abbr or "_TC10"
|
||||||
company.parent_company = "_Test Company 3"
|
company.parent_company = "_Test Company 3"
|
||||||
company.default_currency = "INR"
|
company.default_currency = "INR"
|
||||||
company.country = "Pakistan"
|
company.country = "Pakistan"
|
||||||
|
|||||||
@@ -598,20 +598,18 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_salary_assignment(employee, date):
|
def get_salary_assignments(employee, payroll_period):
|
||||||
assignment = frappe.db.sql(
|
start_date, end_date = frappe.db.get_value(
|
||||||
"""
|
"Payroll Period", payroll_period, ["start_date", "end_date"]
|
||||||
select * from `tabSalary Structure Assignment`
|
|
||||||
where employee=%(employee)s
|
|
||||||
and docstatus = 1
|
|
||||||
and %(on_date)s >= from_date order by from_date desc limit 1""",
|
|
||||||
{
|
|
||||||
"employee": employee,
|
|
||||||
"on_date": date,
|
|
||||||
},
|
|
||||||
as_dict=1,
|
|
||||||
)
|
)
|
||||||
return assignment[0] if assignment else None
|
assignments = frappe.db.get_all(
|
||||||
|
"Salary Structure Assignment",
|
||||||
|
filters={"employee": employee, "docstatus": 1, "from_date": ["between", (start_date, end_date)]},
|
||||||
|
fields=["*"],
|
||||||
|
order_by="from_date",
|
||||||
|
)
|
||||||
|
|
||||||
|
return assignments
|
||||||
|
|
||||||
|
|
||||||
def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
|
def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
|
||||||
|
|||||||
@@ -595,6 +595,46 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Interview Type",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Interview Type",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Interview Round",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Interview Round",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Interview",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Interview",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Interview Feedback",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Interview Feedback",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
@@ -841,7 +881,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-05-13 17:19:40.524444",
|
"modified": "2022-05-30 17:19:40.524444",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR",
|
"name": "HR",
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ frappe.ui.form.on('Loan', {
|
|||||||
frm.trigger("make_loan_refund");
|
frm.trigger("make_loan_refund");
|
||||||
},__('Create'));
|
},__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) {
|
||||||
|
frm.add_custom_button(__('Close Loan'), function() {
|
||||||
|
frm.trigger("close_unsecured_term_loan");
|
||||||
|
},__('Status'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frm.trigger("toggle_fields");
|
frm.trigger("toggle_fields");
|
||||||
},
|
},
|
||||||
@@ -174,6 +180,18 @@ frappe.ui.form.on('Loan', {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
close_unsecured_term_loan: function(frm) {
|
||||||
|
frappe.call({
|
||||||
|
args: {
|
||||||
|
"loan": frm.doc.name
|
||||||
|
},
|
||||||
|
method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan",
|
||||||
|
callback: function () {
|
||||||
|
frm.refresh();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
request_loan_closure: function(frm) {
|
request_loan_closure: function(frm) {
|
||||||
frappe.confirm(__("Do you really want to close this loan"),
|
frappe.confirm(__("Do you really want to close this loan"),
|
||||||
function() {
|
function() {
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ class Loan(AccountsController):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.link_loan_security_pledge()
|
self.link_loan_security_pledge()
|
||||||
|
# Interest accrual for backdated term loans
|
||||||
|
self.accrue_loan_interest()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.unlink_loan_security_pledge()
|
self.unlink_loan_security_pledge()
|
||||||
@@ -180,6 +182,16 @@ class Loan(AccountsController):
|
|||||||
|
|
||||||
self.db_set("maximum_loan_amount", maximum_loan_value)
|
self.db_set("maximum_loan_amount", maximum_loan_value)
|
||||||
|
|
||||||
|
def accrue_loan_interest(self):
|
||||||
|
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||||
|
process_loan_interest_accrual_for_term_loans,
|
||||||
|
)
|
||||||
|
|
||||||
|
if getdate(self.repayment_start_date) < getdate() and self.is_term_loan:
|
||||||
|
process_loan_interest_accrual_for_term_loans(
|
||||||
|
posting_date=getdate(), loan_type=self.loan_type, loan=self.name
|
||||||
|
)
|
||||||
|
|
||||||
def unlink_loan_security_pledge(self):
|
def unlink_loan_security_pledge(self):
|
||||||
pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
|
pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
|
||||||
pledge_list = [d.name for d in pledges]
|
pledge_list = [d.name for d in pledges]
|
||||||
@@ -323,6 +335,22 @@ def get_loan_application(loan_application):
|
|||||||
return loan.as_dict()
|
return loan.as_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def close_unsecured_term_loan(loan):
|
||||||
|
loan_details = frappe.db.get_value(
|
||||||
|
"Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
loan_details.status == "Loan Closure Requested"
|
||||||
|
and loan_details.is_term_loan
|
||||||
|
and not loan_details.is_secured_loan
|
||||||
|
):
|
||||||
|
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||||
|
else:
|
||||||
|
frappe.throw(_("Cannot close this loan until full repayment"))
|
||||||
|
|
||||||
|
|
||||||
def close_loan(loan, total_amount_paid):
|
def close_loan(loan, total_amount_paid):
|
||||||
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
|
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
|
||||||
frappe.db.set_value("Loan", loan, "status", "Closed")
|
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||||
|
|||||||
@@ -449,8 +449,6 @@ class LoanRepayment(AccountsController):
|
|||||||
"remarks": remarks,
|
"remarks": remarks,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"posting_date": getdate(self.posting_date),
|
"posting_date": getdate(self.posting_date),
|
||||||
"party_type": self.applicant_type if self.repay_from_salary else "",
|
|
||||||
"party": self.applicant if self.repay_from_salary else "",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ frappe.ui.form.on("BOM", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frm.add_custom_button(__("New Version"), function() {
|
||||||
|
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||||
|
frappe.set_route("Form", "BOM", new_bom.name);
|
||||||
|
});
|
||||||
|
|
||||||
if(frm.doc.docstatus==1) {
|
if(frm.doc.docstatus==1) {
|
||||||
frm.add_custom_button(__("Work Order"), function() {
|
frm.add_custom_button(__("Work Order"), function() {
|
||||||
frm.trigger("make_work_order");
|
frm.trigger("make_work_order");
|
||||||
@@ -331,7 +336,7 @@ frappe.ui.form.on("BOM", {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (has_template_rm) {
|
if (has_template_rm && has_template_rm.length) {
|
||||||
dialog.fields_dict.items.grid.refresh();
|
dialog.fields_dict.items.grid.refresh();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -467,7 +472,8 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
|
|||||||
"uom": d.uom,
|
"uom": d.uom,
|
||||||
"stock_uom": d.stock_uom,
|
"stock_uom": d.stock_uom,
|
||||||
"conversion_factor": d.conversion_factor,
|
"conversion_factor": d.conversion_factor,
|
||||||
"sourced_by_supplier": d.sourced_by_supplier
|
"sourced_by_supplier": d.sourced_by_supplier,
|
||||||
|
"do_not_explode": d.do_not_explode
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
d = locals[cdt][cdn];
|
d = locals[cdt][cdn];
|
||||||
@@ -640,6 +646,13 @@ frappe.ui.form.on("BOM Operation", "workstation", function(frm, cdt, cdn) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("BOM Item", {
|
||||||
|
do_not_explode: function(frm, cdt, cdn) {
|
||||||
|
get_bom_material_detail(frm.doc, cdt, cdn, false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
frappe.ui.form.on("BOM Item", "qty", function(frm, cdt, cdn) {
|
frappe.ui.form.on("BOM Item", "qty", function(frm, cdt, cdn) {
|
||||||
var d = locals[cdt][cdn];
|
var d = locals[cdt][cdn];
|
||||||
d.stock_qty = d.qty * d.conversion_factor;
|
d.stock_qty = d.qty * d.conversion_factor;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ from erpnext.stock.get_item_details import get_conversion_factor, get_price_list
|
|||||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||||
|
|
||||||
|
|
||||||
|
class BOMRecursionError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BOMTree:
|
class BOMTree:
|
||||||
"""Full tree representation of a BOM"""
|
"""Full tree representation of a BOM"""
|
||||||
|
|
||||||
@@ -251,6 +255,9 @@ class BOM(WebsiteGenerator):
|
|||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
self.validate_bom_currency(item)
|
self.validate_bom_currency(item)
|
||||||
|
|
||||||
|
if item.do_not_explode:
|
||||||
|
item.bom_no = ""
|
||||||
|
|
||||||
ret = self.get_bom_material_detail(
|
ret = self.get_bom_material_detail(
|
||||||
{
|
{
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
@@ -264,8 +271,10 @@ class BOM(WebsiteGenerator):
|
|||||||
"stock_uom": item.stock_uom,
|
"stock_uom": item.stock_uom,
|
||||||
"conversion_factor": item.conversion_factor,
|
"conversion_factor": item.conversion_factor,
|
||||||
"sourced_by_supplier": item.sourced_by_supplier,
|
"sourced_by_supplier": item.sourced_by_supplier,
|
||||||
|
"do_not_explode": item.do_not_explode,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
for r in ret:
|
for r in ret:
|
||||||
if not item.get(r):
|
if not item.get(r):
|
||||||
item.set(r, ret[r])
|
item.set(r, ret[r])
|
||||||
@@ -322,6 +331,9 @@ class BOM(WebsiteGenerator):
|
|||||||
"sourced_by_supplier": args.get("sourced_by_supplier", 0),
|
"sourced_by_supplier": args.get("sourced_by_supplier", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.get("do_not_explode"):
|
||||||
|
ret_item["bom_no"] = ""
|
||||||
|
|
||||||
return ret_item
|
return ret_item
|
||||||
|
|
||||||
def validate_bom_currency(self, item):
|
def validate_bom_currency(self, item):
|
||||||
@@ -520,35 +532,27 @@ class BOM(WebsiteGenerator):
|
|||||||
"""Check whether recursion occurs in any bom"""
|
"""Check whether recursion occurs in any bom"""
|
||||||
|
|
||||||
def _throw_error(bom_name):
|
def _throw_error(bom_name):
|
||||||
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name))
|
frappe.throw(
|
||||||
|
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
|
||||||
|
exc=BOMRecursionError,
|
||||||
|
)
|
||||||
|
|
||||||
bom_list = self.traverse_tree()
|
bom_list = self.traverse_tree()
|
||||||
child_items = (
|
child_items = frappe.get_all(
|
||||||
frappe.get_all(
|
"BOM Item",
|
||||||
"BOM Item",
|
fields=["bom_no", "item_code"],
|
||||||
fields=["bom_no", "item_code"],
|
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
||||||
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
|
||||||
)
|
|
||||||
or []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
child_bom = {d.bom_no for d in child_items}
|
for item in child_items:
|
||||||
child_items_codes = {d.item_code for d in child_items}
|
if self.name == item.bom_no:
|
||||||
|
_throw_error(self.name)
|
||||||
|
if self.item == item.item_code and item.bom_no:
|
||||||
|
# Same item but with different BOM should not be allowed.
|
||||||
|
# Same item can appear recursively once as long as it doesn't have BOM.
|
||||||
|
_throw_error(item.bom_no)
|
||||||
|
|
||||||
if self.name in child_bom:
|
if self.name in {d.bom_no for d in self.items}:
|
||||||
_throw_error(self.name)
|
|
||||||
|
|
||||||
if self.item in child_items_codes:
|
|
||||||
_throw_error(self.item)
|
|
||||||
|
|
||||||
bom_nos = (
|
|
||||||
frappe.get_all(
|
|
||||||
"BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"}
|
|
||||||
)
|
|
||||||
or []
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.name in {d.parent for d in bom_nos}:
|
|
||||||
_throw_error(self.name)
|
_throw_error(self.name)
|
||||||
|
|
||||||
def traverse_tree(self, bom_list=None):
|
def traverse_tree(self, bom_list=None):
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase
|
|||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
|
||||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.manufacturing.doctype.bom.bom import item_query
|
from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query
|
||||||
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
||||||
update_cost_in_all_boms_in_test,
|
update_cost_in_all_boms_in_test,
|
||||||
)
|
)
|
||||||
@@ -266,43 +266,36 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
def test_bom_recursion_1st_level(self):
|
def test_bom_recursion_1st_level(self):
|
||||||
"""BOM should not allow BOM item again in child"""
|
"""BOM should not allow BOM item again in child"""
|
||||||
item_code = "_Test BOM Recursion"
|
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||||
make_item(item_code, {"is_stock_item": 1})
|
|
||||||
|
|
||||||
bom = frappe.new_doc("BOM")
|
bom = frappe.new_doc("BOM")
|
||||||
bom.item = item_code
|
bom.item = item_code
|
||||||
bom.append("items", frappe._dict(item_code=item_code))
|
bom.append("items", frappe._dict(item_code=item_code))
|
||||||
with self.assertRaises(frappe.ValidationError) as err:
|
bom.save()
|
||||||
|
with self.assertRaises(BOMRecursionError):
|
||||||
|
bom.items[0].bom_no = bom.name
|
||||||
bom.save()
|
bom.save()
|
||||||
|
|
||||||
self.assertTrue("recursion" in str(err.exception).lower())
|
|
||||||
frappe.delete_doc("BOM", bom.name, ignore_missing=True)
|
|
||||||
|
|
||||||
def test_bom_recursion_transitive(self):
|
def test_bom_recursion_transitive(self):
|
||||||
item1 = "_Test BOM Recursion"
|
item1 = make_item(properties={"is_stock_item": 1}).name
|
||||||
item2 = "_Test BOM Recursion 2"
|
item2 = make_item(properties={"is_stock_item": 1}).name
|
||||||
make_item(item1, {"is_stock_item": 1})
|
|
||||||
make_item(item2, {"is_stock_item": 1})
|
|
||||||
|
|
||||||
bom1 = frappe.new_doc("BOM")
|
bom1 = frappe.new_doc("BOM")
|
||||||
bom1.item = item1
|
bom1.item = item1
|
||||||
bom1.append("items", frappe._dict(item_code=item2))
|
bom1.append("items", frappe._dict(item_code=item2))
|
||||||
bom1.save()
|
bom1.save()
|
||||||
bom1.submit()
|
|
||||||
|
|
||||||
bom2 = frappe.new_doc("BOM")
|
bom2 = frappe.new_doc("BOM")
|
||||||
bom2.item = item2
|
bom2.item = item2
|
||||||
bom2.append("items", frappe._dict(item_code=item1))
|
bom2.append("items", frappe._dict(item_code=item1))
|
||||||
|
bom2.save()
|
||||||
|
|
||||||
with self.assertRaises(frappe.ValidationError) as err:
|
bom2.items[0].bom_no = bom1.name
|
||||||
|
bom1.items[0].bom_no = bom2.name
|
||||||
|
|
||||||
|
with self.assertRaises(BOMRecursionError):
|
||||||
|
bom1.save()
|
||||||
bom2.save()
|
bom2.save()
|
||||||
bom2.submit()
|
|
||||||
|
|
||||||
self.assertTrue("recursion" in str(err.exception).lower())
|
|
||||||
|
|
||||||
bom1.cancel()
|
|
||||||
frappe.delete_doc("BOM", bom1.name, ignore_missing=True, force=True)
|
|
||||||
frappe.delete_doc("BOM", bom2.name, ignore_missing=True, force=True)
|
|
||||||
|
|
||||||
def test_bom_with_process_loss_item(self):
|
def test_bom_with_process_loss_item(self):
|
||||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||||
@@ -508,6 +501,24 @@ class TestBOM(FrappeTestCase):
|
|||||||
bom.submit()
|
bom.submit()
|
||||||
self.assertEqual(bom.items[0].rate, 42)
|
self.assertEqual(bom.items[0].rate, 42)
|
||||||
|
|
||||||
|
def test_exclude_exploded_items_from_bom(self):
|
||||||
|
bom_no = get_default_bom()
|
||||||
|
new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no))
|
||||||
|
for row in new_bom.items:
|
||||||
|
if row.item_code == "_Test Item Home Desktop Manufactured":
|
||||||
|
self.assertTrue(row.bom_no)
|
||||||
|
row.do_not_explode = True
|
||||||
|
|
||||||
|
new_bom.docstatus = 0
|
||||||
|
new_bom.save()
|
||||||
|
new_bom.load_from_db()
|
||||||
|
|
||||||
|
for row in new_bom.items:
|
||||||
|
if row.item_code == "_Test Item Home Desktop Manufactured" and row.do_not_explode:
|
||||||
|
self.assertFalse(row.bom_no)
|
||||||
|
|
||||||
|
new_bom.delete()
|
||||||
|
|
||||||
|
|
||||||
def get_default_bom(item_code="_Test FG Item 2"):
|
def get_default_bom(item_code="_Test FG Item 2"):
|
||||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"item_name",
|
"item_name",
|
||||||
"operation",
|
"operation",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
|
"do_not_explode",
|
||||||
"bom_no",
|
"bom_no",
|
||||||
"source_warehouse",
|
"source_warehouse",
|
||||||
"allow_alternative_item",
|
"allow_alternative_item",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!doc.do_not_explode",
|
||||||
"fieldname": "bom_no",
|
"fieldname": "bom_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_filter": 1,
|
"in_filter": 1,
|
||||||
@@ -284,18 +286,25 @@
|
|||||||
"fieldname": "sourced_by_supplier",
|
"fieldname": "sourced_by_supplier",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Sourced by Supplier"
|
"label": "Sourced by Supplier"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "do_not_explode",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Do Not Explode"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-08 14:19:37.563300",
|
"modified": "2022-01-24 16:57:57.020232",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Item",
|
"name": "BOM Item",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,10 @@ class JobCardCancelError(frappe.ValidationError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class JobCardOverTransferError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class JobCard(Document):
|
class JobCard(Document):
|
||||||
def onload(self):
|
def onload(self):
|
||||||
excess_transfer = frappe.db.get_single_value(
|
excess_transfer = frappe.db.get_single_value(
|
||||||
@@ -522,23 +526,50 @@ class JobCard(Document):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_transferred_qty_in_job_card(self, ste_doc):
|
def set_transferred_qty_in_job_card_item(self, ste_doc):
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
|
||||||
|
def _validate_over_transfer(row, transferred_qty):
|
||||||
|
"Block over transfer of items if not allowed in settings."
|
||||||
|
required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty")
|
||||||
|
is_excess = flt(transferred_qty) > flt(required_qty)
|
||||||
|
if is_excess:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}"
|
||||||
|
).format(
|
||||||
|
row.idx, frappe.bold(required_qty), frappe.bold(row.item_code), ste_doc.job_card
|
||||||
|
),
|
||||||
|
title=_("Excess Transfer"),
|
||||||
|
exc=JobCardOverTransferError,
|
||||||
|
)
|
||||||
|
|
||||||
for row in ste_doc.items:
|
for row in ste_doc.items:
|
||||||
if not row.job_card_item:
|
if not row.job_card_item:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
qty = frappe.db.sql(
|
sed = frappe.qb.DocType("Stock Entry Detail")
|
||||||
""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
|
se = frappe.qb.DocType("Stock Entry")
|
||||||
WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
|
transferred_qty = (
|
||||||
se.purpose = 'Material Transfer for Manufacture'
|
frappe.qb.from_(sed)
|
||||||
""",
|
.join(se)
|
||||||
(row.job_card_item),
|
.on(sed.parent == se.name)
|
||||||
)[0][0]
|
.select(Sum(sed.qty))
|
||||||
|
.where(
|
||||||
|
(sed.job_card_item == row.job_card_item)
|
||||||
|
& (se.docstatus == 1)
|
||||||
|
& (se.purpose == "Material Transfer for Manufacture")
|
||||||
|
)
|
||||||
|
).run()[0][0]
|
||||||
|
|
||||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty))
|
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
|
||||||
|
if not allow_excess:
|
||||||
|
_validate_over_transfer(row, transferred_qty)
|
||||||
|
|
||||||
|
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
|
||||||
|
|
||||||
def set_transferred_qty(self, update_status=False):
|
def set_transferred_qty(self, update_status=False):
|
||||||
"Set total FG Qty for which RM was transferred."
|
"Set total FG Qty in Job Card for which RM was transferred."
|
||||||
if not self.items:
|
if not self.items:
|
||||||
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
||||||
|
|
||||||
@@ -590,7 +621,7 @@ class JobCard(Document):
|
|||||||
self.set_status(update_status)
|
self.set_status(update_status)
|
||||||
|
|
||||||
def set_status(self, update_status=False):
|
def set_status(self, update_status=False):
|
||||||
if self.status == "On Hold":
|
if self.status == "On Hold" and self.docstatus == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||||
@@ -866,6 +897,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
|
|||||||
target.set("time_logs", [])
|
target.set("time_logs", [])
|
||||||
target.set("employee", [])
|
target.set("employee", [])
|
||||||
target.set("items", [])
|
target.set("items", [])
|
||||||
|
target.set("sub_operations", [])
|
||||||
target.set_sub_operations()
|
target.set_sub_operations()
|
||||||
target.get_required_items()
|
target.get_required_items()
|
||||||
target.validate_time_logs()
|
target.validate_time_logs()
|
||||||
|
|||||||
@@ -2,14 +2,21 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import random_string
|
from frappe.utils import random_string
|
||||||
|
from frappe.utils.data import add_to_date, now
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
|
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||||
|
JobCardOverTransferError,
|
||||||
|
OperationMismatchError,
|
||||||
|
OverlapError,
|
||||||
|
make_corrective_job_card,
|
||||||
|
)
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||||
make_stock_entry as make_stock_entry_from_jc,
|
make_stock_entry as make_stock_entry_from_jc,
|
||||||
)
|
)
|
||||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
|
from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder
|
||||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
@@ -17,34 +24,36 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
|||||||
class TestJobCard(FrappeTestCase):
|
class TestJobCard(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
make_bom_for_jc_tests()
|
make_bom_for_jc_tests()
|
||||||
|
self.transfer_material_against = "Work Order"
|
||||||
|
self.source_warehouse = None
|
||||||
|
self._work_order = None
|
||||||
|
|
||||||
transfer_material_against, source_warehouse = None, None
|
@property
|
||||||
|
def work_order(self) -> WorkOrder:
|
||||||
|
"""Work Order lazily created for tests."""
|
||||||
|
if not self._work_order:
|
||||||
|
self._work_order = make_wo_order_test_record(
|
||||||
|
item="_Test FG Item 2",
|
||||||
|
qty=2,
|
||||||
|
transfer_material_against=self.transfer_material_against,
|
||||||
|
source_warehouse=self.source_warehouse,
|
||||||
|
)
|
||||||
|
return self._work_order
|
||||||
|
|
||||||
tests_that_skip_setup = ("test_job_card_material_transfer_correctness",)
|
def generate_required_stock(self, work_order: WorkOrder) -> None:
|
||||||
tests_that_transfer_against_jc = (
|
"""Create twice the stock for all required items in work order."""
|
||||||
"test_job_card_multiple_materials_transfer",
|
for item in work_order.required_items:
|
||||||
"test_job_card_excess_material_transfer",
|
make_stock_entry(
|
||||||
"test_job_card_partial_material_transfer",
|
item_code=item.item_code,
|
||||||
)
|
target=item.source_warehouse or self.source_warehouse,
|
||||||
|
qty=item.required_qty * 2,
|
||||||
if self._testMethodName in tests_that_skip_setup:
|
basic_rate=100,
|
||||||
return
|
)
|
||||||
|
|
||||||
if self._testMethodName in tests_that_transfer_against_jc:
|
|
||||||
transfer_material_against = "Job Card"
|
|
||||||
source_warehouse = "Stores - _TC"
|
|
||||||
|
|
||||||
self.work_order = make_wo_order_test_record(
|
|
||||||
item="_Test FG Item 2",
|
|
||||||
qty=2,
|
|
||||||
transfer_material_against=transfer_material_against,
|
|
||||||
source_warehouse=source_warehouse,
|
|
||||||
)
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_job_card(self):
|
def test_job_card_operations(self):
|
||||||
|
|
||||||
job_cards = frappe.get_all(
|
job_cards = frappe.get_all(
|
||||||
"Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"]
|
"Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"]
|
||||||
@@ -58,9 +67,6 @@ class TestJobCard(FrappeTestCase):
|
|||||||
doc.operation_id = "Test Data"
|
doc.operation_id = "Test Data"
|
||||||
self.assertRaises(OperationMismatchError, doc.save)
|
self.assertRaises(OperationMismatchError, doc.save)
|
||||||
|
|
||||||
for d in job_cards:
|
|
||||||
frappe.delete_doc("Job Card", d.name)
|
|
||||||
|
|
||||||
def test_job_card_with_different_work_station(self):
|
def test_job_card_with_different_work_station(self):
|
||||||
job_cards = frappe.get_all(
|
job_cards = frappe.get_all(
|
||||||
"Job Card",
|
"Job Card",
|
||||||
@@ -96,19 +102,11 @@ class TestJobCard(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(completed_qty, job_card.for_quantity)
|
self.assertEqual(completed_qty, job_card.for_quantity)
|
||||||
|
|
||||||
doc.cancel()
|
|
||||||
|
|
||||||
for d in job_cards:
|
|
||||||
frappe.delete_doc("Job Card", d.name)
|
|
||||||
|
|
||||||
def test_job_card_overlap(self):
|
def test_job_card_overlap(self):
|
||||||
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
||||||
|
|
||||||
jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||||
jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name})
|
jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
|
||||||
|
|
||||||
jc1 = frappe.get_doc("Job Card", jc1_name)
|
|
||||||
jc2 = frappe.get_doc("Job Card", jc2_name)
|
|
||||||
|
|
||||||
employee = "_T-Employee-00001" # from test records
|
employee = "_T-Employee-00001" # from test records
|
||||||
|
|
||||||
@@ -137,10 +135,10 @@ class TestJobCard(FrappeTestCase):
|
|||||||
|
|
||||||
def test_job_card_multiple_materials_transfer(self):
|
def test_job_card_multiple_materials_transfer(self):
|
||||||
"Test transferring RMs separately against Job Card with multiple RMs."
|
"Test transferring RMs separately against Job Card with multiple RMs."
|
||||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
|
self.transfer_material_against = "Job Card"
|
||||||
make_stock_entry(
|
self.source_warehouse = "Stores - _TC"
|
||||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100
|
|
||||||
)
|
self.generate_required_stock(self.work_order)
|
||||||
|
|
||||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||||
@@ -165,16 +163,58 @@ class TestJobCard(FrappeTestCase):
|
|||||||
# transfer was made for 2 fg qty in first transfer Stock Entry
|
# transfer was made for 2 fg qty in first transfer Stock Entry
|
||||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
|
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
|
||||||
|
|
||||||
|
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1})
|
||||||
def test_job_card_excess_material_transfer(self):
|
def test_job_card_excess_material_transfer(self):
|
||||||
"Test transferring more than required RM against Job Card."
|
"Test transferring more than required RM against Job Card."
|
||||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
|
self.transfer_material_against = "Job Card"
|
||||||
make_stock_entry(
|
self.source_warehouse = "Stores - _TC"
|
||||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
|
|
||||||
|
self.generate_required_stock(self.work_order)
|
||||||
|
|
||||||
|
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||||
|
self.assertEqual(job_card.status, "Open")
|
||||||
|
|
||||||
|
# fully transfer both RMs
|
||||||
|
transfer_entry_1 = make_stock_entry_from_jc(job_card.name)
|
||||||
|
transfer_entry_1.insert()
|
||||||
|
transfer_entry_1.submit()
|
||||||
|
|
||||||
|
# transfer extra qty of both RM due to previously damaged RM
|
||||||
|
transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
|
||||||
|
# deliberately change 'For Quantity'
|
||||||
|
transfer_entry_2.fg_completed_qty = 1
|
||||||
|
transfer_entry_2.items[0].qty = 5
|
||||||
|
transfer_entry_2.items[1].qty = 3
|
||||||
|
transfer_entry_2.insert()
|
||||||
|
transfer_entry_2.submit()
|
||||||
|
|
||||||
|
job_card.reload()
|
||||||
|
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
|
||||||
|
|
||||||
|
# Check if 'For Quantity' is negative
|
||||||
|
# as 'transferred_qty' > Qty to Manufacture
|
||||||
|
transfer_entry_3 = make_stock_entry_from_jc(job_card.name)
|
||||||
|
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
|
||||||
|
|
||||||
|
job_card.append(
|
||||||
|
"time_logs",
|
||||||
|
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
|
||||||
)
|
)
|
||||||
|
job_card.save()
|
||||||
|
job_card.submit()
|
||||||
|
|
||||||
|
# JC is Completed with excess transfer
|
||||||
|
self.assertEqual(job_card.status, "Completed")
|
||||||
|
|
||||||
|
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0})
|
||||||
|
def test_job_card_excess_material_transfer_block(self):
|
||||||
|
|
||||||
|
self.transfer_material_against = "Job Card"
|
||||||
|
self.source_warehouse = "Stores - _TC"
|
||||||
|
|
||||||
|
self.generate_required_stock(self.work_order)
|
||||||
|
|
||||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
|
||||||
self.assertEqual(job_card.status, "Open")
|
|
||||||
|
|
||||||
# fully transfer both RMs
|
# fully transfer both RMs
|
||||||
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||||
@@ -188,39 +228,19 @@ class TestJobCard(FrappeTestCase):
|
|||||||
transfer_entry_2.items[0].qty = 5
|
transfer_entry_2.items[0].qty = 5
|
||||||
transfer_entry_2.items[1].qty = 3
|
transfer_entry_2.items[1].qty = 3
|
||||||
transfer_entry_2.insert()
|
transfer_entry_2.insert()
|
||||||
transfer_entry_2.submit()
|
self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit)
|
||||||
|
|
||||||
job_card.reload()
|
|
||||||
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
|
|
||||||
|
|
||||||
# Check if 'For Quantity' is negative
|
|
||||||
# as 'transferred_qty' > Qty to Manufacture
|
|
||||||
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
|
|
||||||
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
|
|
||||||
|
|
||||||
job_card.append(
|
|
||||||
"time_logs",
|
|
||||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
|
|
||||||
)
|
|
||||||
job_card.save()
|
|
||||||
job_card.submit()
|
|
||||||
|
|
||||||
# JC is Completed with excess transfer
|
|
||||||
self.assertEqual(job_card.status, "Completed")
|
|
||||||
|
|
||||||
def test_job_card_partial_material_transfer(self):
|
def test_job_card_partial_material_transfer(self):
|
||||||
"Test partial material transfer against Job Card"
|
"Test partial material transfer against Job Card"
|
||||||
|
self.transfer_material_against = "Job Card"
|
||||||
|
self.source_warehouse = "Stores - _TC"
|
||||||
|
|
||||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
|
self.generate_required_stock(self.work_order)
|
||||||
make_stock_entry(
|
|
||||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
|
|
||||||
)
|
|
||||||
|
|
||||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
|
||||||
|
|
||||||
# partially transfer
|
# partially transfer
|
||||||
transfer_entry = make_stock_entry_from_jc(job_card_name)
|
transfer_entry = make_stock_entry_from_jc(job_card.name)
|
||||||
transfer_entry.fg_completed_qty = 1
|
transfer_entry.fg_completed_qty = 1
|
||||||
transfer_entry.get_items()
|
transfer_entry.get_items()
|
||||||
transfer_entry.insert()
|
transfer_entry.insert()
|
||||||
@@ -232,7 +252,7 @@ class TestJobCard(FrappeTestCase):
|
|||||||
self.assertEqual(transfer_entry.items[1].qty, 3)
|
self.assertEqual(transfer_entry.items[1].qty, 3)
|
||||||
|
|
||||||
# transfer remaining
|
# transfer remaining
|
||||||
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
|
transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
|
||||||
|
|
||||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
|
self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
|
||||||
self.assertEqual(transfer_entry_2.items[0].qty, 5)
|
self.assertEqual(transfer_entry_2.items[0].qty, 5)
|
||||||
@@ -277,7 +297,49 @@ class TestJobCard(FrappeTestCase):
|
|||||||
self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
|
self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
|
||||||
self.assertEqual(transfer_entry.items[0].qty, 2)
|
self.assertEqual(transfer_entry.items[0].qty, 2)
|
||||||
|
|
||||||
# rollback via tearDown method
|
@change_settings(
|
||||||
|
"Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1}
|
||||||
|
)
|
||||||
|
def test_corrective_costing(self):
|
||||||
|
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||||
|
|
||||||
|
job_card.append(
|
||||||
|
"time_logs",
|
||||||
|
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2},
|
||||||
|
)
|
||||||
|
job_card.submit()
|
||||||
|
|
||||||
|
self.work_order.reload()
|
||||||
|
original_cost = self.work_order.total_operating_cost
|
||||||
|
|
||||||
|
# Create a corrective operation against it
|
||||||
|
corrective_action = frappe.get_doc(
|
||||||
|
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
corrective_job_card = make_corrective_job_card(
|
||||||
|
job_card.name, operation=corrective_action.name, for_operation=job_card.operation
|
||||||
|
)
|
||||||
|
corrective_job_card.hour_rate = 100
|
||||||
|
corrective_job_card.insert()
|
||||||
|
corrective_job_card.append(
|
||||||
|
"time_logs",
|
||||||
|
{
|
||||||
|
"from_time": add_to_date(now(), hours=2),
|
||||||
|
"to_time": add_to_date(now(), hours=2, minutes=30),
|
||||||
|
"completed_qty": 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
corrective_job_card.submit()
|
||||||
|
|
||||||
|
self.work_order.reload()
|
||||||
|
cost_after_correction = self.work_order.total_operating_cost
|
||||||
|
self.assertGreater(cost_after_correction, original_cost)
|
||||||
|
|
||||||
|
corrective_job_card.cancel()
|
||||||
|
self.work_order.reload()
|
||||||
|
cost_after_cancel = self.work_order.total_operating_cost
|
||||||
|
self.assertEqual(cost_after_cancel, original_cost)
|
||||||
|
|
||||||
|
|
||||||
def create_bom_with_multiple_operations():
|
def create_bom_with_multiple_operations():
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ def get_data(filters):
|
|||||||
if filters.get(field):
|
if filters.get(field):
|
||||||
query_filters[field] = ("in", filters.get(field))
|
query_filters[field] = ("in", filters.get(field))
|
||||||
|
|
||||||
query_filters["report_date"] = (">=", filters.get("from_date"))
|
query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]]
|
||||||
query_filters["report_date"] = ("<=", filters.get("to_date"))
|
|
||||||
|
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
"Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc"
|
"Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc"
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ def capture_razorpay_donations(*args, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# to avoid capturing subscription payments as donations
|
# to avoid capturing subscription payments as donations
|
||||||
if payment.description and "subscription" in str(payment.description).lower():
|
if payment.invoice_id or (
|
||||||
|
payment.description and "subscription" in str(payment.description).lower()
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
donor = get_donor(payment.email)
|
donor = get_donor(payment.email)
|
||||||
|
|||||||
@@ -44,21 +44,18 @@ frappe.ui.form.on('Member', {
|
|||||||
frappe.contacts.clear_address_and_contact(frm);
|
frappe.contacts.clear_address_and_contact(frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.call({
|
if (!frm.doc.membership_expiry_date && !frm.doc.__islocal) {
|
||||||
method:"frappe.client.get_value",
|
frappe.call({
|
||||||
args:{
|
method: "erpnext.get_last_membership",
|
||||||
'doctype':"Membership",
|
args: {
|
||||||
'filters':{'member': frm.doc.name},
|
member: frm.doc.member
|
||||||
'fieldname':[
|
},
|
||||||
'to_date'
|
callback: function(data) {
|
||||||
]
|
if (data.message) {
|
||||||
},
|
frappe.model.set_value(frm.doctype, frm.docname, "membership_expiry_date", data.message.to_date);
|
||||||
callback: function (data) {
|
}
|
||||||
if(data.message) {
|
|
||||||
frappe.model.set_value(frm.doctype,frm.docname,
|
|
||||||
"membership_expiry_date", data.message.to_date);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from frappe.model.document import Document
|
|||||||
from frappe.utils import add_days, add_months, add_years, get_link_to_form, getdate, nowdate
|
from frappe.utils import add_days, add_months, add_years, get_link_to_form, getdate, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
from erpnext import get_company_currency
|
||||||
from erpnext.non_profit.doctype.member.member import create_member
|
from erpnext.non_profit.doctype.member.member import create_member
|
||||||
|
|
||||||
|
|
||||||
@@ -61,10 +62,6 @@ class Membership(Document):
|
|||||||
frappe.throw(_("You can only renew if your membership expires within 30 days"))
|
frappe.throw(_("You can only renew if your membership expires within 30 days"))
|
||||||
|
|
||||||
self.from_date = add_days(last_membership.to_date, 1)
|
self.from_date = add_days(last_membership.to_date, 1)
|
||||||
elif frappe.session.user == "Administrator":
|
|
||||||
self.from_date = self.from_date
|
|
||||||
else:
|
|
||||||
self.from_date = nowdate()
|
|
||||||
|
|
||||||
if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly":
|
if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly":
|
||||||
self.to_date = add_years(self.from_date, 1)
|
self.to_date = add_years(self.from_date, 1)
|
||||||
@@ -207,7 +204,7 @@ def make_invoice(membership, member, plan, settings):
|
|||||||
"doctype": "Sales Invoice",
|
"doctype": "Sales Invoice",
|
||||||
"customer": member.customer,
|
"customer": member.customer,
|
||||||
"debit_to": settings.membership_debit_account,
|
"debit_to": settings.membership_debit_account,
|
||||||
"currency": membership.currency,
|
"currency": membership.currency or get_company_currency(settings.company),
|
||||||
"company": settings.company,
|
"company": settings.company,
|
||||||
"is_pos": 0,
|
"is_pos": 0,
|
||||||
"items": [{"item_code": plan.linked_item, "rate": membership.amount, "qty": 1}],
|
"items": [{"item_code": plan.linked_item, "rate": membership.amount, "qty": 1}],
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def make_membership(member, payload={}):
|
|||||||
"member": member,
|
"member": member,
|
||||||
"membership_status": "Current",
|
"membership_status": "Current",
|
||||||
"membership_type": "_rzpy_test_milythm",
|
"membership_type": "_rzpy_test_milythm",
|
||||||
"currency": "INR",
|
"currency": "USD",
|
||||||
"paid": 1,
|
"paid": 1,
|
||||||
"from_date": nowdate(),
|
"from_date": nowdate(),
|
||||||
"amount": 100,
|
"amount": 100,
|
||||||
|
|||||||
@@ -248,7 +248,6 @@ execute:frappe.delete_doc("Report", "Quoted Item Comparison")
|
|||||||
erpnext.patches.v13_0.update_member_email_address
|
erpnext.patches.v13_0.update_member_email_address
|
||||||
erpnext.patches.v13_0.update_custom_fields_for_shopify
|
erpnext.patches.v13_0.update_custom_fields_for_shopify
|
||||||
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
||||||
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
|
|
||||||
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
|
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
|
||||||
erpnext.patches.v13_0.add_po_to_global_search
|
erpnext.patches.v13_0.add_po_to_global_search
|
||||||
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
|
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
|
||||||
@@ -366,3 +365,5 @@ erpnext.patches.v13_0.education_deprecation_warning
|
|||||||
erpnext.patches.v13_0.requeue_recoverable_reposts
|
erpnext.patches.v13_0.requeue_recoverable_reposts
|
||||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||||
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||||
|
erpnext.patches.v13_0.update_employee_advance_status
|
||||||
|
erpnext.patches.v13_0.job_card_status_on_hold
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
# Copyright (c) 2019, Frappe and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
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:
|
|
||||||
create_assignment(employee.name, employee.leave_policy)
|
|
||||||
|
|
||||||
employee_with_assignment.append(employee.name)
|
|
||||||
leave_policy.append(employee.leave_policy)
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
if employee not in employee_with_assignment: # Will ensure no duplicate
|
|
||||||
alloc = frappe.db.exists(
|
|
||||||
"Leave Allocation",
|
|
||||||
{"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1},
|
|
||||||
)
|
|
||||||
if not alloc:
|
|
||||||
create_assignment(employee.name, grade.default_leave_policy)
|
|
||||||
leave_policy.append(grade.default_leave_policy)
|
|
||||||
|
|
||||||
# for old Leave allocation and leave policy from allocation, which may got updated in employee grade.
|
|
||||||
leave_allocations = frappe.db.sql(
|
|
||||||
"SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ",
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
for allocation in leave_allocations:
|
|
||||||
if allocation.leave_policy not in leave_policy:
|
|
||||||
create_assignment(
|
|
||||||
allocation.employee,
|
|
||||||
allocation.leave_policy,
|
|
||||||
leave_period=allocation.leave_period,
|
|
||||||
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
|
|
||||||
|
|
||||||
if not frappe.db.exists("Leave Policy Assignment", filters):
|
|
||||||
lpa = frappe.new_doc("Leave Policy Assignment")
|
|
||||||
lpa.employee = employee
|
|
||||||
lpa.leave_policy = leave_policy
|
|
||||||
|
|
||||||
lpa.flags.ignore_mandatory = True
|
|
||||||
if allocation_exists:
|
|
||||||
lpa.assignment_based_on = "Leave Period"
|
|
||||||
lpa.leave_period = leave_period
|
|
||||||
lpa.leaves_allocated = 1
|
|
||||||
|
|
||||||
lpa.save()
|
|
||||||
if allocation_exists:
|
|
||||||
lpa.submit()
|
|
||||||
# Updating old Leave Allocation
|
|
||||||
frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_employee_with_grade(grade):
|
|
||||||
return frappe.get_list("Employee", filters={"grade": grade})
|
|
||||||
19
erpnext/patches/v13_0/job_card_status_on_hold.py
Normal file
19
erpnext/patches/v13_0/job_card_status_on_hold.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
job_cards = frappe.get_all(
|
||||||
|
"Job Card",
|
||||||
|
{"status": "On Hold", "docstatus": ("!=", 0)},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, job_card in enumerate(job_cards):
|
||||||
|
try:
|
||||||
|
doc = frappe.get_doc("Job Card", job_card)
|
||||||
|
doc.set_status()
|
||||||
|
doc.db_set("status", doc.status, update_modified=False)
|
||||||
|
if idx % 100 == 0:
|
||||||
|
frappe.db.commit()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
29
erpnext/patches/v13_0/update_employee_advance_status.py
Normal file
29
erpnext/patches/v13_0/update_employee_advance_status.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("hr", "doctype", "employee_advance")
|
||||||
|
|
||||||
|
advance = frappe.qb.DocType("Employee Advance")
|
||||||
|
(
|
||||||
|
frappe.qb.update(advance)
|
||||||
|
.set(advance.status, "Returned")
|
||||||
|
.where(
|
||||||
|
(advance.docstatus == 1)
|
||||||
|
& ((advance.return_amount) & (advance.paid_amount == advance.return_amount))
|
||||||
|
& (advance.status == "Paid")
|
||||||
|
)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
(
|
||||||
|
frappe.qb.update(advance)
|
||||||
|
.set(advance.status, "Partly Claimed and Returned")
|
||||||
|
.where(
|
||||||
|
(advance.docstatus == 1)
|
||||||
|
& (
|
||||||
|
(advance.claimed_amount & advance.return_amount)
|
||||||
|
& (advance.paid_amount == (advance.return_amount + advance.claimed_amount))
|
||||||
|
)
|
||||||
|
& (advance.status == "Paid")
|
||||||
|
)
|
||||||
|
).run()
|
||||||
@@ -124,6 +124,8 @@ class AdditionalSalary(Document):
|
|||||||
return_amount += self.amount
|
return_amount += self.amount
|
||||||
|
|
||||||
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
|
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
|
||||||
|
advance = frappe.get_doc("Employee Advance", self.ref_docname)
|
||||||
|
advance.set_status(update=True)
|
||||||
|
|
||||||
def update_employee_referral(self, cancel=False):
|
def update_employee_referral(self, cancel=False):
|
||||||
if self.ref_doctype == "Employee Referral":
|
if self.ref_doctype == "Employee Referral":
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded
|
from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded
|
||||||
|
|
||||||
from erpnext.hr.utils import (
|
from erpnext.hr.utils import (
|
||||||
get_holiday_dates_for_employee,
|
get_holiday_dates_for_employee,
|
||||||
@@ -27,11 +27,14 @@ class EmployeeBenefitApplication(Document):
|
|||||||
validate_active_employee(self.employee)
|
validate_active_employee(self.employee)
|
||||||
self.validate_duplicate_on_payroll_period()
|
self.validate_duplicate_on_payroll_period()
|
||||||
if not self.max_benefits:
|
if not self.max_benefits:
|
||||||
self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)
|
self.max_benefits = flt(
|
||||||
|
get_max_benefits_remaining(self.employee, self.date, self.payroll_period),
|
||||||
|
self.precision("max_benefits"),
|
||||||
|
)
|
||||||
if self.max_benefits and self.max_benefits > 0:
|
if self.max_benefits and self.max_benefits > 0:
|
||||||
self.validate_max_benefit_for_component()
|
self.validate_max_benefit_for_component()
|
||||||
self.validate_prev_benefit_claim()
|
self.validate_prev_benefit_claim()
|
||||||
if self.remaining_benefit > 0:
|
if self.remaining_benefit and self.remaining_benefit > 0:
|
||||||
self.validate_remaining_benefit_amount()
|
self.validate_remaining_benefit_amount()
|
||||||
else:
|
else:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
@@ -110,7 +113,7 @@ class EmployeeBenefitApplication(Document):
|
|||||||
max_benefit_amount = 0
|
max_benefit_amount = 0
|
||||||
for employee_benefit in self.employee_benefits:
|
for employee_benefit in self.employee_benefits:
|
||||||
self.validate_max_benefit(employee_benefit.earning_component)
|
self.validate_max_benefit(employee_benefit.earning_component)
|
||||||
max_benefit_amount += employee_benefit.amount
|
max_benefit_amount += flt(employee_benefit.amount)
|
||||||
if max_benefit_amount > self.max_benefits:
|
if max_benefit_amount > self.max_benefits:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Maximum benefit amount of employee {0} exceeds {1}").format(
|
_("Maximum benefit amount of employee {0} exceeds {1}").format(
|
||||||
@@ -125,7 +128,8 @@ class EmployeeBenefitApplication(Document):
|
|||||||
benefit_amount = 0
|
benefit_amount = 0
|
||||||
for employee_benefit in self.employee_benefits:
|
for employee_benefit in self.employee_benefits:
|
||||||
if employee_benefit.earning_component == earning_component_name:
|
if employee_benefit.earning_component == earning_component_name:
|
||||||
benefit_amount += employee_benefit.amount
|
benefit_amount += flt(employee_benefit.amount)
|
||||||
|
|
||||||
prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
|
prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
|
||||||
self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
|
self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
|
||||||
)
|
)
|
||||||
@@ -207,26 +211,47 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
|
|||||||
def calculate_lwp(employee, start_date, holidays, working_days):
|
def calculate_lwp(employee, start_date, holidays, working_days):
|
||||||
lwp = 0
|
lwp = 0
|
||||||
holidays = "','".join(holidays)
|
holidays = "','".join(holidays)
|
||||||
|
|
||||||
for d in range(working_days):
|
for d in range(working_days):
|
||||||
dt = add_days(cstr(getdate(start_date)), d)
|
date = add_days(cstr(getdate(start_date)), d)
|
||||||
leave = frappe.db.sql(
|
|
||||||
"""
|
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||||
select t1.name, t1.half_day
|
LeaveType = frappe.qb.DocType("Leave Type")
|
||||||
from `tabLeave Application` t1, `tabLeave Type` t2
|
|
||||||
where t2.name = t1.leave_type
|
is_half_day = (
|
||||||
and t2.is_lwp = 1
|
frappe.qb.terms.Case()
|
||||||
and t1.docstatus = 1
|
.when(
|
||||||
and t1.employee = %(employee)s
|
(
|
||||||
and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
|
(LeaveApplication.half_day_date == date)
|
||||||
WHEN t2.include_holiday THEN %(dt)s between from_date and to_date
|
| (LeaveApplication.from_date == LeaveApplication.to_date)
|
||||||
END
|
),
|
||||||
""".format(
|
LeaveApplication.half_day,
|
||||||
holidays
|
)
|
||||||
),
|
.else_(0)
|
||||||
{"employee": employee, "dt": dt},
|
).as_("is_half_day")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(LeaveApplication)
|
||||||
|
.inner_join(LeaveType)
|
||||||
|
.on((LeaveType.name == LeaveApplication.leave_type))
|
||||||
|
.select(LeaveApplication.name, is_half_day)
|
||||||
|
.where(
|
||||||
|
(LeaveType.is_lwp == 1)
|
||||||
|
& (LeaveApplication.docstatus == 1)
|
||||||
|
& (LeaveApplication.status == "Approved")
|
||||||
|
& (LeaveApplication.employee == employee)
|
||||||
|
& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if leave:
|
|
||||||
lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1)
|
# if it's a holiday only include if leave type has "include holiday" enabled
|
||||||
|
if date in holidays:
|
||||||
|
query = query.where((LeaveType.include_holiday == "1"))
|
||||||
|
leaves = query.run(as_dict=True)
|
||||||
|
|
||||||
|
if leaves:
|
||||||
|
lwp += 0.5 if leaves[0].is_half_day else 1
|
||||||
|
|
||||||
return lwp
|
return lwp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,82 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, getdate
|
||||||
|
|
||||||
class TestEmployeeBenefitApplication(unittest.TestCase):
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
pass
|
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
|
||||||
|
from erpnext.hr.utils import get_holiday_dates_for_employee
|
||||||
|
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
|
||||||
|
calculate_lwp,
|
||||||
|
)
|
||||||
|
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
|
||||||
|
create_payroll_period,
|
||||||
|
)
|
||||||
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||||
|
make_holiday_list,
|
||||||
|
make_leave_application,
|
||||||
|
)
|
||||||
|
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmployeeBenefitApplication(FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
date = getdate()
|
||||||
|
make_holiday_list(from_date=get_year_start(date), to_date=get_year_ending(date))
|
||||||
|
|
||||||
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
|
def test_employee_benefit_application(self):
|
||||||
|
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
|
||||||
|
employee = make_employee("test_employee_benefits@salary.com", company="_Test Company")
|
||||||
|
first_sunday = get_first_sunday("Salary Slip Test Holiday List")
|
||||||
|
|
||||||
|
leave_application = make_leave_application(
|
||||||
|
employee,
|
||||||
|
add_days(first_sunday, 1),
|
||||||
|
add_days(first_sunday, 3),
|
||||||
|
"Leave Without Pay",
|
||||||
|
half_day=1,
|
||||||
|
half_day_date=add_days(first_sunday, 1),
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
|
||||||
|
salary_structure = make_salary_structure(
|
||||||
|
"Test Employee Benefits",
|
||||||
|
"Monthly",
|
||||||
|
other_details={"max_benefits": 100000},
|
||||||
|
include_flexi_benefits=True,
|
||||||
|
employee=employee,
|
||||||
|
payroll_period=payroll_period,
|
||||||
|
)
|
||||||
|
salary_slip = make_salary_slip(salary_structure.name, employee=employee, posting_date=getdate())
|
||||||
|
salary_slip.insert()
|
||||||
|
salary_slip.submit()
|
||||||
|
|
||||||
|
application = make_employee_benefit_application(
|
||||||
|
employee, payroll_period.name, date=leave_application.to_date
|
||||||
|
)
|
||||||
|
self.assertEqual(application.employee_benefits[0].max_benefit_amount, 15000)
|
||||||
|
|
||||||
|
holidays = get_holiday_dates_for_employee(employee, payroll_period.start_date, application.date)
|
||||||
|
working_days = date_diff(application.date, payroll_period.start_date) + 1
|
||||||
|
lwp = calculate_lwp(employee, payroll_period.start_date, holidays, working_days)
|
||||||
|
self.assertEqual(lwp, 2.5)
|
||||||
|
|
||||||
|
|
||||||
|
def make_employee_benefit_application(employee, payroll_period, date):
|
||||||
|
frappe.db.delete("Employee Benefit Application")
|
||||||
|
|
||||||
|
return frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Benefit Application",
|
||||||
|
"employee": employee,
|
||||||
|
"date": date,
|
||||||
|
"payroll_period": payroll_period,
|
||||||
|
"employee_benefits": [{"earning_component": "Medical Allowance", "amount": 1500}],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ class EmployeeTaxExemptionDeclaration(Document):
|
|||||||
self.total_declared_amount += flt(d.amount)
|
self.total_declared_amount += flt(d.amount)
|
||||||
|
|
||||||
def set_total_exemption_amount(self):
|
def set_total_exemption_amount(self):
|
||||||
self.total_exemption_amount = get_total_exemption_amount(self.declarations)
|
self.total_exemption_amount = flt(
|
||||||
|
get_total_exemption_amount(self.declarations), self.precision("total_exemption_amount")
|
||||||
|
)
|
||||||
|
|
||||||
def calculate_hra_exemption(self):
|
def calculate_hra_exemption(self):
|
||||||
self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0
|
self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0
|
||||||
@@ -41,9 +43,18 @@ class EmployeeTaxExemptionDeclaration(Document):
|
|||||||
hra_exemption = calculate_annual_eligible_hra_exemption(self)
|
hra_exemption = calculate_annual_eligible_hra_exemption(self)
|
||||||
if hra_exemption:
|
if hra_exemption:
|
||||||
self.total_exemption_amount += hra_exemption["annual_exemption"]
|
self.total_exemption_amount += hra_exemption["annual_exemption"]
|
||||||
self.salary_structure_hra = hra_exemption["hra_amount"]
|
self.total_exemption_amount = flt(
|
||||||
self.annual_hra_exemption = hra_exemption["annual_exemption"]
|
self.total_exemption_amount, self.precision("total_exemption_amount")
|
||||||
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
|
)
|
||||||
|
self.salary_structure_hra = flt(
|
||||||
|
hra_exemption["hra_amount"], self.precision("salary_structure_hra")
|
||||||
|
)
|
||||||
|
self.annual_hra_exemption = flt(
|
||||||
|
hra_exemption["annual_exemption"], self.precision("annual_hra_exemption")
|
||||||
|
)
|
||||||
|
self.monthly_hra_exemption = flt(
|
||||||
|
hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -4,25 +4,28 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import add_months, getdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.hr.utils import DuplicateDeclarationError
|
from erpnext.hr.utils import DuplicateDeclarationError
|
||||||
|
|
||||||
|
|
||||||
class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
class TestEmployeeTaxExemptionDeclaration(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
make_employee("employee@taxexepmtion.com")
|
make_employee("employee@taxexemption.com", company="_Test Company")
|
||||||
make_employee("employee1@taxexepmtion.com")
|
make_employee("employee1@taxexemption.com", company="_Test Company")
|
||||||
create_payroll_period()
|
create_payroll_period(company="_Test Company")
|
||||||
create_exemption_category()
|
create_exemption_category()
|
||||||
frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""")
|
frappe.db.delete("Employee Tax Exemption Declaration")
|
||||||
|
frappe.db.delete("Salary Structure Assignment")
|
||||||
|
|
||||||
def test_duplicate_category_in_declaration(self):
|
def test_duplicate_category_in_declaration(self):
|
||||||
declaration = frappe.get_doc(
|
declaration = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Employee Tax Exemption Declaration",
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||||
"company": erpnext.get_default_company(),
|
"company": erpnext.get_default_company(),
|
||||||
"payroll_period": "_Test Payroll Period",
|
"payroll_period": "_Test Payroll Period",
|
||||||
"currency": erpnext.get_default_currency(),
|
"currency": erpnext.get_default_currency(),
|
||||||
@@ -46,7 +49,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
|||||||
declaration = frappe.get_doc(
|
declaration = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Employee Tax Exemption Declaration",
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||||
"company": erpnext.get_default_company(),
|
"company": erpnext.get_default_company(),
|
||||||
"payroll_period": "_Test Payroll Period",
|
"payroll_period": "_Test Payroll Period",
|
||||||
"currency": erpnext.get_default_currency(),
|
"currency": erpnext.get_default_currency(),
|
||||||
@@ -68,7 +71,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
|||||||
duplicate_declaration = frappe.get_doc(
|
duplicate_declaration = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Employee Tax Exemption Declaration",
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||||
"company": erpnext.get_default_company(),
|
"company": erpnext.get_default_company(),
|
||||||
"payroll_period": "_Test Payroll Period",
|
"payroll_period": "_Test Payroll Period",
|
||||||
"currency": erpnext.get_default_currency(),
|
"currency": erpnext.get_default_currency(),
|
||||||
@@ -83,7 +86,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert)
|
self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert)
|
||||||
duplicate_declaration.employee = frappe.get_value(
|
duplicate_declaration.employee = frappe.get_value(
|
||||||
"Employee", {"user_id": "employee1@taxexepmtion.com"}, "name"
|
"Employee", {"user_id": "employee1@taxexemption.com"}, "name"
|
||||||
)
|
)
|
||||||
self.assertTrue(duplicate_declaration.insert)
|
self.assertTrue(duplicate_declaration.insert)
|
||||||
|
|
||||||
@@ -91,7 +94,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
|||||||
declaration = frappe.get_doc(
|
declaration = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Employee Tax Exemption Declaration",
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||||
"company": erpnext.get_default_company(),
|
"company": erpnext.get_default_company(),
|
||||||
"payroll_period": "_Test Payroll Period",
|
"payroll_period": "_Test Payroll Period",
|
||||||
"currency": erpnext.get_default_currency(),
|
"currency": erpnext.get_default_currency(),
|
||||||
@@ -112,6 +115,298 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(declaration.total_exemption_amount, 100000)
|
self.assertEqual(declaration.total_exemption_amount, 100000)
|
||||||
|
|
||||||
|
def test_india_hra_exemption(self):
|
||||||
|
# set country
|
||||||
|
current_country = frappe.flags.country
|
||||||
|
frappe.flags.country = "India"
|
||||||
|
|
||||||
|
setup_hra_exemption_prerequisites("Monthly")
|
||||||
|
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||||
|
|
||||||
|
declaration = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
|
"employee": employee,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"payroll_period": "_Test Payroll Period",
|
||||||
|
"currency": "INR",
|
||||||
|
"monthly_house_rent": 50000,
|
||||||
|
"rented_in_metro_city": 1,
|
||||||
|
"declarations": [
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
amount=80000,
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test1 Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
amount=60000,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# Monthly HRA received = 3000
|
||||||
|
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||||
|
self.assertEqual(declaration.monthly_hra_exemption, 3000)
|
||||||
|
self.assertEqual(declaration.annual_hra_exemption, 36000)
|
||||||
|
# 100000 Standard Exemption + 36000 HRA exemption
|
||||||
|
self.assertEqual(declaration.total_exemption_amount, 136000)
|
||||||
|
|
||||||
|
# reset
|
||||||
|
frappe.flags.country = current_country
|
||||||
|
|
||||||
|
def test_india_hra_exemption_with_daily_payroll_frequency(self):
|
||||||
|
# set country
|
||||||
|
current_country = frappe.flags.country
|
||||||
|
frappe.flags.country = "India"
|
||||||
|
|
||||||
|
setup_hra_exemption_prerequisites("Daily")
|
||||||
|
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||||
|
|
||||||
|
declaration = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
|
"employee": employee,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"payroll_period": "_Test Payroll Period",
|
||||||
|
"currency": "INR",
|
||||||
|
"monthly_house_rent": 170000,
|
||||||
|
"rented_in_metro_city": 1,
|
||||||
|
"declarations": [
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test1 Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
amount=60000,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# Daily HRA received = 3000
|
||||||
|
# should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum
|
||||||
|
self.assertEqual(declaration.monthly_hra_exemption, 17916.67)
|
||||||
|
self.assertEqual(declaration.annual_hra_exemption, 215000)
|
||||||
|
# 50000 Standard Exemption + 215000 HRA exemption
|
||||||
|
self.assertEqual(declaration.total_exemption_amount, 265000)
|
||||||
|
|
||||||
|
# reset
|
||||||
|
frappe.flags.country = current_country
|
||||||
|
|
||||||
|
def test_india_hra_exemption_with_weekly_payroll_frequency(self):
|
||||||
|
# set country
|
||||||
|
current_country = frappe.flags.country
|
||||||
|
frappe.flags.country = "India"
|
||||||
|
|
||||||
|
setup_hra_exemption_prerequisites("Weekly")
|
||||||
|
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||||
|
|
||||||
|
declaration = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
|
"employee": employee,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"payroll_period": "_Test Payroll Period",
|
||||||
|
"currency": "INR",
|
||||||
|
"monthly_house_rent": 170000,
|
||||||
|
"rented_in_metro_city": 1,
|
||||||
|
"declarations": [
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test1 Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
amount=60000,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# Weekly HRA received = 3000
|
||||||
|
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||||
|
self.assertEqual(declaration.monthly_hra_exemption, 13000)
|
||||||
|
self.assertEqual(declaration.annual_hra_exemption, 156000)
|
||||||
|
# 50000 Standard Exemption + 156000 HRA exemption
|
||||||
|
self.assertEqual(declaration.total_exemption_amount, 206000)
|
||||||
|
|
||||||
|
# reset
|
||||||
|
frappe.flags.country = current_country
|
||||||
|
|
||||||
|
def test_india_hra_exemption_with_fortnightly_payroll_frequency(self):
|
||||||
|
# set country
|
||||||
|
current_country = frappe.flags.country
|
||||||
|
frappe.flags.country = "India"
|
||||||
|
|
||||||
|
setup_hra_exemption_prerequisites("Fortnightly")
|
||||||
|
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||||
|
|
||||||
|
declaration = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
|
"employee": employee,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"payroll_period": "_Test Payroll Period",
|
||||||
|
"currency": "INR",
|
||||||
|
"monthly_house_rent": 170000,
|
||||||
|
"rented_in_metro_city": 1,
|
||||||
|
"declarations": [
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test1 Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
amount=60000,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# Fortnightly HRA received = 3000
|
||||||
|
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||||
|
self.assertEqual(declaration.monthly_hra_exemption, 6500)
|
||||||
|
self.assertEqual(declaration.annual_hra_exemption, 78000)
|
||||||
|
# 50000 Standard Exemption + 78000 HRA exemption
|
||||||
|
self.assertEqual(declaration.total_exemption_amount, 128000)
|
||||||
|
|
||||||
|
# reset
|
||||||
|
frappe.flags.country = current_country
|
||||||
|
|
||||||
|
def test_india_hra_exemption_with_bimonthly_payroll_frequency(self):
|
||||||
|
# set country
|
||||||
|
current_country = frappe.flags.country
|
||||||
|
frappe.flags.country = "India"
|
||||||
|
|
||||||
|
setup_hra_exemption_prerequisites("Bimonthly")
|
||||||
|
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||||
|
|
||||||
|
declaration = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
|
"employee": employee,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"payroll_period": "_Test Payroll Period",
|
||||||
|
"currency": "INR",
|
||||||
|
"monthly_house_rent": 50000,
|
||||||
|
"rented_in_metro_city": 1,
|
||||||
|
"declarations": [
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
amount=80000,
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test1 Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
amount=60000,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# Bimonthly HRA received = 3000
|
||||||
|
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||||
|
self.assertEqual(declaration.monthly_hra_exemption, 1500)
|
||||||
|
self.assertEqual(declaration.annual_hra_exemption, 18000)
|
||||||
|
# 100000 Standard Exemption + 18000 HRA exemption
|
||||||
|
self.assertEqual(declaration.total_exemption_amount, 118000)
|
||||||
|
|
||||||
|
# reset
|
||||||
|
frappe.flags.country = current_country
|
||||||
|
|
||||||
|
def test_india_hra_exemption_with_multiple_salary_structure_assignments(self):
|
||||||
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||||
|
create_salary_structure_assignment,
|
||||||
|
make_salary_structure,
|
||||||
|
)
|
||||||
|
|
||||||
|
# set country
|
||||||
|
current_country = frappe.flags.country
|
||||||
|
frappe.flags.country = "India"
|
||||||
|
|
||||||
|
employee = make_employee("employee@taxexemption2.com", company="_Test Company")
|
||||||
|
payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
|
||||||
|
|
||||||
|
create_tax_slab(
|
||||||
|
payroll_period,
|
||||||
|
allow_tax_exemption=True,
|
||||||
|
currency="INR",
|
||||||
|
effective_date=getdate("2019-04-01"),
|
||||||
|
company="_Test Company",
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# salary structure with base 50000, HRA 3000
|
||||||
|
make_salary_structure(
|
||||||
|
"Monthly Structure for HRA Exemption 1",
|
||||||
|
"Monthly",
|
||||||
|
employee=employee,
|
||||||
|
company="_Test Company",
|
||||||
|
currency="INR",
|
||||||
|
payroll_period=payroll_period.name,
|
||||||
|
from_date=payroll_period.start_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
# salary structure with base 70000, HRA = base * 0.2 = 14000
|
||||||
|
salary_structure = make_salary_structure(
|
||||||
|
"Monthly Structure for HRA Exemption 2",
|
||||||
|
"Monthly",
|
||||||
|
employee=employee,
|
||||||
|
company="_Test Company",
|
||||||
|
currency="INR",
|
||||||
|
payroll_period=payroll_period.name,
|
||||||
|
from_date=payroll_period.start_date,
|
||||||
|
dont_submit=True,
|
||||||
|
)
|
||||||
|
for component_row in salary_structure.earnings:
|
||||||
|
if component_row.salary_component == "HRA":
|
||||||
|
component_row.amount = 0
|
||||||
|
component_row.amount_based_on_formula = 1
|
||||||
|
component_row.formula = "base * 0.2"
|
||||||
|
break
|
||||||
|
|
||||||
|
salary_structure.submit()
|
||||||
|
|
||||||
|
create_salary_structure_assignment(
|
||||||
|
employee,
|
||||||
|
salary_structure.name,
|
||||||
|
from_date=add_months(payroll_period.start_date, 6),
|
||||||
|
company="_Test Company",
|
||||||
|
currency="INR",
|
||||||
|
payroll_period=payroll_period.name,
|
||||||
|
base=70000,
|
||||||
|
allow_duplicate=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
declaration = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Tax Exemption Declaration",
|
||||||
|
"employee": employee,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"payroll_period": payroll_period.name,
|
||||||
|
"currency": "INR",
|
||||||
|
"monthly_house_rent": 50000,
|
||||||
|
"rented_in_metro_city": 1,
|
||||||
|
"declarations": [
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test1 Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
amount=60000,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# Monthly HRA received = 50000 * 6 months + 70000 * 6 months
|
||||||
|
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||||
|
self.assertEqual(declaration.monthly_hra_exemption, 8500)
|
||||||
|
self.assertEqual(declaration.annual_hra_exemption, 102000)
|
||||||
|
# 50000 Standard Exemption + 102000 HRA exemption
|
||||||
|
self.assertEqual(declaration.total_exemption_amount, 152000)
|
||||||
|
|
||||||
|
# reset
|
||||||
|
frappe.flags.country = current_country
|
||||||
|
|
||||||
|
|
||||||
def create_payroll_period(**args):
|
def create_payroll_period(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
@@ -163,3 +458,33 @@ def create_exemption_category():
|
|||||||
"is_active": 1,
|
"is_active": 1,
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_hra_exemption_prerequisites(frequency, employee=None):
|
||||||
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
|
||||||
|
if not employee:
|
||||||
|
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||||
|
|
||||||
|
create_tax_slab(
|
||||||
|
payroll_period,
|
||||||
|
allow_tax_exemption=True,
|
||||||
|
currency="INR",
|
||||||
|
effective_date=getdate("2019-04-01"),
|
||||||
|
company="_Test Company",
|
||||||
|
)
|
||||||
|
|
||||||
|
make_salary_structure(
|
||||||
|
f"{frequency} Structure for HRA Exemption",
|
||||||
|
frequency,
|
||||||
|
employee=employee,
|
||||||
|
company="_Test Company",
|
||||||
|
currency="INR",
|
||||||
|
payroll_period=payroll_period,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
|
||||||
|
)
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ class EmployeeTaxExemptionProofSubmission(Document):
|
|||||||
self.total_actual_amount += flt(d.amount)
|
self.total_actual_amount += flt(d.amount)
|
||||||
|
|
||||||
def set_total_exemption_amount(self):
|
def set_total_exemption_amount(self):
|
||||||
self.exemption_amount = get_total_exemption_amount(self.tax_exemption_proofs)
|
self.exemption_amount = flt(
|
||||||
|
get_total_exemption_amount(self.tax_exemption_proofs), self.precision("exemption_amount")
|
||||||
|
)
|
||||||
|
|
||||||
def calculate_hra_exemption(self):
|
def calculate_hra_exemption(self):
|
||||||
self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0
|
self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0
|
||||||
@@ -39,6 +41,13 @@ class EmployeeTaxExemptionProofSubmission(Document):
|
|||||||
hra_exemption = calculate_hra_exemption_for_period(self)
|
hra_exemption = calculate_hra_exemption_for_period(self)
|
||||||
if hra_exemption:
|
if hra_exemption:
|
||||||
self.exemption_amount += hra_exemption["total_eligible_hra_exemption"]
|
self.exemption_amount += hra_exemption["total_eligible_hra_exemption"]
|
||||||
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
|
self.exemption_amount = flt(self.exemption_amount, self.precision("exemption_amount"))
|
||||||
self.monthly_house_rent = hra_exemption["monthly_house_rent"]
|
self.monthly_hra_exemption = flt(
|
||||||
self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"]
|
hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
|
||||||
|
)
|
||||||
|
self.monthly_house_rent = flt(
|
||||||
|
hra_exemption["monthly_house_rent"], self.precision("monthly_house_rent")
|
||||||
|
)
|
||||||
|
self.total_eligible_hra_exemption = flt(
|
||||||
|
hra_exemption["total_eligible_hra_exemption"], self.precision("total_eligible_hra_exemption")
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,22 +4,26 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
|
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
|
||||||
create_exemption_category,
|
create_exemption_category,
|
||||||
create_payroll_period,
|
create_payroll_period,
|
||||||
|
setup_hra_exemption_prerequisites,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
class TestEmployeeTaxExemptionProofSubmission(FrappeTestCase):
|
||||||
def setup(self):
|
def setUp(self):
|
||||||
make_employee("employee@proofsubmission.com")
|
make_employee("employee@proofsubmission.com", company="_Test Company")
|
||||||
create_payroll_period()
|
create_payroll_period(company="_Test Company")
|
||||||
create_exemption_category()
|
create_exemption_category()
|
||||||
frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""")
|
frappe.db.delete("Employee Tax Exemption Proof Submission")
|
||||||
|
frappe.db.delete("Salary Structure Assignment")
|
||||||
|
|
||||||
def test_exemption_amount_lesser_than_category_max(self):
|
def test_exemption_amount_lesser_than_category_max(self):
|
||||||
declaration = frappe.get_doc(
|
proof = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Employee Tax Exemption Proof Submission",
|
"doctype": "Employee Tax Exemption Proof Submission",
|
||||||
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
||||||
@@ -34,8 +38,8 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertRaises(frappe.ValidationError, declaration.save)
|
self.assertRaises(frappe.ValidationError, proof.save)
|
||||||
declaration = frappe.get_doc(
|
proof = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Employee Tax Exemption Proof Submission",
|
"doctype": "Employee Tax Exemption Proof Submission",
|
||||||
"payroll_period": "Test Payroll Period",
|
"payroll_period": "Test Payroll Period",
|
||||||
@@ -50,11 +54,11 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertTrue(declaration.save)
|
self.assertTrue(proof.save)
|
||||||
self.assertTrue(declaration.submit)
|
self.assertTrue(proof.submit)
|
||||||
|
|
||||||
def test_duplicate_category_in_proof_submission(self):
|
def test_duplicate_category_in_proof_submission(self):
|
||||||
declaration = frappe.get_doc(
|
proof = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Employee Tax Exemption Proof Submission",
|
"doctype": "Employee Tax Exemption Proof Submission",
|
||||||
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
||||||
@@ -74,4 +78,59 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertRaises(frappe.ValidationError, declaration.save)
|
self.assertRaises(frappe.ValidationError, proof.save)
|
||||||
|
|
||||||
|
def test_india_hra_exemption(self):
|
||||||
|
# set country
|
||||||
|
current_country = frappe.flags.country
|
||||||
|
frappe.flags.country = "India"
|
||||||
|
|
||||||
|
employee = frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name")
|
||||||
|
setup_hra_exemption_prerequisites("Monthly", employee)
|
||||||
|
payroll_period = frappe.db.get_value(
|
||||||
|
"Payroll Period", "_Test Payroll Period", ["start_date", "end_date"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
proof = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Tax Exemption Proof Submission",
|
||||||
|
"employee": employee,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"payroll_period": "_Test Payroll Period",
|
||||||
|
"currency": "INR",
|
||||||
|
"house_rent_payment_amount": 600000,
|
||||||
|
"rented_in_metro_city": 1,
|
||||||
|
"rented_from_date": payroll_period.start_date,
|
||||||
|
"rented_to_date": payroll_period.end_date,
|
||||||
|
"tax_exemption_proofs": [
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
type_of_proof="Test Proof",
|
||||||
|
amount=100000,
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
exemption_sub_category="_Test1 Sub Category",
|
||||||
|
exemption_category="_Test Category",
|
||||||
|
type_of_proof="Test Proof",
|
||||||
|
amount=50000,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
self.assertEqual(proof.monthly_house_rent, 50000)
|
||||||
|
|
||||||
|
# Monthly HRA received = 3000
|
||||||
|
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||||
|
self.assertEqual(proof.monthly_hra_exemption, 3000)
|
||||||
|
self.assertEqual(proof.total_eligible_hra_exemption, 36000)
|
||||||
|
|
||||||
|
# total exemptions + house rent payment amount
|
||||||
|
self.assertEqual(proof.total_actual_amount, 750000)
|
||||||
|
|
||||||
|
# 100000 Standard Exemption + 36000 HRA exemption
|
||||||
|
self.assertEqual(proof.exemption_amount, 136000)
|
||||||
|
|
||||||
|
# reset
|
||||||
|
frappe.flags.country = current_country
|
||||||
|
|||||||
@@ -90,9 +90,8 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"options": "Draft\nUnpaid\nPaid",
|
"options": "Draft\nUnpaid\nPaid\nSubmitted\nCancelled",
|
||||||
"read_only": 1,
|
"read_only": 1
|
||||||
"reqd": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.pay_via_salary_slip == 0",
|
"depends_on": "eval: doc.pay_via_salary_slip == 0",
|
||||||
@@ -196,7 +195,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-02 18:21:11.971488",
|
"modified": "2022-05-27 13:56:14.349183",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Gratuity",
|
"name": "Gratuity",
|
||||||
|
|||||||
12
erpnext/payroll/doctype/gratuity/gratuity_list.js
Normal file
12
erpnext/payroll/doctype/gratuity/gratuity_list.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
frappe.listview_settings["Gratuity"] = {
|
||||||
|
get_indicator: function(doc) {
|
||||||
|
let status_color = {
|
||||||
|
"Draft": "red",
|
||||||
|
"Submitted": "blue",
|
||||||
|
"Cancelled": "red",
|
||||||
|
"Paid": "green",
|
||||||
|
"Unpaid": "orange",
|
||||||
|
};
|
||||||
|
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,58 +4,70 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_days, flt, get_datetime, getdate
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import add_days, add_months, floor, flt, get_datetime, get_first_day, getdate
|
||||||
|
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
|
from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
|
||||||
|
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||||
from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
|
from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
|
||||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||||
make_deduction_salary_component,
|
make_deduction_salary_component,
|
||||||
make_earning_salary_component,
|
make_earning_salary_component,
|
||||||
make_employee_salary_slip,
|
make_employee_salary_slip,
|
||||||
|
make_holiday_list,
|
||||||
)
|
)
|
||||||
|
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||||
from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
|
from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
|
||||||
|
|
||||||
test_dependencies = ["Salary Component", "Salary Slip", "Account"]
|
test_dependencies = ["Salary Component", "Salary Slip", "Account"]
|
||||||
|
|
||||||
|
|
||||||
class TestGratuity(unittest.TestCase):
|
class TestGratuity(FrappeTestCase):
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
|
|
||||||
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.sql("DELETE FROM `tabGratuity`")
|
frappe.db.delete("Gratuity")
|
||||||
frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
|
frappe.db.delete("Salary Slip")
|
||||||
|
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
|
||||||
|
|
||||||
|
make_earning_salary_component(
|
||||||
|
setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
|
||||||
|
)
|
||||||
|
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
|
||||||
|
make_holiday_list()
|
||||||
|
|
||||||
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
|
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
|
||||||
new_employee = make_employee("new_employee@salary.com", company="_Test Company")
|
new_employee = make_employee("new_employee@salary.com", company="_Test Company")
|
||||||
salary_slip = get_last_salary_slip(new_employee)
|
salary_slip = get_last_salary_slip(new_employee)
|
||||||
assert salary_slip is None
|
self.assertIsNone(salary_slip)
|
||||||
|
|
||||||
def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
def test_gratuity_based_on_current_slab_via_additional_salary(self):
|
||||||
|
"""
|
||||||
|
Range | Fraction
|
||||||
|
5-0 | 1
|
||||||
|
"""
|
||||||
|
doj = add_days(getdate(), -(6 * 365))
|
||||||
|
relieving_date = getdate()
|
||||||
|
|
||||||
|
employee = make_employee(
|
||||||
|
"test_employee_gratuity@salary.com",
|
||||||
|
company="_Test Company",
|
||||||
|
date_of_joining=doj,
|
||||||
|
relieving_date=relieving_date,
|
||||||
|
)
|
||||||
|
sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
|
||||||
|
|
||||||
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
|
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
|
||||||
|
|
||||||
gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
|
gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
|
||||||
|
|
||||||
# work experience calculation
|
# work experience calculation
|
||||||
date_of_joining, relieving_date = frappe.db.get_value(
|
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
|
||||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
|
||||||
)
|
self.assertEqual(gratuity.current_work_experience, experience)
|
||||||
employee_total_workings_days = (
|
|
||||||
get_datetime(relieving_date) - get_datetime(date_of_joining)
|
|
||||||
).days
|
|
||||||
|
|
||||||
experience = employee_total_workings_days / rule.total_working_days_per_year
|
# amount calculation
|
||||||
gratuity.reload()
|
|
||||||
from math import floor
|
|
||||||
|
|
||||||
self.assertEqual(floor(experience), gratuity.current_work_experience)
|
|
||||||
|
|
||||||
# amount Calculation
|
|
||||||
component_amount = frappe.get_all(
|
component_amount = frappe.get_all(
|
||||||
"Salary Detail",
|
"Salary Detail",
|
||||||
filters={
|
filters={
|
||||||
@@ -65,20 +77,44 @@ class TestGratuity(unittest.TestCase):
|
|||||||
"salary_component": "Basic Salary",
|
"salary_component": "Basic Salary",
|
||||||
},
|
},
|
||||||
fields=["amount"],
|
fields=["amount"],
|
||||||
|
limit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
""" 5 - 0 fraction is 1 """
|
|
||||||
|
|
||||||
gratuity_amount = component_amount[0].amount * experience
|
gratuity_amount = component_amount[0].amount * experience
|
||||||
gratuity.reload()
|
|
||||||
|
|
||||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||||
|
|
||||||
# additional salary creation (Pay via salary slip)
|
# additional salary creation (Pay via salary slip)
|
||||||
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
|
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
|
||||||
|
|
||||||
def test_check_gratuity_amount_based_on_all_previous_slabs(self):
|
# gratuity should be marked "Paid" on the next salary slip submission
|
||||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
salary_slip = make_salary_slip("Test Gratuity", employee=employee)
|
||||||
|
salary_slip.posting_date = getdate()
|
||||||
|
salary_slip.insert()
|
||||||
|
salary_slip.submit()
|
||||||
|
|
||||||
|
gratuity.reload()
|
||||||
|
self.assertEqual(gratuity.status, "Paid")
|
||||||
|
|
||||||
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
|
def test_gratuity_based_on_all_previous_slabs_via_payment_entry(self):
|
||||||
|
"""
|
||||||
|
Range | Fraction
|
||||||
|
0-1 | 0
|
||||||
|
1-5 | 0.7
|
||||||
|
5-0 | 1
|
||||||
|
"""
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
|
|
||||||
|
doj = add_days(getdate(), -(6 * 365))
|
||||||
|
relieving_date = getdate()
|
||||||
|
|
||||||
|
employee = make_employee(
|
||||||
|
"test_employee_gratuity@salary.com",
|
||||||
|
company="_Test Company",
|
||||||
|
date_of_joining=doj,
|
||||||
|
relieving_date=relieving_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
|
||||||
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
|
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
|
||||||
set_mode_of_payment_account()
|
set_mode_of_payment_account()
|
||||||
|
|
||||||
@@ -87,22 +123,11 @@ class TestGratuity(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# work experience calculation
|
# work experience calculation
|
||||||
date_of_joining, relieving_date = frappe.db.get_value(
|
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
|
||||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
|
||||||
)
|
self.assertEqual(gratuity.current_work_experience, experience)
|
||||||
employee_total_workings_days = (
|
|
||||||
get_datetime(relieving_date) - get_datetime(date_of_joining)
|
|
||||||
).days
|
|
||||||
|
|
||||||
experience = employee_total_workings_days / rule.total_working_days_per_year
|
# amount calculation
|
||||||
|
|
||||||
gratuity.reload()
|
|
||||||
|
|
||||||
from math import floor
|
|
||||||
|
|
||||||
self.assertEqual(floor(experience), gratuity.current_work_experience)
|
|
||||||
|
|
||||||
# amount Calculation
|
|
||||||
component_amount = frappe.get_all(
|
component_amount = frappe.get_all(
|
||||||
"Salary Detail",
|
"Salary Detail",
|
||||||
filters={
|
filters={
|
||||||
@@ -112,36 +137,22 @@ class TestGratuity(unittest.TestCase):
|
|||||||
"salary_component": "Basic Salary",
|
"salary_component": "Basic Salary",
|
||||||
},
|
},
|
||||||
fields=["amount"],
|
fields=["amount"],
|
||||||
|
limit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
""" range | Fraction
|
|
||||||
0-1 | 0
|
|
||||||
1-5 | 0.7
|
|
||||||
5-0 | 1
|
|
||||||
"""
|
|
||||||
|
|
||||||
gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount
|
gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount
|
||||||
gratuity.reload()
|
|
||||||
|
|
||||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||||
self.assertEqual(gratuity.status, "Unpaid")
|
self.assertEqual(gratuity.status, "Unpaid")
|
||||||
|
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
pe = get_payment_entry("Gratuity", gratuity.name)
|
||||||
|
pe.reference_no = "123467"
|
||||||
|
pe.reference_date = getdate()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
pay_entry = get_payment_entry("Gratuity", gratuity.name)
|
|
||||||
pay_entry.reference_no = "123467"
|
|
||||||
pay_entry.reference_date = getdate()
|
|
||||||
pay_entry.save()
|
|
||||||
pay_entry.submit()
|
|
||||||
gratuity.reload()
|
gratuity.reload()
|
||||||
|
|
||||||
self.assertEqual(gratuity.status, "Paid")
|
self.assertEqual(gratuity.status, "Paid")
|
||||||
self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2))
|
self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2))
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.db.sql("DELETE FROM `tabGratuity`")
|
|
||||||
frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
|
|
||||||
|
|
||||||
|
|
||||||
def get_gratuity_rule(name):
|
def get_gratuity_rule(name):
|
||||||
rule = frappe.db.exists("Gratuity Rule", name)
|
rule = frappe.db.exists("Gratuity Rule", name)
|
||||||
@@ -151,7 +162,6 @@ def get_gratuity_rule(name):
|
|||||||
rule.applicable_earnings_component = []
|
rule.applicable_earnings_component = []
|
||||||
rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"})
|
rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"})
|
||||||
rule.save()
|
rule.save()
|
||||||
rule.reload()
|
|
||||||
|
|
||||||
return rule
|
return rule
|
||||||
|
|
||||||
@@ -206,23 +216,17 @@ def create_account():
|
|||||||
).insert(ignore_permissions=True)
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
def create_employee_and_get_last_salary_slip():
|
def create_salary_slip(employee):
|
||||||
employee = make_employee("test_employee@salary.com", company="_Test Company")
|
|
||||||
frappe.db.set_value("Employee", employee, "relieving_date", getdate())
|
|
||||||
frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365)))
|
|
||||||
if not frappe.db.exists("Salary Slip", {"employee": employee}):
|
if not frappe.db.exists("Salary Slip", {"employee": employee}):
|
||||||
salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly")
|
posting_date = get_first_day(add_months(getdate(), -1))
|
||||||
|
salary_slip = make_employee_salary_slip(
|
||||||
|
employee, "Monthly", "Test Gratuity", posting_date=posting_date
|
||||||
|
)
|
||||||
|
salary_slip.start_date = posting_date
|
||||||
|
salary_slip.end_date = None
|
||||||
salary_slip.submit()
|
salary_slip.submit()
|
||||||
salary_slip = salary_slip.name
|
salary_slip = salary_slip.name
|
||||||
else:
|
else:
|
||||||
salary_slip = get_last_salary_slip(employee)
|
salary_slip = get_last_salary_slip(employee)
|
||||||
|
|
||||||
if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"):
|
return salary_slip
|
||||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
|
||||||
|
|
||||||
make_holiday_list()
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List"
|
|
||||||
)
|
|
||||||
|
|
||||||
return employee, salary_slip
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from frappe.utils import (
|
|||||||
comma_and,
|
comma_and,
|
||||||
date_diff,
|
date_diff,
|
||||||
flt,
|
flt,
|
||||||
|
get_link_to_form,
|
||||||
getdate,
|
getdate,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ class PayrollEntry(Document):
|
|||||||
|
|
||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
self.validate_employee_details()
|
self.validate_employee_details()
|
||||||
|
self.validate_payroll_payable_account()
|
||||||
if self.validate_attendance:
|
if self.validate_attendance:
|
||||||
if self.validate_employee_attendance():
|
if self.validate_employee_attendance():
|
||||||
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
|
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
|
||||||
@@ -65,6 +67,14 @@ class PayrollEntry(Document):
|
|||||||
if len(emp_with_sal_slip):
|
if len(emp_with_sal_slip):
|
||||||
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
|
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
|
||||||
|
|
||||||
|
def validate_payroll_payable_account(self):
|
||||||
|
if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Account type cannot be set for payroll payable account {0}, please remove and try again"
|
||||||
|
).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account)))
|
||||||
|
)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
frappe.delete_doc(
|
frappe.delete_doc(
|
||||||
"Salary Slip",
|
"Salary Slip",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_months
|
from frappe.utils import add_months
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -35,7 +36,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
|||||||
test_dependencies = ["Holiday List"]
|
test_dependencies = ["Holiday List"]
|
||||||
|
|
||||||
|
|
||||||
class TestPayrollEntry(unittest.TestCase):
|
class TestPayrollEntry(FrappeTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
|||||||
calculate_amounts,
|
calculate_amounts,
|
||||||
create_repayment_entry,
|
create_repayment_entry,
|
||||||
)
|
)
|
||||||
|
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||||
|
process_loan_interest_accrual_for_term_loans,
|
||||||
|
)
|
||||||
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
||||||
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
|
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
|
||||||
get_benefit_component_amount,
|
get_benefit_component_amount,
|
||||||
@@ -117,10 +120,10 @@ class SalarySlip(TransactionBase):
|
|||||||
self.update_payment_status_for_gratuity()
|
self.update_payment_status_for_gratuity()
|
||||||
|
|
||||||
def update_payment_status_for_gratuity(self):
|
def update_payment_status_for_gratuity(self):
|
||||||
add_salary = frappe.db.get_all(
|
additional_salary = frappe.db.get_all(
|
||||||
"Additional Salary",
|
"Additional Salary",
|
||||||
filters={
|
filters={
|
||||||
"payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
|
"payroll_date": ("between", [self.start_date, self.end_date]),
|
||||||
"employee": self.employee,
|
"employee": self.employee,
|
||||||
"ref_doctype": "Gratuity",
|
"ref_doctype": "Gratuity",
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
@@ -129,10 +132,10 @@ class SalarySlip(TransactionBase):
|
|||||||
limit=1,
|
limit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(add_salary):
|
if additional_salary:
|
||||||
status = "Paid" if self.docstatus == 1 else "Unpaid"
|
status = "Paid" if self.docstatus == 1 else "Unpaid"
|
||||||
if add_salary[0].name in [data.additional_salary for data in self.earnings]:
|
if additional_salary[0].name in [entry.additional_salary for entry in self.earnings]:
|
||||||
frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
|
frappe.db.set_value("Gratuity", additional_salary[0].ref_docname, "status", status)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.set_status()
|
self.set_status()
|
||||||
@@ -463,37 +466,14 @@ class SalarySlip(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for d in range(working_days):
|
for d in range(working_days):
|
||||||
dt = add_days(cstr(getdate(self.start_date)), d)
|
date = add_days(cstr(getdate(self.start_date)), d)
|
||||||
leave = frappe.db.sql(
|
leave = get_lwp_or_ppl_for_date(date, self.employee, holidays)
|
||||||
"""
|
|
||||||
SELECT t1.name,
|
|
||||||
CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
|
|
||||||
THEN t1.half_day else 0 END,
|
|
||||||
t2.is_ppl,
|
|
||||||
t2.fraction_of_daily_salary_per_leave
|
|
||||||
FROM `tabLeave Application` t1, `tabLeave Type` t2
|
|
||||||
WHERE t2.name = t1.leave_type
|
|
||||||
AND (t2.is_lwp = 1 or t2.is_ppl = 1)
|
|
||||||
AND t1.docstatus = 1
|
|
||||||
AND t1.employee = %(employee)s
|
|
||||||
AND ifnull(t1.salary_slip, '') = ''
|
|
||||||
AND CASE
|
|
||||||
WHEN t2.include_holiday != 1
|
|
||||||
THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
|
|
||||||
WHEN t2.include_holiday
|
|
||||||
THEN %(dt)s between from_date and to_date
|
|
||||||
END
|
|
||||||
""".format(
|
|
||||||
holidays
|
|
||||||
),
|
|
||||||
{"employee": self.employee, "dt": dt},
|
|
||||||
)
|
|
||||||
|
|
||||||
if leave:
|
if leave:
|
||||||
equivalent_lwp_count = 0
|
equivalent_lwp_count = 0
|
||||||
is_half_day_leave = cint(leave[0][1])
|
is_half_day_leave = cint(leave[0].is_half_day)
|
||||||
is_partially_paid_leave = cint(leave[0][2])
|
is_partially_paid_leave = cint(leave[0].is_ppl)
|
||||||
fraction_of_daily_salary_per_leave = flt(leave[0][3])
|
fraction_of_daily_salary_per_leave = flt(leave[0].fraction_of_daily_salary_per_leave)
|
||||||
|
|
||||||
equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
|
equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
|
||||||
|
|
||||||
@@ -1405,9 +1385,9 @@ class SalarySlip(TransactionBase):
|
|||||||
self.total_loan_repayment += payment.total_payment
|
self.total_loan_repayment += payment.total_payment
|
||||||
|
|
||||||
def get_loan_details(self):
|
def get_loan_details(self):
|
||||||
return frappe.get_all(
|
loan_details = frappe.get_all(
|
||||||
"Loan",
|
"Loan",
|
||||||
fields=["name", "interest_income_account", "loan_account", "loan_type"],
|
fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"],
|
||||||
filters={
|
filters={
|
||||||
"applicant": self.employee,
|
"applicant": self.employee,
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
@@ -1416,6 +1396,15 @@ class SalarySlip(TransactionBase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if loan_details:
|
||||||
|
for loan in loan_details:
|
||||||
|
if loan.is_term_loan:
|
||||||
|
process_loan_interest_accrual_for_term_loans(
|
||||||
|
posting_date=self.posting_date, loan_type=loan.loan_type, loan=loan.name
|
||||||
|
)
|
||||||
|
|
||||||
|
return loan_details
|
||||||
|
|
||||||
def make_loan_repayment_entry(self):
|
def make_loan_repayment_entry(self):
|
||||||
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
|
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
|
||||||
for loan in self.loans:
|
for loan in self.loans:
|
||||||
@@ -1714,3 +1703,46 @@ def get_payroll_payable_account(company, payroll_entry):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return payroll_payable_account
|
return payroll_payable_account
|
||||||
|
|
||||||
|
|
||||||
|
def get_lwp_or_ppl_for_date(date, employee, holidays):
|
||||||
|
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||||
|
LeaveType = frappe.qb.DocType("Leave Type")
|
||||||
|
|
||||||
|
is_half_day = (
|
||||||
|
frappe.qb.terms.Case()
|
||||||
|
.when(
|
||||||
|
(
|
||||||
|
(LeaveApplication.half_day_date == date)
|
||||||
|
| (LeaveApplication.from_date == LeaveApplication.to_date)
|
||||||
|
),
|
||||||
|
LeaveApplication.half_day,
|
||||||
|
)
|
||||||
|
.else_(0)
|
||||||
|
).as_("is_half_day")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(LeaveApplication)
|
||||||
|
.inner_join(LeaveType)
|
||||||
|
.on((LeaveType.name == LeaveApplication.leave_type))
|
||||||
|
.select(
|
||||||
|
LeaveApplication.name,
|
||||||
|
LeaveType.is_ppl,
|
||||||
|
LeaveType.fraction_of_daily_salary_per_leave,
|
||||||
|
(is_half_day),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(((LeaveType.is_lwp == 1) | (LeaveType.is_ppl == 1)))
|
||||||
|
& (LeaveApplication.docstatus == 1)
|
||||||
|
& (LeaveApplication.status == "Approved")
|
||||||
|
& (LeaveApplication.employee == employee)
|
||||||
|
& ((LeaveApplication.salary_slip.isnull()) | (LeaveApplication.salary_slip == ""))
|
||||||
|
& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# if it's a holiday only include if leave type has "include holiday" enabled
|
||||||
|
if date in holidays:
|
||||||
|
query = query.where((LeaveType.include_holiday == "1"))
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
|
"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
|
||||||
)
|
)
|
||||||
def test_payment_days_based_on_attendance(self):
|
def test_payment_days_based_on_attendance(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
|
|
||||||
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
||||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||||
@@ -128,7 +128,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
def test_payment_days_for_mid_joinee_including_holidays(self):
|
def test_payment_days_for_mid_joinee_including_holidays(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
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")
|
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||||
@@ -196,7 +196,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
# tests mid month joining and relieving along with unmarked days
|
# tests mid month joining and relieving along with unmarked days
|
||||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
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")
|
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||||
@@ -236,7 +236,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
def test_payment_days_for_mid_joinee_excluding_holidays(self):
|
def test_payment_days_for_mid_joinee_excluding_holidays(self):
|
||||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
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")
|
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||||
@@ -267,7 +267,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
|
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
|
||||||
def test_payment_days_based_on_leave_application(self):
|
def test_payment_days_based_on_leave_application(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
|
|
||||||
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
||||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||||
@@ -366,7 +366,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
salary_slip.submit()
|
salary_slip.submit()
|
||||||
salary_slip.reload()
|
salary_slip.reload()
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
days_in_month = no_of_days[0]
|
days_in_month = no_of_days[0]
|
||||||
no_of_holidays = no_of_days[1]
|
no_of_holidays = no_of_days[1]
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
create_salary_structure_assignment,
|
create_salary_structure_assignment,
|
||||||
)
|
)
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
|
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
|
||||||
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
|
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
|
||||||
|
|
||||||
@@ -445,7 +445,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
|
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
|
||||||
def test_salary_slip_with_holidays_included(self):
|
def test_salary_slip_with_holidays_included(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Employee",
|
"Employee",
|
||||||
@@ -477,7 +477,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
|
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
|
||||||
def test_salary_slip_with_holidays_excluded(self):
|
def test_salary_slip_with_holidays_excluded(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Employee",
|
"Employee",
|
||||||
@@ -514,7 +514,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
create_salary_structure_assignment,
|
create_salary_structure_assignment,
|
||||||
)
|
)
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
|
|
||||||
# set joinng date in the same month
|
# set joinng date in the same month
|
||||||
employee = make_employee("test_payment_days@salary.com")
|
employee = make_employee("test_payment_days@salary.com")
|
||||||
@@ -842,6 +842,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
"Monthly",
|
"Monthly",
|
||||||
other_details={"max_benefits": 100000},
|
other_details={"max_benefits": 100000},
|
||||||
test_tax=True,
|
test_tax=True,
|
||||||
|
include_flexi_benefits=True,
|
||||||
employee=employee,
|
employee=employee,
|
||||||
payroll_period=payroll_period,
|
payroll_period=payroll_period,
|
||||||
)
|
)
|
||||||
@@ -945,6 +946,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
"Monthly",
|
"Monthly",
|
||||||
other_details={"max_benefits": 100000},
|
other_details={"max_benefits": 100000},
|
||||||
test_tax=True,
|
test_tax=True,
|
||||||
|
include_flexi_benefits=True,
|
||||||
employee=employee,
|
employee=employee,
|
||||||
payroll_period=payroll_period,
|
payroll_period=payroll_period,
|
||||||
)
|
)
|
||||||
@@ -986,20 +988,21 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
activity_type.wage_rate = 25
|
activity_type.wage_rate = 25
|
||||||
activity_type.save()
|
activity_type.save()
|
||||||
|
|
||||||
def get_no_of_days(self):
|
|
||||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
|
||||||
no_of_holidays_in_month = len(
|
|
||||||
[
|
|
||||||
1
|
|
||||||
for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
|
|
||||||
if i[6] != 0
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
def get_no_of_days():
|
||||||
|
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||||
|
no_of_holidays_in_month = len(
|
||||||
|
[
|
||||||
|
1
|
||||||
|
for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||||
|
if i[6] != 0
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||||
|
|
||||||
|
|
||||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
if not salary_structure:
|
if not salary_structure:
|
||||||
@@ -1010,7 +1013,11 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
salary_structure_doc = make_salary_structure(
|
salary_structure_doc = make_salary_structure(
|
||||||
salary_structure, payroll_frequency, employee=employee.name, company=employee.company
|
salary_structure,
|
||||||
|
payroll_frequency,
|
||||||
|
employee=employee.name,
|
||||||
|
company=employee.company,
|
||||||
|
from_date=posting_date,
|
||||||
)
|
)
|
||||||
salary_slip_name = frappe.db.get_value(
|
salary_slip_name = frappe.db.get_value(
|
||||||
"Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}
|
"Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}
|
||||||
@@ -1020,7 +1027,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
|||||||
salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name)
|
salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name)
|
||||||
salary_slip.employee_name = employee.employee_name
|
salary_slip.employee_name = employee.employee_name
|
||||||
salary_slip.payroll_frequency = payroll_frequency
|
salary_slip.payroll_frequency = payroll_frequency
|
||||||
salary_slip.posting_date = nowdate()
|
salary_slip.posting_date = posting_date or nowdate()
|
||||||
salary_slip.insert()
|
salary_slip.insert()
|
||||||
else:
|
else:
|
||||||
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
|
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
|
||||||
@@ -1092,7 +1099,9 @@ def create_account(account_name, company, parent_account, account_type=None):
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
def make_earning_salary_component(setup=False, test_tax=False, company_list=None):
|
def make_earning_salary_component(
|
||||||
|
setup=False, test_tax=False, company_list=None, include_flexi_benefits=False
|
||||||
|
):
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
"salary_component": "Basic Salary",
|
"salary_component": "Basic Salary",
|
||||||
@@ -1113,7 +1122,7 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None
|
|||||||
},
|
},
|
||||||
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
|
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
|
||||||
]
|
]
|
||||||
if test_tax:
|
if include_flexi_benefits:
|
||||||
data.extend(
|
data.extend(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -1132,12 +1141,20 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None
|
|||||||
"pay_against_benefit_claim": 0,
|
"pay_against_benefit_claim": 0,
|
||||||
"type": "Earning",
|
"type": "Earning",
|
||||||
"max_benefit_amount": 15000,
|
"max_benefit_amount": 15000,
|
||||||
|
"depends_on_payment_days": 1,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if test_tax:
|
||||||
|
data.extend(
|
||||||
|
[
|
||||||
{"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"},
|
{"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if setup or test_tax:
|
if setup or test_tax:
|
||||||
make_salary_component(data, test_tax, company_list)
|
make_salary_component(data, test_tax, company_list)
|
||||||
|
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"salary_component": "Basic Salary",
|
"salary_component": "Basic Salary",
|
||||||
@@ -1415,7 +1432,8 @@ def setup_test():
|
|||||||
|
|
||||||
|
|
||||||
def make_holiday_list(list_name=None, from_date=None, to_date=None):
|
def make_holiday_list(list_name=None, from_date=None, to_date=None):
|
||||||
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
if not (from_date and to_date):
|
||||||
|
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
||||||
name = list_name or "Salary Slip Test Holiday List"
|
name = list_name or "Salary Slip Test Holiday List"
|
||||||
|
|
||||||
frappe.delete_doc_if_exists("Holiday List", name, force=True)
|
frappe.delete_doc_if_exists("Holiday List", name, force=True)
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ def make_salary_slip(
|
|||||||
source_name,
|
source_name,
|
||||||
target_doc=None,
|
target_doc=None,
|
||||||
employee=None,
|
employee=None,
|
||||||
|
posting_date=None,
|
||||||
as_print=False,
|
as_print=False,
|
||||||
print_format=None,
|
print_format=None,
|
||||||
for_preview=0,
|
for_preview=0,
|
||||||
@@ -277,6 +278,9 @@ def make_salary_slip(
|
|||||||
"Department", target.department, "payroll_cost_center"
|
"Department", target.department, "payroll_cost_center"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if posting_date:
|
||||||
|
target.posting_date = posting_date
|
||||||
|
|
||||||
target.run_method("process_salary_structure", for_preview=for_preview)
|
target.run_method("process_salary_structure", for_preview=for_preview)
|
||||||
|
|
||||||
doc = get_mapped_doc(
|
doc = get_mapped_doc(
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ def make_salary_structure(
|
|||||||
company=None,
|
company=None,
|
||||||
currency=erpnext.get_default_currency(),
|
currency=erpnext.get_default_currency(),
|
||||||
payroll_period=None,
|
payroll_period=None,
|
||||||
|
include_flexi_benefits=False,
|
||||||
):
|
):
|
||||||
if test_tax:
|
if test_tax:
|
||||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
||||||
@@ -161,7 +162,10 @@ def make_salary_structure(
|
|||||||
"name": salary_structure,
|
"name": salary_structure,
|
||||||
"company": company or erpnext.get_default_company(),
|
"company": company or erpnext.get_default_company(),
|
||||||
"earnings": make_earning_salary_component(
|
"earnings": make_earning_salary_component(
|
||||||
setup=True, test_tax=test_tax, company_list=["_Test Company"]
|
setup=True,
|
||||||
|
test_tax=test_tax,
|
||||||
|
company_list=["_Test Company"],
|
||||||
|
include_flexi_benefits=include_flexi_benefits,
|
||||||
),
|
),
|
||||||
"deductions": make_deduction_salary_component(
|
"deductions": make_deduction_salary_component(
|
||||||
setup=True, test_tax=test_tax, company_list=["_Test Company"]
|
setup=True, test_tax=test_tax, company_list=["_Test Company"]
|
||||||
@@ -208,9 +212,12 @@ def create_salary_structure_assignment(
|
|||||||
company=None,
|
company=None,
|
||||||
currency=erpnext.get_default_currency(),
|
currency=erpnext.get_default_currency(),
|
||||||
payroll_period=None,
|
payroll_period=None,
|
||||||
|
base=None,
|
||||||
|
allow_duplicate=False,
|
||||||
):
|
):
|
||||||
|
if not allow_duplicate and frappe.db.exists(
|
||||||
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
|
"Salary Structure Assignment", {"employee": employee}
|
||||||
|
):
|
||||||
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee))
|
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee))
|
||||||
|
|
||||||
if not payroll_period:
|
if not payroll_period:
|
||||||
@@ -223,7 +230,7 @@ def create_salary_structure_assignment(
|
|||||||
|
|
||||||
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
|
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
|
||||||
salary_structure_assignment.employee = employee
|
salary_structure_assignment.employee = employee
|
||||||
salary_structure_assignment.base = 50000
|
salary_structure_assignment.base = base or 50000
|
||||||
salary_structure_assignment.variable = 5000
|
salary_structure_assignment.variable = 5000
|
||||||
|
|
||||||
if not from_date:
|
if not from_date:
|
||||||
|
|||||||
@@ -234,7 +234,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "actual_start_date",
|
"fieldname": "actual_start_date",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Date",
|
||||||
"label": "Actual Start Date",
|
"label": "Actual Start Date",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -458,7 +458,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"max_attachments": 4,
|
"max_attachments": 4,
|
||||||
"modified": "2021-04-28 16:36:11.654632",
|
"modified": "2022-05-25 22:45:06.108499",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Project",
|
"name": "Project",
|
||||||
@@ -502,4 +502,4 @@
|
|||||||
"timeline_field": "customer",
|
"timeline_field": "customer",
|
||||||
"title_field": "project_name",
|
"title_field": "project_name",
|
||||||
"track_seen": 1
|
"track_seen": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ class TestTimesheet(unittest.TestCase):
|
|||||||
emp = make_employee("test_employee_6@salary.com")
|
emp = make_employee("test_employee_6@salary.com")
|
||||||
|
|
||||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||||
sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer")
|
sales_invoice = make_sales_invoice(
|
||||||
|
timesheet.name, "_Test Item", "_Test Customer", currency="INR"
|
||||||
|
)
|
||||||
sales_invoice.due_date = nowdate()
|
sales_invoice.due_date = nowdate()
|
||||||
sales_invoice.submit()
|
sales_invoice.submit()
|
||||||
timesheet = frappe.get_doc("Timesheet", timesheet.name)
|
timesheet = frappe.get_doc("Timesheet", timesheet.name)
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
|||||||
me.frm.set_query('supplier_address', erpnext.queries.address_query);
|
me.frm.set_query('supplier_address', erpnext.queries.address_query);
|
||||||
|
|
||||||
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
|
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
|
||||||
|
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||||
|
|
||||||
if(this.frm.fields_dict.supplier) {
|
if(this.frm.fields_dict.supplier) {
|
||||||
this.frm.set_query("supplier", function() {
|
this.frm.set_query("supplier", function() {
|
||||||
|
|||||||
@@ -767,11 +767,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
|||||||
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
|
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
|
||||||
$.each(this.frm.doc['payments'] || [], function(index, data) {
|
$.each(this.frm.doc['payments'] || [], function(index, data) {
|
||||||
if(data.default && payment_status && total_amount_to_pay > 0) {
|
if(data.default && payment_status && total_amount_to_pay > 0) {
|
||||||
let base_amount = flt(total_amount_to_pay, precision("base_amount", data));
|
let base_amount, amount;
|
||||||
|
|
||||||
|
if (me.frm.doc.party_account_currency == me.frm.doc.currency) {
|
||||||
|
// if customer/supplier currency is same as company currency
|
||||||
|
// total_amount_to_pay is already in customer/supplier currency
|
||||||
|
// so base_amount has to be calculated using total_amount_to_pay
|
||||||
|
base_amount = flt(total_amount_to_pay * me.frm.doc.conversion_rate, precision("base_amount", data));
|
||||||
|
amount = flt(total_amount_to_pay, precision("amount", data));
|
||||||
|
} else {
|
||||||
|
base_amount = flt(total_amount_to_pay, precision("base_amount", data));
|
||||||
|
amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
|
||||||
|
}
|
||||||
|
|
||||||
frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount);
|
frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount);
|
||||||
let amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
|
|
||||||
frappe.model.set_value(data.doctype, data.name, "amount", amount);
|
frappe.model.set_value(data.doctype, data.name, "amount", amount);
|
||||||
payment_status = false;
|
payment_status = false;
|
||||||
|
|
||||||
} else if(me.frm.doc.paid_amount) {
|
} else if(me.frm.doc.paid_amount) {
|
||||||
frappe.model.set_value(data.doctype, data.name, "amount", 0.0);
|
frappe.model.set_value(data.doctype, data.name, "amount", 0.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1055,7 +1055,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
} else {
|
} else {
|
||||||
// company currency and doc currency is same
|
// company currency and doc currency is same
|
||||||
// this will prevent unnecessary conversion rate triggers
|
// this will prevent unnecessary conversion rate triggers
|
||||||
this.frm.set_value("conversion_rate", 1.0);
|
if(this.frm.doc.currency === this.get_company_currency()) {
|
||||||
|
this.frm.set_value("conversion_rate", 1.0);
|
||||||
|
} else {
|
||||||
|
this.conversion_rate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ class GSTR3BReport(Document):
|
|||||||
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||||
WHERE p.docstatus = 1 and p.name = i.parent
|
WHERE p.docstatus = 1 and p.name = i.parent
|
||||||
and p.is_opening = 'No'
|
and p.is_opening = 'No'
|
||||||
and p.gst_category != 'Registered Composition'
|
|
||||||
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
|
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
|
||||||
month(p.posting_date) = %s and year(p.posting_date) = %s
|
month(p.posting_date) = %s and year(p.posting_date) = %s
|
||||||
and p.company = %s and p.company_gstin = %s
|
and p.company = %s and p.company_gstin = %s
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
|||||||
|
|
||||||
if (!invoice_eligible) return;
|
if (!invoice_eligible) return;
|
||||||
|
|
||||||
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
|
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, qrcode_image, __unsaved } = frm.doc;
|
||||||
|
|
||||||
const add_custom_button = (label, action) => {
|
const add_custom_button = (label, action) => {
|
||||||
if (!frm.custom_buttons[label]) {
|
if (!frm.custom_buttons[label]) {
|
||||||
@@ -150,52 +150,72 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
|||||||
|
|
||||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||||
const action = () => {
|
const action = () => {
|
||||||
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
|
// This confirm is added to just reduce unnecesory API calls. All required logic is implemented on server side.
|
||||||
message += '<br><br>';
|
frappe.confirm(
|
||||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
__("Have you cancelled e-way bill on the portal?"),
|
||||||
|
() => {
|
||||||
const dialog = frappe.msgprint({
|
frappe.call({
|
||||||
title: __('Update E-Way Bill Cancelled Status?'),
|
method: "erpnext.regional.india.e_invoice.utils.cancel_eway_bill",
|
||||||
message: message,
|
args: { doctype, docname: name },
|
||||||
indicator: 'orange',
|
freeze: true,
|
||||||
primary_action: {
|
callback: () => frm.reload_doc(),
|
||||||
action: function() {
|
});
|
||||||
frappe.call({
|
|
||||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
|
||||||
args: { doctype, docname: name },
|
|
||||||
freeze: true,
|
|
||||||
callback: () => frm.reload_doc() && dialog.hide()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
primary_action_label: __('Yes')
|
() => {
|
||||||
});
|
frappe.show_alert(
|
||||||
|
{
|
||||||
|
message: __(
|
||||||
|
"Please cancel e-way bill on the portal first."
|
||||||
|
),
|
||||||
|
indicator: "orange",
|
||||||
|
},
|
||||||
|
5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (irn && !irn_cancelled) {
|
if (irn && !irn_cancelled) {
|
||||||
const action = () => {
|
let is_qrcode_attached = false;
|
||||||
const dialog = frappe.msgprint({
|
if (qrcode_image && frm.attachments) {
|
||||||
title: __("Generate QRCode"),
|
let attachments = frm.attachments.get_attachments();
|
||||||
message: __("Generate and attach QR Code using IRN?"),
|
if (attachments.length != 0) {
|
||||||
primary_action: {
|
for (let i = 0; i < attachments.length; i++) {
|
||||||
action: function() {
|
if (attachments[i].file_url == qrcode_image) {
|
||||||
frappe.call({
|
is_qrcode_attached = true;
|
||||||
method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
|
break;
|
||||||
args: { doctype, docname: name },
|
|
||||||
freeze: true,
|
|
||||||
callback: () => frm.reload_doc() || dialog.hide(),
|
|
||||||
error: () => dialog.hide()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_qrcode_attached) {
|
||||||
|
const action = () => {
|
||||||
|
if (frm.doc.__unsaved) {
|
||||||
|
frappe.throw(__('Please save the document to generate QRCode.'));
|
||||||
|
}
|
||||||
|
const dialog = frappe.msgprint({
|
||||||
|
title: __("Generate QRCode"),
|
||||||
|
message: __("Generate and attach QR Code using IRN?"),
|
||||||
|
primary_action: {
|
||||||
|
action: function() {
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
|
||||||
|
args: { doctype, docname: name },
|
||||||
|
freeze: true,
|
||||||
|
callback: () => frm.reload_doc() || dialog.hide(),
|
||||||
|
error: () => dialog.hide()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
primary_action_label: __('Yes')
|
primary_action_label: __('Yes')
|
||||||
});
|
});
|
||||||
dialog.show();
|
dialog.show();
|
||||||
};
|
};
|
||||||
add_custom_button(__("Generate QRCode"), action);
|
add_custom_button(__("Generate QRCode"), action);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -802,6 +802,8 @@ class GSPConnector:
|
|||||||
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
||||||
# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
|
# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
|
||||||
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
|
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
|
||||||
|
# ewaybill_details_url + ?irn={irn_number} will provide eway bill number and details.
|
||||||
|
self.ewaybill_details_url = self.base_url + "/enriched/ei/api/ewaybill/irn"
|
||||||
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
|
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
|
||||||
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
|
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
|
||||||
|
|
||||||
@@ -1009,13 +1011,32 @@ class GSPConnector:
|
|||||||
return failed
|
return failed
|
||||||
|
|
||||||
def fetch_and_attach_qrcode_from_irn(self):
|
def fetch_and_attach_qrcode_from_irn(self):
|
||||||
qrcode = self.get_qrcode_from_irn(self.invoice.irn)
|
is_qrcode_file_attached = self.invoice.qrcode_image and frappe.db.exists(
|
||||||
if qrcode:
|
"File",
|
||||||
qrcode_file = self.create_qr_code_file(qrcode)
|
{
|
||||||
frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
|
"attached_to_doctype": "Sales Invoice",
|
||||||
frappe.msgprint(_("QR Code attached to the invoice"), alert=True)
|
"attached_to_name": self.invoice.name,
|
||||||
|
"file_url": self.invoice.qrcode_image,
|
||||||
|
"attached_to_field": "qrcode_image",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not is_qrcode_file_attached:
|
||||||
|
if self.invoice.signed_qr_code:
|
||||||
|
self.attach_qrcode_image()
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Sales Invoice", self.invoice.name, "qrcode_image", self.invoice.qrcode_image
|
||||||
|
)
|
||||||
|
frappe.msgprint(_("QR Code attached to the invoice."), alert=True)
|
||||||
|
else:
|
||||||
|
qrcode = self.get_qrcode_from_irn(self.invoice.irn)
|
||||||
|
if qrcode:
|
||||||
|
qrcode_file = self.create_qr_code_file(qrcode)
|
||||||
|
frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
|
||||||
|
frappe.msgprint(_("QR Code attached to the invoice."), alert=True)
|
||||||
|
else:
|
||||||
|
frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
|
||||||
else:
|
else:
|
||||||
frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
|
frappe.msgprint(_("QR Code is already Attached"), indicator="green", alert=True)
|
||||||
|
|
||||||
def get_qrcode_from_irn(self, irn):
|
def get_qrcode_from_irn(self, irn):
|
||||||
import requests
|
import requests
|
||||||
@@ -1185,23 +1206,22 @@ class GSPConnector:
|
|||||||
log_error(data)
|
log_error(data)
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
def cancel_eway_bill(self, eway_bill, reason, remark):
|
def get_ewb_details(self):
|
||||||
|
"""
|
||||||
|
Get e-Waybill Details by IRN API documentaion for validation is not added yet.
|
||||||
|
https://einv-apisandbox.nic.in/version1.03/get-ewaybill-details-by-irn.html#validations
|
||||||
|
NOTE: if ewaybill Validity period lapsed or scanned by officer enroute (not tested yet) it will still return status as "ACT".
|
||||||
|
"""
|
||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
irn = self.invoice.irn
|
||||||
headers["username"] = headers["user_name"]
|
if not irn:
|
||||||
del headers["user_name"]
|
frappe.throw(_("IRN is mandatory to get E-Waybill Details. Please generate IRN first."))
|
||||||
try:
|
|
||||||
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
|
||||||
if res.get("success"):
|
|
||||||
self.invoice.ewaybill = ""
|
|
||||||
self.invoice.eway_bill_cancelled = 1
|
|
||||||
self.invoice.flags.updater_reference = {
|
|
||||||
"doctype": self.invoice.doctype,
|
|
||||||
"docname": self.invoice.name,
|
|
||||||
"label": _("E-Way Bill Cancelled - {}").format(remark),
|
|
||||||
}
|
|
||||||
self.update_invoice()
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = "?irn={irn}".format(irn=irn)
|
||||||
|
res = self.make_request("get", self.ewaybill_details_url + params, headers)
|
||||||
|
if res.get("success"):
|
||||||
|
return res.get("result")
|
||||||
else:
|
else:
|
||||||
raise RequestFailed
|
raise RequestFailed
|
||||||
|
|
||||||
@@ -1210,9 +1230,65 @@ class GSPConnector:
|
|||||||
self.raise_error(errors=errors)
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log_error(data)
|
log_error()
|
||||||
self.raise_error(True)
|
self.raise_error(True)
|
||||||
|
|
||||||
|
def update_ewb_details(self, ewb_details=None):
|
||||||
|
# for any reason user chooses to generate eway bill using portal this will allow to update ewaybill details in the invoice.
|
||||||
|
if not self.invoice.irn:
|
||||||
|
frappe.throw(_("IRN is mandatory to update E-Waybill Details. Please generate IRN first."))
|
||||||
|
if not ewb_details:
|
||||||
|
ewb_details = self.get_ewb_details()
|
||||||
|
if ewb_details:
|
||||||
|
self.invoice.ewaybill = ewb_details.get("EwbNo")
|
||||||
|
self.invoice.eway_bill_validity = ewb_details.get("EwbValidTill")
|
||||||
|
self.invoice.eway_bill_cancelled = 0 if ewb_details.get("Status") == "ACT" else 1
|
||||||
|
self.update_invoice()
|
||||||
|
|
||||||
|
def cancel_eway_bill(self):
|
||||||
|
ewb_details = self.get_ewb_details()
|
||||||
|
if ewb_details:
|
||||||
|
ewb_no = str(ewb_details.get("EwbNo"))
|
||||||
|
ewb_status = ewb_details.get("Status")
|
||||||
|
if ewb_status == "CNL":
|
||||||
|
self.invoice.ewaybill = ""
|
||||||
|
self.invoice.eway_bill_cancelled = 1
|
||||||
|
self.invoice.flags.updater_reference = {
|
||||||
|
"doctype": self.invoice.doctype,
|
||||||
|
"docname": self.invoice.name,
|
||||||
|
"label": _("E-Way Bill Cancelled"),
|
||||||
|
}
|
||||||
|
self.update_invoice()
|
||||||
|
frappe.msgprint(
|
||||||
|
_("E-Way Bill Cancelled successfully"),
|
||||||
|
indicator="green",
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
elif ewb_status == "ACT" and self.invoice.ewaybill == ewb_no:
|
||||||
|
msg = _("E-Way Bill {} is still active.").format(bold(ewb_no))
|
||||||
|
msg += "<br><br>"
|
||||||
|
msg += _(
|
||||||
|
"You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system."
|
||||||
|
)
|
||||||
|
frappe.msgprint(msg)
|
||||||
|
elif ewb_status == "ACT" and self.invoice.ewaybill != ewb_no:
|
||||||
|
# if user cancelled the current eway bill and generated new eway bill using portal, then this will update new ewb number in sales invoice.
|
||||||
|
msg = _("E-Way Bill No. {0} doesn't match {1} saved in the invoice.").format(
|
||||||
|
bold(ewb_no), bold(self.invoice.ewaybill)
|
||||||
|
)
|
||||||
|
msg += "<hr/>"
|
||||||
|
msg += _("E-Way Bill No. {} is updated in the invoice.").format(bold(ewb_no))
|
||||||
|
frappe.msgprint(msg)
|
||||||
|
self.update_ewb_details(ewb_details=ewb_details)
|
||||||
|
else:
|
||||||
|
# this block should not be ever called but added incase there is any change in API.
|
||||||
|
msg = _("Unknown E-Way Status Code {}.").format(ewb_status)
|
||||||
|
msg += "<br><br>"
|
||||||
|
msg += _("Please contact your system administrator.")
|
||||||
|
frappe.throw(msg)
|
||||||
|
else:
|
||||||
|
frappe.msgprint(_("E-Way Bill Details not found for this IRN."))
|
||||||
|
|
||||||
def sanitize_error_message(self, message):
|
def sanitize_error_message(self, message):
|
||||||
"""
|
"""
|
||||||
On validation errors, response message looks something like this:
|
On validation errors, response message looks something like this:
|
||||||
@@ -1280,7 +1356,6 @@ class GSPConnector:
|
|||||||
|
|
||||||
def attach_qrcode_image(self):
|
def attach_qrcode_image(self):
|
||||||
qrcode = self.invoice.signed_qr_code
|
qrcode = self.invoice.signed_qr_code
|
||||||
|
|
||||||
qr_image = io.BytesIO()
|
qr_image = io.BytesIO()
|
||||||
url = qrcreate(qrcode, error="L")
|
url = qrcreate(qrcode, error="L")
|
||||||
url.png(qr_image, scale=2, quiet_zone=1)
|
url.png(qr_image, scale=2, quiet_zone=1)
|
||||||
@@ -1364,12 +1439,22 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cancel_eway_bill(doctype, docname):
|
def cancel_eway_bill(doctype, docname):
|
||||||
# NOTE: cancel_eway_bill api is disabled by Adequare.
|
# NOTE: cancel_eway_bill api is disabled by NIC for E-invoice so this will only check if eway bill is canceled or not and update accordingly.
|
||||||
# gsp_connector = GSPConnector(doctype, docname)
|
# https://einv-apisandbox.nic.in/version1.03/cancel-eway-bill.html#
|
||||||
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
gsp_connector.cancel_eway_bill()
|
||||||
|
|
||||||
frappe.db.set_value(doctype, docname, "ewaybill", "")
|
|
||||||
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
|
@frappe.whitelist()
|
||||||
|
def get_ewb_details(doctype, docname):
|
||||||
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
gsp_connector.get_ewb_details()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_ewb_details(doctype, docname):
|
||||||
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
gsp_connector.update_ewb_details()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
|
|||||||
'shipping_address': frm.doc.shipping_address || '',
|
'shipping_address': frm.doc.shipping_address || '',
|
||||||
'shipping_address_name': frm.doc.shipping_address_name || '',
|
'shipping_address_name': frm.doc.shipping_address_name || '',
|
||||||
'customer_address': frm.doc.customer_address || '',
|
'customer_address': frm.doc.customer_address || '',
|
||||||
|
'company_address': frm.doc.company_address,
|
||||||
'supplier_address': frm.doc.supplier_address,
|
'supplier_address': frm.doc.supplier_address,
|
||||||
'customer': frm.doc.customer,
|
'customer': frm.doc.customer,
|
||||||
'supplier': frm.doc.supplier,
|
'supplier': frm.doc.supplier,
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
from frappe.utils import cint, cstr, date_diff, flt, getdate, nowdate
|
from frappe.utils import (
|
||||||
|
add_days,
|
||||||
|
cint,
|
||||||
|
cstr,
|
||||||
|
date_diff,
|
||||||
|
flt,
|
||||||
|
get_link_to_form,
|
||||||
|
getdate,
|
||||||
|
month_diff,
|
||||||
|
)
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
|
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
|
||||||
from erpnext.hr.utils import get_salary_assignment
|
from erpnext.hr.utils import get_salary_assignments
|
||||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||||
from erpnext.regional.india import number_state_mapping, state_numbers, states
|
from erpnext.regional.india import number_state_mapping, state_numbers, states
|
||||||
|
|
||||||
@@ -360,45 +370,57 @@ def calculate_annual_eligible_hra_exemption(doc):
|
|||||||
basic_component, hra_component = frappe.db.get_value(
|
basic_component, hra_component = frappe.db.get_value(
|
||||||
"Company", doc.company, ["basic_component", "hra_component"]
|
"Company", doc.company, ["basic_component", "hra_component"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (basic_component and hra_component):
|
if not (basic_component and hra_component):
|
||||||
frappe.throw(_("Please mention Basic and HRA component in Company"))
|
frappe.throw(
|
||||||
annual_exemption, monthly_exemption, hra_amount = 0, 0, 0
|
_("Please set Basic and HRA component in Company {0}").format(
|
||||||
|
get_link_to_form("Company", doc.company)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
annual_exemption = monthly_exemption = hra_amount = basic_amount = 0
|
||||||
|
|
||||||
if hra_component and basic_component:
|
if hra_component and basic_component:
|
||||||
assignment = get_salary_assignment(doc.employee, nowdate())
|
assignments = get_salary_assignments(doc.employee, doc.payroll_period)
|
||||||
if assignment:
|
|
||||||
hra_component_exists = frappe.db.exists(
|
|
||||||
"Salary Detail",
|
|
||||||
{
|
|
||||||
"parent": assignment.salary_structure,
|
|
||||||
"salary_component": hra_component,
|
|
||||||
"parentfield": "earnings",
|
|
||||||
"parenttype": "Salary Structure",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if hra_component_exists:
|
if not assignments and doc.docstatus == 1:
|
||||||
basic_amount, hra_amount = get_component_amt_from_salary_slip(
|
|
||||||
doc.employee, assignment.salary_structure, basic_component, hra_component
|
|
||||||
)
|
|
||||||
if hra_amount:
|
|
||||||
if doc.monthly_house_rent:
|
|
||||||
annual_exemption = calculate_hra_exemption(
|
|
||||||
assignment.salary_structure,
|
|
||||||
basic_amount,
|
|
||||||
hra_amount,
|
|
||||||
doc.monthly_house_rent,
|
|
||||||
doc.rented_in_metro_city,
|
|
||||||
)
|
|
||||||
if annual_exemption > 0:
|
|
||||||
monthly_exemption = annual_exemption / 12
|
|
||||||
else:
|
|
||||||
annual_exemption = 0
|
|
||||||
|
|
||||||
elif doc.docstatus == 1:
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Salary Structure must be submitted before submission of Tax Ememption Declaration")
|
_("Salary Structure must be submitted before submission of {0}").format(doc.doctype)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assignment_dates = [assignment.from_date for assignment in assignments]
|
||||||
|
|
||||||
|
for idx, assignment in enumerate(assignments):
|
||||||
|
if has_hra_component(assignment.salary_structure, hra_component):
|
||||||
|
basic_salary_amt, hra_salary_amt = get_component_amt_from_salary_slip(
|
||||||
|
doc.employee,
|
||||||
|
assignment.salary_structure,
|
||||||
|
basic_component,
|
||||||
|
hra_component,
|
||||||
|
assignment.from_date,
|
||||||
|
)
|
||||||
|
to_date = get_end_date_for_assignment(assignment_dates, idx, doc.payroll_period)
|
||||||
|
|
||||||
|
frequency = frappe.get_value(
|
||||||
|
"Salary Structure", assignment.salary_structure, "payroll_frequency"
|
||||||
|
)
|
||||||
|
basic_amount += get_component_pay(frequency, basic_salary_amt, assignment.from_date, to_date)
|
||||||
|
hra_amount += get_component_pay(frequency, hra_salary_amt, assignment.from_date, to_date)
|
||||||
|
|
||||||
|
if hra_amount:
|
||||||
|
if doc.monthly_house_rent:
|
||||||
|
annual_exemption = calculate_hra_exemption(
|
||||||
|
assignment.salary_structure,
|
||||||
|
basic_amount,
|
||||||
|
hra_amount,
|
||||||
|
doc.monthly_house_rent,
|
||||||
|
doc.rented_in_metro_city,
|
||||||
|
)
|
||||||
|
if annual_exemption > 0:
|
||||||
|
monthly_exemption = annual_exemption / 12
|
||||||
|
else:
|
||||||
|
annual_exemption = 0
|
||||||
|
|
||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
{
|
{
|
||||||
"hra_amount": hra_amount,
|
"hra_amount": hra_amount,
|
||||||
@@ -408,10 +430,44 @@ def calculate_annual_eligible_hra_exemption(doc):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component):
|
def has_hra_component(salary_structure, hra_component):
|
||||||
salary_slip = make_salary_slip(
|
return frappe.db.exists(
|
||||||
salary_structure, employee=employee, for_preview=1, ignore_permissions=True
|
"Salary Detail",
|
||||||
|
{
|
||||||
|
"parent": salary_structure,
|
||||||
|
"salary_component": hra_component,
|
||||||
|
"parentfield": "earnings",
|
||||||
|
"parenttype": "Salary Structure",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_end_date_for_assignment(assignment_dates, idx, payroll_period):
|
||||||
|
end_date = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_date = assignment_dates[idx + 1]
|
||||||
|
end_date = add_days(end_date, -1)
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not end_date:
|
||||||
|
end_date = frappe.db.get_value("Payroll Period", payroll_period, "end_date")
|
||||||
|
|
||||||
|
return end_date
|
||||||
|
|
||||||
|
|
||||||
|
def get_component_amt_from_salary_slip(
|
||||||
|
employee, salary_structure, basic_component, hra_component, from_date
|
||||||
|
):
|
||||||
|
salary_slip = make_salary_slip(
|
||||||
|
salary_structure,
|
||||||
|
employee=employee,
|
||||||
|
for_preview=1,
|
||||||
|
ignore_permissions=True,
|
||||||
|
posting_date=from_date,
|
||||||
|
)
|
||||||
|
|
||||||
basic_amt, hra_amt = 0, 0
|
basic_amt, hra_amt = 0, 0
|
||||||
for earning in salary_slip.earnings:
|
for earning in salary_slip.earnings:
|
||||||
if earning.salary_component == basic_component:
|
if earning.salary_component == basic_component:
|
||||||
@@ -424,36 +480,37 @@ def get_component_amt_from_salary_slip(employee, salary_structure, basic_compone
|
|||||||
|
|
||||||
|
|
||||||
def calculate_hra_exemption(
|
def calculate_hra_exemption(
|
||||||
salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city
|
salary_structure, annual_basic, annual_hra, monthly_house_rent, rented_in_metro_city
|
||||||
):
|
):
|
||||||
# TODO make this configurable
|
# TODO make this configurable
|
||||||
exemptions = []
|
exemptions = []
|
||||||
frequency = frappe.get_value("Salary Structure", salary_structure, "payroll_frequency")
|
|
||||||
# case 1: The actual amount allotted by the employer as the HRA.
|
# case 1: The actual amount allotted by the employer as the HRA.
|
||||||
exemptions.append(get_annual_component_pay(frequency, monthly_hra))
|
exemptions.append(annual_hra)
|
||||||
|
|
||||||
actual_annual_rent = monthly_house_rent * 12
|
|
||||||
annual_basic = get_annual_component_pay(frequency, basic)
|
|
||||||
|
|
||||||
# case 2: Actual rent paid less 10% of the basic salary.
|
# case 2: Actual rent paid less 10% of the basic salary.
|
||||||
|
actual_annual_rent = monthly_house_rent * 12
|
||||||
exemptions.append(flt(actual_annual_rent) - flt(annual_basic * 0.1))
|
exemptions.append(flt(actual_annual_rent) - flt(annual_basic * 0.1))
|
||||||
|
|
||||||
# case 3: 50% of the basic salary, if the employee is staying in a metro city (40% for a non-metro city).
|
# case 3: 50% of the basic salary, if the employee is staying in a metro city (40% for a non-metro city).
|
||||||
exemptions.append(annual_basic * 0.5 if rented_in_metro_city else annual_basic * 0.4)
|
exemptions.append(annual_basic * 0.5 if rented_in_metro_city else annual_basic * 0.4)
|
||||||
|
|
||||||
# return minimum of 3 cases
|
# return minimum of 3 cases
|
||||||
return min(exemptions)
|
return min(exemptions)
|
||||||
|
|
||||||
|
|
||||||
def get_annual_component_pay(frequency, amount):
|
def get_component_pay(frequency, amount, from_date, to_date):
|
||||||
|
days = date_diff(to_date, from_date) + 1
|
||||||
|
|
||||||
if frequency == "Daily":
|
if frequency == "Daily":
|
||||||
return amount * 365
|
return amount * days
|
||||||
elif frequency == "Weekly":
|
elif frequency == "Weekly":
|
||||||
return amount * 52
|
return amount * math.floor(days / 7)
|
||||||
elif frequency == "Fortnightly":
|
elif frequency == "Fortnightly":
|
||||||
return amount * 26
|
return amount * math.floor(days / 14)
|
||||||
elif frequency == "Monthly":
|
elif frequency == "Monthly":
|
||||||
return amount * 12
|
return amount * month_diff(to_date, from_date)
|
||||||
elif frequency == "Bimonthly":
|
elif frequency == "Bimonthly":
|
||||||
return amount * 6
|
return amount * (month_diff(to_date, from_date) / 2)
|
||||||
|
|
||||||
|
|
||||||
def validate_house_rent_dates(doc):
|
def validate_house_rent_dates(doc):
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ class Gstr1Report(object):
|
|||||||
hsn_code = self.item_hsn_map.get(item_code)
|
hsn_code = self.item_hsn_map.get(item_code)
|
||||||
tax_rate = 0
|
tax_rate = 0
|
||||||
taxable_value = items.get(item_code)
|
taxable_value = items.get(item_code)
|
||||||
for rates in hsn_wise_tax_rate.get(hsn_code):
|
for rates in hsn_wise_tax_rate.get(hsn_code, []):
|
||||||
if taxable_value > rates.get("minimum_taxable_value"):
|
if taxable_value > rates.get("minimum_taxable_value"):
|
||||||
tax_rate = rates.get("tax_rate")
|
tax_rate = rates.get("tax_rate")
|
||||||
|
|
||||||
@@ -1156,8 +1156,11 @@ def get_company_gstins(company):
|
|||||||
.inner_join(links)
|
.inner_join(links)
|
||||||
.on(address.name == links.parent)
|
.on(address.name == links.parent)
|
||||||
.select(address.gstin)
|
.select(address.gstin)
|
||||||
|
.distinct()
|
||||||
.where(links.link_doctype == "Company")
|
.where(links.link_doctype == "Company")
|
||||||
.where(links.link_name == company)
|
.where(links.link_name == company)
|
||||||
|
.where(address.gstin.isnotnull())
|
||||||
|
.where(address.gstin != "")
|
||||||
.run(as_dict=1)
|
.run(as_dict=1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -777,6 +777,7 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
|
|
||||||
def test_auto_insert_price(self):
|
def test_auto_insert_price(self):
|
||||||
make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
|
make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
|
||||||
|
make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0})
|
||||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
||||||
|
|
||||||
item_price = frappe.db.get_value(
|
item_price = frappe.db.get_value(
|
||||||
@@ -798,6 +799,25 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
100,
|
100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
make_sales_order(
|
||||||
|
item_code="_Test Item for Auto Price List with Discount Percentage",
|
||||||
|
selling_price_list="_Test Price List",
|
||||||
|
price_list_rate=200,
|
||||||
|
discount_percentage=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.get_value(
|
||||||
|
"Item Price",
|
||||||
|
{
|
||||||
|
"price_list": "_Test Price List",
|
||||||
|
"item_code": "_Test Item for Auto Price List with Discount Percentage",
|
||||||
|
},
|
||||||
|
"price_list_rate",
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
# do not update price list
|
# do not update price list
|
||||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
|
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
|
||||||
|
|
||||||
@@ -1587,7 +1607,9 @@ def make_sales_order(**args):
|
|||||||
"warehouse": args.warehouse,
|
"warehouse": args.warehouse,
|
||||||
"qty": args.qty or 10,
|
"qty": args.qty or 10,
|
||||||
"uom": args.uom or None,
|
"uom": args.uom or None,
|
||||||
"rate": args.rate or 100,
|
"price_list_rate": args.price_list_rate or None,
|
||||||
|
"discount_percentage": args.discount_percentage or None,
|
||||||
|
"rate": args.rate or (None if args.price_list_rate else 100),
|
||||||
"against_blanket_order": args.against_blanket_order,
|
"against_blanket_order": args.against_blanket_order,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -187,8 +187,9 @@ def get_so_with_invoices(filters):
|
|||||||
.on(soi.parent == so.name)
|
.on(soi.parent == so.name)
|
||||||
.join(ps)
|
.join(ps)
|
||||||
.on(ps.parent == so.name)
|
.on(ps.parent == so.name)
|
||||||
|
.select(so.name)
|
||||||
|
.distinct()
|
||||||
.select(
|
.select(
|
||||||
so.name,
|
|
||||||
so.customer,
|
so.customer,
|
||||||
so.transaction_date.as_("submitted"),
|
so.transaction_date.as_("submitted"),
|
||||||
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
|
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, qb
|
||||||
|
from frappe.query_builder import CustomFunction
|
||||||
|
from frappe.query_builder.functions import Max
|
||||||
from frappe.utils import date_diff, flt, getdate
|
from frappe.utils import date_diff, flt, getdate
|
||||||
|
|
||||||
|
|
||||||
@@ -18,11 +20,12 @@ def execute(filters=None):
|
|||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
conditions = get_conditions(filters)
|
conditions = get_conditions(filters)
|
||||||
data = get_data(conditions, filters)
|
data = get_data(conditions, filters)
|
||||||
|
so_elapsed_time = get_so_elapsed_time(data)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return [], [], None, []
|
return [], [], None, []
|
||||||
|
|
||||||
data, chart_data = prepare_data(data, filters)
|
data, chart_data = prepare_data(data, so_elapsed_time, filters)
|
||||||
|
|
||||||
return columns, data, None, chart_data
|
return columns, data, None, chart_data
|
||||||
|
|
||||||
@@ -66,7 +69,6 @@ def get_data(conditions, filters):
|
|||||||
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||||
soi.qty, soi.delivered_qty,
|
soi.qty, soi.delivered_qty,
|
||||||
(soi.qty - soi.delivered_qty) AS pending_qty,
|
(soi.qty - soi.delivered_qty) AS pending_qty,
|
||||||
IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver,
|
|
||||||
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
||||||
soi.base_amount as amount,
|
soi.base_amount as amount,
|
||||||
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||||
@@ -77,13 +79,9 @@ def get_data(conditions, filters):
|
|||||||
soi.description as description
|
soi.description as description
|
||||||
FROM
|
FROM
|
||||||
`tabSales Order` so,
|
`tabSales Order` so,
|
||||||
(`tabSales Order Item` soi
|
`tabSales Order Item` soi
|
||||||
LEFT JOIN `tabSales Invoice Item` sii
|
LEFT JOIN `tabSales Invoice Item` sii
|
||||||
ON sii.so_detail = soi.name and sii.docstatus = 1)
|
ON sii.so_detail = soi.name and sii.docstatus = 1
|
||||||
LEFT JOIN `tabDelivery Note Item` dni
|
|
||||||
on dni.so_detail = soi.name
|
|
||||||
LEFT JOIN `tabDelivery Note` dn
|
|
||||||
on dni.parent = dn.name and dn.docstatus = 1
|
|
||||||
WHERE
|
WHERE
|
||||||
soi.parent = so.name
|
soi.parent = so.name
|
||||||
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
||||||
@@ -101,7 +99,48 @@ def get_data(conditions, filters):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def prepare_data(data, filters):
|
def get_so_elapsed_time(data):
|
||||||
|
"""
|
||||||
|
query SO's elapsed time till latest delivery note
|
||||||
|
"""
|
||||||
|
so_elapsed_time = OrderedDict()
|
||||||
|
if data:
|
||||||
|
sales_orders = [x.sales_order for x in data]
|
||||||
|
|
||||||
|
so = qb.DocType("Sales Order")
|
||||||
|
soi = qb.DocType("Sales Order Item")
|
||||||
|
dn = qb.DocType("Delivery Note")
|
||||||
|
dni = qb.DocType("Delivery Note Item")
|
||||||
|
|
||||||
|
to_seconds = CustomFunction("TO_SECONDS", ["date"])
|
||||||
|
|
||||||
|
query = (
|
||||||
|
qb.from_(so)
|
||||||
|
.inner_join(soi)
|
||||||
|
.on(soi.parent == so.name)
|
||||||
|
.left_join(dni)
|
||||||
|
.on(dni.so_detail == soi.name)
|
||||||
|
.left_join(dn)
|
||||||
|
.on(dni.parent == dn.name)
|
||||||
|
.select(
|
||||||
|
so.name.as_("sales_order"),
|
||||||
|
soi.item_code.as_("so_item_code"),
|
||||||
|
(to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_("elapsed_seconds"),
|
||||||
|
)
|
||||||
|
.where((so.name.isin(sales_orders)) & (dn.docstatus == 1))
|
||||||
|
.orderby(so.name, soi.name)
|
||||||
|
.groupby(soi.name)
|
||||||
|
)
|
||||||
|
dn_elapsed_time = query.run(as_dict=True)
|
||||||
|
|
||||||
|
for e in dn_elapsed_time:
|
||||||
|
key = (e.sales_order, e.so_item_code)
|
||||||
|
so_elapsed_time[key] = e.elapsed_seconds
|
||||||
|
|
||||||
|
return so_elapsed_time
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_data(data, so_elapsed_time, filters):
|
||||||
completed, pending = 0, 0
|
completed, pending = 0, 0
|
||||||
|
|
||||||
if filters.get("group_by_so"):
|
if filters.get("group_by_so"):
|
||||||
@@ -116,6 +155,13 @@ def prepare_data(data, filters):
|
|||||||
row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"])
|
row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"])
|
||||||
|
|
||||||
row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"]
|
row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"]
|
||||||
|
|
||||||
|
row["time_taken_to_deliver"] = (
|
||||||
|
so_elapsed_time.get((row.sales_order, row.item_code))
|
||||||
|
if row["status"] in ("To Bill", "Completed")
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
if filters.get("group_by_so"):
|
if filters.get("group_by_so"):
|
||||||
so_name = row["sales_order"]
|
so_name = row["sales_order"]
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"]
|
|||||||
|
|
||||||
|
|
||||||
class TestSalesOrderAnalysis(FrappeTestCase):
|
class TestSalesOrderAnalysis(FrappeTestCase):
|
||||||
def create_sales_order(self, transaction_date):
|
def create_sales_order(self, transaction_date, do_not_save=False, do_not_submit=False):
|
||||||
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
||||||
so = make_sales_order(
|
so = make_sales_order(
|
||||||
transaction_date=transaction_date,
|
transaction_date=transaction_date,
|
||||||
@@ -24,25 +24,31 @@ class TestSalesOrderAnalysis(FrappeTestCase):
|
|||||||
so.taxes_and_charges = ""
|
so.taxes_and_charges = ""
|
||||||
so.taxes = ""
|
so.taxes = ""
|
||||||
so.items[0].delivery_date = add_days(transaction_date, 15)
|
so.items[0].delivery_date = add_days(transaction_date, 15)
|
||||||
so.save()
|
if not do_not_save:
|
||||||
so.submit()
|
so.save()
|
||||||
|
if not do_not_submit:
|
||||||
|
so.submit()
|
||||||
return item, so
|
return item, so
|
||||||
|
|
||||||
def create_sales_invoice(self, so):
|
def create_sales_invoice(self, so, do_not_save=False, do_not_submit=False):
|
||||||
sinv = make_sales_invoice(so.name)
|
sinv = make_sales_invoice(so.name)
|
||||||
sinv.posting_date = so.transaction_date
|
sinv.posting_date = so.transaction_date
|
||||||
sinv.taxes_and_charges = ""
|
sinv.taxes_and_charges = ""
|
||||||
sinv.taxes = ""
|
sinv.taxes = ""
|
||||||
sinv.insert()
|
if not do_not_save:
|
||||||
sinv.submit()
|
sinv.save()
|
||||||
|
if not do_not_submit:
|
||||||
|
sinv.submit()
|
||||||
return sinv
|
return sinv
|
||||||
|
|
||||||
def create_delivery_note(self, so):
|
def create_delivery_note(self, so, do_not_save=False, do_not_submit=False):
|
||||||
dn = make_delivery_note(so.name)
|
dn = make_delivery_note(so.name)
|
||||||
dn.set_posting_time = True
|
dn.set_posting_time = True
|
||||||
dn.posting_date = add_days(so.transaction_date, 1)
|
dn.posting_date = add_days(so.transaction_date, 1)
|
||||||
dn.save()
|
if not do_not_save:
|
||||||
dn.submit()
|
dn.save()
|
||||||
|
if not do_not_submit:
|
||||||
|
dn.submit()
|
||||||
return dn
|
return dn
|
||||||
|
|
||||||
def test_01_so_to_deliver_and_bill(self):
|
def test_01_so_to_deliver_and_bill(self):
|
||||||
@@ -164,3 +170,85 @@ class TestSalesOrderAnalysis(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
# SO's from first 4 test cases should be in output
|
# SO's from first 4 test cases should be in output
|
||||||
self.assertEqual(len(data), 4)
|
self.assertEqual(len(data), 4)
|
||||||
|
|
||||||
|
def test_06_so_pending_delivery_with_multiple_delivery_notes(self):
|
||||||
|
transaction_date = "2021-06-01"
|
||||||
|
item, so = self.create_sales_order(transaction_date)
|
||||||
|
|
||||||
|
# bill 2 items
|
||||||
|
sinv1 = self.create_sales_invoice(so, do_not_save=True)
|
||||||
|
sinv1.items[0].qty = 2
|
||||||
|
sinv1 = sinv1.save().submit()
|
||||||
|
# deliver 2 items
|
||||||
|
dn1 = self.create_delivery_note(so, do_not_save=True)
|
||||||
|
dn1.items[0].qty = 2
|
||||||
|
dn1 = dn1.save().submit()
|
||||||
|
|
||||||
|
# bill 2 items
|
||||||
|
sinv2 = self.create_sales_invoice(so, do_not_save=True)
|
||||||
|
sinv2.items[0].qty = 2
|
||||||
|
sinv2 = sinv2.save().submit()
|
||||||
|
# deliver 1 item
|
||||||
|
dn2 = self.create_delivery_note(so, do_not_save=True)
|
||||||
|
dn2.items[0].qty = 1
|
||||||
|
dn2 = dn2.save().submit()
|
||||||
|
|
||||||
|
columns, data, message, chart = execute(
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2021-06-01",
|
||||||
|
"to_date": "2021-06-30",
|
||||||
|
"sales_order": [so.name],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expected_value = {
|
||||||
|
"status": "To Deliver and Bill",
|
||||||
|
"sales_order": so.name,
|
||||||
|
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||||
|
"qty": 10,
|
||||||
|
"delivered_qty": 3,
|
||||||
|
"pending_qty": 7,
|
||||||
|
"qty_to_bill": 6,
|
||||||
|
"billed_qty": 4,
|
||||||
|
"time_taken_to_deliver": 0,
|
||||||
|
}
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
for key, val in expected_value.items():
|
||||||
|
with self.subTest(key=key, val=val):
|
||||||
|
self.assertEqual(data[0][key], val)
|
||||||
|
|
||||||
|
def test_07_so_delivered_with_multiple_delivery_notes(self):
|
||||||
|
transaction_date = "2021-06-01"
|
||||||
|
item, so = self.create_sales_order(transaction_date)
|
||||||
|
|
||||||
|
dn1 = self.create_delivery_note(so, do_not_save=True)
|
||||||
|
dn1.items[0].qty = 5
|
||||||
|
dn1 = dn1.save().submit()
|
||||||
|
|
||||||
|
dn2 = self.create_delivery_note(so, do_not_save=True)
|
||||||
|
dn2.items[0].qty = 5
|
||||||
|
dn2 = dn2.save().submit()
|
||||||
|
|
||||||
|
columns, data, message, chart = execute(
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2021-06-01",
|
||||||
|
"to_date": "2021-06-30",
|
||||||
|
"sales_order": [so.name],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expected_value = {
|
||||||
|
"status": "To Bill",
|
||||||
|
"sales_order": so.name,
|
||||||
|
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||||
|
"qty": 10,
|
||||||
|
"delivered_qty": 10,
|
||||||
|
"pending_qty": 0,
|
||||||
|
"qty_to_bill": 10,
|
||||||
|
"billed_qty": 0,
|
||||||
|
"time_taken_to_deliver": 86400,
|
||||||
|
}
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
for key, val in expected_value.items():
|
||||||
|
with self.subTest(key=key, val=val):
|
||||||
|
self.assertEqual(data[0][key], val)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
|||||||
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
|
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
|
||||||
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
|
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
|
||||||
|
|
||||||
|
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||||
|
|
||||||
if(this.frm.fields_dict.selling_price_list) {
|
if(this.frm.fields_dict.selling_price_list) {
|
||||||
this.frm.set_query("selling_price_list", function() {
|
this.frm.set_query("selling_price_list", function() {
|
||||||
|
|||||||
@@ -54,5 +54,35 @@ frappe.ui.form.on("Naming Series", {
|
|||||||
frm.events.get_doc_and_prefix(frm);
|
frm.events.get_doc_and_prefix(frm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
naming_series_to_check(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: "preview_series",
|
||||||
|
doc: frm.doc,
|
||||||
|
callback: function(r) {
|
||||||
|
if (!r.exc) {
|
||||||
|
frm.set_value("preview", r.message);
|
||||||
|
} else {
|
||||||
|
frm.set_value("preview", __("Failed to generate preview of series"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
add_series(frm) {
|
||||||
|
const series = frm.doc.naming_series_to_check;
|
||||||
|
|
||||||
|
if (!series) {
|
||||||
|
frappe.show_alert(__("Please type a valid series."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frm.doc.set_options.includes(series)) {
|
||||||
|
const current_series = frm.doc.set_options;
|
||||||
|
frm.set_value("set_options", `${current_series}\n${series}`);
|
||||||
|
} else {
|
||||||
|
frappe.show_alert(__("Series already added to transaction."));
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,360 +1,132 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
"creation": "2022-05-26 03:12:49.087648",
|
||||||
"allow_import": 0,
|
"description": "Set prefix for numbering series on your transactions",
|
||||||
"allow_rename": 0,
|
"doctype": "DocType",
|
||||||
"beta": 0,
|
"engine": "InnoDB",
|
||||||
"creation": "2013-01-25 11:35:08",
|
"field_order": [
|
||||||
"custom": 0,
|
"setup_series",
|
||||||
"description": "Set prefix for numbering series on your transactions",
|
"select_doc_for_series",
|
||||||
"docstatus": 0,
|
"help_html",
|
||||||
"doctype": "DocType",
|
"naming_series_to_check",
|
||||||
"editable_grid": 0,
|
"preview",
|
||||||
|
"add_series",
|
||||||
|
"set_options",
|
||||||
|
"user_must_always_select",
|
||||||
|
"update",
|
||||||
|
"column_break_13",
|
||||||
|
"update_series",
|
||||||
|
"prefix",
|
||||||
|
"current_value",
|
||||||
|
"update_series_start"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"description": "Set prefix for numbering series on your transactions",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "setup_series",
|
||||||
"bold": 0,
|
"fieldtype": "Section Break",
|
||||||
"collapsible": 0,
|
"label": "Setup Series"
|
||||||
"columns": 0,
|
},
|
||||||
"description": "Set prefix for numbering series on your transactions",
|
|
||||||
"fieldname": "setup_series",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Setup Series",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "select_doc_for_series",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Select",
|
||||||
"bold": 0,
|
"label": "Select Transaction"
|
||||||
"collapsible": 0,
|
},
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "select_doc_for_series",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Select Transaction",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"depends_on": "select_doc_for_series",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "help_html",
|
||||||
"bold": 0,
|
"fieldtype": "HTML",
|
||||||
"collapsible": 0,
|
"label": "Help HTML",
|
||||||
"columns": 0,
|
"options": "<div class=\"well\">\n Edit list of Series in the box below. Rules:\n <ul>\n <li>Each Series Prefix on a new line.</li>\n <li>Allowed special characters are \"/\" and \"-\"</li>\n <li>\n Optionally, set the number of digits in the series using dot (.)\n followed by hashes (#). For example, \".####\" means that the series\n will have four digits. Default is five digits.\n </li>\n <li>\n You can also use variables in the series name by putting them\n between (.) dots\n <br>\n Support Variables:\n <ul>\n <li><code>.YYYY.</code> - Year in 4 digits</li>\n <li><code>.YY.</code> - Year in 2 digits</li>\n <li><code>.MM.</code> - Month</li>\n <li><code>.DD.</code> - Day of month</li>\n <li><code>.WW.</code> - Week of the year</li>\n <li><code>.FY.</code> - Fiscal Year</li>\n <li>\n <code>.{fieldname}.</code> - fieldname on the document e.g.\n <code>branch</code>\n </li>\n </ul>\n </li>\n </ul>\n Examples:\n <ul>\n <li>INV-</li>\n <li>INV-10-</li>\n <li>INVK-</li>\n <li>INV-.YYYY.-.{branch}.-.MM.-.####</li>\n </ul>\n</div>\n<br>\n"
|
||||||
"depends_on": "select_doc_for_series",
|
},
|
||||||
"fieldname": "help_html",
|
|
||||||
"fieldtype": "HTML",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Help HTML",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "<div class=\"well\">\nEdit list of Series in the box below. Rules:\n<ul>\n<li>Each Series Prefix on a new line.</li>\n<li>Allowed special characters are \"/\" and \"-\"</li>\n<li>Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.</li>\n</ul>\nExamples:<br>\nINV-<br>\nINV-10-<br>\nINVK-<br>\nINV-.####<br>\n</div>",
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"depends_on": "select_doc_for_series",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "set_options",
|
||||||
"bold": 0,
|
"fieldtype": "Text",
|
||||||
"collapsible": 0,
|
"label": "Series List for this Transaction"
|
||||||
"columns": 0,
|
},
|
||||||
"depends_on": "select_doc_for_series",
|
|
||||||
"fieldname": "set_options",
|
|
||||||
"fieldtype": "Text",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Series List for this Transaction",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_on_submit": 0,
|
"depends_on": "select_doc_for_series",
|
||||||
"bold": 0,
|
"description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.",
|
||||||
"collapsible": 0,
|
"fieldname": "user_must_always_select",
|
||||||
"columns": 0,
|
"fieldtype": "Check",
|
||||||
"depends_on": "select_doc_for_series",
|
"label": "User must always select"
|
||||||
"description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.",
|
},
|
||||||
"fieldname": "user_must_always_select",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "User must always select",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"depends_on": "select_doc_for_series",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "update",
|
||||||
"bold": 0,
|
"fieldtype": "Button",
|
||||||
"collapsible": 0,
|
"label": "Update"
|
||||||
"columns": 0,
|
},
|
||||||
"depends_on": "select_doc_for_series",
|
|
||||||
"fieldname": "update",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Update",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"description": "Change the starting / current sequence number of an existing series.",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "update_series",
|
||||||
"bold": 0,
|
"fieldtype": "Section Break",
|
||||||
"collapsible": 0,
|
"label": "Update Series"
|
||||||
"columns": 0,
|
},
|
||||||
"description": "Change the starting / current sequence number of an existing series.",
|
|
||||||
"fieldname": "update_series",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Update Series",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "prefix",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Select",
|
||||||
"bold": 0,
|
"label": "Prefix"
|
||||||
"collapsible": 0,
|
},
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "prefix",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Prefix",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"description": "This is the number of the last created transaction with this prefix",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "current_value",
|
||||||
"bold": 0,
|
"fieldtype": "Int",
|
||||||
"collapsible": 0,
|
"label": "Current Value"
|
||||||
"columns": 0,
|
},
|
||||||
"description": "This is the number of the last created transaction with this prefix",
|
|
||||||
"fieldname": "current_value",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Current Value",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "update_series_start",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Button",
|
||||||
"bold": 0,
|
"label": "Update Series Number",
|
||||||
"collapsible": 0,
|
"options": "update_series_start"
|
||||||
"columns": 0,
|
},
|
||||||
"fieldname": "update_series_start",
|
{
|
||||||
"fieldtype": "Button",
|
"fieldname": "naming_series_to_check",
|
||||||
"hidden": 0,
|
"fieldtype": "Data",
|
||||||
"ignore_user_permissions": 0,
|
"label": "Try a naming Series"
|
||||||
"ignore_xss_filter": 0,
|
},
|
||||||
"in_filter": 0,
|
{
|
||||||
"in_global_search": 0,
|
"default": " ",
|
||||||
"in_list_view": 0,
|
"fieldname": "preview",
|
||||||
"in_standard_filter": 0,
|
"fieldtype": "Text",
|
||||||
"label": "Update Series Number",
|
"label": "Preview of generated names",
|
||||||
"length": 0,
|
"read_only": 1
|
||||||
"no_copy": 0,
|
},
|
||||||
"options": "update_series_start",
|
{
|
||||||
"permlevel": 0,
|
"fieldname": "column_break_13",
|
||||||
"print_hide": 0,
|
"fieldtype": "Column Break"
|
||||||
"print_hide_if_no_value": 0,
|
},
|
||||||
"read_only": 0,
|
{
|
||||||
"remember_last_selected_value": 0,
|
"fieldname": "add_series",
|
||||||
"report_hide": 0,
|
"fieldtype": "Button",
|
||||||
"reqd": 0,
|
"label": "Add this Series"
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"hide_toolbar": 1,
|
||||||
"hide_heading": 0,
|
"icon": "fa fa-sort-by-order",
|
||||||
"hide_toolbar": 1,
|
"idx": 1,
|
||||||
"icon": "fa fa-sort-by-order",
|
"issingle": 1,
|
||||||
"idx": 1,
|
"links": [],
|
||||||
"image_view": 0,
|
"modified": "2022-05-26 06:06:42.109504",
|
||||||
"in_create": 0,
|
"modified_by": "Administrator",
|
||||||
"is_submittable": 0,
|
"module": "Setup",
|
||||||
"issingle": 1,
|
"name": "Naming Series",
|
||||||
"istable": 0,
|
"owner": "Administrator",
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2017-08-17 03:41:37.685910",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "Setup",
|
|
||||||
"name": "Naming Series",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
"create": 1,
|
||||||
"apply_user_permissions": 0,
|
"email": 1,
|
||||||
"cancel": 0,
|
"print": 1,
|
||||||
"create": 1,
|
"read": 1,
|
||||||
"delete": 0,
|
"role": "System Manager",
|
||||||
"email": 1,
|
"share": 1,
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 0,
|
|
||||||
"role": "System Manager",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
"read_only": 1,
|
||||||
"read_only": 1,
|
"sort_field": "modified",
|
||||||
"read_only_onload": 0,
|
"sort_order": "DESC",
|
||||||
"show_name_in_global_search": 0,
|
"states": []
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user