Compare commits

..

20 Commits

Author SHA1 Message Date
Anurag Mishra
7539cfecd2 Merge branch 'overtime-feature' of https://github.com/anurag810/erpnext into overtime-feature 2021-08-11 15:18:08 +05:30
Anurag Mishra
18f04111d6 fix: Code optimisation 2021-08-11 15:17:42 +05:30
Anurag Mishra
b2ba9aacb2 Merge branch 'version-13-hotfix' into overtime-feature 2021-08-11 13:32:52 +05:30
Anurag Mishra
4a6694e80a fix: changes requested 2021-08-11 12:59:22 +05:30
Frappe PR Bot
f3ae956eae perf: various minor perf fixes for ledger postings (#26775) (#26896)
* perf: only validate if voucher is journal entry

* perf: optimize merge GLE

- Order fields such that comparison will fail faster
- Break out of loops if not matched

* perf: don't try to match SLE if count mismatch

* refactor: simplify initialize_previous_data

* perf: use cache for fetching valuation_method

These are set only once fields

* refactor: simplify get_future_stock_vouchers

* refactor: simplify get_voucherwise_gl_entries

* perf: fetch only required fields for GL comparison

`select *` fetches all fields, output of this function is only used for
comparing.

* perf: reorder conditions in PL cost center check

* perf: reduce query while validating new gle

* perf: use cache for validating warehouse props

These properties don't change often, no need to query everytime.

* perf: use cached stock settings to validate SLE

* docs: update misleading docstring

Co-authored-by: Marica <maricadsouza221197@gmail.com>
(cherry picked from commit 9152715f90)

Co-authored-by: Ankush <ankush@iwebnotes.com>
2021-08-11 12:18:58 +05:30
Rucha Mahabal
fd325a123c fix(style): apply svg container margin only in desktop view (#26894) 2021-08-10 23:49:56 +05:30
Anurag Mishra
8db493b2be chore: formatting 2021-08-10 12:45:51 +05:30
Anurag Mishra
21bc752da5 fix: Removed print statement 2021-08-10 12:45:51 +05:30
Anurag Mishra
96db78f7bc fix: Test Cases 2021-08-10 12:45:51 +05:30
Anurag Mishra
32f3b24c3a fix: Test Cases 2021-08-10 12:45:51 +05:30
Anurag Mishra
10647bfcd7 fix: test 2021-08-10 12:45:51 +05:30
Anurag Mishra
7d272dc648 fix: test Payroll Entry 2021-08-10 12:45:51 +05:30
Anurag Mishra
b96165883e fix: fixed test and resolved conflicts 2021-08-10 12:45:51 +05:30
Anurag Mishra
5683857f9f fix: sider 2021-08-10 12:45:50 +05:30
Anurag Mishra
73b3121127 fix: some enhancemets and test cases 2021-08-10 12:45:50 +05:30
Anurag Mishra
769d774ccc feat: Validation, sider and form dashbord 2021-08-10 12:45:50 +05:30
Anurag Mishra
da2e95dbcc feat: Overtime based on Attendance and Timesheet 2021-08-10 12:44:50 +05:30
Anurag Mishra
3851d15360 feat:overtime 2021-08-10 12:41:45 +05:30
Anurag Mishra
e4fd6d7763 feat: Overtime 2021-08-10 12:41:45 +05:30
Anurag Mishra
8f1850b21c feat: settings for Overtime 2021-08-10 12:38:33 +05:30
76 changed files with 2077 additions and 912 deletions

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
__version__ = '13.9.0'
__version__ = '13.2.0'
def get_default_company(user=None):
'''Get default company for user'''

View File

@@ -58,8 +58,8 @@ class GLEntry(Document):
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
.format(self.voucher_type, self.voucher_no, self.account))
@@ -73,15 +73,19 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account))
def pl_must_have_cost_center(self):
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
self.voucher_type, self.voucher_no, self.account)
msg += " "
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
self.voucher_type)
"""Validate that profit and loss type account GL entries have a cost center."""
frappe.throw(msg, title=_("Missing Cost Center"))
if self.cost_center or self.voucher_type == 'Period Closing Voucher':
return
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
self.voucher_type, self.voucher_no, self.account)
msg += " "
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
self.voucher_type)
frappe.throw(msg, title=_("Missing Cost Center"))
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")

View File

@@ -100,8 +100,8 @@ def merge_similar_entries(gl_map, precision=None):
return merged_gl_map
def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
'cost_center', 'project', 'voucher_detail_no']
account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher',
'cost_center', 'against_voucher_type', 'party_type', 'project']
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions
@@ -110,10 +110,12 @@ def check_if_in_list(gle, gl_map, dimensions=None):
same_head = True
if e.account != gle.account:
same_head = False
continue
for fieldname in account_head_fieldnames:
if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
same_head = False
break
if same_head:
return e
@@ -143,16 +145,19 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
validate_expense_against_budget(args)
def validate_cwip_accounts(gl_map):
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
"""Validate that CWIP account are not used in Journal Entry"""
if gl_map and gl_map[0].voucher_type != "Journal Entry":
return
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
where account_type = 'Capital Work in Progress' and is_group=0""")]
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting"))
if cwip_enabled:
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
where account_type = 'Capital Work in Progress' and is_group=0""")]
for entry in gl_map:
if entry.account in cwip_accounts:
frappe.throw(
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
for entry in gl_map:
if entry.account in cwip_accounts:
frappe.throw(
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
def round_off_debit_credit(gl_map):
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),

View File

@@ -38,8 +38,8 @@ def execute(filters=None):
GROUP BY parent''',{'dimension':[dimension]})
if DCC_allocation:
filters['budget_against_filter'] = [DCC_allocation[0][0]]
ddc_cam_map = get_dimension_account_month_map(filters)
dimension_items = ddc_cam_map.get(DCC_allocation[0][0])
cam_map = get_dimension_account_month_map(filters)
dimension_items = cam_map.get(DCC_allocation[0][0])
if dimension_items:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
@@ -48,6 +48,7 @@ def execute(filters=None):
return columns, data, None, chart
def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation):
for account, monthwise_data in iteritems(dimension_items):
row = [dimension, account]
totals = [0, 0, 0]

View File

@@ -920,7 +920,6 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa
_delete_gl_entries(voucher_type, voucher_no)
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
future_stock_vouchers = []
values = []
condition = ""
@@ -936,30 +935,46 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
condition += " and company = %s"
values.append(company)
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
future_stock_vouchers = frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
and is_cancelled = 0
{condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
tuple([posting_date, posting_time] + values), as_dict=True)
return future_stock_vouchers
return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers]
def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
""" Get voucherwise list of GL entries.
Only fetches GLE fields required for comparing with new GLE.
Check compare_existing_and_expected_gle function below.
"""
gl_entries = {}
if future_stock_vouchers:
for d in frappe.db.sql("""select * from `tabGL Entry`
where posting_date >= %s and voucher_no in (%s)""" %
('%s', ', '.join(['%s']*len(future_stock_vouchers))),
tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1):
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
if not future_stock_vouchers:
return gl_entries
voucher_nos = [d[1] for d in future_stock_vouchers]
gles = frappe.db.sql("""
select name, account, credit, debit, cost_center, project
from `tabGL Entry`
where
posting_date >= %s and voucher_no in (%s)""" %
('%s', ', '.join(['%s'] * len(voucher_nos))),
tuple([posting_date] + voucher_nos), as_dict=1)
for d in gles:
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries
def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
if len(existing_gle) != len(expected_gle):
return False
matched = True
for entry in expected_gle:
account_existed = False

View File

@@ -60,23 +60,10 @@ frappe.ui.form.on("Supplier", {
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
}, __('Create'));
frm.add_custom_button(__('Get Supplier Group Details'), function () {
frm.trigger("get_supplier_group_details");
}, __('Actions'));
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
},
get_supplier_group_details: function(frm) {
frappe.call({
method: "get_supplier_group_details",
doc: frm.doc,
callback: function() {
frm.refresh();
}
});
},
is_internal_supplier: function(frm) {
if (frm.doc.is_internal_supplier == 1) {

View File

@@ -51,23 +51,6 @@ class Supplier(TransactionBase):
validate_party_accounts(self)
self.validate_internal_supplier()
@frappe.whitelist()
def get_supplier_group_details(self):
doc = frappe.get_doc('Supplier Group', self.supplier_group)
self.payment_terms = ""
self.accounts = []
if doc.accounts:
for account in doc.accounts:
child = self.append('accounts')
child.company = account.company
child.account = account.account
if doc.payment_terms:
self.payment_terms = doc.payment_terms
self.save()
def validate_internal_supplier(self):
internal_supplier = frappe.db.get_value("Supplier",
{"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
@@ -103,4 +86,4 @@ class Supplier(TransactionBase):
create_contact(supplier, 'Supplier',
doc.name, args.get('supplier_email_' + str(i)))
except frappe.NameError:
pass
pass

View File

@@ -13,30 +13,6 @@ test_records = frappe.get_test_records('Supplier')
class TestSupplier(unittest.TestCase):
def test_get_supplier_group_details(self):
doc = frappe.new_doc("Supplier Group")
doc.supplier_group_name = "_Testing Supplier Group"
doc.payment_terms = "_Test Payment Term Template 3"
doc.accounts = []
test_account_details = {
"company": "_Test Company",
"account": "Creditors - _TC",
}
doc.append("accounts", test_account_details)
doc.save()
s_doc = frappe.new_doc("Supplier")
s_doc.supplier_name = "Testing Supplier"
s_doc.supplier_group = "_Testing Supplier Group"
s_doc.payment_terms = ""
s_doc.accounts = []
s_doc.insert()
s_doc.get_supplier_group_details()
self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
self.assertEqual(s_doc.accounts[0].company, "_Test Company")
self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
s_doc.delete()
doc.delete()
def test_supplier_default_payment_terms(self):
# Payment Term based on Days after invoice date
frappe.db.set_value(
@@ -160,4 +136,4 @@ def create_supplier(**args):
return doc
except frappe.DuplicateEntryError:
return frappe.get_doc("Supplier", args.supplier_name)
return frappe.get_doc("Supplier", args.supplier_name)

View File

@@ -1,73 +0,0 @@
# Version 13.3.0 Release Notes
### Features & Enhancements
- Purchase receipt creation from purchase invoice ([#25126](https://github.com/frappe/erpnext/pull/25126))
- New Document Transaction Deletion ([#25354](https://github.com/frappe/erpnext/pull/25354))
- Employee Referral ([#24997](https://github.com/frappe/erpnext/pull/24997))
- Add Create Expense Claim button in Delivery Trip ([#25526](https://github.com/frappe/erpnext/pull/25526))
- Reduced rate of asset depreciation as per IT Act ([#25648](https://github.com/frappe/erpnext/pull/25648))
- Improve DATEV export ([#25238](https://github.com/frappe/erpnext/pull/25238))
- Add pick batch button ([#25413](https://github.com/frappe/erpnext/pull/25413))
- Enable custom field search on POS ([#25421](https://github.com/frappe/erpnext/pull/25421))
- New check field in subscriptions for (not) submitting invoices ([#25394](https://github.com/frappe/erpnext/pull/25394))
- Show POS reserved stock in stock projected qty report ([#25593](https://github.com/frappe/erpnext/pull/25593))
- e-way bill validity field ([#25555](https://github.com/frappe/erpnext/pull/25555))
- Significant reduction in time taken to save sales documents ([#25475](https://github.com/frappe/erpnext/pull/25475))
### Fixes
- Bank statement import via google sheet ([#25677](https://github.com/frappe/erpnext/pull/25677))
- Invoices not getting fetched during payment reconciliation ([#25598](https://github.com/frappe/erpnext/pull/25598))
- Error on applying TDS without party ([#25632](https://github.com/frappe/erpnext/pull/25632))
- Allow to cancel loan with cancelled repayment entry ([#25507](https://github.com/frappe/erpnext/pull/25507))
- Can't open general ledger from consolidated financial report ([#25542](https://github.com/frappe/erpnext/pull/25542))
- Add 'Partially Received' to Status drop-down list in Material Request ([#24857](https://github.com/frappe/erpnext/pull/24857))
- Updated item filters for material request ([#25531](https://github.com/frappe/erpnext/pull/25531))
- Added validation in stock entry to check duplicate serial nos ([#25611](https://github.com/frappe/erpnext/pull/25611))
- Update shopify api version ([#25600](https://github.com/frappe/erpnext/pull/25600))
- Dialog variable assignment after definition in POS ([#25680](https://github.com/frappe/erpnext/pull/25680))
- Added tax_types list ([#25587](https://github.com/frappe/erpnext/pull/25587))
- Include search fields in Project Link field query ([#25505](https://github.com/frappe/erpnext/pull/25505))
- Item stock levels displaying inconsistently ([#25506](https://github.com/frappe/erpnext/pull/25506))
- Change today to now to get data for reposting ([#25703](https://github.com/frappe/erpnext/pull/25703))
- Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet ([#25700](https://github.com/frappe/erpnext/pull/25700))
- Minor fixes in loan ([#25546](https://github.com/frappe/erpnext/pull/25546))
- Fieldname when updating docfield property ([#25516](https://github.com/frappe/erpnext/pull/25516))
- Use get_serial_nos for splitting ([#25590](https://github.com/frappe/erpnext/pull/25590))
- Show item's full name on hover over item in POS ([#25554](https://github.com/frappe/erpnext/pull/25554))
- Stock ledger entry created against draft stock entry ([#25540](https://github.com/frappe/erpnext/pull/25540))
- Incorrect expense account set in pos invoice ([#25543](https://github.com/frappe/erpnext/pull/25543))
- Stock balance and batch-wise balance history report showing different closing stock ([#25575](https://github.com/frappe/erpnext/pull/25575))
- Make strings translatable ([#25521](https://github.com/frappe/erpnext/pull/25521))
- Serial no changed after saving stock reconciliation ([#25541](https://github.com/frappe/erpnext/pull/25541))
- Ignore fraction difference while making round off gl entry ([#25438](https://github.com/frappe/erpnext/pull/25438))
- Sync shopify customer addresses ([#25481](https://github.com/frappe/erpnext/pull/25481))
- Total stock summary report not working ([#25551](https://github.com/frappe/erpnext/pull/25551))
- Rename field has not updated value of deposit and withdrawal fields ([#25545](https://github.com/frappe/erpnext/pull/25545))
- Unexpected keyword argument 'merge_logs' ([#25489](https://github.com/frappe/erpnext/pull/25489))
- Validation message of quality inspection in purchase receipt ([#25667](https://github.com/frappe/erpnext/pull/25667))
- Added is_stock_item filter ([#25530](https://github.com/frappe/erpnext/pull/25530))
- Fetch total stock at company in PO ([#25532](https://github.com/frappe/erpnext/pull/25532))
- Updated filters for process statement of accounts ([#25384](https://github.com/frappe/erpnext/pull/25384))
- Incorrect expense account set in pos invoice ([#25571](https://github.com/frappe/erpnext/pull/25571))
- Client script breaking while settings tax labels ([#25653](https://github.com/frappe/erpnext/pull/25653))
- Empty payment term column in accounts receivable report ([#25556](https://github.com/frappe/erpnext/pull/25556))
- Designation insufficient permission on lead doctype. ([#25331](https://github.com/frappe/erpnext/pull/25331))
- Force https for shopify webhook registration ([#25630](https://github.com/frappe/erpnext/pull/25630))
- Patch regional fields for old companies ([#25673](https://github.com/frappe/erpnext/pull/25673))
- Woocommerce order sync issue ([#25692](https://github.com/frappe/erpnext/pull/25692))
- Allow to receive same serial numbers multiple times ([#25471](https://github.com/frappe/erpnext/pull/25471))
- Update Allocated amount after Paid Amount is changed in PE ([#25515](https://github.com/frappe/erpnext/pull/25515))
- Updating Standard Notification's channel field ([#25564](https://github.com/frappe/erpnext/pull/25564))
- Report summary showing inflated values when values are accumulated in Group Company ([#25577](https://github.com/frappe/erpnext/pull/25577))
- UI fixes related to overflowing payment section ([#25652](https://github.com/frappe/erpnext/pull/25652))
- List invoices in Payment Reconciliation Payment ([#25524](https://github.com/frappe/erpnext/pull/25524))
- Ageing errors in PSOA ([#25490](https://github.com/frappe/erpnext/pull/25490))
- Prevent spurious defaults for items when making prec from dnote ([#25559](https://github.com/frappe/erpnext/pull/25559))
- Stock reconciliation getting time out error during submission ([#25557](https://github.com/frappe/erpnext/pull/25557))
- Timesheet filter date exclusive issue ([#25626](https://github.com/frappe/erpnext/pull/25626))
- Update cost center in the item table fetched from POS Profile ([#25609](https://github.com/frappe/erpnext/pull/25609))
- Updated modified time in purchase invoice to pull new fields ([#25678](https://github.com/frappe/erpnext/pull/25678))
- Stock and Accounts Settings form refactor ([#25534](https://github.com/frappe/erpnext/pull/25534))
- Payment amount showing in foreign currency ([#25292](https://github.com/frappe/erpnext/pull/25292))

View File

@@ -1,54 +0,0 @@
# Version 13.4.0 Release Notes
### Features & Enhancements
- Multiple GST enhancement and fixes ([#25249](https://github.com/frappe/erpnext/pull/25249))
- Linking supplier with an item group for filtering items ([#25683](https://github.com/frappe/erpnext/pull/25683))
- Leave Policy Assignment Refactor ([#24327](https://github.com/frappe/erpnext/pull/24327))
- Dimension-wise Accounts Balance Report ([#25260](https://github.com/frappe/erpnext/pull/25260))
- Show net values in Party Accounts ([#25714](https://github.com/frappe/erpnext/pull/25714))
- Add pending qty section to batch/serial selector dialog ([#25519](https://github.com/frappe/erpnext/pull/25519))
- enhancements in Training Event ([#25782](https://github.com/frappe/erpnext/pull/25782))
- Refactored timesheet ([#25701](https://github.com/frappe/erpnext/pull/25701))
### Fixes
- Process Statement of Accounts formatting ([#25777](https://github.com/frappe/erpnext/pull/25777))
- Removed serial no validation for sales invoice ([#25817](https://github.com/frappe/erpnext/pull/25817))
- Fetch email id from dialog box in pos past order summary ([#25808](https://github.com/frappe/erpnext/pull/25808))
- Don't map set warehouse from delivery note to purchase receipt ([#25672](https://github.com/frappe/erpnext/pull/25672))
- Apply permission while selecting projects ([#25765](https://github.com/frappe/erpnext/pull/25765))
- Error on adding bank account to plaid ([#25658](https://github.com/frappe/erpnext/pull/25658))
- Set disable rounded total if it is globally enabled ([#25789](https://github.com/frappe/erpnext/pull/25789))
- Wrong amount on CR side in general ledger report for customer when different account currencies are involved ([#25654](https://github.com/frappe/erpnext/pull/25654))
- Stock move dialog duplicate submit actions (V13) ([#25486](https://github.com/frappe/erpnext/pull/25486))
- Cashflow mapper not showing data ([#25815](https://github.com/frappe/erpnext/pull/25815))
- Ignore rounding diff while importing JV using data import ([#25816](https://github.com/frappe/erpnext/pull/25816))
- Woocommerce order sync issue ([#25688](https://github.com/frappe/erpnext/pull/25688))
- Expected amount in pos closing payments table ([#25737](https://github.com/frappe/erpnext/pull/25737))
- Show only company addresses for ITC reversal entry ([#25867](https://github.com/frappe/erpnext/pull/25867))
- Timeout error while loading warehouse tree ([#25694](https://github.com/frappe/erpnext/pull/25694))
- Plaid Withdrawals and Deposits are recorded incorrectly ([#25784](https://github.com/frappe/erpnext/pull/25784))
- Return case for item with available qty equal to one ([#25760](https://github.com/frappe/erpnext/pull/25760))
- The status of repost item valuation showing In Progress since long time ([#25754](https://github.com/frappe/erpnext/pull/25754))
- Updated applicable charges form in landed cost voucher ([#25732](https://github.com/frappe/erpnext/pull/25732))
- Rearrange buttons for Company DocType ([#25617](https://github.com/frappe/erpnext/pull/25617))
- Show uom for item in selector dialog ([#25697](https://github.com/frappe/erpnext/pull/25697))
- Warehouse not found in stock entry ([#25776](https://github.com/frappe/erpnext/pull/25776))
- Use dictionary filter instead of list (bp #25874 pre-release) ([#25875](https://github.com/frappe/erpnext/pull/25875))
- Send emails on rfq submit ([#25695](https://github.com/frappe/erpnext/pull/25695))
- Cannot bypass e-invoicing for non gst item invoices ([#25759](https://github.com/frappe/erpnext/pull/25759))
- Validation message of quality inspection in purchase receipt ([#25666](https://github.com/frappe/erpnext/pull/25666))
- Dialog variable assignment after definition in POS ([#25681](https://github.com/frappe/erpnext/pull/25681))
- Wrong quantity after transaction for parallel stock transactions ([#25779](https://github.com/frappe/erpnext/pull/25779))
- Item Variant Details Report ([#25797](https://github.com/frappe/erpnext/pull/25797))
- Duplicate stock entry on multiple click ([#25742](https://github.com/frappe/erpnext/pull/25742))
- Bank statement import via google sheet ([#25676](https://github.com/frappe/erpnext/pull/25676))
- Change today to now to get data for reposting ([#25702](https://github.com/frappe/erpnext/pull/25702))
- Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet ([#25698](https://github.com/frappe/erpnext/pull/25698))
- Ageing error in PSOA ([#25857](https://github.com/frappe/erpnext/pull/25857))
- Breaking cost center validation ([#25660](https://github.com/frappe/erpnext/pull/25660))
- Project filter for Kanban Board ([#25744](https://github.com/frappe/erpnext/pull/25744))
- Show allow zero valuation only when auto checked ([#25778](https://github.com/frappe/erpnext/pull/25778))
- Missing cost center message on creating gl entries ([#25755](https://github.com/frappe/erpnext/pull/25755))
- Address template with upper filter throws jinja error ([#25756](https://github.com/frappe/erpnext/pull/25756))

View File

@@ -1,54 +0,0 @@
# Version 13.5.0 Release Notes
### Features & Enhancements
- Tax deduction against advance payments ([#25831](https://github.com/frappe/erpnext/pull/25831))
- Cost-center wise period closing entry ([#25766](https://github.com/frappe/erpnext/pull/25766))
- Create Quality Inspections from account and stock documents ([#25221](https://github.com/frappe/erpnext/pull/25221))
- Item Taxes based on net rate ([#25961](https://github.com/frappe/erpnext/pull/25961))
- Enable/disable gl entry posting for change given in pos ([#25822](https://github.com/frappe/erpnext/pull/25822))
- Add Inactive status to Employee ([#26029](https://github.com/frappe/erpnext/pull/26029))
- Added check box to combine items with same BOM ([#25478](https://github.com/frappe/erpnext/pull/25478))
- Item Tax Templates for Germany ([#25858](https://github.com/frappe/erpnext/pull/25858))
- Refactored leave balance report ([#25771](https://github.com/frappe/erpnext/pull/25771))
- Refactored Vehicle Expenses Report ([#25727](https://github.com/frappe/erpnext/pull/25727))
- Refactored maintenance schedule and visit document ([#25358](https://github.com/frappe/erpnext/pull/25358))
### Fixes
- Cannot add same item with different rates ([#25849](https://github.com/frappe/erpnext/pull/25849))
- Show only company addresses for ITC reversal entry ([#25866](https://github.com/frappe/erpnext/pull/25866))
- Hiding Rounding Adjustment field ([#25380](https://github.com/frappe/erpnext/pull/25380))
- Auto tax calculations in Payment Entry ([#26055](https://github.com/frappe/erpnext/pull/26055))
- Not able to select the item code in work order ([#25915](https://github.com/frappe/erpnext/pull/25915))
- Cannot reset plaid link for a bank account ([#25869](https://github.com/frappe/erpnext/pull/25869))
- Student invalid password reset link ([#25826](https://github.com/frappe/erpnext/pull/25826))
- Multiple pos issues ([#25928](https://github.com/frappe/erpnext/pull/25928))
- Add Product Bundles to POS ([#25860](https://github.com/frappe/erpnext/pull/25860))
- Enable Parallel tests ([#25862](https://github.com/frappe/erpnext/pull/25862))
- Service item check on e-Invoicing ([#25986](https://github.com/frappe/erpnext/pull/25986))
- Choose correct Salary Structure Assignment when getting data for formula eval ([#25981](https://github.com/frappe/erpnext/pull/25981))
- Ignore internal transfer invoices from GST Reports ([#25969](https://github.com/frappe/erpnext/pull/25969))
- Taxable value for invoices with additional discount ([#26056](https://github.com/frappe/erpnext/pull/26056))
- Validate negative allocated amount in Payment Entry ([#25799](https://github.com/frappe/erpnext/pull/25799))
- Allow all System Managers to delete company transactions ([#25834](https://github.com/frappe/erpnext/pull/25834))
- Wrong round off gl entry posted in case of purchase invoice ([#25775](https://github.com/frappe/erpnext/pull/25775))
- Use dictionary filter instead of list ([#25874](https://github.com/frappe/erpnext/pull/25874))
- Ageing error in PSOA ([#25855](https://github.com/frappe/erpnext/pull/25855))
- On click of duplicate button system has not copied the difference account ([#25988](https://github.com/frappe/erpnext/pull/25988))
- Assign Product Bundle's conversion_factor to Pack… ([#25840](https://github.com/frappe/erpnext/pull/25840))
- Rename Loan Management workspace to Loans ([#25856](https://github.com/frappe/erpnext/pull/25856))
- Fix stock quantity calculation when negative_stock_allowe… ([#25859](https://github.com/frappe/erpnext/pull/25859))
- Update cost center from pos profile ([#25971](https://github.com/frappe/erpnext/pull/25971))
- Ensure website theme is applied correctly ([#25863](https://github.com/frappe/erpnext/pull/25863))
- Only display GST card in Accounting Workspace if it's in India ([#26000](https://github.com/frappe/erpnext/pull/26000))
- Incorrect gstin fetched incase of branch company address ([#25841](https://github.com/frappe/erpnext/pull/25841))
- Sort account balances by account name ([#26009](https://github.com/frappe/erpnext/pull/26009))
- Custom conversion factor field not mapped from job card to stock entry ([#25956](https://github.com/frappe/erpnext/pull/25956))
- Chart of accounts importer always error ([#25882](https://github.com/frappe/erpnext/pull/25882))
- Create POS Invoice for Product Bundles ([#25847](https://github.com/frappe/erpnext/pull/25847))
- Wrap dates in getdate for leave application ([#25899](https://github.com/frappe/erpnext/pull/25899))
- Closing entry shows incorrect expected amount ([#25868](https://github.com/frappe/erpnext/pull/25868))
- Add Hold status column in the Issue Summary Report ([#25828](https://github.com/frappe/erpnext/pull/25828))
- Rendering of broken image on pos ([#25872](https://github.com/frappe/erpnext/pull/25872))
- Timeout error in the repost item valuation ([#25854](https://github.com/frappe/erpnext/pull/25854))

View File

@@ -1,72 +0,0 @@
# Version 13.6.0 Release Notes
### Features & Enhancements
- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523))
- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044))
- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184))
- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878))
- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705))
- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030))
- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696))
- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891))
### Fixes
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176))
- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092))
- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978))
- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073))
- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245))
- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230))
- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125))
- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134))
- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196))
- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083))
- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941))
- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945))
- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011))
- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070))
- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071))
- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122))
- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220))
- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003))
- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229))
- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269))
- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045))
- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170))
- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032))
- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095))
- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023))
- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191))
- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188))
- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217))
- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152))
- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108))
- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202))
- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906))
- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894))
- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997))
- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051))
- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043))
- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143))
- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211))
- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126))
- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192))
- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081))
- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187))
- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195))
- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947))
- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951))
- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968))
- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037))
- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198))
- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100))
- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098))
- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062))
- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031))
- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203))
- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185))
- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934))
- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201))

View File

@@ -1,69 +0,0 @@
# Version 13.7.0 Release Notes
### Features & Enhancements
- Optionally allow rejected quality inspection on submission ([#26133](https://github.com/frappe/erpnext/pull/26133))
- Bootstrapped GST Setup for India ([#25415](https://github.com/frappe/erpnext/pull/25415))
- Fetching details from supplier/customer groups ([#26454](https://github.com/frappe/erpnext/pull/26454))
- Provision to make subcontracted purchase order from the production plan ([#26240](https://github.com/frappe/erpnext/pull/26240))
- Optimized code for reposting item valuation ([#26432](https://github.com/frappe/erpnext/pull/26432))
### Fixes
- Auto process deferred accounting for multi-company setup ([#26277](https://github.com/frappe/erpnext/pull/26277))
- Error while fetching item taxes ([#26218](https://github.com/frappe/erpnext/pull/26218))
- Validation check for batch for stock reconciliation type in stock entry(bp #26370 ) ([#26488](https://github.com/frappe/erpnext/pull/26488))
- Error popup for COA errors ([#26358](https://github.com/frappe/erpnext/pull/26358))
- Precision for expected values in payment entry test ([#26394](https://github.com/frappe/erpnext/pull/26394))
- Bank statement import ([#26287](https://github.com/frappe/erpnext/pull/26287))
- LMS progress issue ([#26253](https://github.com/frappe/erpnext/pull/26253))
- Paging buttons not working on item group portal page ([#26497](https://github.com/frappe/erpnext/pull/26497))
- Omit item discount amount for e-invoicing ([#26353](https://github.com/frappe/erpnext/pull/26353))
- Validate LCV for Invoices without Update Stock ([#26333](https://github.com/frappe/erpnext/pull/26333))
- Remove cancelled entries in consolidated financial statements ([#26331](https://github.com/frappe/erpnext/pull/26331))
- Fetching employee in payroll entry ([#26271](https://github.com/frappe/erpnext/pull/26271))
- To fetch the correct field in Tax Rule ([#25927](https://github.com/frappe/erpnext/pull/25927))
- Order and time of operations in multilevel BOM work order ([#25886](https://github.com/frappe/erpnext/pull/25886))
- Fixed Budget Variance Graph color from all black to default ([#26368](https://github.com/frappe/erpnext/pull/26368))
- TDS computation summary shows cancelled invoices (#26456) ([#26486](https://github.com/frappe/erpnext/pull/26486))
- Do not consider cancelled entries in party dashboard ([#26231](https://github.com/frappe/erpnext/pull/26231))
- Add validation for 'for_qty' else throws errors ([#25829](https://github.com/frappe/erpnext/pull/25829))
- Move the rename abbreviation job to long queue (#26434) ([#26462](https://github.com/frappe/erpnext/pull/26462))
- Query for Training Event ([#26388](https://github.com/frappe/erpnext/pull/26388))
- Item group portal issues (backport) ([#26493](https://github.com/frappe/erpnext/pull/26493))
- When lead is created with mobile_no, mobile_no value gets lost ([#26298](https://github.com/frappe/erpnext/pull/26298))
- WIP needs to be set before submit on skip_transfer (bp #26499) ([#26507](https://github.com/frappe/erpnext/pull/26507))
- Incorrect valuation rate in stock reconciliation ([#26259](https://github.com/frappe/erpnext/pull/26259))
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
- Changed profitability analysis report width ([#26165](https://github.com/frappe/erpnext/pull/26165))
- Unable to download GSTR-1 json ([#26468](https://github.com/frappe/erpnext/pull/26468))
- Unallocated amount in Payment Entry after taxes ([#26472](https://github.com/frappe/erpnext/pull/26472))
- Include Stock Reco logic in `update_qty_in_future_sle` ([#26158](https://github.com/frappe/erpnext/pull/26158))
- Update cost not working in the draft BOM ([#26279](https://github.com/frappe/erpnext/pull/26279))
- Cancellation of Loan Security Pledges ([#26252](https://github.com/frappe/erpnext/pull/26252))
- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26405](https://github.com/frappe/erpnext/pull/26405))
- Delete accounts (an empty file) ([#25323](https://github.com/frappe/erpnext/pull/25323))
- Errors on parallel requests creation of company for India ([#26470](https://github.com/frappe/erpnext/pull/26470))
- Incorrect bom no added for non-variant items on variant boms ([#26320](https://github.com/frappe/erpnext/pull/26320))
- Incorrect discount amount on amended document ([#26466](https://github.com/frappe/erpnext/pull/26466))
- Added a message to enable appointment booking if disabled ([#26334](https://github.com/frappe/erpnext/pull/26334))
- fix(pos): taxes amount in pos item cart ([#26411](https://github.com/frappe/erpnext/pull/26411))
- Track changes on batch ([#26382](https://github.com/frappe/erpnext/pull/26382))
- Stock entry with putaway rule not working ([#26350](https://github.com/frappe/erpnext/pull/26350))
- Only "Tax" type accounts should be shown for selection in GST Settings ([#26300](https://github.com/frappe/erpnext/pull/26300))
- Added permission for employee to book appointment ([#26255](https://github.com/frappe/erpnext/pull/26255))
- Allow to make job card without employee ([#26312](https://github.com/frappe/erpnext/pull/26312))
- Project Portal Enhancements ([#26290](https://github.com/frappe/erpnext/pull/26290))
- BOM stock report not working ([#26332](https://github.com/frappe/erpnext/pull/26332))
- Order Items by weightage in the web items query ([#26284](https://github.com/frappe/erpnext/pull/26284))
- Removed values out of sync validation from stock transactions ([#26226](https://github.com/frappe/erpnext/pull/26226))
- Payroll-entry minor fix ([#26349](https://github.com/frappe/erpnext/pull/26349))
- Allow user to change the To Date in the blanket order even after submit of order ([#26241](https://github.com/frappe/erpnext/pull/26241))
- Value fetching for custom field in POS ([#26367](https://github.com/frappe/erpnext/pull/26367))
- Iteration through accounts only when accounts exist ([#26391](https://github.com/frappe/erpnext/pull/26391))
- Employee Inactive status implications ([#26244](https://github.com/frappe/erpnext/pull/26244))
- Multi-currency issue ([#26458](https://github.com/frappe/erpnext/pull/26458))
- FG item not fetched in manufacture entry ([#26509](https://github.com/frappe/erpnext/pull/26509))
- Set query for training events ([#26303](https://github.com/frappe/erpnext/pull/26303))
- Fetch batch items in stock reconciliation ([#26213](https://github.com/frappe/erpnext/pull/26213))
- Employee selection not working in payroll entry ([#26278](https://github.com/frappe/erpnext/pull/26278))
- POS item cart dom updates (#26459) ([#26461](https://github.com/frappe/erpnext/pull/26461))
- dunning calculation of grand total when rate of interest is 0% ([#26285](https://github.com/frappe/erpnext/pull/26285))

View File

@@ -1,39 +0,0 @@
# Version 13.8.0 Release Notes
### Features & Enhancements
- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222))
- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677))
- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564))
### Fixes
- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600))
- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651))
- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460))
- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438))
- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206))
- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487))
- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502))
- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644))
- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431))
- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508))
- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420))
- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558))
- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712))
- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721))
- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691))
- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518))
- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646))
- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727))
- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294))
- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292))
- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436))
- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412))
- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613))
- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614))
- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734))
- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485))
- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560))
- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576))
- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643))
- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418))
- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498))

View File

@@ -1,46 +0,0 @@
# Version 13.9.0 Release Notes
### Features & Enhancements
- Organizational Chart ([#26261](https://github.com/frappe/erpnext/pull/26261))
- Enable discount accounting ([#26579](https://github.com/frappe/erpnext/pull/26579))
- Added multi-select fields in promotional scheme to create multiple pricing rules ([#25622](https://github.com/frappe/erpnext/pull/25622))
- Over transfer allowance for material transfers ([#26814](https://github.com/frappe/erpnext/pull/26814))
- Enhancements in Tax Withholding Category ([#26661](https://github.com/frappe/erpnext/pull/26661))
### Fixes
- Sales Return cancellation if linked with Payment Entry ([#26883](https://github.com/frappe/erpnext/pull/26883))
- Production plan not fetching sales order of a variant ([#25845](https://github.com/frappe/erpnext/pull/25845))
- Stock Analytics Report must consider warehouse during calculation ([#26908](https://github.com/frappe/erpnext/pull/26908))
- Incorrect date difference calculation ([#26805](https://github.com/frappe/erpnext/pull/26805))
- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206))
- Cannot cancel payment entry if linked with invoices ([#26703](https://github.com/frappe/erpnext/pull/26703))
- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576))
- Fetch Payment Terms from linked Sales/Purchase Order ([#26723](https://github.com/frappe/erpnext/pull/26723))
- Let all System Managers be able to delete Company transactions ([#26819](https://github.com/frappe/erpnext/pull/26819))
- Bank remittance report issue ([#26398](https://github.com/frappe/erpnext/pull/26398))
- Faulty Gl Entry for Asset LCVs ([#26803](https://github.com/frappe/erpnext/pull/26803))
- Clean Serial No input on Server Side ([#26878](https://github.com/frappe/erpnext/pull/26878))
- Supplier invoice importer fix v13 ([#26633](https://github.com/frappe/erpnext/pull/26633))
- POS payment modes displayed wrong total ([#26808](https://github.com/frappe/erpnext/pull/26808))
- Fetching of item tax from hsn code ([#26736](https://github.com/frappe/erpnext/pull/26736))
- Cannot cancel invoice if IRN cancelled on portal ([#26879](https://github.com/frappe/erpnext/pull/26879))
- Validate python expressions ([#26856](https://github.com/frappe/erpnext/pull/26856))
- POS Item Cart non-stop scroll issue ([#26693](https://github.com/frappe/erpnext/pull/26693))
- Add mandatory depends on condition for export type field ([#26958](https://github.com/frappe/erpnext/pull/26958))
- Cannot generate IRNs for standalone credit notes ([#26824](https://github.com/frappe/erpnext/pull/26824))
- Added progress bar in Repost Item Valuation to check the status of reposting ([#26630](https://github.com/frappe/erpnext/pull/26630))
- TDS calculation for first threshold breach for TDS category 194Q ([#26710](https://github.com/frappe/erpnext/pull/26710))
- Student category mapping from the program enrollment tool ([#26739](https://github.com/frappe/erpnext/pull/26739))
- Cost center & account validation in Sales/Purchase Taxes and Charges ([#26881](https://github.com/frappe/erpnext/pull/26881))
- Reset weight_per_unit on replacing Item ([#26791](https://github.com/frappe/erpnext/pull/26791))
- Do not fetch fully return issued purchase receipts ([#26825](https://github.com/frappe/erpnext/pull/26825))
- Incorrect amount in work order required items table. ([#26585](https://github.com/frappe/erpnext/pull/26585))
- Additional discount calculations in Invoices ([#26553](https://github.com/frappe/erpnext/pull/26553))
- Refactored Asset Repair ([#26415](https://github.com/frappe/erpnext/pull/25798))
- Exchange rate revaluation posting date and precision fixes ([#26650](https://github.com/frappe/erpnext/pull/26650))
- POS Invoice consolidated Sales Invoice field set to no copy ([#26768](https://github.com/frappe/erpnext/pull/26768))
- Consider grand total for threshold check ([#26683](https://github.com/frappe/erpnext/pull/26683))
- Budget variance missing values ([#26966](https://github.com/frappe/erpnext/pull/26966))
- GL Entries for exchange gain loss ([#26728](https://github.com/frappe/erpnext/pull/26728))
- Add missing cess amount in GSTR-3B report ([#26544](https://github.com/frappe/erpnext/pull/26544))
- GST Reports timeout issue ([#26575](https://github.com/frappe/erpnext/pull/26575))

View File

@@ -1,15 +1,73 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
cur_frm.add_fetch('employee', 'company', 'company');
cur_frm.add_fetch('employee', 'employee_name', 'employee_name');
frappe.ui.form.on('Attendance', {
onload: function(frm) {
cur_frm.cscript.onload = function(doc, cdt, cdn) {
if(doc.__islocal) cur_frm.set_value("attendance_date", frappe.datetime.get_today());
}
frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type").then((r)=>{
if (!r) {
// for not fetching from Shift Type
delete cur_frm.fetch_dict["shift"];
}
});
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{
query: "erpnext.controllers.queries.employee_query"
if (frm.doc.__islocal) {
frm.set_value("attendance_date", frappe.datetime.get_today());
}
frm.set_query("employee", () => {
return {
query: "erpnext.controllers.queries.employee_query"
};
});
},
employee: function(frm) {
if (frm.doc.employee) {
frm.events.set_shift(frm);
frm.events.set_overtime_type(frm);
}
},
set_shift: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.get_shift_type",
args: {
employee: frm.doc.employee,
attendance_date: frm.doc.attendance_date
},
callback: function(r) {
if (r.message) {
frm.set_value("shift", r.message);
}
}
});
},
set_overtime_type: function(frm) {
frappe.db.get_single_value("Payroll Settings", "overtime_based_on").then((r)=>{
if (r == "Attendance") {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.get_overtime_type",
args: {
employee: frm.doc.employee,
},
callback: function(r) {
if (r.message) {
frm.set_value("overtime_type", r.message);
}
}
});
} else {
frm.set_value("overtime_type", '');
}
});
},
overtime_duration: function(frm) {
let duration = frm.doc.overtime_duration.split(":");
let overtime_duration_words = duration[0] + " Hours " + duration[1] + " Minutes";
frm.set_value("overtime_duration_words", overtime_duration_words);
}
}
});

View File

@@ -11,7 +11,6 @@
"naming_series",
"employee",
"employee_name",
"working_hours",
"status",
"leave_type",
"leave_application",
@@ -20,13 +19,19 @@
"company",
"department",
"attendance_request",
"details_section",
"shift_details_section",
"shift",
"in_time",
"out_time",
"column_break_18",
"shift_duration",
"working_time",
"late_entry",
"early_exit",
"overtime_details_section",
"overtime_type",
"column_break_27",
"overtime_duration",
"amended_from"
],
"fields": [
@@ -69,14 +74,6 @@
"oldfieldtype": "Data",
"read_only": 1
},
{
"depends_on": "working_hours",
"fieldname": "working_hours",
"fieldtype": "Float",
"label": "Working Hours",
"precision": "1",
"read_only": 1
},
{
"default": "Present",
"fieldname": "status",
@@ -125,6 +122,7 @@
"reqd": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@@ -146,7 +144,8 @@
"fieldname": "shift",
"fieldtype": "Link",
"label": "Shift",
"options": "Shift Type"
"options": "Shift Type",
"read_only": 1
},
{
"fieldname": "attendance_request",
@@ -177,11 +176,6 @@
"fieldtype": "Check",
"label": "Early Exit"
},
{
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"depends_on": "shift",
"fieldname": "in_time",
@@ -199,13 +193,60 @@
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"depends_on": "overtime_type",
"fieldname": "overtime_details_section",
"fieldtype": "Section Break",
"label": "Overtime Details"
},
{
"depends_on": "overtime_type",
"fieldname": "overtime_type",
"fieldtype": "Link",
"label": "Overtime Type",
"options": "Overtime Type",
"read_only": 1
},
{
"depends_on": "working_time",
"fieldname": "working_time",
"fieldtype": "Duration",
"label": "Total Working Time",
"precision": "1",
"read_only": 1
},
{
"default": "0000",
"fieldname": "overtime_duration",
"fieldtype": "Duration",
"hide_days": 1,
"label": "Overtime Duration"
},
{
"depends_on": "shift",
"fieldname": "shift_details_section",
"fieldtype": "Section Break",
"label": "Shift Details"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"description": "Shift duration for a day",
"fetch_from": "shift.standard_working_time",
"fieldname": "shift_duration",
"fieldtype": "Duration",
"label": "Shift Duration",
"read_only": 1
}
],
"icon": "fa fa-ok",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-18 17:26:09.703215",
"modified": "2021-08-11 11:55:50.076043",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance",

View File

@@ -4,11 +4,10 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import getdate, nowdate
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, formatdate
from erpnext.hr.utils import validate_active_employee
from frappe.utils import cstr, get_datetime, formatdate, getdate, nowdate
class Attendance(Document):
def validate(self):
@@ -19,12 +18,17 @@ class Attendance(Document):
self.validate_duplicate_record()
self.validate_employee_status()
self.check_leave_record()
self.set_overtime_type()
self.set_default_shift()
if not frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type"):
self.shift_duration = None
def validate_attendance_date(self):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
# leaves can be marked for future dates
if self.status != 'On Leave' and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()):
if self.status != "On Leave" and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()):
frappe.throw(_("Attendance can not be marked for future dates"))
elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining):
frappe.throw(_("Attendance date can not be less than employee's joining date"))
@@ -45,6 +49,25 @@ class Attendance(Document):
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
def set_default_shift(self):
if not self.shift:
self.shift = get_shift_type(self.employee, self.attendance_date)
def set_overtime_type(self):
self.overtime_type = get_overtime_type(self.employee)
if self.overtime_type:
if frappe.db.get_single_value("Payroll Settings", "overtime_based_on") != "Attendance":
frappe.msgprint(_('Set "Calculate Overtime Based On Attendance" to Attendance for Overtime Slip Creation'))
maximum_overtime_hours_allowed = frappe.db.get_single_value("Payroll Settings", "maximum_overtime_hours_allowed")
if maximum_overtime_hours_allowed and maximum_overtime_hours_allowed * 3600 < self.overtime_duration:
self.overtime_duration = maximum_overtime_hours_allowed * 3600
frappe.msgprint(_("Overtime Duration can not be greater than {0} Hours. You can change this in Payroll settings").format(
str(maximum_overtime_hours_allowed)
))
def check_leave_record(self):
leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date
@@ -58,11 +81,11 @@ class Attendance(Document):
for d in leave_record:
self.leave_type = d.leave_type
if d.half_day_date == getdate(self.attendance_date):
self.status = 'Half Day'
self.status = "Half Day"
frappe.msgprint(_("Employee {0} on Half day on {1}")
.format(self.employee, formatdate(self.attendance_date)))
else:
self.status = 'On Leave'
self.status = "On Leave"
frappe.msgprint(_("Employee {0} is on Leave on {1}")
.format(self.employee, formatdate(self.attendance_date)))
@@ -80,6 +103,66 @@ class Attendance(Document):
if not emp:
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
def calculate_overtime_duration(self):
#this method is only for Calculation of overtime based on Attendance through Employee Checkins
self.overtime_duration = None
if not self.shift_duration and self.shift:
self.shift_duration = frappe.db.get_value("Shift Type", self.shift, "shift_duration")
if not self.overtime_type:
self.overtime_type = get_overtime_type(self.employee)
if int(self.working_time) > int(self.shift_duration):
self.overtime_duration = int(self.working_time) - int(self.shift_duration)
@frappe.whitelist()
def get_shift_type(employee, attendance_date):
shift_assignment = frappe.db.sql('''SELECT name, shift_type
FROM
`tabShift Assignment`
WHERE
docstatus = 1
AND employee = %(employee)s AND start_date <= %(attendance_date)s
AND (end_date >= %(attendance_date)s OR end_date IS null)
AND status = "Active"
''', {
"employee": employee,
"attendance_date": attendance_date,
}, as_dict = 1)
if len(shift_assignment):
shift = shift_assignment[0].shift_type
else:
shift = frappe.db.get_value("Employee", employee, "default_shift")
return shift
@frappe.whitelist()
def get_overtime_type(employee):
overtime_type = None
emp_details = frappe.db.get_value("Employee", employee, ["department", "grade"], as_dict=1)
emp_department = emp_details.department
if emp_department:
overtime_type_doc = frappe.get_list("Overtime Type", filters={
"applicable_for": "Department", "department": emp_department}, fields=["name"])
if len(overtime_type_doc):
overtime_type = overtime_type_doc[0].name
emp_grade = emp_details.grade
if emp_grade:
overtime_type_doc = frappe.get_list("Overtime Type", filters={
"applicable_for": "Employee Grade", "employee_grade": emp_grade},
fields=["name"])
if len(overtime_type_doc):
overtime_type = overtime_type_doc[0].name
overtime_type_doc = frappe.get_list("Overtime Type", filters={
"applicable_for": "Employee", "employee": employee}, fields=["name"])
if len(overtime_type_doc):
overtime_type = overtime_type_doc[0].name
return overtime_type
@frappe.whitelist()
def get_events(start, end, filters=None):
events = []
@@ -134,7 +217,6 @@ def mark_attendance(employee, attendance_date, status, shift=None, leave_type=No
@frappe.whitelist()
def mark_bulk_attendance(data):
import json
from pprint import pprint
if isinstance(data, frappe.string_types):
data = json.loads(data)
data = frappe._dict(data)
@@ -184,7 +266,7 @@ def get_unmarked_days(employee, month):
month_start, month_end = dates_of_month[0], dates_of_month[length-1]
records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
records = frappe.get_all("Attendance", fields = ["attendance_date", "employee"] , filters = [
["attendance_date", ">=", month_start],
["attendance_date", "<=", month_end],
["employee", "=", employee],

View File

@@ -4,8 +4,10 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import now, cint, get_datetime
from frappe.utils import cint, get_datetime
from frappe.model.document import Document
from datetime import timedelta
from math import modf
from frappe import _
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
@@ -29,17 +31,25 @@ class EmployeeCheckin(Document):
def fetch_shift(self):
shift_actual_timings = get_actual_start_end_datetime_of_shift(self.employee, get_datetime(self.time), True)
if shift_actual_timings[0] and shift_actual_timings[1]:
if shift_actual_timings[2].shift_type.determine_check_in_and_check_out == 'Strictly based on Log Type in Employee Checkin' and not self.log_type and not self.skip_auto_attendance:
frappe.throw(_('Log Type is required for check-ins falling in the shift: {0}.').format(shift_actual_timings[2].shift_type.name))
if not self.attendance:
self.shift = shift_actual_timings[2].shift_type.name
self.shift_actual_start = shift_actual_timings[0]
self.shift_actual_end = shift_actual_timings[1]
self.shift_start = shift_actual_timings[2].start_datetime
self.shift_end = shift_actual_timings[2].end_datetime
else:
self.shift = None
allow_overtime = False
if shift_actual_timings[2]:
allow_overtime = frappe.db.get_value("Shift Type", shift_actual_timings[2].shift_type.name, "allow_overtime")
if not allow_overtime:
if shift_actual_timings[0] and shift_actual_timings[1]:
if shift_actual_timings[2].shift_type.determine_check_in_and_check_out == 'Strictly based on Log Type in Employee Checkin' and not self.log_type and not self.skip_auto_attendance:
frappe.throw(_('Log Type is required for check-ins falling in the shift: {0}.').format(shift_actual_timings[2].shift_type.name))
if not self.attendance:
self.shift = shift_actual_timings[2].shift_type.name
self.shift_actual_start = shift_actual_timings[0]
self.shift_actual_end = shift_actual_timings[1]
self.shift_start = shift_actual_timings[2].start_datetime
self.shift_end = shift_actual_timings[2].end_datetime
elif allow_overtime:
#because after Actual time it takes check-in/out invalid
#if employee checkout late or check-in before before shift timing adding time buffer.
self.shift = shift_actual_timings[2].shift_type.name
self.shift_start = shift_actual_timings[2].start_datetime
self.shift_end = shift_actual_timings[2].end_datetime
@frappe.whitelist()
def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'):
@@ -56,7 +66,8 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
if not employee_field_value or not timestamp:
frappe.throw(_("'employee_field_value' and 'timestamp' are required."))
employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value}, ["name", "employee_name", employee_fieldname], as_dict=True)
employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value},
["name", "employee_name", employee_fieldname], as_dict=True)
if employee:
employee = employee[0]
else:
@@ -73,7 +84,6 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
return doc
def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, in_time=None, out_time=None, shift=None):
"""Creates an attendance and links the attendance to the Employee Checkin.
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
@@ -93,12 +103,21 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
elif attendance_status in ('Present', 'Absent', 'Half Day'):
employee_doc = frappe.get_doc('Employee', employee)
if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
working_timedelta = '00:00:00'
working_time = None
working_time = modf(working_hours)
if working_time[1] or working_time[0]:
working_timedelta = timedelta(hours =int(working_time[1]), minutes = int(working_time[0] * 60))
from erpnext.hr.doctype.shift_type.shift_type import convert_time_into_duration
working_time = convert_time_into_duration(working_timedelta)
doc_dict = {
'doctype': 'Attendance',
'employee': employee,
'attendance_date': attendance_date,
'status': attendance_status,
'working_hours': working_hours,
'working_time': working_time,
'company': employee_doc.company,
'shift': shift,
'late_entry': late_entry,
@@ -107,6 +126,9 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
'out_time': out_time
}
attendance = frappe.get_doc(doc_dict).insert()
if frappe.db.get_value("Shift Type", shift, "allow_overtime"):
attendance.calculate_overtime_duration()
attendance.save()
attendance.submit()
frappe.db.sql("""update `tabEmployee Checkin`
set attendance = %s
@@ -121,10 +143,10 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status))
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
"""Given a set of logs in chronological order calculates the total working hours based on the parameters.
Zero is returned for all invalid cases.
:param logs: The List of 'Employee Checkin'.
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'

View File

@@ -40,13 +40,14 @@ class TestEmployeeCheckin(unittest.TestCase):
log_names = [log.name for log in logs]
logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'attendance':attendance.name})
self.assertEqual(logs_count, 4)
attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_hours':8.2,
attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_time': 29460,
'employee':employee, 'attendance_date':now_date})
self.assertEqual(attendance_count, 1)
self.assertEqual(attendance_count, 1)
def test_calculate_working_hours(self):
check_in_out_type = ['Alternating entries as IN and OUT during the same shift',
'Strictly based on Log Type in Employee Checkin']
'Strictly based on Log Type in Employee Checkin']
working_hours_calc_type = ['First Check-in and Last Check-out',
'Every Valid Check-in and Check-out']
logs_type_1 = [
@@ -88,12 +89,12 @@ def make_n_checkins(employee, n, hours_to_reverse=1):
return logs
def make_checkin(employee, time=now_datetime()):
def make_checkin(employee, time=now_datetime(), log_type = "IN"):
log = frappe.get_doc({
"doctype": "Employee Checkin",
"employee" : employee,
"time" : time,
"device_id" : "device1",
"log_type" : "IN"
"log_type" : log_type
}).insert()
return log

View File

@@ -2,6 +2,16 @@
// For license information, please see license.txt
frappe.ui.form.on('HR Settings', {
refresh: function(frm) {
frm.set_query('overtime_salary_component', function() {
return {
filters: {
type: "Earning"
}
};
});
},
restrict_backdated_leave_application: function(frm) {
frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application);
}

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate
from frappe.utils import cstr, getdate, now_datetime, nowdate
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from erpnext.hr.utils import validate_active_employee
@@ -61,12 +61,12 @@ class ShiftAssignment(Document):
def throw_overlap_error(self, shift_details):
shift_details = frappe._dict(shift_details)
if shift_details.docstatus == 1 and shift_details.status == "Active":
msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name))
msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name)) + " "
if shift_details.start_date:
msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y"))
msg += _("from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y")) + " "
title = "Ongoing Shift"
if shift_details.end_date:
msg += _(" to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y"))
msg += _("to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y"))
title = "Active Shift"
if msg:
frappe.throw(msg, title=title)
@@ -236,13 +236,15 @@ def get_shift_details(shift_type_name, for_date=nowdate()):
end_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.end_time
actual_start = start_datetime - timedelta(minutes=shift_type.begin_check_in_before_shift_start_time)
actual_end = end_datetime + timedelta(minutes=shift_type.allow_check_out_after_shift_end_time)
allow_overtime = shift_type.allow_overtime
return frappe._dict({
'shift_type': shift_type,
'start_datetime': start_datetime,
'end_datetime': end_datetime,
'actual_start': actual_start,
'actual_end': actual_end
'actual_end': actual_end,
'allow_overtime': allow_overtime
})
@@ -254,22 +256,32 @@ def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_defa
"""
actual_shift_start = actual_shift_end = shift_details = None
shift_timings_as_per_timestamp = get_employee_shift_timings(employee, for_datetime, consider_default_shift)
timestamp_list = []
for shift in shift_timings_as_per_timestamp:
if shift:
timestamp_list.extend([shift.actual_start, shift.actual_end])
else:
timestamp_list.extend([None, None])
timestamp_index = None
for index, timestamp in enumerate(timestamp_list):
if timestamp and for_datetime <= timestamp:
timestamp_index = index
break
if timestamp_index and timestamp_index%2 == 1:
shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)]
actual_shift_start = shift_details.actual_start
actual_shift_end = shift_details.actual_end
elif timestamp_index:
shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)]
if shift_timings_as_per_timestamp[0] and not shift_timings_as_per_timestamp[0].allow_overtime:
# If Shift is not allowed for automatic calculation of overtime, then previous, current and next
# shift will also should be considered for valid and invalid checkins.
# if checkin time is not in current shift thenit will check prev and next shift for checkin validation.
timestamp_list = []
for shift in shift_timings_as_per_timestamp:
if shift:
timestamp_list.extend([shift.actual_start, shift.actual_end])
else:
timestamp_list.extend([None, None])
timestamp_index = None
for index, timestamp in enumerate(timestamp_list):
if timestamp and for_datetime <= timestamp:
timestamp_index = index
break
if timestamp_index and timestamp_index % 2 == 1:
shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)]
actual_shift_start = shift_details.actual_start
actual_shift_end = shift_details.actual_end
elif timestamp_index:
shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)]
else:
# for overtime calculation there is no valid and invalid checkins it should return the current shift and after that total working
# hours will be taken in consideration for overtime calculation. there will be no actual_shift_start/end.
shift_details = shift_timings_as_per_timestamp[1]
return actual_shift_start, actual_shift_end, shift_details

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "prompt",
"creation": "2018-04-13 16:22:52.954783",
"doctype": "DocType",
@@ -7,20 +8,24 @@
"field_order": [
"start_time",
"end_time",
"standard_working_time",
"column_break_3",
"holiday_list",
"enable_auto_attendance",
"allow_overtime",
"auto_attendance_settings_section",
"determine_check_in_and_check_out",
"working_hours_calculation_based_on",
"column_break_10",
"begin_check_in_before_shift_start_time",
"allow_check_out_after_shift_end_time",
"column_break_10",
"section_break_15",
"working_hours_threshold_for_half_day",
"working_hours_threshold_for_absent",
"column_break_19",
"process_attendance_after",
"last_sync_of_checkin",
"grace_period_settings_auto_attendance_section",
"grace_period_settings_section",
"enable_entry_grace_period",
"late_entry_grace_period",
"column_break_18",
@@ -29,6 +34,7 @@
],
"fields": [
{
"default": "00:00:00",
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
@@ -36,6 +42,7 @@
"reqd": 1
},
{
"default": "00:00:00",
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
@@ -84,6 +91,7 @@
},
{
"default": "60",
"depends_on": "eval: doc.allow_overtime == 0",
"description": "The time before the shift start time during which Employee Check-in is considered for attendance.",
"fieldname": "begin_check_in_before_shift_start_time",
"fieldtype": "Int",
@@ -121,6 +129,7 @@
},
{
"default": "60",
"depends_on": "eval: doc.allow_overtime == 0",
"description": "Time after the end of shift during which check-out is considered for attendance.",
"fieldname": "allow_check_out_after_shift_end_time",
"fieldtype": "Int",
@@ -132,12 +141,6 @@
"fieldtype": "Section Break",
"label": "Auto Attendance Settings"
},
{
"depends_on": "enable_auto_attendance",
"fieldname": "grace_period_settings_auto_attendance_section",
"fieldtype": "Section Break",
"label": "Grace Period Settings For Auto Attendance"
},
{
"default": "0",
"description": "Mark attendance based on 'Employee Checkin' for Employees assigned to this shift.",
@@ -156,9 +159,60 @@
"fieldname": "last_sync_of_checkin",
"fieldtype": "Datetime",
"label": "Last Sync of Checkin"
},
{
"fieldname": "standard_working_time",
"fieldtype": "Duration",
"label": "Standard Working Time",
"read_only": 1
},
{
"default": "0",
"depends_on": "enable_auto_attendance",
"description": "Overtime will be calculated and Overtime Duration will reflect in attendance records. Check Payroll Settings for more options. ",
"fieldname": "allow_overtime",
"fieldtype": "Check",
"label": "Allow Overtime"
},
{
"depends_on": "enable_auto_attendance",
"fieldname": "section_break_15",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
},
{
"depends_on": "enable_auto_attendance",
"fieldname": "grace_period_settings_section",
"fieldtype": "Section Break",
"label": "Grace Period Settings"
}
],
"modified": "2019-07-30 01:05:24.660666",
"links": [
{
"group": "Attendance and Checkin",
"link_doctype": "Attendance",
"link_fieldname": "shift"
},
{
"group": "Attendance and Checkin",
"link_doctype": "Employee Checkin",
"link_fieldname": "shift"
},
{
"group": "Request and Assignment",
"link_doctype": "Shift Request",
"link_fieldname": "shift_type"
},
{
"group": "Request and Assignment",
"link_doctype": "Shift Assignment",
"link_fieldname": "shift_type"
}
],
"modified": "2021-08-11 12:07:33.227032",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Type",

View File

@@ -4,19 +4,46 @@
from __future__ import unicode_literals
import itertools
from datetime import timedelta
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, getdate, get_datetime
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift, get_employee_shift
from erpnext.hr.doctype.employee_checkin.employee_checkin import mark_attendance_and_link_log, calculate_working_hours
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from datetime import timedelta
class ShiftType(Document):
def validate(self):
self.validate_overtime()
self.set_working_hours()
def set_working_hours(self):
end_time = self.end_time.split(':')
start_time = self.start_time.split(':')
shift_end = timedelta(hours = int(end_time[0]), minutes = int(end_time[1]), seconds = int(end_time[2]))
shift_start = timedelta(hours = int(start_time[0]), minutes = int(start_time[1]), seconds = int(start_time[2]))
if shift_end > shift_start:
time_difference = shift_end - shift_start
else:
# for night shift
time_difference = shift_start - shift_end
self.standard_working_time = convert_time_into_duration(time_difference)
def validate_overtime(self):
if self.allow_overtime:
if not frappe.db.get_single_value('Payroll Settings', 'fetch_standard_working_hours_from_shift_type'):
frappe.throw(_('Please enable "Fetch Standard Working Hours from Shift Type" in payroll Settings for Overtime.'))
if frappe.db.get_single_value("Payroll Settings", "overtime_based_on") != "Attendance":
frappe.throw(_('Please set Overtime based on "Attendance" in payroll Settings for Overtime.'))
@frappe.whitelist()
def process_auto_attendance(self):
self.validate_overtime()
if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin:
return
filters = {
@@ -27,10 +54,19 @@ class ShiftType(Document):
'shift': self.name
}
logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time")
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
if self.allow_overtime == 1:
checkins_log = itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_start']))
else:
checkins_log = itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start']))
for key, group in checkins_log:
single_shift_logs = list(group)
attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs)
mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, in_time, out_time, self.name)
for employee in self.get_assigned_employee(self.process_attendance_after, True):
self.mark_absent_for_dates_with_no_attendance(employee)
@@ -41,8 +77,10 @@ class ShiftType(Document):
1. These logs belongs to an single shift, single employee and is not in a holiday date.
2. Logs are in chronological order
"""
late_entry = early_exit = False
total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on)
if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)):
late_entry = True
@@ -51,8 +89,10 @@ class ShiftType(Document):
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
return 'Absent', total_working_hours, late_entry, early_exit, in_time, out_time
if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day:
return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time
return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time
def mark_absent_for_dates_with_no_attendance(self, employee):
@@ -126,3 +166,8 @@ def get_filtered_date_list(employee, start_date, end_date, filter_attendance=Tru
{"employee":employee, "start_date":start_date, "end_date":end_date, "holiday_list":holiday_list}, as_list=True)
return [getdate(date[0]) for date in dates]
def convert_time_into_duration(time_difference):
time_difference = str(time_difference).split(':')
return (int(time_difference[0]) * 3600) + (int(time_difference[1]) * 60) + int(time_difference[2])

View File

@@ -1,16 +0,0 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'shift',
'non_standard_fieldnames': {
'Shift Request': 'shift_type',
'Shift Assignment': 'shift_type'
},
'transactions': [
{
'items': ['Attendance', 'Employee Checkin', 'Shift Request', 'Shift Assignment']
}
]
}

View File

@@ -8,3 +8,13 @@ import unittest
class TestShiftType(unittest.TestCase):
pass
def create_shift_type():
shift_type = frappe.new_doc("Shift Type")
shift_type.name = "test shift"
shift_type.start_time = "9:00:00"
shift_type.end_time = "18:00:00"
shift_type.enable_auto_attendance = 1
shift_type.save()
return shift_type

View File

@@ -32,17 +32,16 @@ def get_children(parent=None, company=None, exclude_node=None):
def get_connections(employee):
num_connections = 0
nodes_to_expand = frappe.get_list('Employee', filters=[
connections = frappe.get_list('Employee', filters=[
['reports_to', '=', employee]
])
num_connections += len(nodes_to_expand)
num_connections += len(connections)
while nodes_to_expand:
parent = nodes_to_expand.pop(0)
descendants = frappe.get_list('Employee', filters=[
['reports_to', '=', parent.name]
])
num_connections += len(descendants)
nodes_to_expand.extend(descendants)
while connections:
for entry in connections:
connections = frappe.get_list('Employee', filters=[
['reports_to', '=', entry.name]
])
num_connections += len(connections)
return num_connections

View File

@@ -296,7 +296,7 @@ erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items
erpnext.patches.v12_0.show_einvoice_irn_cancelled_field
erpnext.patches.v13_0.delete_orphaned_tables
erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16
erpnext.patches.v13_0.update_export_type_for_gst
erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.shopify_deprecation_warning

View File

@@ -8,19 +8,11 @@ def execute():
# Update custom fields
fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'})
if fieldname:
frappe.db.set_value('Custom Field', fieldname,
{
'default': '',
'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
})
frappe.db.set_value('Custom Field', fieldname, 'default', '')
fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'})
if fieldname:
frappe.db.set_value('Custom Field', fieldname,
{
'default': '',
'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)'
})
frappe.db.set_value('Custom Field', fieldname, 'default', '')
# Update Customer/Supplier Masters
frappe.db.sql("""

View File

@@ -235,11 +235,12 @@ def get_gratuity_rule_slabs(gratuity_rule):
return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx")
def get_salary_structure(employee):
return frappe.get_list("Salary Structure Assignment", filters = {
salary_structure_assignment = frappe.get_list("Salary Structure Assignment", filters = {
"employee": employee, 'docstatus': 1
},
fields=["from_date", "salary_structure"],
order_by = "from_date desc")[0].salary_structure
order_by = "from_date desc")
return salary_structure_assignment[0].salary_structure if len(salary_structure_assignment) else None
def get_last_salary_slip(employee):
return frappe.get_list("Salary Slip", filters = {

View File

@@ -166,15 +166,15 @@ def set_mode_of_payment_account():
def create_account():
return frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Payment Account",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Bank Accounts - _TC",
"account_type": "Bank",
}).insert(ignore_permissions=True)
"doctype": "Account",
"company": "_Test Company",
"account_name": "Payment Account",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Bank Accounts - _TC",
"account_type": "Bank",
}).insert(ignore_permissions=True)
def create_employee_and_get_last_salary_slip():
employee = make_employee("test_employee@salary.com", company='_Test Company')

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Overtime Details', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,101 @@
{
"actions": [],
"creation": "2021-05-27 13:39:56.788736",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_document_type",
"reference_document",
"column_break_2",
"date",
"start_date",
"end_date",
"section_break_5",
"overtime_type",
"overtime_duration",
"column_break_10",
"standard_working_time"
],
"fields": [
{
"fieldname": "reference_document_type",
"fieldtype": "Link",
"label": "Reference Document Type ",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "overtime_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Overtime Type ",
"options": "Overtime Type",
"reqd": 1
},
{
"fieldname": "overtime_duration",
"fieldtype": "Duration",
"hide_days": 1,
"in_list_view": 1,
"label": "Overtime Duration",
"reqd": 1
},
{
"fieldname": "reference_document",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Document",
"options": "reference_document_type",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Start Date",
"read_only": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "standard_working_time",
"fieldtype": "Duration",
"label": "Standard Working Time",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-14 17:39:36.147530",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Overtime Details",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class OvertimeDetails(Document):
pass

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestOvertimeDetails(unittest.TestCase):
pass

View File

@@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2021-05-25 12:49:03.287694",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"salary_component"
],
"fields": [
{
"fieldname": "salary_component",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Salary Component ",
"options": "Salary Component",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-16 14:48:59.476787",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Overtime Salary Component",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class OvertimeSalaryComponent(Document):
pass

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Overtime Slip', {
onload: function (frm) {
frm.set_query("employee", () => {
return {
query: "erpnext.controllers.queries.employee_query"
};
});
},
employee: function (frm) {
if (frm.doc.employee) {
frm.events.set_frequency_and_dates(frm).then(() => {
frm.events.get_emp_details_and_overtime_duration(frm);
});
}
},
from_date: function (frm) {
if (frm.doc.employee) {
frm.events.set_frequency_and_dates(frm).then(() => {
frm.events.get_emp_details_and_overtime_duration(frm);
});
}
},
set_frequency_and_dates: function (frm) {
if (frm.doc.employee) {
return frappe.call({
method: 'get_frequency_and_dates',
doc: frm.doc,
callback: function () {
frm.refresh();
}
});
}
},
get_emp_details_and_overtime_duration: function (frm) {
if (frm.doc.employee) {
return frappe.call({
method: 'get_emp_and_overtime_details',
doc: frm.doc,
callback: function () {
frm.refresh();
}
});
}
},
});
frappe.ui.form.on('Overtime Details', {
date: function (frm, cdt, cdn) {
let child = locals[cdt][cdn];
if (child.date) {
frappe.call({
method: "erpnext.payroll.doctype.overtime_slip.overtime_slip.get_standard_working_hours",
args: {
employee: frm.doc.employee,
date: child.date,
},
callback: function (r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, 'standard_working_time', r.message);
}
}
});
} else {
frappe.model.set_value(cdt, cdn, 'standard_working_time', 0);
}
}
});

View File

@@ -0,0 +1,204 @@
{
"actions": [],
"autoname": "HR-OT-SLIP-.#####",
"creation": "2021-05-27 12:47:32.372698",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee_details",
"posting_date",
"employee",
"employee_name",
"column_break_4",
"status",
"company",
"department",
"section_break_7",
"from_date",
"to_date",
"column_break_10",
"payroll_frequency",
"salary_slip",
"section_break_12",
"overtime_details",
"section_break_13",
"total_overtime_duration",
"column_break_17",
"amended_from"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nApproved\nRejected",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Overtime Slip",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "overtime_details",
"fieldtype": "Table",
"options": "Overtime Details",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Payroll Details"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "payroll_frequency",
"fieldtype": "Select",
"label": "Payroll Frequency",
"options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily"
},
{
"fieldname": "section_break_13",
"fieldtype": "Section Break"
},
{
"fieldname": "total_overtime_duration",
"fieldtype": "Duration",
"label": "Total Overtime Duration"
},
{
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Overtime Details"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "salary_slip",
"fieldtype": "Link",
"label": "Salary Slip",
"options": "Salary Slip",
"read_only": 1
},
{
"fieldname": "employee_details",
"fieldtype": "Section Break",
"label": "Employee Details"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-08-11 12:30:27.536389",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Overtime Slip",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1
}

View File

@@ -0,0 +1,189 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, bold
from frappe.utils import get_datetime, getdate, get_link_to_form, formatdate
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
from erpnext.payroll.doctype.gratuity.gratuity import get_salary_structure
from frappe.model.document import Document
class OvertimeSlip(Document):
def validate(self):
if not (self.from_date or self.to_date or self.payroll_frequency):
self.get_frequency_and_dates()
self.validate_overlap()
if self.from_date >= self.to_date:
frappe.throw(_("From date can not be greater than To date"))
if not len(self.overtime_details):
self.get_emp_and_overtime_details()
def validate_overlap(self):
if not self.name:
# hack! if name is null, it could cause problems with !=
self.name = "new-overtime-slip-1"
overtime_slips = frappe.db.get_all("Overtime Slip", filters = {
"docstatus": ("<", 2),
"employee": self.employee,
"to_date": (">=", self.from_date),
"from_date": ("<=", self.to_date),
"name": ("!=", self.name)
})
if len(overtime_slips):
form_link = get_link_to_form("Overtime Slip", overtime_slips[0].name)
msg = _("Overtime Slip:{0} has been created between {1} and {1}").format(
bold(form_link),
bold(formatdate(self.from_date)), bold(formatdate(self.to_date)))
frappe.throw(msg)
def on_submit(self):
if self.status == "Pending":
frappe.throw(_("Overtime Slip with Status 'Approved' or 'Rejected' are allowed for Submission"))
@frappe.whitelist()
def get_emp_and_overtime_details(self):
overtime_based_on = frappe.db.get_single_value("Payroll Settings", "overtime_based_on")
records = []
if overtime_based_on == "Attendance":
records = self.get_attendance_record()
if len(records):
self.create_overtime_details_row_for_attendance(records)
elif overtime_based_on == "Timesheet":
records = self.get_timesheet_record()
if len(records):
self.create_overtime_details_row_for_timesheet(records)
else:
link_to_settings = get_link_to_form('Payroll Settings', 'Payroll Settings', 'Payroll Settings')
frappe.throw(_('Select "Calculate Overtime Based On" in {0}').format(link_to_settings))
if len(self.overtime_details):
self.total_overtime_duration = sum([int(detail.overtime_duration) for detail in self.overtime_details])
if not len(records):
self.overtime_details = []
frappe.msgprint(_("No {0} records found for Overtime").format(overtime_based_on))
def create_overtime_details_row_for_attendance(self, records):
self.overtime_details = []
for record in records:
if record.standard_working_time:
standard_working_time = record.standard_working_time
else:
standard_working_time = frappe.db.get_single_value("HR Settings", "standard_working_hours") * 3600
if not standard_working_time:
frappe.throw(_('Please Set "Standard Working Hours" in HR settings'))
if record.overtime_duration:
self.append("overtime_details", {
"reference_document_type": "Attendance",
"reference_document": record.name,
"date": record.attendance_date,
"overtime_type": record.overtime_type,
"overtime_duration": record.overtime_duration,
"standard_working_time": standard_working_time,
})
def create_overtime_details_row_for_timesheet(self, records):
self.overtime_details = []
from math import modf
standard_working_time = frappe.db.get_single_value("HR Settings", "standard_working_hours") * 3600
if not standard_working_time:
frappe.throw(_('Please Set "Standard Working Hours" in HR settings'))
for record in records:
if record.overtime_hours:
overtime_hours = modf(record.overtime_hours)
record.overtime_hours = overtime_hours[1]*3600 + overtime_hours[0]*60
self.append("overtime_details", {
"reference_document_type": "Timesheet",
"reference_document": record.name,
"date": record.overtime_on,
"start_date": record.start_date,
"end_date": record.end_date,
"overtime_type": record.overtime_type,
"overtime_duration": record.overtime_hours,
"standard_working_time": standard_working_time
})
def get_attendance_record(self):
if self.from_date and self.to_date:
records = frappe.db.sql("""SELECT overtime_duration, name, attendance_date, overtime_type, standard_working_time
FROM `tabAttendance`
WHERE
attendance_date >= %s AND attendance_date <= %s
AND employee = %s
AND docstatus = 1 AND status= 'Present'
AND (
overtime_duration IS NOT NULL OR overtime_duration != '00:00:00.000000'
)
""", (getdate(self.from_date), getdate(self.to_date), self.employee), as_dict=1)
return records
return []
def get_timesheet_record(self):
if self.from_date and self.to_date:
records = frappe.db.sql("""SELECT ts.name, ts.start_date, ts.end_date, tsd.overtime_on, tsd.overtime_type, tsd.overtime_hours
FROM `tabTimesheet` AS ts
INNER JOIN `tabTimesheet Detail` As tsd ON tsd.parent = ts.name
WHERE
ts.docstatus = 1
AND end_date > %(from_date)s AND end_date <= %(to_date)s
AND start_date >= %(from_date)s AND start_date < %(to_date)s
AND employee = %(employee)s
AND (
total_overtime_hours IS NOT NULL OR total_overtime_hours != 0
)
""", {"from_date": get_datetime(self.from_date), "to_date": get_datetime(self.to_date),"employee": self.employee}, as_dict=1)
return records
return []
@frappe.whitelist()
def get_frequency_and_dates(self):
date = self.from_date or self.posting_date
salary_structure = get_salary_structure(self.employee)
if salary_structure:
payroll_frequency = frappe.db.get_value("Salary Structure", salary_structure, "payroll_frequency")
date_details = get_start_end_dates(payroll_frequency, date, frappe.db.get_value('Employee', self.employee, "company"))
self.from_date = date_details.start_date
self.to_date = date_details.end_date
self.payroll_frequency = payroll_frequency
else:
frappe.throw(_("No Salary Structure Assignment found for Employee: {0}").format(self.employee))
@frappe.whitelist()
def get_standard_working_hours(employee, date):
shift_assignment = frappe.db.sql('''SELECT shift_type FROM `tabShift Assignment`
WHERE employee = %(employee)s
AND start_date < %(date)s
and (end_date > %(date)s or end_date is NULL or end_date = "") ''', {
"employee": employee, "date": get_datetime(date)}
, as_dict=1)
standard_working_time = 0
fetch_from_shift = frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type")
if len(shift_assignment) and fetch_from_shift:
standard_working_time = frappe.db.get_value("Shift Type", shift_assignment[0].shift_type, "standard_working_time")
elif not len(shift_assignment) and fetch_from_shift:
shift = frappe.db.get_value("Employee", employee, "default_shift")
if shift:
standard_working_time = frappe.db.get_value("Shift Type", shift, "standard_working_time")
else:
frappe.throw(_("Set Default Shift in Employee:{0}").format(employee))
elif not fetch_from_shift:
standard_working_time = frappe.db.get_single_value("HR Settings", "standard_working_hours") * 3600
if not standard_working_time:
link_to_settings = get_link_to_form('HR Settings', 'HR Settings', 'HR Settings')
frappe.throw(_('Please Set "Standard Working Hours" in {0}').format(link_to_settings))
return standard_working_time

View File

@@ -0,0 +1,155 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.payroll.doctype.overtime_type.test_overtime_type import create_overtime_type
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.shift_type.test_shift_type import create_shift_type
from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
from frappe.utils import today, add_days, get_datetime
from datetime import timedelta
import unittest
class TestOvertimeSlip(unittest.TestCase):
def tearDown(self):
for doctype in ["Overtime Type","Overtime Slip", "Attendance", "Employee Checkin", "Shift Type"]:
frappe.db.sql("DELETE FROM `tab{0}`".format(doctype))
frappe.db.sql("DELETE FROM `tabEmployee` WHERE user_id = 'test_employee@overtime.com'")
frappe.db.commit()
def test_overtime_based_on_attendance_without_shift_type(self):
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 0)
frappe.db.set_value("HR Settings", None, "standard_working_hours", 7)
employee = make_employee("test_employee@overtime.com", company="_Test Company")
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
overtime_type = create_overtime_type(employee=employee)
attendance_record = create_attendance_records_for_overtime(employee, overtime_type.name)
slip = create_overtime_slip(employee)
for detail in slip.overtime_details:
self.assertIn(detail.reference_document, attendance_record.keys())
if detail.reference_document in attendance_record.keys():
self.assertEqual(detail.overtime_duration, attendance_record[detail.reference_document]["overtime_duration"])
self.assertEqual(str(detail.date), attendance_record[detail.reference_document]["attendance_date"])
def test_overtime_based_on_attendance_with_shift_type_through_employee_checkins(self):
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 1)
shift_type = create_shift_type()
shift_type.allow_overtime = 1
shift_type.process_attendance_after = add_days(today(), -1)
shift_type.last_sync_of_checkin = get_datetime(add_days(today(), 1))
shift_type.save()
employee = make_employee("test_employee@overtime.com", company="_Test Company")
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
frappe.db.set_value("Employee", employee, "default_shift", shift_type.name)
checkin = make_checkin(employee, time = get_datetime(today()) + timedelta(hours=9), log_type="IN")
checkout = make_checkin(employee, time = get_datetime(today()) + timedelta(hours=20), log_type="OUT")
self.assertEqual(checkin.shift, shift_type.name)
self.assertEqual(checkout.shift, shift_type.name)
create_overtime_type(employee=employee)
shift_type.reload()
shift_type.process_auto_attendance()
checkin.reload()
attendance_records = frappe.get_all("Attendance", filters = {
"shift": shift_type.name, "status": "Present"
}, fields = ["name", "overtime_duration", "overtime_type", "attendance_date"])
records = {}
for record in attendance_records:
records[record.name] = {
"overtime_duration": record.overtime_duration,
"overtime_type": record.overtime_type,
"attendance_date": record.attendance_date
}
slip = create_overtime_slip(employee)
for detail in slip.overtime_details:
self.assertIn(detail.reference_document, records.keys())
if detail.reference_document in records.keys():
self.assertEqual(detail.overtime_duration, records[detail.reference_document]["overtime_duration"])
self.assertEqual(str(detail.date), str(records[detail.reference_document]["attendance_date"]))
def test_overtime_based_on_timesheet(self):
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Timesheet")
frappe.db.set_value("HR Settings", None, "standard_working_hours", 7)
employee = make_employee("test_employee@overtime.com", company="_Test Company")
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
overtime_type = create_overtime_type(employee=employee)
time_log, timesheet = create_timesheet_record_for_overtime(employee, overtime_type.name)
slip = create_overtime_slip(employee)
for detail in slip.overtime_details:
self.assertEqual(time_log.overtime_hours * 3600, detail.overtime_duration)
self.assertEqual(time_log.overtime_on, get_datetime(detail.date))
self.assertEqual(time_log.overtime_type, detail.overtime_type)
self.assertEqual(timesheet, detail.reference_document)
def create_attendance_records_for_overtime(employee, overtime_type):
records = {}
for x in range(2):
attendance = frappe.new_doc("Attendance")
attendance.employee = employee
attendance.status = "Present"
attendance.attendance_date = add_days(today(), -(x))
attendance.overtime_type = overtime_type
#to convert to duration
attendance.overtime_duration = 2 * 3600
attendance.save()
attendance.submit()
records[attendance.name] = {
"overtime_duration": attendance.overtime_duration,
"overtime_type": attendance.overtime_type,
"attendance_date": attendance.attendance_date
}
return records
def create_timesheet_record_for_overtime(employee, overtime_type):
timesheet =frappe.new_doc("Timesheet")
timesheet.employee = employee
timesheet.time_logs = []
time_log = {
"activity_type": "Planning",
"from_time": get_datetime(add_days(today(), -1)),
"to_time": get_datetime(add_days(today(), 2)),
"expected_hours": 48,
"hours": 48,
"is_overtime": 1,
"overtime_type": overtime_type,
"overtime_on": get_datetime(today()),
"overtime_hours": 7
}
timesheet.append("time_logs", time_log)
timesheet.save()
timesheet.submit()
return frappe._dict(time_log), timesheet.name
def create_overtime_slip(employee):
slip = frappe.new_doc("Overtime Slip")
slip.employee = employee
slip.overtime_details = []
slip.save()
return slip

View File

@@ -0,0 +1,15 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Overtime Type', {
setup: function(frm) {
frm.set_query("applicable_for", () => {
let doctype_list = ["Employee", "Department", "Employee Grade"];
return {
filters: {
name: ["in", doctype_list]
}
};
});
}
});

View File

@@ -0,0 +1,150 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2021-05-25 12:49:09.178306",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"applicable_for",
"employee",
"department",
"employee_grade",
"column_break_3",
"applicable_salary_component",
"pay_rate_multipliers_section",
"standard_multiplier",
"applicable_for_weekend",
"weekend_multiplier",
"column_break_9",
"applicable_for_public_holiday",
"public_holiday_multiplier"
],
"fields": [
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "applicable_salary_component",
"fieldtype": "Table MultiSelect",
"label": "Applicable Salary Component",
"options": "Overtime Salary Component",
"reqd": 1
},
{
"description": "Pay Rate Multipliers apply to the hourly wage for the position you\u2019re working during the overtime hours.",
"fieldname": "pay_rate_multipliers_section",
"fieldtype": "Section Break",
"label": "Pay Rate Multipliers"
},
{
"fieldname": "standard_multiplier",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Standard Multiplier",
"reqd": 1
},
{
"default": "0",
"description": "If unchecked, the standard multiplier will be taken as default for the weekend.\n",
"fieldname": "applicable_for_weekend",
"fieldtype": "Check",
"label": "Applicable for Weekend"
},
{
"depends_on": "eval: doc.applicable_for_weekend == 1",
"fieldname": "weekend_multiplier",
"fieldtype": "Float",
"label": "Weekend Multiplier",
"mandatory_depends_on": "eval: doc.applicable_for_weekend == 1"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If unchecked, the standard multiplier will be taken as default for Public Holiday.",
"fieldname": "applicable_for_public_holiday",
"fieldtype": "Check",
"label": "Applicable for Public Holiday"
},
{
"depends_on": "eval: doc.applicable_for_public_holiday == 1",
"fieldname": "public_holiday_multiplier",
"fieldtype": "Float",
"label": "Public Holiday Multiplier",
"mandatory_depends_on": "eval: doc.applicable_for_public_holiday == 1"
},
{
"fieldname": "applicable_for",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Applicable For",
"options": "DocType",
"reqd": 1
},
{
"depends_on": "eval: doc.applicable_for == \"Employee\"",
"fieldname": "employee",
"fieldtype": "Link",
"label": "Employee",
"mandatory_depends_on": "eval: doc.applicable_for == \"Employee\"",
"options": "Employee"
},
{
"depends_on": "eval: doc.applicable_for == \"Department\"",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"mandatory_depends_on": "eval: doc.applicable_for == \"Department\"",
"options": "Department"
},
{
"depends_on": "eval: doc.applicable_for == \"Employee Grade\"",
"fieldname": "employee_grade",
"fieldtype": "Link",
"label": "Employee Grade",
"mandatory_depends_on": "eval: doc.applicable_for == \"Employee Grade\"",
"options": "Employee Grade"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "Attendance",
"link_fieldname": "overtime_type"
},
{
"link_doctype": "Timesheet",
"link_fieldname": "overtime_type"
},
{
"link_doctype": "Overtime Details",
"link_fieldname": "overtime_type"
}
],
"modified": "2021-08-11 11:29:52.944112",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Overtime Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class OvertimeType(Document):
pass

View File

@@ -0,0 +1,41 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
import unittest
class TestOvertimeType(unittest.TestCase):
pass
def create_overtime_type(**args):
args = frappe._dict(args)
overtime_type = frappe.new_doc("Overtime Type")
overtime_type.name = "Test Overtime"
overtime_type.applicable_for = args.applicable_for or "Employee"
if overtime_type.applicable_for == "Department":
overtime_type.department = args.department
elif overtime_type.applicable_for == "Employee Grade":
overtime_type.employee_grade = args.employee_grade
else:
overtime_type.employee = args.employee
overtime_type.standard_multiplier = 1.25
overtime_type.applicable_for_weekend = args.applicable_for_weekend or 0
overtime_type.applicable_for_public_holiday = args.applicable_for_public_holiday or 0
if args.applicable_for_weekend:
overtime_type.weekend_multiplier = 1.5
if args.applicable_for_public_holidays:
overtime_type.public_holiday_multiplier = 2
overtime_type.append("applicable_salary_component", {
"salary_component": "Basic Salary"
})
overtime_type.save()
return overtime_type

View File

@@ -14,6 +14,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_comp
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment
from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts
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.salary_slip.test_salary_slip import get_salary_component_for_overtime
test_dependencies = ['Holiday List']
@@ -21,6 +22,9 @@ class TestPayrollEntry(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List')
frappe.db.set_value("Payroll Settings", None, "overtime_salary_component", "Overtime Allowance")
get_salary_component_for_overtime()
def setUp(self):
for dt in ["Salary Slip", "Salary Component", "Salary Component Account",

View File

@@ -16,7 +16,13 @@
"email_salary_slip_to_employee",
"encrypt_salary_slips_in_emails",
"show_leave_balances_in_salary_slip",
"password_policy"
"password_policy",
"section_break_12",
"overtime_based_on",
"maximum_overtime_hours_allowed",
"column_break_14",
"overtime_salary_component",
"fetch_standard_working_hours_from_shift_type"
],
"fields": [
{
@@ -91,13 +97,49 @@
"fieldname": "show_leave_balances_in_salary_slip",
"fieldtype": "Check",
"label": "Show Leave Balances in Salary Slip"
},
{
"fieldname": "overtime_salary_component",
"fieldtype": "Link",
"label": "Overtime Salary Component",
"options": "Salary Component"
},
{
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Overtime Calculation"
},
{
"default": "0",
"depends_on": "eval: doc.overtime_based_on == \"Attendance\"",
"description": "If unchecked, Standard Working Hours as defined in HR Settings will be taken into consideration.",
"fieldname": "fetch_standard_working_hours_from_shift_type",
"fieldtype": "Check",
"label": "Fetch Standard Working Hours from Shift Type"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"default": "Attendance",
"fieldname": "overtime_based_on",
"fieldtype": "Select",
"label": "Calculate Overtime Based On",
"options": "Attendance\nTimesheet"
},
{
"description": "Overtime payment will not be given for more than the defined hours limit.",
"fieldname": "maximum_overtime_hours_allowed",
"fieldtype": "Int",
"label": "Maximum Overtime Hours Allowed For Payment "
}
],
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-03-03 17:49:59.579723",
"modified": "2021-08-11 11:45:10.568381",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Settings",

View File

@@ -11,6 +11,7 @@
"amount",
"year_to_date",
"section_break_5",
"overtime_slips",
"additional_salary",
"statistical_component",
"depends_on_payment_days",
@@ -235,11 +236,25 @@
"label": "Year To Date",
"options": "currency",
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary",
"fieldname": "is_recurring_additional_salary",
"fieldtype": "Check",
"label": "Is Recurring Additional Salary",
"read_only": 1
},
{
"fieldname": "overtime_slips",
"fieldtype": "Small Text",
"label": "Overtime Slip(s)",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-01-14 13:39:15.847158",
"modified": "2021-08-09 17:00:13.386980",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",

View File

@@ -423,7 +423,7 @@
{
"fieldname": "net_pay_info",
"fieldtype": "Section Break",
"label": "net pay info"
"label": "Net Pay Info"
},
{
"fieldname": "net_pay",
@@ -630,8 +630,13 @@
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 22:44:09.772331",
"links": [
{
"link_doctype": "Overtime Slip",
"link_fieldname": "salary_slip"
}
],
"modified": "2021-08-11 13:35:14.650988",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",

View File

@@ -82,8 +82,23 @@ class SalarySlip(TransactionBase):
if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
self.email_salary_slip()
self.update_overtime_slip()
self.update_payment_status_for_gratuity()
def update_overtime_slip(self):
overtime_slips = []
for data in self.earnings:
if data.overtime_slips:
overtime_slips.extend(data.overtime_slips.split(", "))
if self.docstatus == 1:
for slip in overtime_slips:
frappe.db.set_value("Overtime Slip", slip, "salary_slip", self.name)
if self.docstatus == 2:
for slip in overtime_slips:
frappe.db.set_value("Overtime Slip", slip, "salary_slip", None)
def update_payment_status_for_gratuity(self):
add_salary = frappe.db.get_all("Additional Salary",
filters = {
@@ -101,6 +116,7 @@ class SalarySlip(TransactionBase):
def on_cancel(self):
self.set_status()
self.update_status()
self.update_overtime_slip
self.update_payment_status_for_gratuity()
self.cancel_loan_repayment_entry()
@@ -336,9 +352,9 @@ class SalarySlip(TransactionBase):
return payment_days
def get_holidays_for_employee(self, start_date, end_date):
def get_holidays_for_employee(self, start_date, end_date, as_dict = 0):
holiday_list = get_holiday_list_for_employee(self.employee)
holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
holidays = frappe.db.sql('''select holiday_date, weekly_off from `tabHoliday`
where
parent=%(holiday_list)s
and holiday_date >= %(start_date)s
@@ -346,11 +362,12 @@ class SalarySlip(TransactionBase):
"holiday_list": holiday_list,
"start_date": start_date,
"end_date": end_date
})
holidays = [cstr(i) for i in holidays]
return holidays
}, as_dict=1)
if as_dict:
return holidays
else:
holidays = [cstr(data.holiday_date)for data in holidays]
return holidays
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
lwp = 0
@@ -496,6 +513,7 @@ class SalarySlip(TransactionBase):
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
self.add_structure_components(component_type)
self.process_overtime_slips()
self.add_additional_salary_components(component_type)
if component_type == "earnings":
self.add_employee_benefits(payroll_period)
@@ -509,6 +527,130 @@ class SalarySlip(TransactionBase):
if amount and struct_row.statistical_component == 0:
self.update_component_row(struct_row, amount, component_type)
def process_overtime_slips(self):
overtime_slips = self.get_overtime_slips()
amounts, processed_overtime_slips = self.get_overtime_type_details_and_amount(overtime_slips)
self.add_overtime_component(amounts, processed_overtime_slips)
def get_overtime_slips(self):
return frappe.get_all("Overtime Slip", filters = {
'employee': self.employee,
'posting_date': ("between", [self.start_date, self.end_date]),
'salary_slip': '',
'docstatus': 1
}, fields = ["name", "from_date", 'to_date'])
def get_overtime_type_details_and_amount(self, overtime_slips):
standard_duration_amount, weekends_duration_amount = 0, 0
public_holidays_duration_amount, calculated_amount = 0, 0
processed_overtime_slips = []
overtime_types_details = {}
for slip in overtime_slips:
holiday_date_map = self.get_holiday_map(slip.from_date, slip.to_date)
details = self.get_overtime_details(slip.name)
for detail in details:
overtime_hours = detail.overtime_duration / 3600
overtime_types_details = self.set_overtime_types_details(overtime_types_details, detail)
standard_working_hours = detail.standard_working_time/3600
applicable_hourly_wages = overtime_types_details[detail.overtime_type]["applicable_daily_amount"]/standard_working_hours
weekend_multiplier, public_holiday_multiplier = self.get_multipliers(overtime_types_details, detail)
overtime_date = cstr(detail.date)
if overtime_date in holiday_date_map.keys():
if holiday_date_map[overtime_date].weekly_off == 1:
calculated_amount = overtime_hours * applicable_hourly_wages * weekend_multiplier
weekends_duration_amount += calculated_amount
elif holiday_date_map[overtime_date].weekly_off == 0:
calculated_amount = overtime_hours * applicable_hourly_wages * public_holiday_multiplier
public_holidays_duration_amount += calculated_amount
else:
calculated_amount = overtime_hours * applicable_hourly_wages *\
overtime_types_details[detail.overtime_type]['standard_multiplier']
standard_duration_amount += calculated_amount
processed_overtime_slips.append(slip.name)
return [weekends_duration_amount, public_holidays_duration_amount, standard_duration_amount] , processed_overtime_slips
def get_multipliers(self, overtime_types_details, detail):
weekend_multiplier = overtime_types_details[detail.overtime_type]['standard_multiplier']
public_holiday_multiplier = overtime_types_details[detail.overtime_type]['standard_multiplier']
if overtime_types_details[detail.overtime_type]['applicable_for_weekend']:
weekend_multiplier = overtime_types_details[detail.overtime_type]['weekend_multiplier']
if overtime_types_details[detail.overtime_type]['applicable_for_public_holiday']:
public_holiday_multiplier = overtime_types_details[detail.overtime_type]['public_holiday_multiplier']
return weekend_multiplier, public_holiday_multiplier
def get_holiday_map(self, from_date, to_date):
holiday_date = self.get_holidays_for_employee(from_date, to_date, as_dict=1)
holiday_date_map = {}
for date in holiday_date:
holiday_date_map[cstr(date.holiday_date)] = date
return holiday_date_map
def set_overtime_types_details(self, overtime_types_details, detail):
if detail.overtime_type not in overtime_types_details:
details, applicable_components = self.get_overtime_type_detail(detail.overtime_type)
overtime_types_details[detail.overtime_type] = details
if len(applicable_components):
overtime_types_details[detail.overtime_type]["components"] = applicable_components
else:
frappe.throw(_("Select applicable components in Overtime Type: {0}").format(
frappe.bold(detail.overtime_type)))
if "applicable_amount" not in overtime_types_details[detail.overtime_type].keys():
component_amount = sum([data.default_amount for data in self.earnings if data.salary_component in
overtime_types_details[detail.overtime_type]["components"] and not data.get('additional_salary', None)])
overtime_types_details[detail.overtime_type]["applicable_daily_amount"] = component_amount/self.total_working_days
return overtime_types_details
def add_overtime_component(self, amounts, processed_overtime_slips):
if len(amounts):
overtime_salary_component = frappe.db.get_single_value("Payroll Settings", "overtime_salary_component")
if not overtime_salary_component:
frappe.throw(_('Select {0} in {1}').format(
frappe.bold("Overtime Salary Component"), frappe.bold("Payroll Settings")
))
else:
component_data = frappe._dict(get_salary_component_data(overtime_salary_component) or {})
component_data.salary_component = overtime_salary_component
self.update_component_row(
component_data,
sum(amounts),
'earnings',
processed_overtime_slips = processed_overtime_slips
)
def get_overtime_details(self, parent):
return frappe.get_all(
"Overtime Details",
filters = {"parent": parent},
fields = ["date", "overtime_type", "overtime_duration", "standard_working_time"]
)
def get_overtime_type_detail(self, name):
detail = frappe.get_all("Overtime Type",
filters = {"name": name},
fields = ["name", "standard_multiplier", "weekend_multiplier", "public_holiday_multiplier",
"applicable_for_weekend", "applicable_for_public_holiday"]
)[0]
components = frappe.get_all("Overtime Salary Component",
filters = {"parent": name}, fields = ["salary_component"])
components = [data. salary_component for data in components]
return detail, components
def get_data_for_eval(self):
'''Returns data for evaluating formula'''
data = frappe._dict()
@@ -639,7 +781,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions")
def update_component_row(self, component_data, amount, component_type, additional_salary=None):
def update_component_row(self, component_data, amount, component_type, additional_salary=None, processed_overtime_slips =[], is_recurring=0):
component_row = None
for d in self.get(component_type):
if d.salary_component != component_data.salary_component:
@@ -679,6 +821,10 @@ class SalarySlip(TransactionBase):
abbr = component_data.get('abbr') or component_data.get('salary_component_abbr')
component_row.set('abbr', abbr)
processed_overtime_slips = ", ".join(processed_overtime_slips)
if processed_overtime_slips:
component_row.overtime_slips = processed_overtime_slips
if additional_salary:
component_row.default_amount = 0
component_row.additional_amount = amount

View File

@@ -23,6 +23,8 @@ class TestSalarySlip(unittest.TestCase):
def tearDown(self):
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.db.sql("DELETE FROM `tabOvertime Type`")
frappe.db.sql("DELETE FROM `tabOvertime Slip`")
frappe.set_user("Administrator")
def test_payment_days_based_on_attendance(self):
@@ -460,6 +462,49 @@ class TestSalarySlip(unittest.TestCase):
# undelete fixture data
frappe.db.rollback()
def test_overtime_calculation(self):
from erpnext.payroll.doctype.overtime_type.test_overtime_type import create_overtime_type
from erpnext.payroll.doctype.overtime_slip.test_overtime_slip import create_overtime_slip
from erpnext.payroll.doctype.overtime_slip.test_overtime_slip import create_attendance_records_for_overtime
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
employee = make_employee("overtime_calc@salary.slip")
salary_structure = make_salary_structure("structure for Overtime 2", "Monthly", employee=employee)
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 0)
get_salary_component_for_overtime()
frappe.db.set_value("Payroll Settings", None, "overtime_salary_component", "Overtime Allowance")
overtime_type = create_overtime_type(employee = employee).name
create_attendance_records_for_overtime(employee, overtime_type=overtime_type)
slip = create_overtime_slip(employee)
slip.status = "Approved"
slip.submit()
salary_slip = make_salary_slip(salary_structure.name, employee = employee)
overtime_component_details = {}
applicable_amount = 0
for earning in salary_slip.earnings:
if earning.salary_component == "Overtime Allowance":
overtime_component_details = earning
if earning.salary_component == "Basic Salary":
applicable_amount = earning.default_amount
self.assertIn("Overtime Allowance", overtime_component_details.salary_component)
self.assertEqual(slip.name, overtime_component_details.overtime_slips)
daily_wages = applicable_amount/ salary_slip.total_working_days
hourly_wages = daily_wages/ frappe.db.get_single_value("Hr Settings", "standard_working_hours")
overtime_amount = hourly_wages * 4 * 1.25
#since multiplier is defined as 1.25
# formula = sum(applicable_component)/(working_days)/ daily_standard_working_time * overtime hours * multiplier
self.assertEquals(flt(overtime_amount, 2), flt(overtime_component_details.amount, 2))
def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50
@@ -475,6 +520,17 @@ class TestSalarySlip(unittest.TestCase):
return [no_of_days_in_month[1], no_of_holidays_in_month]
def get_salary_component_for_overtime():
component = [{
"salary_component": 'Overtime Allowance',
"abbr":'OA',
"type": "Earning",
"amount_based_on_formula": 0
}]
company = erpnext.get_default_company()
make_salary_component(component, test_tax = 0, company_list=[company])
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
@@ -482,12 +538,8 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
employee = frappe.db.get_value("Employee",
{
"user_id": user
},
["name", "company", "employee_name"],
as_dict=True)
employee = frappe.db.get_value("Employee", {"user_id": user},
["name", "company", "employee_name"], as_dict=True)
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})

