diff --git a/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py b/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py index 9c5664a17..358b0da2f 100644 --- a/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py +++ b/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py @@ -14,7 +14,6 @@ class EmployeePayslipDownloadWizard(models.TransientModel): 'hr.employee', required=True, default=lambda self: self.env.user.employee_id.id, - readonly=True, ) download_type = fields.Selection( selection=[ @@ -39,6 +38,13 @@ class EmployeePayslipDownloadWizard(models.TransientModel): string='Available Payslips', compute='_compute_payslip_count', ) + is_hr_manager = fields.Boolean(compute="_compute_is_hr_manager") + + def _compute_is_hr_manager(self): + for rec in self: + import pdb + pdb.set_trace() + rec.is_hr_manager = self.env.user.has_group('hr.group_hr_manager') @api.onchange('download_type', 'period_id') def _onchange_download_type_period_id(self): diff --git a/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py b/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py index 574227e74..6ebe1fd82 100644 --- a/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py +++ b/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py @@ -18,9 +18,9 @@ class ITTaxStatementWizard(models.TransientModel): contract_id = fields.Many2one('hr.contract', related='employee_id.contract_id', required=True) currency_id = fields.Many2one('res.currency', related='employee_id.company_id.currency_id') - period_id = fields.Many2one('payroll.period', required=True) - period_line = fields.Many2one('payroll.period.line', - domain="[('period_id', '=', period_id), ('to_date', '<', fields.Date.today())]") + period_id = fields.Many2one('payroll.period', required=True) + period_line = fields.Many2one('payroll.period.line', + domain="[('period_id', '=', period_id), ('to_date', '<', fields.Date.today())]") # Taxpayer profile taxpayer_name = fields.Char(related='employee_id.name') @@ -95,33 +95,38 @@ class ITTaxStatementWizard(models.TransientModel): ('old', 'Old Regime'), ('new', 'New Regime') ], string="Beneficial Regime", readonly=True) + is_hr_manager = fields.Boolean(compute="_compute_is_hr_manager") - def _get_age_category(self, age): - if age < 60: - return 'below_60' - elif age < 80: - return '60_to_80' - return 'above_80' - - def _get_effective_period_start(self): - self.ensure_one() - period_start = self.period_id.from_date if self.period_id else False - if not period_start: - return False - if self.emp_doj and self.period_id.to_date and self.period_id.from_date <= self.emp_doj <= self.period_id.to_date: - return max(period_start, self.emp_doj.replace(day=1)) - return period_start - - def _get_effective_period_lines(self): - self.ensure_one() - if not self.period_id: - return self.env['payroll.period.line'] - - period_lines = self.period_id.period_line_ids.sorted('from_date') - effective_start = self._get_effective_period_start() - if not effective_start: - return period_lines - return period_lines.filtered(lambda line: line.to_date and line.to_date >= effective_start) + def _compute_is_hr_manager(self): + for rec in self: + rec.is_hr_manager = self.env.user.has_group('hr.group_hr_manager') + + def _get_age_category(self, age): + if age < 60: + return 'below_60' + elif age < 80: + return '60_to_80' + return 'above_80' + + def _get_effective_period_start(self): + self.ensure_one() + period_start = self.period_id.from_date if self.period_id else False + if not period_start: + return False + if self.emp_doj and self.period_id.to_date and self.period_id.from_date <= self.emp_doj <= self.period_id.to_date: + return max(period_start, self.emp_doj.replace(day=1)) + return period_start + + def _get_effective_period_lines(self): + self.ensure_one() + if not self.period_id: + return self.env['payroll.period.line'] + + period_lines = self.period_id.period_line_ids.sorted('from_date') + effective_start = self._get_effective_period_start() + if not effective_start: + return period_lines + return period_lines.filtered(lambda line: line.to_date and line.to_date >= effective_start) def _find_applicable_slab(self, regime, period_id, age, residence_type): """Find the applicable tax slab without forcing both regimes to exist.""" @@ -136,7 +141,7 @@ class ITTaxStatementWizard(models.TransientModel): ('residence_type', '=', 'both') ], limit=1) - def _get_applicable_slab(self, regime, period_id, age, residence_type): + def _get_applicable_slab(self, regime, period_id, age, residence_type): """Get the applicable tax slab based on regime, age, and residence type""" age_category = self._get_age_category(age) slab_master = self._find_applicable_slab(regime, period_id, age, residence_type) @@ -145,21 +150,21 @@ class ITTaxStatementWizard(models.TransientModel): "No tax slab found for %s Regime with Age Category: %s and Residence Type: %s" ) % (regime.capitalize(), age_category.replace('_', ' ').title(), residence_type)) - return slab_master - - @api.onchange('employee_id', 'period_id') - def _onchange_employee_id_period_id(self): - domain_by_record = {} - for rec in self: - domain = [('period_id', '=', rec.period_id.id), ('to_date', '<', fields.Date.today())] if rec.period_id else [] - if rec.emp_doj: - domain.append(('to_date', '>=', rec.emp_doj.replace(day=1))) - - if rec.period_line and rec.period_line not in rec._get_effective_period_lines(): - rec.period_line = False - domain_by_record[rec.id] = domain - if len(self) == 1: - return {'domain': {'period_line': domain_by_record.get(self.id, [])}} + return slab_master + + @api.onchange('employee_id', 'period_id') + def _onchange_employee_id_period_id(self): + domain_by_record = {} + for rec in self: + domain = [('period_id', '=', rec.period_id.id), ('to_date', '<', fields.Date.today())] if rec.period_id else [] + if rec.emp_doj: + domain.append(('to_date', '>=', rec.emp_doj.replace(day=1))) + + if rec.period_line and rec.period_line not in rec._get_effective_period_lines(): + rec.period_line = False + domain_by_record[rec.id] = domain + if len(self) == 1: + return {'domain': {'period_line': domain_by_record.get(self.id, [])}} def _get_standard_deduction(self, regime, slab_master=False): if slab_master: @@ -329,7 +334,7 @@ class ITTaxStatementWizard(models.TransientModel): return list(grouped.values()) - def fetch_salary_components(self): + def fetch_salary_components(self): """fetch salary components from payroll data""" for rec in self: data = { @@ -345,10 +350,10 @@ class ITTaxStatementWizard(models.TransientModel): } if not rec.employee_id or not rec.contract_id or not rec.period_id or not rec.period_line: return data - period_lines = rec._get_effective_period_lines() - - for line in period_lines: - components = rec._get_salary_components_for_period_line(line) + period_lines = rec._get_effective_period_lines() + + for line in period_lines: + components = rec._get_salary_components_for_period_line(line) if line.from_date and rec.period_line.from_date and line.from_date <= rec.period_line.from_date: data['basic_salary']['actual'].append(components['basic_salary']) data['hra_salary']['actual'].append(components['hra_salary']) @@ -412,7 +417,7 @@ class ITTaxStatementWizard(models.TransientModel): ) rec.standard_deduction = rec._get_standard_deduction(rec.tax_regime, slab_master) - def fetch_deduction_components(self): + def fetch_deduction_components(self): for rec in self: data = { 'professional_tax': {'actual': [], 'projected': []}, @@ -421,12 +426,12 @@ class ITTaxStatementWizard(models.TransientModel): if not rec.employee_id or not rec.contract_id or not rec.period_id or not rec.period_line: return data - for line in rec._get_effective_period_lines(): - rule_amounts = rec._get_rule_amounts_for_period_line(line, ['PT', 'PFE']) - bucket = 'actual' if line.from_date <= rec.period_line.from_date else 'projected' - data['professional_tax'][bucket].append(rule_amounts['PT']) - data['nps_employer_contribution'][bucket].append(rule_amounts['PFE']) - return data + for line in rec._get_effective_period_lines(): + rule_amounts = rec._get_rule_amounts_for_period_line(line, ['PT', 'PFE']) + bucket = 'actual' if line.from_date <= rec.period_line.from_date else 'projected' + data['professional_tax'][bucket].append(rule_amounts['PT']) + data['nps_employer_contribution'][bucket].append(rule_amounts['PFE']) + return data @api.onchange('employee_id') @@ -521,6 +526,14 @@ class ITTaxStatementWizard(models.TransientModel): tax_with_surcharge = total_before_mr - mr return surcharge, mr, tax_with_surcharge + def fetch_current_employer_deducted_tax(self): + for rec in self: + payslip_ids = self.env['hr.payslip'].sudo().search([('employee_id','=',rec.employee_id.id),('state','in',['done','paid']),('date_from','>=',rec.period_id.from_date),('date_to','<=',rec.period_id.to_date)]) + amount_deducted = 0.0 + for payslip in payslip_ids: + amount_deducted += sum(payslip.line_ids.filtered(lambda l:l.salary_rule_id.code == 'TDS').mapped('amount')) + + return amount_deducted def _compute_tax_old_regime(self, taxable, slab_master=False): # Get applicable slab slab_master = slab_master or self._get_applicable_slab( @@ -549,6 +562,8 @@ class ITTaxStatementWizard(models.TransientModel): total_tax = tax_with_surcharge + cess + current_employer_deducted_tax = self.fetch_current_employer_deducted_tax() + return { 'taxable_income': taxable, 'slab_tax': slab_tax, @@ -558,7 +573,9 @@ class ITTaxStatementWizard(models.TransientModel): 'marginal_relief': marginal_relief, 'tax_with_surcharge': tax_with_surcharge, 'cess_4pct': cess, - 'total_tax': total_tax + 'total_tax': total_tax, + 'current_employer_deducted_tax': current_employer_deducted_tax, + 'balance_tax': total_tax - (-current_employer_deducted_tax) } def _compute_tax_new_regime(self, taxable, slab_master=False): @@ -587,6 +604,7 @@ class ITTaxStatementWizard(models.TransientModel): cess = tax_with_surcharge * cess_rate[0] / 100 total_tax = tax_with_surcharge + cess + current_employer_deducted_tax = self.fetch_current_employer_deducted_tax() return { 'taxable_income': taxable, 'slab_tax': slab_tax, @@ -596,7 +614,9 @@ class ITTaxStatementWizard(models.TransientModel): 'marginal_relief': marginal_relief, 'tax_with_surcharge': tax_with_surcharge, 'cess_4pct': cess, - 'total_tax': total_tax + 'total_tax': total_tax, + 'current_employer_deducted_tax': current_employer_deducted_tax, + 'balance_tax': total_tax - (-current_employer_deducted_tax) } def _compute_house_property_income(self): @@ -759,19 +779,19 @@ class ITTaxStatementWizard(models.TransientModel): 'target': 'current', } - def _prepare_income_tax_data(self, include_comparison=False): - """Prepare data for the tax statement report""" - today = date.today() - display_fy_start = self.period_id.from_date - fy_end = self.period_id.to_date - effective_fy_start = self._get_effective_period_start() or display_fy_start - total_months = ((fy_end.year - effective_fy_start.year) * 12 + - (fy_end.month - effective_fy_start.month) + 1) - - line_start = self.period_line.from_date - current_month_index = ((line_start.year - effective_fy_start.year) * 12 + - (line_start.month - effective_fy_start.month) + 1) - values = self._get_tax_base_values(include_comparison=include_comparison) + def _prepare_income_tax_data(self, include_comparison=False): + """Prepare data for the tax statement report""" + today = date.today() + display_fy_start = self.period_id.from_date + fy_end = self.period_id.to_date + effective_fy_start = self._get_effective_period_start() or display_fy_start + total_months = ((fy_end.year - effective_fy_start.year) * 12 + + (fy_end.month - effective_fy_start.month) + 1) + + line_start = self.period_line.from_date + current_month_index = ((line_start.year - effective_fy_start.year) * 12 + + (line_start.month - effective_fy_start.month) + 1) + values = self._get_tax_base_values(include_comparison=include_comparison) salary_components_data = values['salary_components_data'] annual_gross_salary = values['annual_gross_salary'] gross_salary_actual = values['gross_salary_actual'] @@ -806,16 +826,16 @@ class ITTaxStatementWizard(models.TransientModel): # Prepare data structure matching screenshot format # Financial year (period_id) - display_fy_start = self.period_id.from_date - fy_end = self.period_id.to_date - effective_fy_start = self._get_effective_period_start() or display_fy_start - total_months = ((fy_end.year - effective_fy_start.year) * 12 + - (fy_end.month - effective_fy_start.month) + 1) - - # Current month (period_line) - line_start = self.period_line.from_date - current_month_index = ((line_start.year - effective_fy_start.year) * 12 + - (line_start.month - effective_fy_start.month) + 1) + display_fy_start = self.period_id.from_date + fy_end = self.period_id.to_date + effective_fy_start = self._get_effective_period_start() or display_fy_start + total_months = ((fy_end.year - effective_fy_start.year) * 12 + + (fy_end.month - effective_fy_start.month) + 1) + + # Current month (period_line) + line_start = self.period_line.from_date + current_month_index = ((line_start.year - effective_fy_start.year) * 12 + + (line_start.month - effective_fy_start.month) + 1) tax_result['roundoff_taxable_income'] = float(round(tax_result["taxable_income"] / 10) * 10) birthday = self.employee_id.birthday if birthday: @@ -835,8 +855,8 @@ class ITTaxStatementWizard(models.TransientModel): 'total': total, }) data = { - 'financial_year': f"{display_fy_start.year}-{fy_end.year}", - 'assessment_year': fy_end.year + 1, + 'financial_year': f"{display_fy_start.year}-{fy_end.year}", + 'assessment_year': fy_end.year, 'report_time': today.strftime('%d-%m-%Y %H:%M'), 'user': 'ESS', 'emp_code': self.employee_id.employee_id, diff --git a/addons_extensions/employee_it_declaration/report/it_tax_template.xml b/addons_extensions/employee_it_declaration/report/it_tax_template.xml index 70acae040..d44989933 100644 --- a/addons_extensions/employee_it_declaration/report/it_tax_template.xml +++ b/addons_extensions/employee_it_declaration/report/it_tax_template.xml @@ -38,6 +38,8 @@ + + @@ -378,7 +380,7 @@ Less: Tax deducted current employer (up to previous month) 0 - 0 + Less: Tax deducted from previous Employer / Self Tax Paid @@ -389,7 +391,7 @@ Balance Tax for the year 0 - + Less: Adhoc tax deducted in Off-Cycle in current month @@ -399,7 +401,7 @@ Balance Tax 0 - + Tax deducted from current month salary diff --git a/addons_extensions/employee_it_declaration/security/ir.model.access.csv b/addons_extensions/employee_it_declaration/security/ir.model.access.csv index 00e9aa80a..b55d8b1c5 100644 --- a/addons_extensions/employee_it_declaration/security/ir.model.access.csv +++ b/addons_extensions/employee_it_declaration/security/ir.model.access.csv @@ -54,14 +54,14 @@ access_nsc_interest_entry_user,nsc.interest.entry,model_nsc_interest_entry,base. access_house_rent_declaration_user,access.house.rent.declaration.user,model_house_rent_declaration,base.group_user,1,1,1,1 -access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0 -access_it_tax_statement_wizard,it.tax.statement.wizard,model_it_tax_statement_wizard,base.group_user,1,0,0,0 -access_employee_payslip_download_wizard,employee.payslip.download.wizard,model_employee_payslip_download_wizard,base.group_user,1,1,1,0 - -access_it_tax_statement_manager,it.tax.statement,model_it_tax_statement,hr.group_hr_manager,1,1,1,1 +access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0 +access_it_tax_statement_wizard,it.tax.statement.wizard,model_it_tax_statement_wizard,base.group_user,1,1,1,0 +access_employee_payslip_download_wizard,employee.payslip.download.wizard,model_employee_payslip_download_wizard,base.group_user,1,1,1,0 + +access_it_tax_statement_manager,it.tax.statement,model_it_tax_statement,hr.group_hr_manager,1,1,1,1 access_it_tax_statement_wizard_manager,it.tax.statement.wizard,model_it_tax_statement_wizard,hr.group_hr_manager,1,1,1,1 access_it_slab_master,it.slab.master,model_it_slab_master,base.group_user,1,1,1,1 access_it_slab_master_rules,it.slab.master.rules,model_it_slab_master_rules,base.group_user,1,1,1,1 -access_it_sur_charge_rules,it.sur.charge.rules.user,model_it_sur_charge_rules,base.group_user,1,1,1,1 +access_it_sur_charge_rules,it.sur.charge.rules.user,model_it_sur_charge_rules,base.group_user,1,1,1,1 diff --git a/addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml b/addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml index 62a666ea4..cf0b7b0cf 100644 --- a/addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml +++ b/addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml @@ -21,7 +21,8 @@ - + + diff --git a/addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml b/addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml index 604ca120d..59bfaa342 100644 --- a/addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml +++ b/addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml @@ -43,7 +43,8 @@ - + + @@ -88,11 +89,13 @@ it.tax.statement.wizard tax-statement form + [("activity_ids.active", "in", [True, False])]

Create a new employment type

+ ", "/helpdesk/ticket//", @@ -170,6 +245,8 @@ class CustomerPortal(portal.CustomerPortal): return request.redirect('/my') values = self._ticket_get_page_view_values(ticket_sudo, access_token, **kw) + values['ticket_created'] = kw.get('created') + values['ticket_reopened'] = kw.get('reopened') return request.render("helpdesk.tickets_followup", values) @http.route([ @@ -195,3 +272,18 @@ class CustomerPortal(portal.CustomerPortal): ticket_sudo.with_context(mail_create_nosubscribe=True).message_post(body=body, message_type='comment', subtype_xmlid='mail.mt_note') return request.redirect('/my/ticket/%s/%s?ticket_closed=1' % (ticket_id, access_token or '')) + + @http.route([ + '/my/ticket/rerequest/', + '/my/ticket/rerequest//', + ], type='http', auth="public", website=True) + def ticket_rerequest(self, ticket_id=None, access_token=None, **kw): + try: + ticket_sudo = self._document_check_access('helpdesk.ticket', ticket_id, access_token) + except (AccessError, MissingError): + return request.redirect('/my') + + if ticket_sudo.stage_id.fold: + ticket_sudo.action_portal_rerequest() + + return request.redirect('/my/ticket/%s/%s?reopened=1' % (ticket_id, access_token or ticket_sudo.access_token or '')) diff --git a/addons_extensions/helpdesk/data/helpdesk_data.xml b/addons_extensions/helpdesk/data/helpdesk_data.xml index 8ac5ae973..6fbb5dac5 100644 --- a/addons_extensions/helpdesk/data/helpdesk_data.xml +++ b/addons_extensions/helpdesk/data/helpdesk_data.xml @@ -1,42 +1,88 @@ - - - - - Customer Care - customer-care - - - - - - - - New - 0 - - - - - In Progress - 1 - - - - On Hold - 2 - - - - Solved - - 3 - - - - Cancelled - 4 - - - - - + + + + + Customer Care + customer-care + general + + + + + + + + + + Technical Support + technical-support + technical + + portal + + + + + + + + + + Personal Concern Support + personal-concern-support + personal + + portal + + + + + + + + + + Damage / Asset Support + damage-asset-support + damage + + portal + + + + + + + + + + + New + 0 + + + + + In Progress + 1 + + + + On Hold + 2 + + + + Solved + + 3 + + + + Cancelled + 4 + + + + + diff --git a/addons_extensions/helpdesk/data/mail_template_data.xml b/addons_extensions/helpdesk/data/mail_template_data.xml index 2135064e0..57c946b8f 100644 --- a/addons_extensions/helpdesk/data/mail_template_data.xml +++ b/addons_extensions/helpdesk/data/mail_template_data.xml @@ -1,9 +1,9 @@ - - Helpdesk: Ticket Received - - {{ object.name }} + + Helpdesk: Ticket Received + + Ticket Received - {{ object.ticket_ref or object.id }} {{ (object.team_id.alias_email_from or object.company_id.email_formatted or object.user_id.email_formatted or user.email_formatted) }} {{ (object.partner_email if not object.sudo().partner_id.email or object.sudo().partner_id.email != object.partner_email else '') }} {{ object.partner_id.id if object.sudo().partner_id.email and object.sudo().partner_id.email == object.partner_email else '' }} @@ -11,7 +11,7 @@
Dear Madam/Sir,

- Your request + Your ticket has been received. Please wait patiently while our team reviews your request: Table legs are unbalanced @@ -51,12 +51,33 @@ {{ object.partner_id.lang or object.user_id.lang or user.lang }} - + + + + Helpdesk: New Ticket Assigned User + + New Ticket Assigned - {{ object.ticket_ref or object.id }} + {{ (object.company_id.email_formatted or user.email_formatted) }} + {{ object.user_id.email_formatted if object.user_id.email else '' }} + Notify the assigned helpdesk user when a new ticket is created. + +
+ Hello there,

+ You have a new ticket assigned to you.

+ Reference: 15
+ Type: Technical Issue
+ Customer: Customer
+ Subject: Ticket Subject

+ Open Ticket +
+
+ +
Helpdesk: Ticket Closed - Ticket Closed - Reference {{ object.id if object.id else 15 }} + Ticket Closed - Reference {{ object.ticket_ref or object.id }} {{ (object.team_id.alias_email_from or object.company_id.email_formatted or object.user_id.email_formatted or user.email_formatted) }} {{ (object.partner_email if not object.sudo().partner_id.email or object.sudo().partner_id.email != object.partner_email else '') }} {{ object.partner_id.id if object.sudo().partner_id.email and object.sudo().partner_id.email == object.partner_email else '' }} @@ -64,10 +85,14 @@
Dear Madam/Sir,

- We would like to inform you that we have closed your ticket (reference 15). - We trust that the services provided have met your expectations and that you have found a satisfactory resolution to your issue.

- However, if you have any further questions or comments, please do not hesitate to reply to this email to re-open your ticket. - Our team is always here to help you and we will be happy to assist you with any further concerns you may have.

+ Your ticket (reference 15) is now closed. + We trust that the services provided have met your expectations and that you have found a satisfactory resolution to your issue.

+ Please give feedback or leave a review if you would like to. This is optional, but it helps us improve.

+ However, if you have any further questions or comments, please reply to this email or use the re-request option in your portal to re-open your ticket. + Our team is always here to help you and we will be happy to assist you with any further concerns you may have.

+ Thank you for choosing our services and for your cooperation throughout this process. We truly value your business and appreciate the opportunity to serve you.

Kind regards,

Helpdesk Team. @@ -75,7 +100,25 @@ {{ object.partner_id.lang or object.user_id.lang or user.lang }} - + + + + Helpdesk: Ticket Re-requested + + Ticket Re-requested - {{ object.ticket_ref or object.id }} + {{ (object.company_id.email_formatted or user.email_formatted) }} + {{ object.user_id.email_formatted if object.user_id.email else '' }} + Notify the assigned helpdesk user when a customer re-requests help on a closed ticket. + +
+ Hello there,

+ The customer has re-requested help on ticket 15.

+ Subject: Ticket Subject

+ Open Ticket +
+
+ +
Helpdesk: Ticket Rating Request diff --git a/addons_extensions/helpdesk/models/helpdesk_team.py b/addons_extensions/helpdesk/models/helpdesk_team.py index a783f0bc8..e5bf63dc2 100644 --- a/addons_extensions/helpdesk/models/helpdesk_team.py +++ b/addons_extensions/helpdesk/models/helpdesk_team.py @@ -72,6 +72,20 @@ class HelpdeskTeam(models.Model): privacy_visibility_warning = fields.Char(compute='_compute_privacy_visibility_warning', export_string_translation=False) access_instruction_message = fields.Char(compute='_compute_access_instruction_message', export_string_translation=False) ticket_ids = fields.One2many('helpdesk.ticket', 'team_id', string='Tickets') + portal_ticket_type = fields.Selection([ + ('technical', 'Technical Issue'), + ('personal', 'Personal / Harassment'), + ('damage', 'Damage / Asset Issue'), + ('general', 'General Request'), + ('others', 'Others') + ], string='Portal Ticket Type', default='general', + help="Used by the website ticket form to route requests to the correct team.") + responsible_manager_id = fields.Many2one( + 'res.users', string='Responsible Manager', + domain=lambda self: [('groups_id', 'in', self.env.ref('helpdesk.group_helpdesk_user').id)]) + # approver_id = fields.Many2one( + # 'res.users', string='Approver', + # domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_user').id)]) use_alias = fields.Boolean('Use Alias', default=True) has_external_mail_server = fields.Boolean(compute='_compute_has_external_mail_server', export_string_translation=False) diff --git a/addons_extensions/helpdesk/models/helpdesk_ticket.py b/addons_extensions/helpdesk/models/helpdesk_ticket.py index 0cf9441e3..c848c53a1 100644 --- a/addons_extensions/helpdesk/models/helpdesk_ticket.py +++ b/addons_extensions/helpdesk/models/helpdesk_ticket.py @@ -16,6 +16,14 @@ TICKET_PRIORITY = [ ('3', 'Urgent'), ] +HELPDESK_TICKET_TYPES = [ + ('technical', 'Technical Issue'), + ('personal', 'Personal / Harassment'), + ('damage', 'Damage / Asset Issue'), + ('general', 'General Request'), + ('others', 'Others') +] + class HelpdeskTicket(models.Model): _name = 'helpdesk.ticket' _description = 'Helpdesk Ticket' @@ -61,6 +69,13 @@ class HelpdeskTicket(models.Model): name = fields.Char(string='Subject', required=True, index=True, tracking=True) team_id = fields.Many2one('helpdesk.team', string='Helpdesk Team', default=_default_team_id, index=True, tracking=True) + portal_ticket_type = fields.Selection(HELPDESK_TICKET_TYPES, string='Ticket Type',store=True, tracking=True, relatated='team_id.portal_ticket_type') + responsible_manager_id = fields.Many2one( + 'res.users', string='Responsible Manager', related='team_id.responsible_manager_id', + store=True, readonly=True) + # approver_id = fields.Many2one( + # 'res.users', string='Approver', related='team_id.approver_id', + # store=True, readonly=True) use_sla = fields.Boolean(related='team_id.use_sla') team_privacy_visibility = fields.Selection(related='team_id.privacy_visibility', export_string_translation=False) description = fields.Html(sanitize_attributes=False) @@ -156,6 +171,8 @@ class HelpdeskTicket(models.Model): if ticket_sudo.team_id and ticket_sudo.team_id.privacy_visibility == 'invited_internal': ticket_user_ids = ticket_sudo.team_id.message_partner_ids.user_ids.ids ticket.domain_user_ids = [Command.set(user_ids + ticket_user_ids)] + if ticket_sudo.team_id.auto_assignment: + ticket.domain_user_ids = [Command.set(ticket_sudo.team_id.member_ids.ids)] def _compute_access_url(self): super(HelpdeskTicket, self)._compute_access_url() @@ -259,6 +276,12 @@ class HelpdeskTicket(models.Model): if not ticket.stage_id or ticket.stage_id not in ticket.team_id.stage_ids: ticket.stage_id = ticket.team_id._determine_stage()[ticket.team_id.id] + + @api.onchange('team_id') + def onchange_user_and_stage_ids(self): + for ticket in self.filtered(lambda ticket: ticket.team_id): + ticket.user_id = ticket.team_id._determine_user_to_assign()[ticket.team_id.id] + @api.depends('partner_id') def _compute_partner_name(self): for ticket in self: @@ -522,6 +545,7 @@ class HelpdeskTicket(models.Model): # apply SLA tickets.sudo()._sla_apply() + tickets._send_ticket_created_notifications() return tickets @@ -579,8 +603,56 @@ class HelpdeskTicket(models.Model): message = _("This ticket was successfully closed %s hours before its SLA deadline.", round(abs(min_hours))) if min_hours < 0 \ else _("This ticket was closed %s hours after its SLA deadline.", round(min_hours)) ticket.message_post(body=message, subtype_xmlid="mail.mt_note", author_id=odoobot_partner_id) + closed_tickets._send_ticket_closed_notifications() return res + def _send_template_once(self, template_xmlid, marker): + template = self.env.ref(template_xmlid, raise_if_not_found=False) + if not template: + return + for ticket in self: + if ticket.message_ids.filtered(lambda message: message.body and marker in message.body): + continue + template.sudo().send_mail(ticket.id, force_send=True, email_layout_xmlid='mail.mail_notification_light') + ticket.message_post(body=marker, subtype_xmlid='mail.mt_note') + + def _send_ticket_created_notifications(self): + if self.env.context.get('helpdesk_skip_create_notifications') or self.env.context.get('install_mode'): + return + self.filtered(lambda ticket: ticket.partner_email)._send_template_once( + 'helpdesk.new_ticket_request_email_template', + '', + ) + self.filtered(lambda ticket: ticket.user_id and ticket.user_id.email)._send_template_once( + 'helpdesk.ticket_assigned_user_email_template', + '', + ) + + def _send_ticket_closed_notifications(self): + if self.env.context.get('helpdesk_skip_close_notifications') or self.env.context.get('install_mode'): + return + self.filtered(lambda ticket: ticket.partner_email)._send_template_once( + 'helpdesk.solved_ticket_request_email_template', + '', + ) + + def action_portal_rerequest(self): + for ticket in self: + opening_stage = ticket.team_id._determine_stage()[ticket.team_id.id] + vals = {'closed_by_partner': False} + if opening_stage: + vals['stage_id'] = opening_stage.id + ticket.with_context(helpdesk_skip_create_notifications=True).write(vals) + ticket.message_post( + body=_('The customer has re-requested help on this ticket.'), + message_type='comment', + subtype_xmlid='mail.mt_note', + ) + self.filtered(lambda ticket: ticket.user_id and ticket.user_id.email)._send_template_once( + 'helpdesk.ticket_reopened_user_email_template', + '', + ) + def copy_data(self, default=None): vals_list = super().copy_data(default=default) return [dict(vals, name=self.env._("%s (copy)", ticket.name)) for ticket, vals in zip(self, vals_list)] @@ -787,6 +859,11 @@ class HelpdeskTicket(models.Model): partner_ids = [x.id for x in self.env['mail.thread']._mail_find_partner_from_emails(self._ticket_email_split(msg), records=self) if x] if partner_ids: self.message_subscribe(partner_ids) + incoming_email = tools.email_normalize(msg.get('from') or '') + for ticket in self.filtered(lambda t: t.stage_id.fold): + partner_email = tools.email_normalize(ticket.partner_email or ticket.partner_id.email or '') + if incoming_email and incoming_email == partner_email: + ticket.action_portal_rerequest() return super(HelpdeskTicket, self).message_update(msg, update_vals=update_vals) def _message_compute_subject(self): diff --git a/addons_extensions/helpdesk/report/helpdesk_sla_report_analysis.py b/addons_extensions/helpdesk/report/helpdesk_sla_report_analysis.py index 69c89eb16..deffb5c25 100644 --- a/addons_extensions/helpdesk/report/helpdesk_sla_report_analysis.py +++ b/addons_extensions/helpdesk/report/helpdesk_sla_report_analysis.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, tools -from odoo.addons.helpdesk.models.helpdesk_ticket import TICKET_PRIORITY +from odoo.addons.helpdesk.models.helpdesk_ticket import HELPDESK_TICKET_TYPES, TICKET_PRIORITY from odoo.addons.rating.models.rating_data import RATING_LIMIT_MIN @@ -21,6 +21,7 @@ class HelpdeskSLAReport(models.Model): name = fields.Char(string='Subject', readonly=True) create_date = fields.Datetime("Ticket Creation Date", readonly=True) priority = fields.Selection(TICKET_PRIORITY, string='Minimum Priority', readonly=True) + portal_ticket_type = fields.Selection(HELPDESK_TICKET_TYPES, string='Ticket Type', readonly=True, related='team_id.portal_ticket_type') user_id = fields.Many2one('res.users', string="Assigned To", readonly=True) partner_id = fields.Many2one('res.partner', string="Customer", readonly=True) partner_name = fields.Char(string='Customer Name', readonly=True) @@ -76,6 +77,7 @@ class HelpdeskSLAReport(models.Model): NULLIF(T.rating_last_value, 0) AS rating_last_value, AVG(rt.rating) as rating_avg, T.priority AS priority, + T.portal_ticket_type AS portal_ticket_type, NULLIF(T.close_hours, 0) AS ticket_close_hours, CASE WHEN EXTRACT(EPOCH FROM (COALESCE(T.assign_date, NOW() AT TIME ZONE 'UTC') - T.create_date)) / 3600 < 1 THEN NULL diff --git a/addons_extensions/helpdesk/report/helpdesk_ticket_analysis.py b/addons_extensions/helpdesk/report/helpdesk_ticket_analysis.py index 96fd56d02..e2268d640 100644 --- a/addons_extensions/helpdesk/report/helpdesk_ticket_analysis.py +++ b/addons_extensions/helpdesk/report/helpdesk_ticket_analysis.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models, tools -from odoo.addons.helpdesk.models.helpdesk_ticket import TICKET_PRIORITY +from odoo.addons.helpdesk.models.helpdesk_ticket import HELPDESK_TICKET_TYPES, TICKET_PRIORITY from odoo.addons.rating.models.rating_data import RATING_LIMIT_MIN @@ -25,6 +25,7 @@ class HelpdeskTicketReport(models.Model): sla_status_ids = fields.One2many('helpdesk.sla.status', 'ticket_id', string="SLA Status") create_date = fields.Datetime("Ticket Creation Date", readonly=True) priority = fields.Selection(TICKET_PRIORITY, string='Minimum Priority', readonly=True) + portal_ticket_type = fields.Selection(HELPDESK_TICKET_TYPES, string='Ticket Type', readonly=True, related='team_id.portal_ticket_type') user_id = fields.Many2one('res.users', string="Assigned To", readonly=True) partner_id = fields.Many2one('res.partner', string="Customer", readonly=True) partner_name = fields.Char(string='Customer Name', readonly=True) @@ -60,6 +61,7 @@ class HelpdeskTicketReport(models.Model): T.name AS name, T.create_date AS create_date, T.priority AS priority, + T.portal_ticket_type AS portal_ticket_type, T.user_id AS user_id, T.partner_id AS partner_id, T.partner_name AS partner_name, diff --git a/addons_extensions/helpdesk/views/helpdesk_portal_templates.xml b/addons_extensions/helpdesk/views/helpdesk_portal_templates.xml index 50efb2b5b..e899fe3cb 100644 --- a/addons_extensions/helpdesk/views/helpdesk_portal_templates.xml +++ b/addons_extensions/helpdesk/views/helpdesk_portal_templates.xml @@ -39,6 +39,9 @@ Tickets +
There are currently no Ticket for your account.
@@ -47,6 +50,7 @@ Ticket + Type Reported on Assigned to Stage @@ -82,6 +86,7 @@ # + @@ -100,6 +105,161 @@ + + + +