Merge branch 'develop' into salary_slip_error

This commit is contained in:
Marica 2021-03-09 18:20:54 +05:30 committed by GitHub
commit 5accf8adf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 724 additions and 382 deletions

View File

@ -695,6 +695,7 @@ frappe.ui.form.on('Sales Invoice', {
refresh_field(['timesheets'])
}
})
frm.refresh();
},
onload: function(frm) {
@ -810,6 +811,65 @@ frappe.ui.form.on('Sales Invoice', {
},
refresh: function(frm) {
if (frm.doc.project) {
frm.add_custom_button(__('Fetch Timesheet'), function() {
let d = new frappe.ui.Dialog({
title: __('Fetch Timesheet'),
fields: [
{
"label" : "From",
"fieldname": "from_time",
"fieldtype": "Date",
"reqd": 1,
},
{
fieldtype: 'Column Break',
fieldname: 'col_break_1',
},
{
"label" : "To",
"fieldname": "to_time",
"fieldtype": "Date",
"reqd": 1,
}
],
primary_action: function() {
let data = d.get_values();
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_projectwise_timesheet_data",
args: {
from_time: data.from_time,
to_time: data.to_time,
project: frm.doc.project
},
callback: function(r) {
if(!r.exc) {
if(r.message.length > 0) {
frm.clear_table('timesheets')
r.message.forEach((d) => {
frm.add_child('timesheets',{
'time_sheet': d.parent,
'billing_hours': d.billing_hours,
'billing_amount': d.billing_amt,
'timesheet_detail': d.name
});
});
frm.refresh_field('timesheets')
}
else {
frappe.msgprint(__('No Timesheet Found.'))
}
d.hide();
}
}
});
},
primary_action_label: __('Get Timesheets')
});
d.show();
})
}
if (frappe.boot.active_domains.includes("Healthcare")) {
frm.set_df_property("patient", "hidden", 0);
frm.set_df_property("patient_name", "hidden", 0);

View File

@ -1059,7 +1059,8 @@ class SalesInvoice(SellingController):
)
def make_gle_for_rounding_adjustment(self, gl_entries):
if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment:
if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment \
and not self.is_internal_transfer():
round_off_account, round_off_cost_center = \
get_round_off_account_and_cost_center(self.company)

View File

@ -11,15 +11,18 @@
],
"fields": [
{
"allow_in_quick_entry": 1,
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-30 19:41:25.783852",
"modified": "2021-03-03 11:50:38.748872",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Category",

View File

@ -617,6 +617,7 @@ def get_partywise_advanced_payment_amount(party_type, posting_date = None, futur
FROM `tabGL Entry`
WHERE
party_type = %s and against_voucher is null
and is_cancelled = 0
and {1} GROUP BY party"""
.format(("credit") if party_type == "Customer" else "debit", cond) , party_type)

View File

@ -22,8 +22,8 @@
</p>
</div>
{% endif %}
<h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<h5 class="font-bold" style="margin-left: 15px; margin-top: 0px;">1. Transaction Details</h5>
<div class="col-xs-8 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
@ -54,8 +54,8 @@
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
</div>
</div>
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<h5 class="font-bold" style="margin-left: 15px; margin-bottom: 0px;">2. Party Details</h5>
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
@ -89,7 +89,7 @@
</div>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">3. Item Details</h5>
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>

View File

@ -258,7 +258,7 @@
{% } %}
{% } else { %}
{% if(data[i]["party"]|| "&nbsp;") { %}
{% if((data[i]["party"]) != __("'Total'")) { %}
{% if(!data[i]["is_total_row"]) { %}
<td>
{% if(!(filters.customer || filters.supplier)) { %}
{%= data[i]["party"] %}

View File

@ -1061,7 +1061,7 @@
"type": "Link"
}
],
"modified": "2020-12-01 13:38:35.349024",
"modified": "2021-03-04 00:38:35.349024",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -1071,7 +1071,7 @@
"pin_to_top": 0,
"shortcuts": [
{
"label": "Chart Of Accounts",
"label": "Chart of Accounts",
"link_to": "Account",
"type": "DocType"
},
@ -1116,4 +1116,4 @@
"type": "Dashboard"
}
]
}
}

View File

@ -96,7 +96,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 12:00:23.276329",
"modified": "2021-03-02 17:34:04.190677",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@ -113,5 +113,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
}
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -142,6 +142,11 @@ class SellingController(StockController):
self.base_net_total * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person))
if sales_person.commission_rate:
sales_person.incentives = flt(
sales_person.allocated_amount * flt(sales_person.commission_rate) / 100.0,
self.precision("incentives", sales_person))
total += sales_person.allocated_percentage
if sales_team and total != 100.0:

View File

@ -103,7 +103,7 @@
}
],
"links": [],
"modified": "2021-01-29 12:02:16.106942",
"modified": "2021-03-02 17:35:14.084342",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Mpesa Settings",
@ -147,5 +147,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -70,7 +70,7 @@
],
"issingle": 1,
"links": [],
"modified": "2020-10-29 20:24:56.916104",
"modified": "2021-03-02 17:35:27.544259",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",
@ -88,5 +88,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -330,7 +330,7 @@
],
"issingle": 1,
"links": [],
"modified": "2020-11-05 20:44:03.664891",
"modified": "2021-03-02 17:35:41.953317",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Settings",
@ -348,5 +348,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -278,6 +278,9 @@ doc_events = {
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
},
('Sales Invoice', 'Purchase Invoice'): {
'validate': ['erpnext.regional.india.utils.validate_document_name']
},
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",

View File

@ -18,7 +18,6 @@ class ValueMultiplierError(frappe.ValidationError): pass
class LeaveAllocation(Document):
def validate(self):
self.validate_period()
self.validate_new_leaves_allocated_value()
self.validate_allocation_overlap()
self.validate_back_dated_allocation()
self.set_total_leaves_allocated()
@ -72,11 +71,6 @@ class LeaveAllocation(Document):
if frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"):
frappe.throw(_("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type))
def validate_new_leaves_allocated_value(self):
"""validate that leave allocation is in multiples of 0.5"""
if flt(self.new_leaves_allocated) % 0.5:
frappe.throw(_("Leaves must be allocated in multiples of 0.5"), ValueMultiplierError)
def validate_allocation_overlap(self):
leave_allocation = frappe.db.sql("""
SELECT

View File

@ -106,12 +106,14 @@
"fieldname": "leaves_allocated",
"fieldtype": "Check",
"hidden": 1,
"label": "Leaves Allocated"
"label": "Leaves Allocated",
"no_copy": 1,
"print_hide": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-12-31 16:43:30.695206",
"modified": "2021-03-01 17:54:01.014509",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _, bold
from frappe.utils import getdate, date_diff, comma_and, formatdate
from frappe.utils import getdate, date_diff, comma_and, formatdate, get_datetime, flt
from math import ceil
import json
from six import string_types
@ -84,17 +84,52 @@ class LeavePolicyAssignment(Document):
return allocation.name, new_leaves_allocated
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
from frappe.model.meta import get_field_precision
precision = get_field_precision(frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated"))
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_compensatory == 1:
new_leaves_allocated = 0
elif leave_type_details.get(leave_type).is_earned_leave == 1:
if self.assignment_based_on == "Leave Period":
new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
else:
new_leaves_allocated = 0
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
if getdate(date_of_joining) > getdate(self.effective_from):
elif getdate(date_of_joining) > getdate(self.effective_from):
remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
new_leaves_allocated = 0
return flt(new_leaves_allocated, precision)
def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
from erpnext.hr.utils import get_monthly_earned_leave
current_month = get_datetime().month
current_year = get_datetime().year
from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date")
if getdate(date_of_joining) > getdate(from_date):
from_date = date_of_joining
from_date_month = get_datetime(from_date).month
from_date_year = get_datetime(from_date).year
months_passed = 0
if current_year == from_date_year and current_month > from_date_month:
months_passed = current_month - from_date_month
elif current_year > from_date_year:
months_passed = (12 - from_date_month) + current_month
if months_passed > 0:
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
new_leaves_allocated = monthly_earned_leave * months_passed
return new_leaves_allocated
@frappe.whitelist()
def grant_leave_for_multiple_employees(leave_policy_assignments):
leave_policy_assignments = json.loads(leave_policy_assignments)
@ -156,7 +191,8 @@ def automatically_allocate_leaves_based_on_leave_policy():
def get_leave_type_details():
leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type",
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory",
"is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"])
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details

View File

@ -172,7 +172,7 @@
"fieldname": "rounding",
"fieldtype": "Select",
"label": "Rounding",
"options": "0.5\n1.0"
"options": "\n0.25\n0.5\n1.0"
},
{
"depends_on": "is_carry_forward",
@ -197,6 +197,7 @@
"label": "Based On Date Of Joining"
},
{
"default": "0",
"depends_on": "eval:doc.is_lwp == 0",
"fieldname": "is_ppl",
"fieldtype": "Check",
@ -213,7 +214,7 @@
"icon": "fa fa-flag",
"idx": 1,
"links": [],
"modified": "2020-10-15 15:49:47.555105",
"modified": "2021-03-02 11:22:33.776320",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",

View File

@ -16,7 +16,7 @@
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_in_quick_entry": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -46,6 +46,12 @@
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
}
],
"has_web_view": 0,
@ -56,7 +62,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2021-02-24 09:55:00.536328",
"modified": "2021-02-26 10:55:00.536328",
"modified_by": "Administrator",
"module": "HR",
"name": "Skill",

View File

@ -36,7 +36,7 @@ frappe.team_updates = {
start: me.start
},
callback: function(r) {
if(r.message) {
if (r.message && r.message.length > 0) {
r.message.forEach(function(d) {
me.add_row(d);
});
@ -75,6 +75,6 @@ frappe.team_updates = {
}
me.last_feed_date = date;
$(frappe.render_template('team_update_row', data)).appendTo(me.body)
$(frappe.render_template('team_update_row', data)).appendTo(me.body);
}
}
}

View File

@ -316,13 +316,7 @@ def allocate_earned_leaves():
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
if annual_allocation:
earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
if e_leave_type.rounding == "0.5":
earned_leaves = round(earned_leaves * 2) / 2
else:
earned_leaves = round(earned_leaves)
earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
@ -335,6 +329,21 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type
today_date = today()
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
earned_leaves = 0.0
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
if annual_leaves:
earned_leaves = flt(annual_leaves) / divide_by_frequency[frequency]
if rounding:
if rounding == "0.25":
earned_leaves = round(earned_leaves * 4) / 4
elif rounding == "0.5":
earned_leaves = round(earned_leaves * 2) / 2
else:
earned_leaves = round(earned_leaves)
return earned_leaves
def get_leave_allocations(date, leave_type):
return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy

View File

@ -528,6 +528,10 @@ class WorkOrder(Document):
if not reset_only_qty:
self.required_items = []
operation = None
if self.get('operations') and len(self.operations) == 1:
operation = self.operations[0].operation
if self.bom_no and self.qty:
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=self.qty,
fetch_exploded = self.use_multi_level_bom)
@ -536,6 +540,9 @@ class WorkOrder(Document):
for d in self.get("required_items"):
if item_dict.get(d.item_code):
d.required_qty = item_dict.get(d.item_code).get("qty")
if not d.operation:
d.operation = operation
else:
# Attribute a big number (999) to idx for sorting putpose in case idx is NULL
# For instance in BOM Explosion Item child table, the items coming from sub assembly items
@ -543,7 +550,7 @@ class WorkOrder(Document):
self.append('required_items', {
'rate': item.rate,
'amount': item.amount,
'operation': item.operation,
'operation': item.operation or operation,
'item_code': item.item_code,
'item_name': item.item_name,
'description': item.description,
@ -879,7 +886,7 @@ def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto
doc.schedule_time_logs(row)
doc.insert()
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)))
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
return doc

View File

@ -1,13 +1,24 @@
import frappe
from frappe import _
from frappe.utils import getdate, get_time
from frappe.utils import getdate, get_time, today
from erpnext.stock.stock_ledger import update_entries_after
from erpnext.accounts.utils import update_gl_entries_after
def execute():
frappe.reload_doc('stock', 'doctype', 'repost_item_valuation')
for doctype in ('repost_item_valuation', 'stock_entry_detail', 'purchase_receipt_item',
'purchase_invoice_item', 'delivery_note_item', 'sales_invoice_item', 'packed_item'):
frappe.reload_doc('stock', 'doctype', doctype)
frappe.reload_doc('buying', 'doctype', 'purchase_receipt_item_supplied')
reposting_project_deployed_on = get_creation_time()
posting_date = getdate(reposting_project_deployed_on)
posting_time = get_time(reposting_project_deployed_on)
if posting_date == today():
return
frappe.clear_cache()
frappe.flags.warehouse_account_map = {}
data = frappe.db.sql('''
SELECT
@ -41,8 +52,6 @@ def execute():
print("Reposting General Ledger Entries...")
posting_date = getdate(reposting_project_deployed_on)
posting_time = get_time(reposting_project_deployed_on)
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
update_gl_entries_after(posting_date, posting_time, company=row.name)

View File

@ -15,6 +15,7 @@
"daily_wages_fraction_for_half_day",
"email_salary_slip_to_employee",
"encrypt_salary_slips_in_emails",
"show_leave_balances_in_salary_slip",
"password_policy"
],
"fields": [
@ -23,58 +24,44 @@
"fieldname": "payroll_based_on",
"fieldtype": "Select",
"label": "Calculate Payroll Working Days Based On",
"options": "Leave\nAttendance",
"show_days": 1,
"show_seconds": 1
"options": "Leave\nAttendance"
},
{
"fieldname": "max_working_hours_against_timesheet",
"fieldtype": "Float",
"label": "Max working hours against Timesheet",
"show_days": 1,
"show_seconds": 1
"label": "Max working hours against Timesheet"
},
{
"default": "0",
"description": "If checked, Total no. of Working Days will include holidays, and this will reduce the value of Salary Per Day",
"fieldname": "include_holidays_in_total_working_days",
"fieldtype": "Check",
"label": "Include holidays in Total no. of Working Days",
"show_days": 1,
"show_seconds": 1
"label": "Include holidays in Total no. of Working Days"
},
{
"default": "0",
"description": "If checked, hides and disables Rounded Total field in Salary Slips",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total",
"show_days": 1,
"show_seconds": 1
"label": "Disable Rounded Total"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"default": "0.5",
"description": "The fraction of daily wages to be paid for half-day attendance",
"fieldname": "daily_wages_fraction_for_half_day",
"fieldtype": "Float",
"label": "Fraction of Daily Salary for Half Day",
"show_days": 1,
"show_seconds": 1
"label": "Fraction of Daily Salary for Half Day"
},
{
"default": "1",
"description": "Emails salary slip to employee based on preferred email selected in Employee",
"fieldname": "email_salary_slip_to_employee",
"fieldtype": "Check",
"label": "Email Salary Slip to Employee",
"show_days": 1,
"show_seconds": 1
"label": "Email Salary Slip to Employee"
},
{
"default": "0",
@ -82,9 +69,7 @@
"description": "The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.",
"fieldname": "encrypt_salary_slips_in_emails",
"fieldtype": "Check",
"label": "Encrypt Salary Slips in Emails",
"show_days": 1,
"show_seconds": 1
"label": "Encrypt Salary Slips in Emails"
},
{
"depends_on": "eval: doc.encrypt_salary_slips_in_emails == 1",
@ -92,24 +77,27 @@
"fieldname": "password_policy",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Password Policy",
"show_days": 1,
"show_seconds": 1
"label": "Password Policy"
},
{
"depends_on": "eval:doc.payroll_based_on == 'Attendance'",
"fieldname": "consider_unmarked_attendance_as",
"fieldtype": "Select",
"label": "Consider Unmarked Attendance As",
"options": "Present\nAbsent",
"show_days": 1,
"show_seconds": 1
"options": "Present\nAbsent"
},
{
"default": "0",
"fieldname": "show_leave_balances_in_salary_slip",
"fieldtype": "Check",
"label": "Show Leave Balances in Salary Slip"
}
],
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-06-22 17:00:58.408030",
"modified": "2021-03-03 17:49:59.579723",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Settings",
@ -126,5 +114,6 @@
}
],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"track_changes": 1
}

View File

@ -80,6 +80,8 @@
"total_in_words",
"column_break_69",
"base_total_in_words",
"leave_details_section",
"leave_details",
"section_break_75",
"amended_from"
],
@ -612,13 +614,25 @@
"label": "Month To Date(Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "leave_details_section",
"fieldtype": "Section Break",
"label": "Leave Details"
},
{
"fieldname": "leave_details",
"fieldtype": "Table",
"label": "Leave Details",
"options": "Salary Slip Leave",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
"modified": "2021-01-14 13:37:38.180920",
"modified": "2021-02-19 11:48:05.383945",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",

View File

@ -19,6 +19,7 @@ from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_appli
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
from six import iteritems
class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs):
@ -53,6 +54,7 @@ class SalarySlip(TransactionBase):
self.compute_year_to_date()
self.compute_month_to_date()
self.compute_component_wise_year_to_date()
self.add_leave_balances()
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 +1127,7 @@ class SalarySlip(TransactionBase):
#calculate total working hours, earnings based on hourly wages and totals
def calculate_total_for_salary_slip_based_on_timesheet(self):
if self.timesheets:
self.total_working_hours = 0
for timesheet in self.timesheets:
if timesheet.working_hours:
self.total_working_hours += timesheet.working_hours
@ -1214,6 +1217,22 @@ class SalarySlip(TransactionBase):
return period_start_date, period_end_date
def add_leave_balances(self):
self.set('leave_details', [])
if frappe.db.get_single_value('Payroll Settings', 'show_leave_balances_in_salary_slip'):
from erpnext.hr.doctype.leave_application.leave_application import get_leave_details
leave_details = get_leave_details(self.employee, self.end_date)
for leave_type, leave_values in iteritems(leave_details['leave_allocation']):
self.append('leave_details', {
'leave_type': leave_type,
'total_allocated_leaves': flt(leave_values.get('total_leaves')),
'expired_leaves': flt(leave_values.get('expired_leaves')),
'used_leaves': flt(leave_values.get('leaves_taken')),
'pending_leaves': flt(leave_values.get('pending_leaves')),
'available_leaves': flt(leave_values.get('remaining_leaves'))
})
def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`

View File

@ -0,0 +1,78 @@
{
"actions": [],
"creation": "2021-02-19 11:45:18.173417",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"leave_type",
"total_allocated_leaves",
"expired_leaves",
"used_leaves",
"pending_leaves",
"available_leaves"
],
"fields": [
{
"fieldname": "leave_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Leave Type",
"no_copy": 1,
"options": "Leave Type",
"read_only": 1
},
{
"fieldname": "total_allocated_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Total Allocated Leave",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "expired_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Expired Leave",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "used_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Used Leave",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "pending_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Pending Leave",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "available_leaves",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Available Leave",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-19 10:47:48.546724",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip Leave",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@ -17,319 +17,326 @@ class CircularReferenceError(frappe.ValidationError): pass
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
class Task(NestedSet):
nsm_parent_field = 'parent_task'
nsm_parent_field = 'parent_task'
def get_feed(self):
return '{0}: {1}'.format(_(self.status), self.subject)
def get_feed(self):
return '{0}: {1}'.format(_(self.status), self.subject)
def get_customer_details(self):
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
if cust:
ret = {'customer_name': cust and cust[0][0] or ''}
return ret
def get_customer_details(self):
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
if cust:
ret = {'customer_name': cust and cust[0][0] or ''}
return ret
def validate(self):
self.validate_dates()
self.validate_parent_expected_end_date()
self.validate_parent_project_dates()
self.validate_progress()
self.validate_status()
self.update_depends_on()
self.validate_dependencies_for_template_task()
def validate(self):
self.validate_dates()
self.validate_parent_expected_end_date()
self.validate_parent_project_dates()
self.validate_progress()
self.validate_status()
self.update_depends_on()
self.validate_dependencies_for_template_task()
def validate_dates(self):
if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
frappe.bold("Expected End Date")))
def validate_dates(self):
if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
frappe.bold("Expected End Date")))
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
frappe.bold("Actual End Date")))
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
frappe.bold("Actual End Date")))
def validate_parent_expected_end_date(self):
if self.parent_task:
parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date")
if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date):
frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date)))
def validate_parent_expected_end_date(self):
if self.parent_task:
parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date")
if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date):
frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date)))
def validate_parent_project_dates(self):
if not self.project or frappe.flags.in_test:
return
def validate_parent_project_dates(self):
if not self.project or frappe.flags.in_test:
return
expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
if expected_end_date:
validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
if expected_end_date:
validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
def validate_status(self):
if self.is_template and self.status != "Template":
self.status = "Template"
if self.status!=self.get_db_value("status") and self.status == "Completed":
for d in self.depends_on:
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
def validate_status(self):
if self.is_template and self.status != "Template":
self.status = "Template"
if self.status!=self.get_db_value("status") and self.status == "Completed":
for d in self.depends_on:
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
close_all_assignments(self.doctype, self.name)
close_all_assignments(self.doctype, self.name)
def validate_progress(self):
if flt(self.progress or 0) > 100:
frappe.throw(_("Progress % for a task cannot be more than 100."))
def validate_progress(self):
if flt(self.progress or 0) > 100:
frappe.throw(_("Progress % for a task cannot be more than 100."))
if flt(self.progress) == 100:
self.status = 'Completed'
if flt(self.progress) == 100:
self.status = 'Completed'
if self.status == 'Completed':
self.progress = 100
if self.status == 'Completed':
self.progress = 100
def validate_dependencies_for_template_task(self):
if self.is_template:
self.validate_parent_template_task()
self.validate_depends_on_tasks()
def validate_parent_template_task(self):
if self.parent_task:
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
def validate_depends_on_tasks(self):
if self.depends_on:
for task in self.depends_on:
if not frappe.db.get_value("Task", task.task, "is_template"):
dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
def validate_dependencies_for_template_task(self):
if self.is_template:
self.validate_parent_template_task()
self.validate_depends_on_tasks()
def update_depends_on(self):
depends_on_tasks = self.depends_on_tasks or ""
for d in self.depends_on:
if d.task and d.task not in depends_on_tasks:
depends_on_tasks += d.task + ","
self.depends_on_tasks = depends_on_tasks
def validate_parent_template_task(self):
if self.parent_task:
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
def update_nsm_model(self):
frappe.utils.nestedset.update_nsm(self)
def validate_depends_on_tasks(self):
if self.depends_on:
for task in self.depends_on:
if not frappe.db.get_value("Task", task.task, "is_template"):
dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
def on_update(self):
self.update_nsm_model()
self.check_recursion()
self.reschedule_dependent_tasks()
self.update_project()
self.unassign_todo()
self.populate_depends_on()
def update_depends_on(self):
depends_on_tasks = self.depends_on_tasks or ""
for d in self.depends_on:
if d.task and d.task not in depends_on_tasks:
depends_on_tasks += d.task + ","
self.depends_on_tasks = depends_on_tasks
def unassign_todo(self):
if self.status == "Completed":
close_all_assignments(self.doctype, self.name)
if self.status == "Cancelled":
clear(self.doctype, self.name)
def update_nsm_model(self):
frappe.utils.nestedset.update_nsm(self)
def update_total_expense_claim(self):
self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
def on_update(self):
self.update_nsm_model()
self.check_recursion()
self.reschedule_dependent_tasks()
self.update_project()
self.unassign_todo()
self.populate_depends_on()
def update_time_and_costing(self):
tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
,self.name, as_dict=1)[0]
if self.status == "Open":
self.status = "Working"
self.total_costing_amount= tl.total_costing_amount
self.total_billing_amount= tl.total_billing_amount
self.actual_time= tl.time
self.act_start_date= tl.start_date
self.act_end_date= tl.end_date
def unassign_todo(self):
if self.status == "Completed":
close_all_assignments(self.doctype, self.name)
if self.status == "Cancelled":
clear(self.doctype, self.name)
def update_project(self):
if self.project and not self.flags.from_project:
frappe.get_cached_doc("Project", self.project).update_project()
def update_total_expense_claim(self):
self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
def check_recursion(self):
if self.flags.ignore_recursion_check: return
check_list = [['task', 'parent'], ['parent', 'task']]
for d in check_list:
task_list, count = [self.name], 0
while (len(task_list) > count ):
tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
(d[0], d[1], '%s'), cstr(task_list[count]))
count = count + 1
for b in tasks:
if b[0] == self.name:
frappe.throw(_("Circular Reference Error"), CircularReferenceError)
if b[0]:
task_list.append(b[0])
def update_time_and_costing(self):
tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
,self.name, as_dict=1)[0]
if self.status == "Open":
self.status = "Working"
self.total_costing_amount= tl.total_costing_amount
self.total_billing_amount= tl.total_billing_amount
self.actual_time= tl.time
self.act_start_date= tl.start_date
self.act_end_date= tl.end_date
if count == 15:
break
def update_project(self):
if self.project and not self.flags.from_project:
frappe.get_cached_doc("Project", self.project).update_project()
def reschedule_dependent_tasks(self):
end_date = self.exp_end_date or self.act_end_date
if end_date:
for task_name in frappe.db.sql("""
select name from `tabTask` as parent
where parent.project = %(project)s
and parent.name in (
select parent from `tabTask Depends On` as child
where child.task = %(task)s and child.project = %(project)s)
""", {'project': self.project, 'task':self.name }, as_dict=1):
task = frappe.get_doc("Task", task_name.name)
if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
task.exp_start_date = add_days(end_date, 1)
task.exp_end_date = add_days(task.exp_start_date, task_duration)
task.flags.ignore_recursion_check = True
task.save()
def check_recursion(self):
if self.flags.ignore_recursion_check: return
check_list = [['task', 'parent'], ['parent', 'task']]
for d in check_list:
task_list, count = [self.name], 0
while (len(task_list) > count ):
tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
(d[0], d[1], '%s'), cstr(task_list[count]))
count = count + 1
for b in tasks:
if b[0] == self.name:
frappe.throw(_("Circular Reference Error"), CircularReferenceError)
if b[0]:
task_list.append(b[0])
def has_webform_permission(self):
project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
if project_user:
return True
if count == 15:
break
def populate_depends_on(self):
if self.parent_task:
parent = frappe.get_doc('Task', self.parent_task)
if self.name not in [row.task for row in parent.depends_on]:
parent.append("depends_on", {
"doctype": "Task Depends On",
"task": self.name,
"subject": self.subject
})
parent.save()
def reschedule_dependent_tasks(self):
end_date = self.exp_end_date or self.act_end_date
if end_date:
for task_name in frappe.db.sql("""
select name from `tabTask` as parent
where parent.project = %(project)s
and parent.name in (
select parent from `tabTask Depends On` as child
where child.task = %(task)s and child.project = %(project)s)
""", {'project': self.project, 'task':self.name }, as_dict=1):
task = frappe.get_doc("Task", task_name.name)
if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
task.exp_start_date = add_days(end_date, 1)
task.exp_end_date = add_days(task.exp_start_date, task_duration)
task.flags.ignore_recursion_check = True
task.save()
def on_trash(self):
if check_if_child_exists(self.name):
throw(_("Child Task exists for this Task. You can not delete this Task."))
def has_webform_permission(self):
project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
if project_user:
return True
self.update_nsm_model()
def populate_depends_on(self):
if self.parent_task:
parent = frappe.get_doc('Task', self.parent_task)
if self.name not in [row.task for row in parent.depends_on]:
parent.append("depends_on", {
"doctype": "Task Depends On",
"task": self.name,
"subject": self.subject
})
parent.save()
def after_delete(self):
self.update_project()
def on_trash(self):
if check_if_child_exists(self.name):
throw(_("Child Task exists for this Task. You can not delete this Task."))
def update_status(self):
if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
from datetime import datetime
if self.exp_end_date < datetime.now().date():
self.db_set('status', 'Overdue', update_modified=False)
self.update_project()
self.update_nsm_model()
def after_delete(self):
self.update_project()
def update_status(self):
if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
from datetime import datetime
if self.exp_end_date < datetime.now().date():
self.db_set('status', 'Overdue', update_modified=False)
self.update_project()
@frappe.whitelist()
def check_if_child_exists(name):
child_tasks = frappe.get_all("Task", filters={"parent_task": name})
child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
return child_tasks
child_tasks = frappe.get_all("Task", filters={"parent_task": name})
child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
return child_tasks
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_project(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond
return frappe.db.sql(""" select name from `tabProject`
where %(key)s like %(txt)s
%(mcond)s
order by name
limit %(start)s, %(page_len)s""" % {
'key': searchfield,
'txt': frappe.db.escape('%' + txt + '%'),
'mcond':get_match_cond(doctype),
'start': start,
'page_len': page_len
})
from erpnext.controllers.queries import get_match_cond
meta = frappe.get_meta(doctype)
searchfields = meta.get_search_fields()
search_columns = ", " + ", ".join(searchfields) if searchfields else ''
search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
return frappe.db.sql(""" select name {search_columns} from `tabProject`
where %(key)s like %(txt)s
%(mcond)s
{search_condition}
order by name
limit %(start)s, %(page_len)s""".format(search_columns = search_columns,
search_condition=search_cond), {
'key': searchfield,
'txt': '%' + txt + '%',
'mcond':get_match_cond(doctype),
'start': start,
'page_len': page_len
})
@frappe.whitelist()
def set_multiple_status(names, status):
names = json.loads(names)
for name in names:
task = frappe.get_doc("Task", name)
task.status = status
task.save()
names = json.loads(names)
for name in names:
task = frappe.get_doc("Task", name)
task.status = status
task.save()
def set_tasks_as_overdue():
tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
for task in tasks:
if task.status == "Pending Review":
if getdate(task.review_date) > getdate(today()):
continue
frappe.get_doc("Task", task.name).update_status()
tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
for task in tasks:
if task.status == "Pending Review":
if getdate(task.review_date) > getdate(today()):
continue
frappe.get_doc("Task", task.name).update_status()
@frappe.whitelist()
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
def set_missing_values(source, target):
target.append("time_logs", {
"hours": source.actual_time,
"completed": source.status == "Completed",
"project": source.project,
"task": source.name
})
def set_missing_values(source, target):
target.append("time_logs", {
"hours": source.actual_time,
"completed": source.status == "Completed",
"project": source.project,
"task": source.name
})
doclist = get_mapped_doc("Task", source_name, {
"Task": {
"doctype": "Timesheet"
}
}, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
doclist = get_mapped_doc("Task", source_name, {
"Task": {
"doctype": "Timesheet"
}
}, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
return doclist
return doclist
@frappe.whitelist()
def get_children(doctype, parent, task=None, project=None, is_root=False):
filters = [['docstatus', '<', '2']]
filters = [['docstatus', '<', '2']]
if task:
filters.append(['parent_task', '=', task])
elif parent and not is_root:
# via expand child
filters.append(['parent_task', '=', parent])
else:
filters.append(['ifnull(`parent_task`, "")', '=', ''])
if task:
filters.append(['parent_task', '=', task])
elif parent and not is_root:
# via expand child
filters.append(['parent_task', '=', parent])
else:
filters.append(['ifnull(`parent_task`, "")', '=', ''])
if project:
filters.append(['project', '=', project])
if project:
filters.append(['project', '=', project])
tasks = frappe.get_list(doctype, fields=[
'name as value',
'subject as title',
'is_group as expandable'
], filters=filters, order_by='name')
tasks = frappe.get_list(doctype, fields=[
'name as value',
'subject as title',
'is_group as expandable'
], filters=filters, order_by='name')
# return tasks
return tasks
# return tasks
return tasks
@frappe.whitelist()
def add_node():
from frappe.desk.treeview import make_tree_args
args = frappe.form_dict
args.update({
"name_field": "subject"
})
args = make_tree_args(**args)
from frappe.desk.treeview import make_tree_args
args = frappe.form_dict
args.update({
"name_field": "subject"
})
args = make_tree_args(**args)
if args.parent_task == 'All Tasks' or args.parent_task == args.project:
args.parent_task = None
if args.parent_task == 'All Tasks' or args.parent_task == args.project:
args.parent_task = None
frappe.get_doc(args).insert()
frappe.get_doc(args).insert()
@frappe.whitelist()
def add_multiple_tasks(data, parent):
data = json.loads(data)
new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
data = json.loads(data)
new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
for d in data:
if not d.get("subject"): continue
new_doc['subject'] = d.get("subject")
new_task = frappe.get_doc(new_doc)
new_task.insert()
for d in data:
if not d.get("subject"): continue
new_doc['subject'] = d.get("subject")
new_task = frappe.get_doc(new_doc)
new_task.insert()
def on_doctype_update():
frappe.db.add_index("Task", ["lft", "rgt"])
frappe.db.add_index("Task", ["lft", "rgt"])
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))

View File

@ -204,14 +204,16 @@ class Timesheet(Document):
ts_detail.billing_rate = 0.0
@frappe.whitelist()
def get_projectwise_timesheet_data(project, parent=None):
cond = ''
def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None):
condition = ''
if parent:
cond = "and parent = %(parent)s"
condition = "AND parent = %(parent)s"
if from_time and to_time:
condition += "AND from_time BETWEEN %(from_time)s AND %(to_time)s"
return frappe.db.sql("""select name, parent, billing_hours, billing_amount as billing_amt
from `tabTimesheet Detail` where parenttype = 'Timesheet' and docstatus=1 and project = %(project)s {0} and billable = 1
and sales_invoice is null""".format(cond), {'project': project, 'parent': parent}, as_dict=1)
and sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@ -158,16 +158,18 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
let me = this;
frappe.flags.round_off_applicable_accounts = [];
return frappe.call({
"method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
"args": {
"company": me.frm.doc.company,
"account_list": frappe.flags.round_off_applicable_accounts
},
callback: function(r) {
frappe.flags.round_off_applicable_accounts.push(...r.message);
}
});
if (me.frm.doc.company) {
return frappe.call({
"method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
"args": {
"company": me.frm.doc.company,
"account_list": frappe.flags.round_off_applicable_accounts
},
callback: function(r) {
frappe.flags.round_off_applicable_accounts.push(...r.message);
}
});
}
},
determine_exclusive_rate: function() {

View File

@ -70,18 +70,18 @@
},
{
"fieldname": "corrective_action",
"fieldtype": "Text",
"fieldtype": "Text Editor",
"label": "Corrective Action"
},
{
"fieldname": "preventive_action",
"fieldtype": "Text",
"fieldtype": "Text Editor",
"label": "Preventive Action"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-26 15:27:47.247814",
"modified": "2021-02-26 15:27:47.247814",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Non Conformance",
@ -115,4 +115,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@ -33,8 +33,7 @@
},
{
"fieldname": "sb_00",
"fieldtype": "Section Break",
"label": "Agenda"
"fieldtype": "Section Break"
},
{
"fieldname": "agenda",
@ -44,13 +43,12 @@
},
{
"fieldname": "sb_01",
"fieldtype": "Section Break",
"label": "Minutes"
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-27 16:36:45.657883",
"modified": "2021-02-27 16:36:45.657883",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Meeting",
@ -85,4 +83,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@ -1,12 +1,12 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
refresh(frm) {
const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
async refresh(frm) {
const einvoicing_enabled = await frappe.db.get_single_value("E Invoice Settings", "enable");
const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
if (cint(einvoicing_enabled) == 0 || !valid_supply_type || company_transaction) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
@ -83,7 +83,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
const action = () => {
const d = new frappe.ui.Dialog({
title: __('Generate E-Way Bill'),
wide: 1,
size: "large",
fields: get_ewaybill_fields(frm),
primary_action: function() {
const data = d.get_values();
@ -252,7 +252,7 @@ const request_irn_generation = (frm) => {
const get_preview_dialog = (frm, action) => {
const dialog = new frappe.ui.Dialog({
title: __("Preview"),
wide: 1,
size: "large",
fields: [
{
"label": "Preview",

View File

@ -202,9 +202,11 @@ 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:
is_applicable = t.tax_amount and t.account_head in gst_accounts_list
if is_applicable:
# this contains item wise tax rate & tax amount (incl. discount)
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount
item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
@ -229,7 +231,7 @@ def get_invoice_value_details(invoice):
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total)
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount)
else:
invoice_value_details.base_total = abs(invoice.base_net_total)
# since tax already considers discount amount

View File

@ -105,8 +105,9 @@ def add_print_formats():
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',

View File

@ -0,0 +1,38 @@
from __future__ import unicode_literals
import unittest
import frappe
from unittest.mock import patch
from erpnext.regional.india.utils import validate_document_name
class TestIndiaUtils(unittest.TestCase):
@patch("frappe.get_cached_value")
def test_validate_document_name(self, mock_get_cached):
mock_get_cached.return_value = "India" # mock country
posting_date = "2021-05-01"
invalid_names = [ "SI$1231", "012345678901234567", "SI 2020 05",
"SI.2020.0001", "PI2021 - 001" ]
for name in invalid_names:
doc = frappe._dict(name=name, posting_date=posting_date)
self.assertRaises(frappe.ValidationError, validate_document_name, doc)
valid_names = [ "012345678901236", "SI/2020/0001", "SI/2020-0001",
"2020-PI-0001", "PI2020-0001" ]
for name in valid_names:
doc = frappe._dict(name=name, posting_date=posting_date)
try:
validate_document_name(doc)
except frappe.ValidationError:
self.fail("Valid name {} throwing error".format(name))
@patch("frappe.get_cached_value")
def test_validate_document_name_not_india(self, mock_get_cached):
mock_get_cached.return_value = "Not India"
doc = frappe._dict(name="SI$123", posting_date="2021-05-01")
try:
validate_document_name(doc)
except frappe.ValidationError:
self.fail("Regional validation related to India are being applied to other countries")

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe, re, json
from frappe import _
import erpnext
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
from erpnext.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
from erpnext.controllers.accounts_controller import get_taxes_and_charges
@ -14,6 +14,13 @@ from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.utils import get_account_currency
from frappe.model.utils import get_fetch_values
GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") #alphanumeric and - /
GSTIN_FORMAT = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
GSTIN_UIN_FORMAT = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state:
doc.gst_state_number = state_numbers[doc.gst_state]
@ -37,12 +44,10 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
if gst_category and gst_category == 'UIN Holders':
p = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
if not p.match(doc.gstin):
if not GSTIN_UIN_FORMAT.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
else:
p = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
if not p.match(doc.gstin):
if not GSTIN_FORMAT.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
validate_gstin_check_digit(doc.gstin)
@ -59,8 +64,7 @@ def validate_pan_for_india(doc, method):
if doc.get('country') != 'India' or not doc.pan:
return
p = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
if not p.match(doc.pan):
if not PAN_NUMBER_FORMAT.match(doc.pan):
frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
def validate_tax_category(doc, method):
@ -148,6 +152,20 @@ def get_itemised_tax_breakup_data(doc, account_wise=False):
def set_place_of_supply(doc, method=None):
doc.place_of_supply = get_place_of_supply(doc, doc.doctype)
def validate_document_name(doc, method=None):
"""Validate GST invoice number requirements."""
country = frappe.get_cached_value("Company", doc.company, "country")
# Date was chosen as start of next FY to avoid irritating current users.
if country != "India" or getdate(doc.posting_date) < getdate("2021-04-01"):
return
if len(doc.name) > 16:
frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series."))
if not GST_INVOICE_NUMBER_FORMAT.match(doc.name):
frappe.throw(_("Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series."))
# don't remove this function it is used in tests
def test_method():
'''test function'''
@ -800,4 +818,4 @@ def get_regional_round_off_accounts(company, account_list):
account_list.extend(gst_account_list)
return account_list
return account_list

View File

@ -189,9 +189,7 @@ def make_custom_fields(update=True):
def setup_report():
report_name = 'Electronic Invoice Register'
frappe.db.sql(""" update `tabReport` set disabled = 0 where
name = %s """, report_name)
frappe.db.set_value("Report", report_name, "disabled", 0)
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(

View File

@ -36,5 +36,4 @@ def make_custom_fields(update=True):
def add_print_formats():
frappe.reload_doc("regional", "print_format", "irs_1099_form")
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
name in('IRS 1099 Form') """)
frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0)

View File

@ -140,7 +140,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 12:12:56.784014",
"modified": "2021-03-02 17:35:53.603607",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
@ -157,5 +157,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -140,7 +140,7 @@ frappe.ui.form.on("Company", {
doc: frm.doc,
freeze: true,
callback: function() {
frappe.msgprint(__("Default tax templates for sales and purchase are created."));
frappe.msgprint(__("Default tax templates for sales, purchase and items are created."));
}
})
},

View File

@ -29,6 +29,7 @@ def make_tax_account_and_template(company, account_name, tax_rate, template_name
try:
if accounts:
make_sales_and_purchase_tax_templates(accounts, template_name)
make_item_tax_templates(accounts, template_name)
except frappe.NameError:
if frappe.message_log: frappe.message_log.pop()
except RootNotEditable:
@ -84,6 +85,27 @@ def make_sales_and_purchase_tax_templates(accounts, template_name=None):
doc = frappe.get_doc(purchase_tax_template)
doc.insert(ignore_permissions=True)
def make_item_tax_templates(accounts, template_name=None):
if not template_name:
template_name = accounts[0].name
item_tax_template = {
"doctype": "Item Tax Template",
"title": template_name,
"company": accounts[0].company,
'taxes': []
}
for account in accounts:
item_tax_template['taxes'].append({
"tax_type": account.name,
"tax_rate": account.tax_rate
})
# Items
frappe.get_doc(copy.deepcopy(item_tax_template)).insert(ignore_permissions=True)
def get_tax_account_group(company):
tax_group = frappe.db.get_value("Account",
{"account_name": "Duties and Taxes", "is_group": 1, "company": company})

View File

@ -190,7 +190,7 @@
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-02-11 18:48:30.433058",
"modified": "2021-03-02 17:34:57.642565",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Settings",
@ -207,5 +207,6 @@
}
],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"track_changes": 1
}

View File

@ -93,7 +93,7 @@ class Batch(Document):
if create_new_batch:
if batch_number_series:
self.batch_id = make_autoname(batch_number_series)
self.batch_id = make_autoname(batch_number_series, doc=self)
elif batch_uses_naming_series():
self.batch_id = self.get_name_from_naming_series()
else:

View File

@ -324,10 +324,12 @@ class PurchaseReceipt(BuyingController):
else:
loss_account = self.get_company_default("default_expense_account")
cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append(self.get_gl_dict({
"account": loss_account,
"against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center,
"cost_center": cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": divisional_loss,
"project": d.project