View File

@@ -127,7 +127,8 @@ class TestTimesheet(unittest.TestCase):
"activity_type": "_Test Activity Type",
"from_time": now_datetime(),
"to_time": now_datetime() + datetime.timedelta(hours=3),
"company": "_Test Company"
"company": "_Test Company",
"hours": 3
}
)
timesheet.append(
@@ -137,7 +138,8 @@ class TestTimesheet(unittest.TestCase):
"activity_type": "_Test Activity Type",
"from_time": now_datetime(),
"to_time": now_datetime() + datetime.timedelta(hours=3),
"company": "_Test Company"
"company": "_Test Company",
"hours": 3
}
)

View File

@@ -2,20 +2,20 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Timesheet", {
setup: function(frm) {
setup: function (frm) {
frappe.require("/assets/erpnext/js/projects/timer.js");
frm.add_fetch('employee', 'employee_name', 'employee_name');
frm.fields_dict.employee.get_query = function() {
frm.fields_dict.employee.get_query = function () {
return {
filters:{
filters: {
'status': 'Active'
}
};
};
frm.fields_dict['time_logs'].grid.get_field('task').get_query = function(frm, cdt, cdn) {
frm.fields_dict['time_logs'].grid.get_field('task').get_query = function (frm, cdt, cdn) {
var child = locals[cdt][cdn];
return{
return {
filters: {
'project': child.project,
'status': ["!=", "Cancelled"]
@@ -23,8 +23,8 @@ frappe.ui.form.on("Timesheet", {
};
};
frm.fields_dict['time_logs'].grid.get_field('project').get_query = function() {
return{
frm.fields_dict['time_logs'].grid.get_field('project').get_query = function () {
return {
filters: {
'company': frm.doc.company
}
@@ -32,7 +32,7 @@ frappe.ui.form.on("Timesheet", {
};
},
onload: function(frm){
onload: function (frm) {
if (frm.doc.__islocal && frm.doc.time_logs) {
calculate_time_and_amount(frm);
}
@@ -42,33 +42,37 @@ frappe.ui.form.on("Timesheet", {
}
},
refresh: function(frm) {
if(frm.doc.docstatus==1) {
if(frm.doc.per_billed < 100 && frm.doc.total_billable_hours && frm.doc.total_billable_hours > frm.doc.total_billed_hours){
frm.add_custom_button(__('Create Sales Invoice'), function() { frm.trigger("make_invoice") },
"fa fa-file-text");
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
if (frm.doc.per_billed < 100 && frm.doc.total_billable_hours && frm.doc.total_billable_hours > frm.doc.total_billed_hours) {
frm.add_custom_button(__('Create Sales Invoice'), function () {
frm.trigger("make_invoice");
},
"fa fa-file-text");
}
if(!frm.doc.salary_slip && frm.doc.employee){
frm.add_custom_button(__('Create Salary Slip'), function() { frm.trigger("make_salary_slip") },
"fa fa-file-text");
if (!frm.doc.salary_slip && frm.doc.employee) {
frm.add_custom_button(__('Create Salary Slip'), function () {
frm.trigger("make_salary_slip");
},
"fa fa-file-text");
}
}
if (frm.doc.docstatus < 1) {
if (frm.doc.docstatus < 1 && !cur_frm.doc.__islocal) {
let button = 'Start Timer';
$.each(frm.doc.time_logs || [], function(i, row) {
$.each(frm.doc.time_logs || [], function (i, row) {
if ((row.from_time <= frappe.datetime.now_datetime()) && !row.completed) {
button = 'Resume Timer';
}
});
frm.add_custom_button(__(button), function() {
frm.add_custom_button(__(button), function () {
var flag = true;
$.each(frm.doc.time_logs || [], function(i, row) {
$.each(frm.doc.time_logs || [], function (i, row) {
// Fetch the row for which from_time is not present
if (flag && row.activity_type && !row.from_time){
if (flag && row.activity_type && !row.from_time) {
erpnext.timesheet.timer(frm, row);
row.from_time = frappe.datetime.now_datetime();
frm.refresh_fields("time_logs");
@@ -77,7 +81,7 @@ frappe.ui.form.on("Timesheet", {
}
// Fetch the row for timer where activity is not completed and from_time is before now_time
if (flag && row.from_time <= frappe.datetime.now_datetime() && !row.completed) {
let timestamp = moment(frappe.datetime.now_datetime()).diff(moment(row.from_time),"seconds");
let timestamp = moment(frappe.datetime.now_datetime()).diff(moment(row.from_time), "seconds");
erpnext.timesheet.timer(frm, row, timestamp);
flag = false;
}
@@ -88,7 +92,7 @@ frappe.ui.form.on("Timesheet", {
}
}).addClass("btn-primary");
}
if(frm.doc.per_billed > 0) {
if (frm.doc.per_billed > 0) {
frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false);
frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false);
}
@@ -96,15 +100,15 @@ frappe.ui.form.on("Timesheet", {
frm.trigger('set_dynamic_field_label');
},
customer: function(frm) {
frm.set_query('parent_project', function(doc) {
customer: function (frm) {
frm.set_query('parent_project', function (doc) {
return {
filters: {
"customer": doc.customer
}
};
});
frm.set_query('project', 'time_logs', function(doc) {
frm.set_query('project', 'time_logs', function (doc) {
return {
filters: {
"customer": doc.customer
@@ -114,7 +118,7 @@ frappe.ui.form.on("Timesheet", {
frm.refresh();
},
currency: function(frm) {
currency: function (frm) {
let base_currency = frappe.defaults.get_global_default('currency');
if (base_currency != frm.doc.currency) {
frappe.call({
@@ -123,7 +127,7 @@ frappe.ui.form.on("Timesheet", {
from_currency: frm.doc.currency,
to_currency: base_currency
},
callback: function(r) {
callback: function (r) {
if (r.message) {
frm.set_value('exchange_rate', flt(r.message));
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + " = [?] " + base_currency);
@@ -134,14 +138,14 @@ frappe.ui.form.on("Timesheet", {
frm.trigger('set_dynamic_field_label');
},
exchange_rate: function(frm) {
$.each(frm.doc.time_logs, function(i, d) {
exchange_rate: function (frm) {
$.each(frm.doc.time_logs, function (i, d) {
calculate_billing_costing_amount(frm, d.doctype, d.name);
});
calculate_time_and_amount(frm);
},
set_dynamic_field_label: function(frm) {
set_dynamic_field_label: function (frm) {
let base_currency = frappe.defaults.get_global_default('currency');
frm.set_currency_labels(["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"], base_currency);
frm.set_currency_labels(["total_costing_amount", "total_billable_amount", "total_billed_amount"], frm.doc.currency);
@@ -154,7 +158,7 @@ frappe.ui.form.on("Timesheet", {
frm.set_currency_labels(["billing_rate", "billing_amount", "costing_rate", "costing_amount"], frm.doc.currency, "time_logs");
let time_logs_grid = frm.fields_dict.time_logs.grid;
$.each(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], function(i, d) {
$.each(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], function (i, d) {
if (frappe.meta.get_docfield(time_logs_grid.doctype, d))
time_logs_grid.set_column_disp(d, frm.doc.currency != base_currency);
});
@@ -162,7 +166,7 @@ frappe.ui.form.on("Timesheet", {
frm.refresh_fields();
},
make_invoice: function(frm) {
make_invoice: function (frm) {
let fields = [{
"fieldtype": "Link",
"label": __("Item Code"),
@@ -187,7 +191,7 @@ frappe.ui.form.on("Timesheet", {
dialog.set_primary_action(__('Create Sales Invoice'), () => {
var args = dialog.get_values();
if(!args) return;
if (!args) return;
dialog.hide();
return frappe.call({
type: "GET",
@@ -199,8 +203,8 @@ frappe.ui.form.on("Timesheet", {
"currency": frm.doc.currency
},
freeze: true,
callback: function(r) {
if(!r.exc) {
callback: function (r) {
if (!r.exc) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
@@ -210,20 +214,20 @@ frappe.ui.form.on("Timesheet", {
dialog.show();
},
make_salary_slip: function(frm) {
make_salary_slip: function (frm) {
frappe.model.open_mapped_doc({
method: "erpnext.projects.doctype.timesheet.timesheet.make_salary_slip",
frm: frm
});
},
parent_project: function(frm) {
parent_project: function (frm) {
set_project_in_timelog(frm);
}
});
frappe.ui.form.on("Timesheet Detail", {
time_logs_remove: function(frm) {
time_logs_remove: function (frm) {
calculate_time_and_amount(frm);
},
@@ -236,54 +240,61 @@ frappe.ui.form.on("Timesheet Detail", {
}
},
from_time: function(frm, cdt, cdn) {
from_time: function (frm, cdt, cdn) {
calculate_end_time(frm, cdt, cdn);
},
to_time: function(frm, cdt, cdn) {
to_time: function (frm, cdt, cdn) {
var child = locals[cdt][cdn];
if(frm._setting_hours) return;
if (frm._setting_hours) return;
var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600;
frappe.model.set_value(cdt, cdn, "hours", hours);
},
time_logs_add: function(frm, cdt, cdn) {
if(frm.doc.parent_project) {
time_logs_add: function (frm, cdt, cdn) {
if (frm.doc.parent_project) {
frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project);
}
},
hours: function(frm, cdt, cdn) {
hours: function (frm, cdt, cdn) {
calculate_end_time(frm, cdt, cdn);
calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm);
},
billing_hours: function(frm, cdt, cdn) {
billing_hours: function (frm, cdt, cdn) {
calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm);
},
billing_rate: function(frm, cdt, cdn) {
billing_rate: function (frm, cdt, cdn) {
calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm);
},
costing_rate: function(frm, cdt, cdn) {
costing_rate: function (frm, cdt, cdn) {
calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm);
},
is_billable: function(frm, cdt, cdn) {
is_billable: function (frm, cdt, cdn) {
update_billing_hours(frm, cdt, cdn);
update_time_rates(frm, cdt, cdn);
calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm);
},
activity_type: function(frm, cdt, cdn) {
is_overtime: function(frm, cdt, cdn) {
let child = locals[cdt][cdn];
if (child.is_overtime) {
get_overtime_type(frm, cdt, cdn);
}
},
activity_type: function (frm, cdt, cdn) {
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: {
@@ -291,8 +302,8 @@ frappe.ui.form.on("Timesheet Detail", {
activity_type: frm.selected_doc.activity_type,
currency: frm.doc.currency
},
callback: function(r){
if(r.message){
callback: function (r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, 'billing_rate', r.message['billing_rate']);
frappe.model.set_value(cdt, cdn, 'costing_rate', r.message['costing_rate']);
calculate_billing_costing_amount(frm, cdt, cdn);
@@ -302,16 +313,38 @@ frappe.ui.form.on("Timesheet Detail", {
}
});
var calculate_end_time = function(frm, cdt, cdn) {
var get_overtime_type = function(frm, cdt, cdn) {
if (frm.doc.employee) {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.get_overtime_type",
args: {
employee: frm.doc.employee
},
callback: function (r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, 'overtime_type', r.message);
} else {
frappe.model.set_value(cdt, cdn, 'is_overtime', 0);
frappe.throw(__("Define Overtime Type for Employee "+frm.doc.employee+" "));
}
}
});
} else {
frappe.model.set_value(cdt, cdn, 'is_overtime', 0);
frappe.throw({message: __("Select Employee if applicable for overtime"), title: "Employee Missing"});
}
};
var calculate_end_time = function (frm, cdt, cdn) {
let child = locals[cdt][cdn];
if(!child.from_time) {
if (!child.from_time) {
// if from_time value is not available then set the current datetime
frappe.model.set_value(cdt, cdn, "from_time", frappe.datetime.get_datetime_as_string());
}
let d = moment(child.from_time);
if(child.hours) {
if (child.hours) {
d.add(child.hours, "hours");
frm._setting_hours = true;
frappe.model.set_value(cdt, cdn, "to_time",
@@ -321,7 +354,7 @@ var calculate_end_time = function(frm, cdt, cdn) {
}
};
var update_billing_hours = function(frm, cdt, cdn) {
var update_billing_hours = function (frm, cdt, cdn) {
let child = frappe.get_doc(cdt, cdn);
if (!child.is_billable) {
frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0);
@@ -331,14 +364,14 @@ var update_billing_hours = function(frm, cdt, cdn) {
}
};
var update_time_rates = function(frm, cdt, cdn) {
var update_time_rates = function (frm, cdt, cdn) {
let child = frappe.get_doc(cdt, cdn);
if (!child.is_billable) {
frappe.model.set_value(cdt, cdn, 'billing_rate', 0.0);
}
};
var calculate_billing_costing_amount = function(frm, cdt, cdn) {
var calculate_billing_costing_amount = function (frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
let billing_amount = 0.0;
let base_billing_amount = 0.0;
@@ -356,13 +389,13 @@ var calculate_billing_costing_amount = function(frm, cdt, cdn) {
frappe.model.set_value(cdt, cdn, 'costing_amount', flt(row.costing_rate) * flt(row.hours));
};
var calculate_time_and_amount = function(frm) {
var calculate_time_and_amount = function (frm) {
let tl = frm.doc.time_logs || [];
let total_working_hr = 0;
let total_billing_hr = 0;
let total_billable_amount = 0;
let total_costing_amount = 0;
for(var i=0; i<tl.length; i++) {
for (var i = 0; i < tl.length; i++) {
if (tl[i].hours) {
total_working_hr += tl[i].hours;
total_billable_amount += tl[i].billing_amount;
@@ -381,10 +414,14 @@ var calculate_time_and_amount = function(frm) {
};
// set employee (and company) to the one that's currently logged in
const set_employee_and_company = function(frm) {
const options = { user_id: frappe.session.user };
const set_employee_and_company = function (frm) {
const options = {
user_id: frappe.session.user
};
const fields = ['name', 'company'];
frappe.db.get_value('Employee', options, fields).then(({ message }) => {
frappe.db.get_value('Employee', options, fields).then(({
message
}) => {
if (message) {
// there is an employee with the currently logged in user_id
frm.set_value("employee", message.name);
@@ -394,9 +431,9 @@ const set_employee_and_company = function(frm) {
};
function set_project_in_timelog(frm) {
if(frm.doc.parent_project) {
$.each(frm.doc.time_logs || [], function(i, item) {
if (frm.doc.parent_project) {
$.each(frm.doc.time_logs || [], function (i, item) {
frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project);
});
}
}
}

View File

@@ -14,11 +14,11 @@
"customer",
"currency",
"exchange_rate",
"sales_invoice",
"column_break_3",
"salary_slip",
"status",
"parent_project",
"salary_slip",
"sales_invoice",
"employee_detail",
"employee",
"employee_name",
@@ -29,7 +29,10 @@
"end_date",
"section_break_5",
"time_logs",
"working_hours",
"overtime_details_section",
"overtime_type",
"total_overtime_hours",
"column_break_26",
"total_hours",
"billing_details",
"total_billable_hours",
@@ -116,7 +119,8 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee"
"options": "Employee",
"reqd": 1
},
{
"depends_on": "employee",
@@ -173,10 +177,6 @@
"options": "Timesheet Detail",
"reqd": 1
},
{
"fieldname": "working_hours",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"default": "0",
@@ -314,13 +314,36 @@
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate"
},
{
"depends_on": "eval: doc.total_overtime_hours",
"fieldname": "overtime_details_section",
"fieldtype": "Section Break",
"label": "Overtime Details"
},
{
"fieldname": "overtime_type",
"fieldtype": "Link",
"label": "Overtime Type",
"options": "Overtime Type",
"read_only": 1
},
{
"fieldname": "total_overtime_hours",
"fieldtype": "Float",
"label": "Total Overtime Hours",
"read_only": 1
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-clock-o",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-09 12:08:53.930200",
"modified": "2021-06-17 17:45:12.064677",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",

View File

@@ -4,16 +4,12 @@
from __future__ import unicode_literals
import frappe
from frappe import _
import json
from frappe import _
from datetime import timedelta
from erpnext.controllers.queries import get_match_cond
from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint, date_diff, add_to_date
from frappe.utils import flt, time_diff_in_hours, getdate
from frappe.model.document import Document
from erpnext.manufacturing.doctype.workstation.workstation import (check_if_within_operating_hours,
WorkstationHolidayError)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.setup.utils import get_exchange_rate
from erpnext.hr.utils import validate_active_employee
@@ -31,6 +27,7 @@ class Timesheet(Document):
self.update_cost()
self.calculate_total_amounts()
self.calculate_percentage_billed()
self.validate_overtime()
self.set_dates()
def set_employee_name(self):
@@ -65,6 +62,43 @@ class Timesheet(Document):
if self.total_billed_amount > 0 and self.total_billable_amount > 0:
self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount
def validate_overtime(self):
total_overtime_hours= 0
overtime_type = None
for data in self.time_logs:
overtime_type = data.overtime_type
if data.is_overtime:
if frappe.db.get_single_value("Payroll Settings", "overtime_based_on") == "Timesheet":
if not self.employee:
frappe.throw(_("Select Employee, if applicable for overtime"))
if not data.overtime_type:
frappe.throw(_("Define Overtime Type for Employee {0}").format(self.employee))
if data.overtime_on:
if data.overtime_on <= data.from_time or data.overtime_on >= data.to_time:
frappe.throw(_("Row {0}: {3} should be within {1} and {2}").format(
str(data.idx),
data.from_time,
data.to_time,
frappe.bold("Overtime On"))
)
maximum_overtime_hours_allowed = frappe.db.get_single_value("Payroll Settings", "maximum_overtime_hours_allowed")
if int(maximum_overtime_hours_allowed) and data.overtime_hours > maximum_overtime_hours_allowed:
frappe.throw(_("Row {0}: Overtime Hours can not be greater than {1} for a day. You can change this in Payroll Settings").
format(
str(data.idx),
frappe.bold(str(maximum_overtime_hours_allowed))
))
else:
total_overtime_hours += data.overtime_hours
else:
frappe.throw(_('Please Set "Calculate Overtime Based On" to TimeSheet In Payroll Settings'))
if total_overtime_hours:
self.total_overtime_hours = total_overtime_hours
self.overtime_type =overtime_type
def update_billing_hours(self, args):
if args.is_billable:
if flt(args.billing_hours) == 0.0:
@@ -104,20 +138,8 @@ class Timesheet(Document):
self.update_task_and_project()
def on_submit(self):
self.validate_mandatory_fields()
self.update_task_and_project()
def validate_mandatory_fields(self):
for data in self.time_logs:
if not data.from_time and not data.to_time:
frappe.throw(_("Row {0}: From Time and To Time is mandatory.").format(data.idx))
if not data.activity_type and self.employee:
frappe.throw(_("Row {0}: Activity Type is mandatory.").format(data.idx))
if flt(data.hours) == 0.0:
frappe.throw(_("Row {0}: Hours value must be greater than zero.").format(data.idx))
def update_task_and_project(self):
tasks, projects = [], []

View File

@@ -14,10 +14,16 @@
"to_time",
"hours",
"completed",
"section_break_9",
"is_overtime",
"overtime_type",
"column_break_12",
"overtime_on",
"overtime_hours",
"section_break_7",
"completed_qty",
"workstation",
"column_break_12",
"column_break_18",
"operation",
"operation_id",
"project_details",
@@ -47,14 +53,16 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Activity Type",
"options": "Activity Type"
"options": "Activity Type",
"reqd": 1
},
{
"columns": 2,
"fieldname": "from_time",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "From Time"
"label": "From Time",
"reqd": 1
},
{
"fieldname": "section_break_3",
@@ -70,12 +78,14 @@
"fieldname": "hours",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Hrs"
"label": "Working Hours",
"reqd": 1
},
{
"fieldname": "to_time",
"fieldtype": "Datetime",
"label": "To Time"
"label": "To Time",
"reqd": 1
},
{
"default": "0",
@@ -262,12 +272,47 @@
"label": "Costing Amount",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "is_overtime",
"fieldtype": "Check",
"label": "Is Applicable For Overtime"
},
{
"depends_on": "eval: doc.is_overtime",
"fieldname": "overtime_on",
"fieldtype": "Date",
"label": "Overtime On",
"mandatory_depends_on": "eval: doc.is_overtime"
},
{
"depends_on": "eval: doc.is_overtime",
"fieldname": "overtime_hours",
"fieldtype": "Float",
"label": "Overtime Hours",
"mandatory_depends_on": "eval: doc.is_overtime"
},
{
"depends_on": "eval: doc.is_overtime",
"fieldname": "overtime_type",
"fieldtype": "Link",
"label": "Overtime Type",
"options": "Overtime Type"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-05-18 12:19:33.205940",
"modified": "2021-06-17 17:44:09.306304",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet Detail",

View File

@@ -98,12 +98,10 @@ erpnext.HierarchyChart = class {
company.refresh();
$(`[data-fieldname="company"]`).trigger('change');
$(`[data-fieldname="company"] .link-field`).css('z-index', 2);
}
setup_actions() {
let me = this;
this.page.clear_inner_toolbar();
this.page.add_inner_button(__('Export'), function() {
me.export_chart();
});
@@ -125,7 +123,6 @@ erpnext.HierarchyChart = class {
}
export_chart() {
frappe.dom.freeze(__('Exporting...'));
this.page.main.css({
'min-height': '',
'max-height': '',
@@ -149,8 +146,6 @@ erpnext.HierarchyChart = class {
a.href = dataURL;
a.download = 'hierarchy_chart';
a.click();
}).finally(() => {
frappe.dom.unfreeze();
});
this.setup_page_style();
@@ -174,9 +169,7 @@ erpnext.HierarchyChart = class {
this.page.main
.find('#hierarchy-chart-wrapper')
.append(this.$hierarchy);
this.nodes = {};
this.all_nodes_expanded = false;
}
make_svg_markers() {
@@ -209,7 +202,7 @@ erpnext.HierarchyChart = class {
render_root_nodes(expanded_view=false) {
let me = this;
return frappe.call({
frappe.call({
method: me.method,
args: {
company: me.company
@@ -236,8 +229,8 @@ erpnext.HierarchyChart = class {
expand_node = node;
});
me.root_node = expand_node;
if (!expanded_view) {
me.root_node = expand_node;
me.expand_node(expand_node);
}
}
@@ -287,12 +280,10 @@ erpnext.HierarchyChart = class {
]);
} else {
frappe.run_serially([
() => frappe.dom.freeze(),
() => this.setup_hierarchy(),
() => this.render_root_nodes(true),
() => this.get_all_nodes(node.id, node.name),
(data_list) => this.render_children_of_all_nodes(data_list),
() => frappe.dom.unfreeze()
(data_list) => this.render_children_of_all_nodes(data_list)
]);
}
}
@@ -368,7 +359,7 @@ erpnext.HierarchyChart = class {
node = this.nodes[entry.parent];
if (node) {
this.render_child_nodes_for_expanded_view(node, entry.data);
} else if (data_list.length) {
} else {
data_list.push(entry);
}
}

View File

@@ -206,12 +206,10 @@
margin: 0px 0px 16px 0px;
}
.hierarchy, .hierarchy-mobile {
.level {
margin-right: 8px;
align-items: flex-start;
flex-direction: column;
}
.level {
margin-right: 8px;
align-items: flex-start;
flex-direction: column;
}
#arrows {

View File

@@ -190,10 +190,8 @@ def get_item_list(invoice):
item.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty)
if flt(item.qty) != 0.0:
item.unit_rate = abs(item.taxable_value / item.qty)
else:
item.unit_rate = abs(item.taxable_value)
item.unit_rate = abs(item.taxable_value / item.qty)
item.gross_amount = abs(item.taxable_value)
item.taxable_value = abs(item.taxable_value)
item.discount_amount = 0

View File

@@ -642,8 +642,7 @@ def make_custom_fields(update=True):
'fieldtype': 'Select',
'insert_after': 'gst_category',
'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax',
'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)'
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
}
],
'Customer': [
@@ -661,8 +660,7 @@ def make_custom_fields(update=True):
'fieldtype': 'Select',
'insert_after': 'gst_category',
'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax',
'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
}
],
'Member': [

View File

@@ -130,10 +130,6 @@ frappe.ui.form.on("Customer", {
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
}, __('Create'));
frm.add_custom_button(__('Get Customer Group Details'), function () {
frm.trigger("get_customer_group_details");
}, __('Actions'));
// indicator
erpnext.utils.set_party_dashboard_indicators(frm);
@@ -149,15 +145,4 @@ frappe.ui.form.on("Customer", {
if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name);
},
get_customer_group_details: function(frm) {
frappe.call({
method: "get_customer_group_details",
doc: frm.doc,
callback: function() {
frm.refresh();
}
});
}
});
});

View File

@@ -78,29 +78,6 @@ class Customer(TransactionBase):
if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100:
frappe.throw(_("Total contribution percentage should be equal to 100"))
@frappe.whitelist()
def get_customer_group_details(self):
doc = frappe.get_doc('Customer Group', self.customer_group)
self.accounts = self.credit_limits = []
self.payment_terms = self.default_price_list = ""
tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]
fields = ["payment_terms", "default_price_list"]
for row in tables:
table, field = row[0], row[1]
if not doc.get(table): continue
for entry in doc.get(table):
child = self.append(table)
child.update({"company": entry.company, field: entry.get(field)})
for field in fields:
if not doc.get(field): continue
self.update({field: doc.get(field)})
self.save()
def check_customer_group_change(self):
frappe.flags.customer_group_changed = False

View File

@@ -27,42 +27,6 @@ class TestCustomer(unittest.TestCase):
def tearDown(self):
set_credit_limit('_Test Customer', '_Test Company', 0)
def test_get_customer_group_details(self):
doc = frappe.new_doc("Customer Group")
doc.customer_group_name = "_Testing Customer Group"
doc.payment_terms = "_Test Payment Term Template 3"
doc.accounts = []
doc.default_price_list = "Standard Buying"
doc.credit_limits = []
test_account_details = {
"company": "_Test Company",
"account": "Creditors - _TC",
}
test_credit_limits = {
"company": "_Test Company",
"credit_limit": 350000
}
doc.append("accounts", test_account_details)
doc.append("credit_limits", test_credit_limits)
doc.insert()
c_doc = frappe.new_doc("Customer")
c_doc.customer_name = "Testing Customer"
c_doc.customer_group = "_Testing Customer Group"
c_doc.payment_terms = c_doc.default_price_list = ""
c_doc.accounts = c_doc.credit_limits= []
c_doc.insert()
c_doc.get_customer_group_details()
self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3")
self.assertEqual(c_doc.accounts[0].company, "_Test Company")
self.assertEqual(c_doc.accounts[0].account, "Creditors - _TC")
self.assertEqual(c_doc.credit_limits[0].company, "_Test Company")
self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000)
c_doc.delete()
doc.delete()
def test_party_details(self):
from erpnext.accounts.party import get_party_details

View File

@@ -23,8 +23,8 @@ def search_by_term(search_term, warehouse, price_list):
item_stock_qty = get_stock_availability(item_code, warehouse)
price_list_rate, currency = frappe.db.get_value('Item Price', {
'price_list': price_list,
'item_code': item_code
'price_list': price_list,
'item_code': item_code
}, ["price_list_rate", "currency"]) or [None, None]
item_info.update({

View File

@@ -64,6 +64,12 @@ def set_default_settings(args):
hr_settings.leave_status_notification_template = _("Leave Status Notification")
hr_settings.save()
payroll_settings = frappe.get_doc("Payroll Settings")
payroll_settings.overtime_based_on = "Attendance"
payroll_settings.overtime_salary_component = _("Overtime Allowance")
payroll_settings.save()
def set_no_copy_fields_in_variant_settings():
# set no copy fields of an item doctype to item variant settings
doc = frappe.get_doc('Item Variant Settings')

View File

@@ -54,6 +54,7 @@ def install(country=None):
{'doctype': 'Salary Component', 'salary_component': _('Income Tax'), 'description': _('Income Tax'), 'type': 'Deduction', 'is_income_tax_component': 1},
{'doctype': 'Salary Component', 'salary_component': _('Basic'), 'description': _('Basic'), 'type': 'Earning'},
{'doctype': 'Salary Component', 'salary_component': _('Arrear'), 'description': _('Arrear'), 'type': 'Earning'},
{'doctype': 'Salary Component', 'salary_component': _('Overtime Allowance'), 'description': _('Overtime Allowance'), 'type': 'Earning'},
{'doctype': 'Salary Component', 'salary_component': _('Leave Encashment'), 'description': _('Leave Encashment'), 'type': 'Earning'},
@@ -286,6 +287,7 @@ def set_more_defaults():
update_selling_defaults()
update_buying_defaults()
update_hr_defaults()
update_payroll_defaults()
add_uom_data()
update_item_variant_settings()
@@ -315,6 +317,13 @@ def update_hr_defaults():
hr_settings.leave_status_notification_template = _("Leave Status Notification")
hr_settings.save()
def update_payroll_defaults():
payroll_settings = frappe.get_doc("Payroll Settings")
payroll_settings.overtime_based_on = "Attendance"
payroll_settings.overtime_salary_component = _("Overtime Allowance")
payroll_settings.save()
def update_item_variant_settings():
# set no copy fields of an item doctype to item variant settings
doc = frappe.get_doc('Item Variant Settings')
@@ -428,6 +437,7 @@ def install_post_company_fixtures(args=None):
frappe.local.flags.ignore_update_nsm = True
make_records(records[1:])
frappe.local.flags.ignore_update_nsm = False
rebuild_tree("Department", "parent_department")

View File

@@ -55,8 +55,8 @@ class StockLedgerEntry(Document):
"sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
#check for item quantity available in stock
def actual_amt_check(self):
"""Validate that qty at warehouse for selected batch is >=0"""
if self.batch_no and not self.get("allow_negative_stock"):
batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
from `tabStock Ledger Entry`
@@ -107,7 +107,7 @@ class StockLedgerEntry(Document):
self.stock_uom = item_det.stock_uom
def check_stock_frozen_date(self):
stock_settings = frappe.get_doc('Stock Settings', 'Stock Settings')
stock_settings = frappe.get_cached_doc('Stock Settings')
if stock_settings.stock_frozen_upto:
if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto)

View File

@@ -114,42 +114,14 @@ def get_period(posting_date, filters):
def get_periodic_data(entry, filters):
"""Structured as:
Item 1
- Balance (updated and carried forward):
- Warehouse A : bal_qty/value
- Warehouse B : bal_qty/value
- Jun 2021 (sum of warehouse quantities used in report)
- Warehouse A : bal_qty/value
- Warehouse B : bal_qty/value
- Jul 2021 (sum of warehouse quantities used in report)
- Warehouse A : bal_qty/value
- Warehouse B : bal_qty/value
Item 2
- Balance (updated and carried forward):
- Warehouse A : bal_qty/value
- Warehouse B : bal_qty/value
- Jun 2021 (sum of warehouse quantities used in report)
- Warehouse A : bal_qty/value
- Warehouse B : bal_qty/value
- Jul 2021 (sum of warehouse quantities used in report)
- Warehouse A : bal_qty/value
- Warehouse B : bal_qty/value
"""
periodic_data = {}
for d in entry:
period = get_period(d.posting_date, filters)
bal_qty = 0
# if period against item does not exist yet, instantiate it
# insert existing balance dict against period, and add/subtract to it
if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period):
previous_balance = periodic_data[d.item_code]['balance'].copy()
periodic_data[d.item_code][period] = previous_balance
if d.voucher_type == "Stock Reconciliation":
if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get('balance').get(d.warehouse):
bal_qty = periodic_data[d.item_code]['balance'][d.warehouse]
if periodic_data.get(d.item_code):
bal_qty = periodic_data[d.item_code]["balance"]
qty_diff = d.qty_after_transaction - bal_qty
else:
@@ -160,12 +132,12 @@ def get_periodic_data(entry, filters):
else:
value = d.stock_value_difference
# period-warehouse wise balance
periodic_data.setdefault(d.item_code, {}).setdefault('balance', {}).setdefault(d.warehouse, 0.0)
periodic_data.setdefault(d.item_code, {}).setdefault(period, {}).setdefault(d.warehouse, 0.0)
periodic_data.setdefault(d.item_code, {}).setdefault(period, 0.0)
periodic_data.setdefault(d.item_code, {}).setdefault("balance", 0.0)
periodic_data[d.item_code]["balance"] += value
periodic_data[d.item_code][period] = periodic_data[d.item_code]["balance"]
periodic_data[d.item_code]['balance'][d.warehouse] += value
periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]['balance'][d.warehouse]
return periodic_data
@@ -188,8 +160,7 @@ def get_data(filters):
total = 0
for dummy, end_date in ranges:
period = get_period(end_date, filters)
period_data = periodic_data.get(item_data.name, {}).get(period)
amount = sum(period_data.values()) if period_data else 0
amount = flt(periodic_data.get(item_data.name, {}).get(period))
row[scrub(period)] = amount
total += amount
row["total"] = total

View File

@@ -235,15 +235,12 @@ def filter_items_with_no_transactions(iwb_map, float_precision):
return iwb_map
def get_items(filters):
"Get items based on item code, item group or brand."
conditions = []
if filters.get("item_code"):
conditions.append("item.name=%(item_code)s")
else:
if filters.get("item_group"):
conditions.append(get_item_group_condition(filters.get("item_group")))
if filters.get("brand"): # used in stock analytics report
conditions.append("item.brand=%(brand)s")
items = []
if conditions:

View File

@@ -271,15 +271,13 @@ class update_entries_after(object):
}
"""
self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse]
previous_sle = get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
setattr(warehouse_dict, key, flt(previous_sle.get(key)))
warehouse_dict.update({
self.data[args.warehouse] = frappe._dict({
"previous_sle": previous_sle,
"qty_after_transaction": flt(previous_sle.qty_after_transaction),
"valuation_rate": flt(previous_sle.valuation_rate),
"stock_value": flt(previous_sle.stock_value),
"prev_stock_value": previous_sle.stock_value or 0.0,
"stock_queue": json.loads(previous_sle.stock_queue or "[]"),
"stock_value_difference": 0.0

View File

@@ -224,7 +224,7 @@ def get_avg_purchase_rate(serial_nos):
def get_valuation_method(item_code):
"""get valuation method from item or default"""
val_method = frappe.db.get_value('Item', item_code, 'valuation_method')
val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True)
if not val_method:
val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
return val_method
@@ -275,17 +275,17 @@ def get_valid_serial_nos(sr_nos, qty=0, item_code=''):
return valid_serial_nos
def validate_warehouse_company(warehouse, company):
warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company")
warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company", cache=True)
if warehouse_company and warehouse_company != company:
frappe.throw(_("Warehouse {0} does not belong to company {1}").format(warehouse, company),
InvalidWarehouseCompany)
def is_group_warehouse(warehouse):
if frappe.db.get_value("Warehouse", warehouse, "is_group"):
if frappe.db.get_value("Warehouse", warehouse, "is_group", cache=True):
frappe.throw(_("Group node warehouse is not allowed to select for transactions"))
def validate_disabled_warehouse(warehouse):
if frappe.db.get_value("Warehouse", warehouse, "disabled"):
if frappe.db.get_value("Warehouse", warehouse, "disabled", cache=True):
frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse)))
def update_included_uom_in_report(columns, result, include_uom, conversion_factors):