diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 0f115f9cc20..cd88b117614 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -159,10 +159,10 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") month = now_datetime().month - if month > 10: - month = 10 + if month > 9: + month = 9 - for i in range(month): + for i in range(month+1): jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) @@ -181,10 +181,10 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Project") month = now_datetime().month - if month > 10: - month = 10 + if month > 9: + month = 9 - for i in range(month): + for i in range(month + 1): jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index d486ff60285..ac98dccdb5e 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -267,6 +267,8 @@ class POSInvoice(SalesInvoice): from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} + if not pos_profile: + frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) self.pos_profile = pos_profile.get('name') profile = {} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 40009ac69d0..50eb400775e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext import frappe.defaults -from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form +from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate from frappe import _, msgprint, throw from erpnext.accounts.party import get_party_account, get_due_date from frappe.model.mapper import get_mapped_doc @@ -549,7 +549,12 @@ class SalesInvoice(SellingController): self.against_income_account = ','.join(against_acc) def add_remarks(self): - if not self.remarks: self.remarks = 'No Remarks' + if not self.remarks: + if self.po_no and self.po_date: + self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, + formatdate(self.po_date)) + else: + self.remarks = _("No Remarks") def validate_auto_set_posting_time(self): # Don't auto set the posting date and time if invoice is amended diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3c681eeecf2..eb223ee42ca 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1885,8 +1885,8 @@ class TestSalesInvoice(unittest.TestCase): "item_code": "_Test Item", "uom": "Nos", "warehouse": "_Test Warehouse - _TC", - "qty": 2, - "rate": 100, + "qty": 2000, + "rate": 12, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", @@ -1895,31 +1895,52 @@ class TestSalesInvoice(unittest.TestCase): "item_code": "_Test Item 2", "uom": "Nos", "warehouse": "_Test Warehouse - _TC", - "qty": 4, - "rate": 150, + "qty": 420, + "rate": 15, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", }) + si.discount_amount = 100 si.save() einvoice = make_einvoice(si) - total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']]) - total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']]) - total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']]) - total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']]) - total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']]) + total_item_ass_value = 0 + total_item_cgst_value = 0 + total_item_sgst_value = 0 + total_item_igst_value = 0 + total_item_value = 0 + + for item in einvoice['ItemList']: + total_item_ass_value += item['AssAmt'] + total_item_cgst_value += item['CgstAmt'] + total_item_sgst_value += item['SgstAmt'] + total_item_igst_value += item['IgstAmt'] + total_item_value += item['TotItemVal'] + + self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount']) + self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt']) + + value_details = einvoice['ValDtls'] self.assertEqual(einvoice['Version'], '1.1') - self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value) - self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value) - self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value) - self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value) - self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value) + self.assertEqual(value_details['AssVal'], total_item_ass_value) + self.assertEqual(value_details['CgstVal'], total_item_cgst_value) + self.assertEqual(value_details['SgstVal'], total_item_sgst_value) + self.assertEqual(value_details['IgstVal'], total_item_igst_value) + + self.assertEqual( + value_details['TotInvVal'], + value_details['AssVal'] + value_details['CgstVal'] + + value_details['SgstVal'] + value_details['IgstVal'] + + value_details['OthChrg'] - value_details['Discount'] + ) + + self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertTrue(einvoice['EwbDtls']) -def make_sales_invoice_for_ewaybill(): +def make_test_address_for_ewaybill(): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): address = frappe.get_doc({ "address_line1": "_Test Address Line 1", @@ -1967,7 +1988,8 @@ def make_sales_invoice_for_ewaybill(): }) address.save() - + +def make_test_transporter_for_ewaybill(): if not frappe.db.exists('Supplier', '_Test Transporter'): frappe.get_doc({ "doctype": "Supplier", @@ -1978,12 +2000,17 @@ def make_sales_invoice_for_ewaybill(): "is_transporter": 1 }).insert() +def make_sales_invoice_for_ewaybill(): + make_test_address_for_ewaybill() + make_test_transporter_for_ewaybill() + gst_settings = frappe.get_doc("GST Settings") gst_account = frappe.get_all( "GST Account", fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"}) + filters = {"company": "_Test Company"} + ) if not gst_account: gst_settings.append("gst_accounts", { @@ -1995,7 +2022,7 @@ def make_sales_invoice_for_ewaybill(): gst_settings.save() - si = create_sales_invoice(do_not_save =1, rate = '60000') + si = create_sales_invoice(do_not_save=1, rate='60000') si.distance = 2000 si.company_address = "_Test Address for Eway bill-Billing" diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index 16bef565252..2162a02eff9 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -47,21 +47,22 @@ def get_data(filters): for d in gl_entries: asset_data = assets_details.get(d.against_voucher) - if not asset_data.get("accumulated_depreciation_amount"): - asset_data.accumulated_depreciation_amount = d.debit - else: - asset_data.accumulated_depreciation_amount += d.debit + if asset_data: + if not asset_data.get("accumulated_depreciation_amount"): + asset_data.accumulated_depreciation_amount = d.debit + else: + asset_data.accumulated_depreciation_amount += d.debit - row = frappe._dict(asset_data) - row.update({ - "depreciation_amount": d.debit, - "depreciation_date": d.posting_date, - "amount_after_depreciation": (flt(row.gross_purchase_amount) - - flt(row.accumulated_depreciation_amount)), - "depreciation_entry": d.voucher_no - }) + row = frappe._dict(asset_data) + row.update({ + "depreciation_amount": d.debit, + "depreciation_date": d.posting_date, + "amount_after_depreciation": (flt(row.gross_purchase_amount) - + flt(row.accumulated_depreciation_amount)), + "depreciation_entry": d.voucher_no + }) - data.append(row) + data.append(row) return data diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 2d6b64532b1..79e1775b9db 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -22,6 +22,7 @@ frappe.ui.form.on('Patient Appointment', { filters: {'status': 'Active'} }; }); + frm.set_query('practitioner', function() { return { filters: { @@ -29,6 +30,7 @@ frappe.ui.form.on('Patient Appointment', { } }; }); + frm.set_query('service_unit', function(){ return { filters: { @@ -39,6 +41,16 @@ frappe.ui.form.on('Patient Appointment', { }; }); + frm.set_query('therapy_plan', function() { + return { + filters: { + 'patient': frm.doc.patient + } + }; + }); + + frm.trigger('set_therapy_type_filter'); + if (frm.is_new()) { frm.page.set_primary_action(__('Check Availability'), function() { if (!frm.doc.patient) { @@ -136,6 +148,24 @@ frappe.ui.form.on('Patient Appointment', { } }, + therapy_plan: function(frm) { + frm.trigger('set_therapy_type_filter'); + }, + + set_therapy_type_filter: function(frm) { + if (frm.doc.therapy_plan) { + frm.call('get_therapy_types').then(r => { + frm.set_query('therapy_type', function() { + return { + filters: { + 'name': ['in', r.message] + } + }; + }); + }); + } + }, + therapy_type: function(frm) { if (frm.doc.therapy_type) { frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => { diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index ac35acc21ac..35600e48092 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -23,9 +23,9 @@ "procedure_template", "get_procedure_from_encounter", "procedure_prescription", + "therapy_plan", "therapy_type", "get_prescribed_therapies", - "therapy_plan", "practitioner", "practitioner_name", "department", @@ -284,7 +284,7 @@ "report_hide": 1 }, { - "depends_on": "eval:doc.patient;", + "depends_on": "eval:doc.patient && doc.therapy_plan;", "fieldname": "therapy_type", "fieldtype": "Link", "label": "Therapy", @@ -292,17 +292,16 @@ "set_only_once": 1 }, { - "depends_on": "eval:doc.patient && doc.__islocal;", + "depends_on": "eval:doc.patient && doc.therapy_plan && doc.__islocal;", "fieldname": "get_prescribed_therapies", "fieldtype": "Button", "label": "Get Prescribed Therapies" }, { - "depends_on": "eval: doc.patient && doc.therapy_type", + "depends_on": "eval: doc.patient;", "fieldname": "therapy_plan", "fieldtype": "Link", "label": "Therapy Plan", - "mandatory_depends_on": "eval: doc.patient && doc.therapy_type", "options": "Therapy Plan" }, { @@ -348,7 +347,7 @@ } ], "links": [], - "modified": "2020-05-21 03:04:21.400893", + "modified": "2020-12-16 13:16:58.578503", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index e685b20a8c8..dc820cb464e 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -91,6 +91,17 @@ class PatientAppointment(Document): if fee_validity: frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) + def get_therapy_types(self): + if not self.therapy_plan: + return + + therapy_types = [] + doc = frappe.get_doc('Therapy Plan', self.therapy_plan) + for entry in doc.therapy_plan_details: + therapy_types.append(entry.therapy_type) + + return therapy_types + @frappe.whitelist() def check_payment_fields_reqd(patient): @@ -145,7 +156,7 @@ def invoice_appointment(appointment_doc): sales_invoice.flags.ignore_mandatory = True sales_invoice.save(ignore_permissions=True) sales_invoice.submit() - frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True) + frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index bbb42227154..a0327bdaa0b 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -111,13 +111,14 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-12-17 16:27:20.311060", + "modified": "2020-12-31 16:43:30.695206", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -131,6 +132,7 @@ "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -144,6 +146,7 @@ "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index cd40a665d43..2e0a4d13ab2 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe, math, json import erpnext from frappe import _ +from six import string_types from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.controllers.accounts_controller import AccountsController @@ -280,10 +281,13 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict return write_off @frappe.whitelist() -def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): - # if loan is passed it will be considered as full unpledge +def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0): + # if no security_map is passed it will be considered as full unpledge + if security_map and isinstance(security_map, string_types): + security_map = json.loads(security_map) + if loan: - pledge_qty_map = get_pledged_security_qty(loan) + pledge_qty_map = security_map or get_pledged_security_qty(loan) loan_doc = frappe.get_doc('Loan', loan) unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, loan_doc.applicant_type, loan_doc.applicant) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index a63d06590f8..8b1f9a2266f 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -45,7 +45,7 @@ class TestLoan(unittest.TestCase): create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) self.applicant1 = make_employee("robert_loan@loan.com") - make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR') + make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company") if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) @@ -325,6 +325,43 @@ class TestLoan(unittest.TestCase): self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) + def test_partial_loan_security_unpledge(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 2000.00 + }, + { + "loan_security": "Test Security 2", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000) + repayment_entry.submit() + + unpledge_map = {'Test Security 2': 2000} + + unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_request.submit() + unpledge_request.status = 'Approved' + unpledge_request.save() + unpledge_request.submit() + unpledge_request.load_from_db() + self.assertEqual(unpledge_request.docstatus, 1) + def test_disbursal_check_with_shortfall(self): pledges = [{ "loan_security": "Test Security 2", diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 8ec0bfb62c0..64698068842 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -81,7 +81,6 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): process_loan_security_shortfall) def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall): - existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") if existing_shortfall: diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index c29f325bfc9..61c418d3d31 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -30,6 +30,8 @@ class LoanSecurityUnpledge(Document): d.idx, frappe.bold(d.loan_security))) def validate_unpledge_qty(self): + from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import get_ltv_ratio + pledge_qty_map = get_pledged_security_qty(self.loan) ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", @@ -47,6 +49,8 @@ class LoanSecurityUnpledge(Document): pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount) security_value = 0 + unpledge_qty_map = {} + ltv_ratio = 0 for security in self.securities: pledged_qty = pledge_qty_map.get(security.loan_security, 0) @@ -57,13 +61,15 @@ class LoanSecurityUnpledge(Document): msg += _("You are trying to unpledge more.") frappe.throw(msg, title=_("Loan Security Unpledge Error")) - qty_after_unpledge = pledged_qty - security.qty - ltv_ratio = ltv_ratio_map.get(security.loan_security_type) + unpledge_qty_map.setdefault(security.loan_security, 0) + unpledge_qty_map[security.loan_security] += security.qty - current_price = loan_security_price_map.get(security.loan_security) - if not current_price: - frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security))) + for security in pledge_qty_map: + if not ltv_ratio: + ltv_ratio = get_ltv_ratio(security) + qty_after_unpledge = pledge_qty_map.get(security, 0) - unpledge_qty_map.get(security, 0) + current_price = loan_security_price_map.get(security) security_value += qty_after_unpledge * current_price if not security_value and flt(pending_principal_amount, 2) > 0: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d69dabf15cd..f2e4f72d673 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -711,6 +711,7 @@ erpnext.patches.v13_0.delete_old_sales_reports execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 +execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings") erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v12_0.add_taxjar_integration_field diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index d0782765dee..2474bc3b82c 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -8,6 +8,7 @@ def execute(): if not company: return + frappe.reload_doc("custom", "doctype", "custom_field") frappe.reload_doc("regional", "doctype", "e_invoice_settings") custom_fields = { 'Sales Invoice': [ diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index 561e967d6df..8cf09aa6925 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import nowdate +from frappe.utils import nowdate, flt from erpnext.accounts.doctype.account.test_account import create_account from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans from erpnext.loan_management.doctype.loan.loan import make_repayment_entry @@ -113,15 +113,15 @@ def execute(): interest_paid = 0 principal_paid = 0 - if total_interest > entry.interest_amount: - interest_paid = entry.interest_amount + if flt(total_interest) > flt(entry.interest_amount): + interest_paid = flt(entry.interest_amount) else: - interest_paid = total_interest + interest_paid = flt(total_interest) - if total_principal > entry.payable_principal_amount: - principal_paid = entry.payable_principal_amount + if flt(total_principal) > flt(entry.payable_principal_amount): + principal_paid = flt(entry.payable_principal_amount) else: - principal_paid = total_principal + principal_paid = flt(total_principal) frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` + %s, @@ -129,8 +129,8 @@ def execute(): WHERE name = %s""", (principal_paid, interest_paid, entry.name)) - total_principal -= principal_paid - total_interest -= interest_paid + total_principal = flt(total_principal) - principal_paid + total_interest = flt(total_interest) - interest_paid def create_loan_type(loan, loan_type_name, penalty_account): loan_type_doc = frappe.new_doc('Loan Type') diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 0609d191497..311f3527f6e 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -86,19 +86,21 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): self.assertEqual(declaration.total_exemption_amount, 100000) -def create_payroll_period(): - if not frappe.db.exists("Payroll Period", "_Test Payroll Period"): +def create_payroll_period(**args): + args = frappe._dict(args) + name = args.name or "_Test Payroll Period" + if not frappe.db.exists("Payroll Period", name): from datetime import date payroll_period = frappe.get_doc(dict( doctype = 'Payroll Period', - name = "_Test Payroll Period", - company = erpnext.get_default_company(), - start_date = date(date.today().year, 1, 1), - end_date = date(date.today().year, 12, 31) + name = name, + company = args.company or erpnext.get_default_company(), + start_date = args.start_date or date(date.today().year, 1, 1), + end_date = args.end_date or date(date.today().year, 12, 31) )).insert() return payroll_period else: - return frappe.get_doc("Payroll Period", "_Test Payroll Period") + return frappe.get_doc("Payroll Period", name) def create_exemption_category(): if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"): diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py index 253f023f68b..81e364778ca 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -3,8 +3,11 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +#import frappe +import erpnext from frappe.model.document import Document class IncomeTaxSlab(Document): - pass + def validate(self): + if self.company: + self.currency = erpnext.get_company_currency(self.company) diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json index 8a55224dca7..09c7eb9a456 100644 --- a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json +++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json @@ -17,8 +17,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Employee", - "options": "Employee", - "read_only": 1 + "options": "Employee" }, { "fetch_from": "employee.employee_name", @@ -52,7 +51,7 @@ ], "istable": 1, "links": [], - "modified": "2020-09-30 12:40:07.999878", + "modified": "2020-12-17 15:43:29.542977", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Employee Detail", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index cb48abbc363..2288a277917 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -10,15 +10,22 @@ frappe.ui.form.on('Payroll Entry', { } frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet); - frm.set_query("department", function() { + frm.events.department_filters(frm); + frm.events.payroll_payable_account_filters(frm); + }, + + department_filters: function (frm) { + frm.set_query("department", function () { return { "filters": { "company": frm.doc.company, } }; }); + }, - frm.set_query("payroll_payable_account", function() { + payroll_payable_account_filters: function (frm) { + frm.set_query("payroll_payable_account", function () { return { filters: { "company": frm.doc.company, @@ -29,12 +36,12 @@ frappe.ui.form.on('Payroll Entry', { }); }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.docstatus == 0) { - if(!frm.is_new()) { + if (!frm.is_new()) { frm.page.clear_primary_action(); frm.add_custom_button(__("Get Employees"), - function() { + function () { frm.events.get_employee_details(frm); } ).toggleClass('btn-primary', !(frm.doc.employees || []).length); @@ -42,7 +49,7 @@ frappe.ui.form.on('Payroll Entry', { if ((frm.doc.employees || []).length) { frm.page.clear_primary_action(); frm.page.set_primary_action(__('Create Salary Slips'), () => { - frm.save('Submit').then(()=>{ + frm.save('Submit').then(() => { frm.page.clear_primary_action(); frm.refresh(); frm.events.refresh(frm); @@ -61,48 +68,48 @@ frappe.ui.form.on('Payroll Entry', { doc: frm.doc, method: 'fill_employee_details', }).then(r => { - if (r.docs && r.docs[0].employees){ + if (r.docs && r.docs[0].employees) { frm.employees = r.docs[0].employees; frm.dirty(); frm.save(); frm.refresh(); - if(r.docs[0].validate_attendance){ + if (r.docs[0].validate_attendance) { render_employee_attendance(frm, r.message); } } - }) + }); }, - create_salary_slips: function(frm) { + create_salary_slips: function (frm) { frm.call({ doc: frm.doc, method: "create_salary_slips", - callback: function(r) { + callback: function () { frm.refresh(); frm.toolbar.refresh(); } - }) + }); }, - add_context_buttons: function(frm) { - if(frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { + add_context_buttons: function (frm) { + if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { frm.events.add_bank_entry_button(frm); - } else if(frm.doc.salary_slips_created) { - frm.add_custom_button(__("Submit Salary Slip"), function() { + } else if (frm.doc.salary_slips_created) { + frm.add_custom_button(__("Submit Salary Slip"), function () { submit_salary_slip(frm); }).addClass("btn-primary"); } }, - add_bank_entry_button: function(frm) { + add_bank_entry_button: function (frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries', args: { 'name': frm.doc.name }, - callback: function(r) { + callback: function (r) { if (r.message && !r.message.submitted) { - frm.add_custom_button("Make Bank Entry", function() { + frm.add_custom_button("Make Bank Entry", function () { make_bank_entry(frm); }).addClass("btn-primary"); } @@ -141,8 +148,37 @@ frappe.ui.form.on('Payroll Entry', { }, payroll_frequency: function (frm) { - frm.trigger("set_start_end_dates"); - frm.events.clear_employee_table(frm); + frm.trigger("set_start_end_dates").then( ()=> { + frm.events.clear_employee_table(frm); + frm.events.get_employee_with_salary_slip_and_set_query(frm); + }); + }, + + employee_filters: function (frm, emp_list) { + frm.set_query('employee', 'employees', () => { + return { + filters: { + name: ["not in", emp_list] + } + }; + }); + }, + + get_employee_with_salary_slip_and_set_query: function (frm) { + frappe.db.get_list('Salary Slip', { + filters: { + start_date: frm.doc.start_date, + end_date: frm.doc.end_date, + docstatus: 1, + }, + fields: ['employee'] + }).then((emp) => { + var emp_list = []; + emp.forEach((employee_data) => { + emp_list.push(Object.values(employee_data)[0]); + }); + frm.events.employee_filters(frm, emp_list); + }); }, company: function (frm) { @@ -164,17 +200,17 @@ frappe.ui.form.on('Payroll Entry', { from_currency: frm.doc.currency, to_currency: company_currency, }, - callback: function(r) { + callback: function (r) { frm.set_value("exchange_rate", flt(r.message)); frm.set_df_property('exchange_rate', 'hidden', 0); - frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency - + " = [?] " + company_currency); + frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + + " = [?] " + company_currency); } }); } else { frm.set_value("exchange_rate", 1.0); frm.set_df_property('exchange_rate', 'hidden', 1); - frm.set_df_property("exchange_rate", "description", "" ); + frm.set_df_property("exchange_rate", "description", ""); } } }, @@ -192,9 +228,9 @@ frappe.ui.form.on('Payroll Entry', { }, start_date: function (frm) { - if(!in_progress && frm.doc.start_date){ + if (!in_progress && frm.doc.start_date) { frm.trigger("set_end_date"); - }else{ + } else { // reset flag in_progress = false; } @@ -228,7 +264,7 @@ frappe.ui.form.on('Payroll Entry', { } }, - set_end_date: function(frm){ + set_end_date: function (frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { @@ -243,19 +279,19 @@ frappe.ui.form.on('Payroll Entry', { }); }, - validate_attendance: function(frm){ - if(frm.doc.validate_attendance && frm.doc.employees){ + validate_attendance: function (frm) { + if (frm.doc.validate_attendance && frm.doc.employees) { frappe.call({ method: 'validate_employee_attendance', args: {}, - callback: function(r) { + callback: function (r) { render_employee_attendance(frm, r.message); }, doc: frm.doc, freeze: true, freeze_message: __('Validating Employee Attendance...') }); - }else{ + } else { frm.fields_dict.attendance_detail_html.html(""); } }, @@ -270,18 +306,20 @@ frappe.ui.form.on('Payroll Entry', { const submit_salary_slip = function (frm) { frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'), - function() { + function () { frappe.call({ method: 'submit_salary_slips', args: {}, - callback: function() {frm.events.refresh(frm);}, + callback: function () { + frm.events.refresh(frm); + }, doc: frm.doc, freeze: true, freeze_message: __('Submitting Salary Slips and creating Journal Entry...') }); }, - function() { - if(frappe.dom.freeze_count) { + function () { + if (frappe.dom.freeze_count) { frappe.dom.unfreeze(); frm.events.refresh(frm); } @@ -295,9 +333,11 @@ let make_bank_entry = function (frm) { return frappe.call({ doc: cur_frm.doc, method: "make_payment_entry", - callback: function() { + callback: function () { frappe.set_route( - 'List', 'Journal Entry', {"Journal Entry Account.reference_name": frm.doc.name} + 'List', 'Journal Entry', { + "Journal Entry Account.reference_name": frm.doc.name + } ); }, freeze: true, @@ -309,11 +349,19 @@ let make_bank_entry = function (frm) { } }; - -let render_employee_attendance = function(frm, data) { +let render_employee_attendance = function (frm, data) { frm.fields_dict.attendance_detail_html.html( frappe.render_template('employees_to_mark_attendance', { data: data }) ); -} +}; + +frappe.ui.form.on('Payroll Employee Detail', { + employee: function(frm) { + frm.events.clear_employee_table(frm); + if (!frm.doc.payroll_frequency) { + frappe.throw(__("Please set a Payroll Frequency")); + } + } +}); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json index 7a48dd14758..0444134aa4d 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json @@ -129,8 +129,7 @@ "fieldname": "employees", "fieldtype": "Table", "label": "Employee Details", - "options": "Payroll Employee Detail", - "read_only": 1 + "options": "Payroll Employee Detail" }, { "fieldname": "section_break_13", @@ -290,7 +289,7 @@ "icon": "fa fa-cog", "is_submittable": 1, "links": [], - "modified": "2020-10-23 13:00:33.753228", + "modified": "2020-12-17 15:13:17.766210", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Entry", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 8c2d9740ece..a25a6e7a32c 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.model.document import Document from dateutil.relativedelta import relativedelta -from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff +from frappe.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT, date_diff, comma_and from frappe import _ from erpnext.accounts.utils import get_fiscal_year from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee @@ -19,16 +19,26 @@ class PayrollEntry(Document): # check if salary slips were manually submitted entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) if cint(entries) == len(self.employees): - self.set_onload("submitted_ss", True) + self.set_onload("submitted_ss", True) def on_submit(self): self.create_salary_slips() def before_submit(self): + self.validate_employee_details() if self.validate_attendance: if self.validate_employee_attendance(): frappe.throw(_("Cannot Submit, Employees left to mark attendance")) + def validate_employee_details(self): + emp_with_sal_slip = [] + for employee_details in self.employees: + if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + emp_with_sal_slip.append(employee_details.employee) + + if len(emp_with_sal_slip): + frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip))) + def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` where payroll_entry=%s """, (self.name))) @@ -71,8 +81,17 @@ class PayrollEntry(Document): and t2.docstatus = 1 %s order by t2.from_date desc """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True) + + emp_list = self.remove_payrolled_employees(emp_list) return emp_list + def remove_payrolled_employees(self, emp_list): + for employee_details in emp_list: + if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + emp_list.remove(employee_details) + + return emp_list + def fill_employee_details(self): self.set('employees', []) employees = self.get_emp_list() @@ -542,7 +561,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): title = _("Creating Salary Slips...")) else: salary_slip_name = frappe.db.sql( - '''SELECT + '''SELECT name FROM `tabSalary Slip` WHERE company=%s diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 54106c8d166..e098ec79b0f 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -22,7 +22,7 @@ class TestPayrollEntry(unittest.TestCase): frappe.db.sql("delete from `tab%s`" % dt) make_earning_salary_component(setup=True, company_list=["_Test Company"]) - make_deduction_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"]) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) @@ -107,9 +107,9 @@ class TestPayrollEntry(unittest.TestCase): frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC") - - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) + currency=frappe.db.get_value("Company", "_Test Company", "default_currency") + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) + make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index f7e22c63879..8e05bb2057e 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -125,15 +125,15 @@ frappe.ui.form.on("Salary Slip", { change_form_labels: function(frm, company_currency) { frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction", - "base_net_pay", "base_rounded_total", "base_total_in_words"], + "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"], company_currency); - frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"], + frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date"], frm.doc.currency); // toggle fields frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction", - "base_net_pay", "base_rounded_total", "base_total_in_words"], + "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"], frm.doc.currency != company_currency); }, @@ -214,14 +214,16 @@ frappe.ui.form.on('Salary Slip Timesheet', { }); var calculate_totals = function(frm) { - if (frm.doc.earnings || frm.doc.deductions) { - frappe.call({ - method: "set_totals", - doc: frm.doc, - callback: function() { - frm.refresh_fields(); - } - }); + if (frm.doc.docstatus === 0) { + if (frm.doc.earnings || frm.doc.deductions) { + frappe.call({ + method: "set_totals", + doc: frm.doc, + callback: function() { + frm.refresh_fields(); + } + }); + } } }; diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 386618cf083..43deee43aac 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -69,9 +69,13 @@ "net_pay_info", "net_pay", "base_net_pay", + "year_to_date", + "base_year_to_date", "column_break_53", "rounded_total", "base_rounded_total", + "month_to_date", + "base_month_to_date", "section_break_55", "total_in_words", "column_break_69", @@ -578,13 +582,41 @@ { "fieldname": "column_break_69", "fieldtype": "Column Break" + }, + { + "fieldname": "year_to_date", + "fieldtype": "Currency", + "label": "Year To Date", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "month_to_date", + "fieldtype": "Currency", + "label": "Month To Date", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "base_year_to_date", + "fieldtype": "Currency", + "label": "Year To Date(Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "base_month_to_date", + "fieldtype": "Currency", + "label": "Month To Date(Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-10-21 23:02:59.400249", + "modified": "2020-12-21 23:43:44.959840", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 20365b191d0..99d8a8317cd 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import datetime, math -from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate +from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day from frappe.model.naming import make_autoname from frappe import msgprint, _ @@ -18,6 +18,7 @@ from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_fac from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry +from erpnext.accounts.utils import get_fiscal_year class SalarySlip(TransactionBase): def __init__(self, *args, **kwargs): @@ -49,6 +50,8 @@ class SalarySlip(TransactionBase): self.get_working_days_details(lwp = self.leave_without_pay) self.calculate_net_pay() + self.compute_year_to_date() + self.compute_month_to_date() if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") @@ -1125,6 +1128,46 @@ class SalarySlip(TransactionBase): self.gross_pay += self.earnings[i].amount self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) + def compute_year_to_date(self): + year_to_date = 0 + payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) + + if payroll_period: + period_start_date = payroll_period.start_date + period_end_date = payroll_period.end_date + else: + # get dates based on fiscal year if no payroll period exists + fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1) + period_start_date = fiscal_year.year_start_date + period_end_date = fiscal_year.year_end_date + + salary_slip_sum = frappe.get_list('Salary Slip', + fields = ['sum(net_pay) as sum'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', period_start_date], + 'end_date' : ['<', period_end_date]}) + + + year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 + + year_to_date += self.net_pay + self.year_to_date = year_to_date + + def compute_month_to_date(self): + month_to_date = 0 + first_day_of_the_month = get_first_day(self.start_date) + salary_slip_sum = frappe.get_list('Salary Slip', + fields = ['sum(net_pay) as sum'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', first_day_of_the_month], + 'end_date' : ['<', self.start_date] + }) + + month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 + + month_to_date += self.net_pay + self.month_to_date = month_to_date + def unlink_ref_doc_from_salary_slip(ref_no): linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` where journal_entry=%s and docstatus < 2""", (ref_no)) @@ -1135,4 +1178,4 @@ def unlink_ref_doc_from_salary_slip(ref_no): def generate_password_for_pdf(policy_template, employee): employee = frappe.get_doc("Employee", employee) - return policy_template.format(**employee.as_dict()) + return policy_template.format(**employee.as_dict()) \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 5daf1d439d1..d6fb4195988 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -9,7 +9,7 @@ import calendar import random from erpnext.accounts.utils import get_fiscal_year from frappe.utils.make_random import get_random -from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day +from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.hr.doctype.employee.test_employee import make_employee @@ -240,7 +240,11 @@ class TestSalarySlip(unittest.TestCase): interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') - make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR') + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', + payroll_period=payroll_period) + frappe.db.sql("""delete from `tabLoan""") loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 @@ -290,6 +294,33 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(salary_slip.gross_pay, 78000) self.assertEqual(salary_slip.base_gross_pay, 78000*70) + def test_year_to_date_computation(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + applicant = make_employee("test_ytd@salary.com", company="_Test Company") + + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"), + company="_Test Company") + + salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD", + "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) + + # clear salary slip for this employee + frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") + + create_salary_slips_for_payroll_period(applicant, salary_structure.name, + payroll_period, deduct_random=False) + + salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name': + 'test_ytd@salary.com'}, order_by = 'posting_date') + + year_to_date = 0 + for slip in salary_slips: + year_to_date += slip.net_pay + self.assertEqual(slip.year_to_date, year_to_date) + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -410,10 +441,7 @@ 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}) - if not frappe.db.exists('Salary Structure', salary_structure): - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) - else: - salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure) + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) if not salary_slip_name: @@ -557,14 +585,6 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No "amount": 200, "exempted_from_income_tax": 1 - }, - { - "salary_component": 'TDS', - "abbr":'T', - "type": "Deduction", - "depends_on_payment_days": 0, - "variable_based_on_taxable_salary": 1, - "round_to_the_nearest_integer": 1 } ] if not test_tax: @@ -575,6 +595,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No "type": "Deduction", "round_to_the_nearest_integer": 1 }) + else: + data.append({ + "salary_component": 'TDS', + "abbr":'T', + "type": "Deduction", + "depends_on_payment_days": 0, + "variable_based_on_taxable_salary": 1, + "round_to_the_nearest_integer": 1 + }) if setup or test_tax: make_salary_component(data, test_tax, company_list) @@ -631,8 +660,13 @@ def create_benefit_claim(employee, payroll_period, amount, component): }).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()): - frappe.db.sql("""delete from `tabIncome Tax Slab`""") +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None, + company=None): + if not currency: + currency = erpnext.get_default_currency() + + if company: + currency = erpnext.get_company_currency(company) slabs = [ { @@ -652,26 +686,33 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = } ] - income_tax_slab = frappe.new_doc("Income Tax Slab") - income_tax_slab.name = "Tax Slab: " + payroll_period.name - income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) - income_tax_slab.currency = currency + income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency}) + if not income_tax_slab_name: + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency) + income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + income_tax_slab.company = company or '' + income_tax_slab.currency = currency - if allow_tax_exemption: - income_tax_slab.allow_tax_exemption = 1 - income_tax_slab.standard_tax_exemption_amount = 50000 + if allow_tax_exemption: + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = 50000 - for item in slabs: - income_tax_slab.append("slabs", item) + for item in slabs: + income_tax_slab.append("slabs", item) - income_tax_slab.append("other_taxes_and_charges", { - "description": "cess", - "percent": 4 - }) + income_tax_slab.append("other_taxes_and_charges", { + "description": "cess", + "percent": 4 + }) - income_tax_slab.save() - if not dont_submit: - income_tax_slab.submit() + income_tax_slab.save() + if not dont_submit: + income_tax_slab.submit() + + return income_tax_slab.name + else: + return income_tax_slab_name def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): deducted_dates = [] diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index abb669740b6..f2fb558a14b 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -114,7 +114,7 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(sal_struct.currency, 'USD') def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, - test_tax=False, company=None, currency=erpnext.get_default_currency()): + test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) @@ -141,16 +141,24 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do if employee and not frappe.db.get_value("Salary Structure Assignment", {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency) + create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency, + payroll_period=payroll_period) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()): +def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(), + payroll_period=None): + if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) - payroll_period = create_payroll_period() - create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) + if not payroll_period: + payroll_period = create_payroll_period() + + income_tax_slab = frappe.db.get_value("Income Tax Slab", {"currency": currency}) + + if not income_tax_slab: + income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee @@ -162,7 +170,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) - salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" + salary_structure_assignment.income_tax_slab = income_tax_slab salary_structure_assignment.submit() return salary_structure_assignment diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index dccb5df1a11..a0c3013061d 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -43,7 +43,7 @@ class SalaryStructureAssignment(Document): def set_payroll_payable_account(self): if not self.payroll_payable_account: - payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account') + payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account') if not payroll_payable_account: payroll_payable_account = frappe.db.get_value( "Account", { diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json index e5751da5612..60f490d6166 100644 --- a/erpnext/regional/india/e_invoice/einv_template.json +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -59,7 +59,7 @@ {item_list} ], "ValDtls": {{ - "AssVal": "{invoice_value_details.base_net_total}", + "AssVal": "{invoice_value_details.base_total}", "CgstVal": "{invoice_value_details.total_cgst_amt}", "SgstVal": "{invoice_value_details.total_sgst_amt}", "IgstVal": "{invoice_value_details.total_igst_amt}", diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index cb92c42464e..02ce6c14c90 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -88,7 +88,7 @@ def get_party_details(address_name): gstin = address.get('gstin') gstin_details = get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') + legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName') location = gstin_details.get('AddrLoc') or address.get('city') state_code = gstin_details.get('StateCode') pincode = gstin_details.get('AddrPncd') @@ -146,12 +146,12 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.qty = abs(item.qty) - item.description = d.item_name - item.taxable_value = abs(item.base_net_amount) item.discount_amount = abs(item.discount_amount * item.qty) - item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) - item.gross_amount = abs(item.unit_rate * item.qty) + item.description = d.item_name + item.qty = abs(item.qty) + item.unit_rate = abs(item.base_amount / item.qty) + item.gross_amount = abs(item.base_amount) + item.taxable_value = abs(item.base_amount) item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None @@ -180,35 +180,35 @@ def update_item_taxes(invoice, item): item[attr] = 0 for t in invoice.taxes: + # this contains item wise tax rate & tax amount (incl. discount) item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) if t.account_head in gst_accounts_list: + item_tax_rate = item_tax_detail[0] + # item tax amount excluding discount amount + item_tax_amount = (item_tax_rate / 100) * item.base_amount + if t.account_head in gst_accounts.cess_account: + item_tax_amount_after_discount = item_tax_detail[1] if t.charge_type == 'On Item Quantity': - item.cess_nadv_amount += abs(item_tax_detail[1]) + item.cess_nadv_amount += abs(item_tax_amount_after_discount) else: - item.cess_rate += item_tax_detail[0] - item.cess_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.igst_account: - item.tax_rate += item_tax_detail[0] - item.igst_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.sgst_account: - item.tax_rate += item_tax_detail[0] - item.sgst_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.cgst_account: - item.tax_rate += item_tax_detail[0] - item.cgst_amount += abs(item_tax_detail[1]) - + item.cess_rate += item_tax_rate + item.cess_amount += abs(item_tax_amount_after_discount) + + for tax_type in ['igst', 'cgst', 'sgst']: + if t.account_head in gst_accounts[f'{tax_type}_account']: + item.tax_rate += item_tax_rate + item[f'{tax_type}_amount'] += abs(item_tax_amount) + return item def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - invoice_value_details.base_net_total = abs(invoice.base_net_total) - invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 - # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off - invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) - disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') - invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) - invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) + invoice_value_details.base_total = abs(invoice.base_total) + invoice_value_details.invoice_discount_amt = invoice.discount_amount + invoice_value_details.round_off = invoice.rounding_adjustment + invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) + invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) @@ -226,15 +226,14 @@ def update_invoice_taxes(invoice, invoice_value_details): for t in invoice.taxes: if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: + # using after discount amt since item also uses after discount amt for cess calc invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.igst_account: - invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.sgst_account: - invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.cgst_account: - invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) + + for tax_type in ['igst', 'cgst', 'sgst']: + if t.account_head in gst_accounts[f'{tax_type}_account']: + invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount) else: - invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_other_charges += abs(t.base_tax_amount) return invoice_value_details @@ -358,7 +357,8 @@ def validate_einvoice(validations, einvoice, errors=[]): einvoice[fieldname] = str(value) elif value_type == 'number': is_integer = '.' not in str(field_validation.get('maximum')) - einvoice[fieldname] = flt(value, 2) if not is_integer else cint(value) + precision = 3 if '.999' in str(field_validation.get('maximum')) else 2 + einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value) value = einvoice[fieldname] max_length = field_validation.get('maxLength') @@ -386,15 +386,15 @@ class GSPConnector(): self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None self.credentials = self.get_credentials() - self.base_url = 'https://gsp.adaequare.com/' - self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' - self.gstin_details_url = self.base_url + 'test/enriched/ei/api/master/gstin' - self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' - self.irn_details_url = self.base_url + 'test/enriched/ei/api/invoice/irn' - self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' - self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' - self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' - + self.base_url = 'https://gsp.adaequare.com' + self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token' + self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' + self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' + self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' + self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' + self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi' + self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' + def get_credentials(self): if self.invoice: gstin = self.get_seller_gstin() diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 566f20cfa12..7a72fe31023 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -28,7 +28,7 @@ def delete_company_transactions(company_name): "Party Account", "Employee", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", "POS Profile", "BOM", "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", - "Item Default"): + "Item Default", "Customer", "Supplier"): delete_for_doctype(doctype, company_name) # reset company values diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 92d268f0993..2fc7da83896 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -259,11 +259,16 @@ class StockEntry(StockController): item_code.append(item.item_code) def validate_fg_completed_qty(self): + item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item and d.qty != self.fg_completed_qty: - frappe.throw(_("Finished product quantity {0} and For Quantity {1} cannot be different") - .format(d.qty, self.fg_completed_qty)) + if d.is_finished_item: + item_wise_qty.setdefault(d.item_code, []).append(d.qty) + + for item_code, qty_list in iteritems(item_wise_qty): + if self.fg_completed_qty != sum(qty_list): + frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different") + .format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty))) def validate_difference_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -319,7 +324,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.bom_no: + if d.is_finished_item or d.is_scrap_item: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -699,7 +704,7 @@ class StockEntry(StockController): # SLE for target warehouse self.get_sle_for_target_warehouse(sl_entries, finished_item_row) - + # reverse sl entries if cancel if self.docstatus == 2: sl_entries.reverse() @@ -727,9 +732,9 @@ class StockEntry(StockController): sle.dependant_sle_voucher_detail_no = d.name elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse): sle.dependant_sle_voucher_detail_no = finished_item_row.name - + sl_entries.append(sle) - + def get_sle_for_target_warehouse(self, sl_entries, finished_item_row): for d in self.get('items'): if cstr(d.t_warehouse): diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 859aea2eb60..3ff396ba77e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -217,7 +217,7 @@ "fieldname": "role_allowed_to_create_edit_back_dated_transactions", "fieldtype": "Link", "label": "Role Allowed to Create/Edit Back-dated Transactions", - "options": "User" + "options": "Role" }, { "fieldname": "column_break_26", @@ -234,7 +234,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-23 22:26:54.225608", + "modified": "2020-12-29 12:53:31.162247", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings",