# -*- coding: utf-8 -*- import ast import calendar from datetime import datetime, time, timedelta from dateutil import relativedelta from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY from odoo.osv import expression from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError class Project(models.Model): _inherit = "project.project" _order = "project_no desc" def _get_task_type(self): """ :return: project task type if it default """ type_ids = self.env['project.task.type'].search([('is_default', '=', True)]) return type_ids name = fields.Char("Name", index=True, tracking=True, required=False) project_no = fields.Char("Project Number", default=lambda self: self._get_next_projectno(), tracking=True, copy=False) category_id = fields.Many2one('project.category', string='Project Category', tracking=True) parent_id = fields.Many2one('project.project', index=True, string='Parent Project', tracking=True) sub_project_id = fields.One2many('project.project', 'parent_id', string='Sub-Project', tracking=True) department_id = fields.Many2one('hr.department', string='Department') consultant_id = fields.Many2one('res.partner', string='Consultant') beneficiary_id = fields.Many2one('res.partner', string='Beneficiary') lessons_learned = fields.Html(string='Lessons learned') launch_date = fields.Datetime("Launch date", tracking=True) start = fields.Date(string="Start Date", index=True, tracking=True) date = fields.Date(string='End Date', index=True, tracking=True) project_phase_ids = fields.One2many('project.phase', 'project_id', string="Project Phases") tag_ids = fields.Many2many('project.tags', string='Tags') type_ids = fields.Many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', string='Tasks Stages', default=_get_task_type) priority = fields.Selection([('0', 'Normal'),('1', 'Important'),], default='0', index=True, string="Priority") # address fields street = fields.Char() street2 = fields.Char() zip = fields.Char(change_default=True) country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.company.country_id, ondelete='restrict', tracking=True) state_id = fields.Many2one("res.country.state", string='Region', ondelete='restrict') city = fields.Char() # Contract fields contract_number = fields.Char(string="Contract Number", tracking=True) signature_date = fields.Date(string="Signature Date", tracking=True) contract_value = fields.Float("Total Contract Value") tax_amount = fields.Float("Taxes Amount") contract_value_untaxed = fields.Float("Contract Value") state = fields.Selection([ ('draft', 'PMO'), ('confirm', 'PM'), ('done', 'Done')], string='State', copy=False, default='draft', required=True, tracking=True) status = fields.Selection([ ('new', 'New'), ('open', 'Running'), ('hold', 'Hold'), ('close', 'Closed')], string='Status', copy=False, default='new', tracking=True) hold_reason = fields.Text(string="Hold Reason") close_reason = fields.Text(string="Close Reason") project_team_ids = fields.One2many('project.team', 'project_id', string="Project Team") invoice_ids = fields.One2many('project.invoice', 'project_id', "Invoice", copy=False, help="The Details of invoice") total_invoiced_amount = fields.Float('Total Invoiced Amount', compute='compute_total_invoiced_amount') no_of_invoices = fields.Integer(compute='compute_total_invoiced_payment') no_of_paid_invoices = fields.Integer(compute='compute_total_invoiced_payment') total_invoiced_payment = fields.Float('Paid Amount', compute='compute_total_invoiced_payment') residual_amount = fields.Float('Remaining Amount', compute='compute_residual_amount') back_log_amount = fields.Float('Back Log Amount', compute='compute_back_log_amount') # TODO check if this fields needed is_down_payment = fields.Boolean('Down Payment') to_invoice = fields.Boolean(compute="compute_to_invoice", store=True) invoice_method = fields.Selection([ ('per_stage', 'Invoicing Per Phase'), ('per_period', 'Invoicing Per Period'), ], string='Invoicing Method', default=lambda self: self.env.company.invoice_method, ) invoice_period = fields.Integer(string="Invoicing Every", default=lambda self: self.env.company.invoice_period) resource_calendar_id = fields.Many2one('resource.calendar', string='Working Time', default=lambda self: self.env.company.resource_calendar_id, related=False, help="Working Time used in this project") sale_order_amount = fields.Monetary("SO Total Amount", tracking=True) type = fields.Selection([ ('revenue', 'Revenue'), ('expense', 'Expense'), ('internal', 'Internal')], default=lambda self: self.env.company.type, string='Type') man_hours = fields.Boolean('Man hours', default=False) progress = fields.Float(compute="get_progress", store=True, string="Progress") task_count_all = fields.Integer(compute='_compute_task_count_all', string="All Task Count") task_count_finished = fields.Integer(compute='_compute_task_count_finished', string="Finished Task Count") task_count_new = fields.Integer(compute='_compute_task_count_new', string="New Task Count") task_count_inprogress = fields.Integer(compute='_compute_task_count_inprogress', string="In progress Task Count") privacy_visibility = fields.Selection([ ('followers', 'Invited internal users'), ('employees', 'All internal users'), ('portal', 'Invited portal users and all internal users'), ], string='Visibility', required=False, default='portal', help="People to whom this project and its tasks will be visible.\n\n" "- Invited internal users: when following a project, internal users will get access to all of its tasks without distinction. " "Otherwise, they will only get access to the specific tasks they are following.\n " "A user with the project > administrator access right level can still access this project and its tasks, even if they are not explicitly part of the followers.\n\n" "- All internal users: all internal users can access the project and all of its tasks without distinction.\n\n" "- Invited portal users and all internal users: all internal users can access the project and all of its tasks without distinction.\n" "When following a project, portal users will get access to all of its tasks without distinction. Otherwise, they will only get access to the specific tasks they are following.") allowed_internal_user_ids = fields.Many2many('res.users', 'project_allowed_internal_users_rel', string="Allowed Internal Users", default=lambda self: self.env.user, domain=[('share', '=', False)]) allowed_portal_user_ids = fields.Many2many('res.users', 'project_allowed_portal_users_rel', string="Allowed Portal Users", domain=[('share', '=', True)]) def get_attached_domain(self): return ['|','|','|', '&',('res_model', '=', 'project.project'), ('res_id', 'in', self.ids), '&',('res_model', '=', 'project.task'), ('res_id', 'in', self.task_ids.ids), '&',('res_model', '=', 'project.invoice'), ('res_id', 'in', self.invoice_ids.ids), '&',('res_model', '=', 'project.phase'), ('res_id', 'in', self.project_phase_ids.ids), ] def _compute_attached_docs_count(self): Attachment = self.env['ir.attachment'] for project in self: project.doc_count = Attachment.search_count(self.get_attached_domain()) def attachment_tree_view(self): action = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment') action['domain'] = str(self.get_attached_domain()) action['context'] = "{'default_res_model': '%s','default_res_id': %d}" % (self._name, self.id) return action def _compute_task_count_all(self): task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id']) result = dict((data['project_id'][0], data['project_id_count']) for data in task_data) for project in self: project.task_count_all = result.get(project.id, 0) def _compute_task_count_finished(self): task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids), ('stage_id.is_closed', '=', True)], ['project_id'], ['project_id']) result = dict((data['project_id'][0], data['project_id_count']) for data in task_data) for project in self: project.task_count_finished = result.get(project.id, 0) def _compute_task_count_new(self): task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids), ('stage_id.sequence', '=', 0)], ['project_id'], ['project_id']) result = dict((data['project_id'][0], data['project_id_count']) for data in task_data) for project in self: project.task_count_new = result.get(project.id, 0) def _compute_task_count_inprogress(self): task_data = self.env['project.task'].read_group( [('project_id', 'in', self.ids), ('stage_id.is_closed', '=', False), ('stage_id.fold', '=', False), ('stage_id.sequence', '!=', 0)], ['project_id'], ['project_id']) result = dict((data['project_id'][0], data['project_id_count']) for data in task_data) for project in self: project.task_count_inprogress = result.get(project.id, 0) @api.depends('project_phase_ids', 'project_phase_ids.weight', 'project_phase_ids.progress') def get_progress(self): for rec in self: progress = 0.0 for phase in rec.project_phase_ids: progress += phase.weight * phase.progress / 100 rec.progress = progress def compute_total_invoiced_amount(self): for rec in self: rec.total_invoiced_amount = sum(rec.invoice_ids.mapped('amount')) def compute_total_invoiced_payment(self): for rec in self: rec.total_invoiced_payment = 0 rec.no_of_invoices = 0 rec.no_of_paid_invoices = 0 if rec.invoice_ids: rec.no_of_invoices = len(rec.invoice_ids) rec.no_of_paid_invoices = len(rec.invoice_ids.sudo().filtered(lambda x: x.invoice_id)) rec.total_invoiced_payment = sum(rec.invoice_ids.mapped("payment_amount")) @api.depends('total_invoiced_amount', 'total_invoiced_payment') def compute_residual_amount(self): for rec in self: rec.residual_amount = rec.total_invoiced_amount - rec.total_invoiced_payment @api.depends('total_invoiced_amount', 'contract_value') def compute_back_log_amount(self): for rec in self: rec.back_log_amount = rec.contract_value - rec.total_invoiced_amount @api.model def _get_next_projectno(self): next_sequence = "/ " sequence = self.env['ir.sequence'].search( ['|', ('company_id', '=', self.env.company[0].id), ('company_id', '=', False), ('code', '=', 'project.project')], limit=1) if sequence: next_sequence = sequence.get_next_char(sequence.number_next_actual) return next_sequence @api.model def create(self, vals): company_id = vals.get('company_id', self.default_get(['company_id'])['company_id']) self_comp = self.with_company(company_id) if not vals.get('project_no', False): vals['project_no'] = self_comp.env['ir.sequence'].next_by_code('project.project') or '/' return super().create(vals) def write(self, vals): """ If invoice method=per_stage when add stage create project invoice """ line_ids = [] res = super(Project, self).write(vals) if self.type != 'internal' and vals.get('project_phase_ids') and self.invoice_method == 'per_stage': phase_ids = self.invoice_ids.mapped('phase_id').ids for line in self.sudo().sale_order_id.order_line: line_ids.append( (0, 0, {'order_line_id': line.id, 'product_id': line.product_id.id, 'product_uom': line.product_uom.id, 'price_unit': line.price_unit, 'discount': line.discount, 'tax_id': [(6, 0, line.tax_id.ids)]})) self.invoice_ids = [(0, 0, {'name': phase.display_name, 'phase_id': phase.id, 'project_invline_ids': line_ids}) for phase in self.project_phase_ids.filtered(lambda x: x.id not in phase_ids)] return res def name_get(self): res = [] for record in self: if record.project_no: res.append((record.id, ("[" + record.project_no + "] " + (record.name or " ")))) else: res.append((record.id, record.name)) return res @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): args = args or [] if operator == 'ilike' and not (name or '').strip(): domain = [] else: connector = '&' if operator in expression.NEGATIVE_TERM_OPERATORS else '|' domain = [connector, ('project_no', operator, name), ('name', operator, name)] return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid) # State funtions def action_draft(self): return self.write({'state': 'draft'}) def action_confirm(self): for record in self: # Send Notification to Project manger if record.user_id: users = [record.user_id] notification_ids = [(0, 0, { 'res_partner_id': user.partner_id.id, 'notification_type': 'inbox'}) for user in users if user.partner_id] if notification_ids: self.env['mail.message'].create({ 'message_type': "notification", 'body': _( "You Have been assign as a manger for The project %s kindly complete project data") % record.display_name, 'subject': _("You Have been assign As project manger"), 'partner_ids': [(4, user.partner_id.id) for user in users if users], 'model': record._name, 'res_id': record.id, 'notification_ids': notification_ids, 'author_id': self.env.user.partner_id and self.env.user.partner_id.id}) if record.type != 'internal': record.create_project_invoice() return self.write({'state': 'confirm'}) def action_done(self): for record in self: line_ids = [] # check project Stages if record.type != 'internal': if not record.project_phase_ids and record.invoice_method == 'per_stage': raise ValidationError(_("Kindly Enter project Stage first.")) return self.write({'state': 'done'}) def create_project_invoice(self): """ Create Project Invoice in confirm base on invoice_method = per_period """ for record in self: line_ids = [] if record.invoice_ids: return True if (record.date and record.start) and record.invoice_method == 'per_period': for line in record.sudo().sale_order_id.order_line.filtered(lambda x: not x.is_downpayment): line_ids.append( (0, 0, {'order_line_id': line.id, 'product_id': line.product_id.id, 'product_uom': line.product_uom.id, 'price_unit': line.price_unit, 'discount': line.discount, 'tax_id': [(6, 0, line.tax_id.ids)]})) diff = record.date - record.start project_days = diff.days period = project_days / record.invoice_period invoice_date = record.start + relativedelta.relativedelta(days=record.invoice_period) for rec in range(int(period)): record.invoice_ids = [(0, 0, {'project_invline_ids': line_ids, 'plan_date': invoice_date})] invoice_date = invoice_date + relativedelta.relativedelta(days=record.invoice_period) def action_view_tasks_analysis(self): """ return the action to see the tasks analysis report of the project """ action = self.env['ir.actions.act_window']._for_xml_id('project.action_project_task_user_tree') action_context = ast.literal_eval(action['context']) if action['context'] else {} action_context['search_default_project_id'] = self.id return dict(action, context=action_context) def action_creat_tasks(self): action = self.with_context(active_id=self.id, active_ids=self.ids) \ .env.ref('project_base.create_tast_proj') \ .sudo().read()[0] action['display_name'] = self.name return action def action_view_finished_tasks(self): action = self.with_context(active_id=self.id, active_ids=self.ids) \ .env.ref('project_base.act_project_project_2_project_task_finished') \ .sudo().read()[0] action['display_name'] = self.name return action def action_view_inprogress_tasks(self): action = self.with_context(active_id=self.id, active_ids=self.ids) \ .env.ref('project_base.act_project_project_2_project_task_inprogress') \ .sudo().read()[0] action['display_name'] = self.name return action def action_view_new_tasks(self): action = self.with_context(active_id=self.id, active_ids=self.ids) \ .env.ref('project_base.act_project_project_2_project_task_new') \ .sudo().read()[0] action['display_name'] = self.name return action # Status funtions def action_open(self): for rec in self: # check that all invoice planned issue dates is entred invoice_ids = self.invoice_ids.filtered(lambda x: x.plan_date == False) if invoice_ids: raise ValidationError(_('Please be sure to enter all planned issue dates for invoices.')) return self.write({'status': 'open'}) def action_reopen(self): return self.write({'status': 'open', 'close_reason': False}) def action_hold(self): return self.write({'status': 'hold'}) def action_close(self): return self.write({'status': 'close'}) def action_project_invoice(self): action_window = { "type": "ir.actions.act_window", "res_model": "project.invoice", "name": "project Invoice", 'view_type': 'form', 'view_mode': 'tree,form', "context": {"create": True, "project_id": self.id, 'search_default_project': 1}, "domain": [('project_id', '=', self.id)] } return action_window def action_paid_invoice(self): action_window = { "type": "ir.actions.act_window", "res_model": "project.invoice", "name": "project Invoice", 'view_type': 'form', 'view_mode': 'tree,form', "context": {"create": True, "project_id": self.id, 'search_default_project': 1}, "domain": [('project_id', '=', self.id), ('state', '=', 'done')] } return action_window def action_view_expense(self): self.ensure_one() action_window = { "type": "ir.actions.act_window", "res_model": "hr.expense", "name": "Expenses", "views": [[False, "tree"]], "context": {"create": False}, "domain": [('analytic_account_id', '=', self.analytic_account_id.id)] } return action_window def action_view_purchase(self): self.ensure_one() action_window = { "type": "ir.actions.act_window", "res_model": "purchase.order", "name": "Purchase", "views": [[False, "tree"]], "context": {"create": False}, "domain": [('project_id', '=', self.id)] } return action_window @api.model def update_project_status(self): """ this funtion call by cron to close the project if end date come """ projects = self.env['project.project'].search([('status', '=', 'open'), ('date', '<=', fields.Date.today())]) for record in projects: close_reason = "The Project Auto Closed by system because reach end date %s" % (record.date) record.write({'status': 'close', 'close_reason': close_reason, 'state': 'confirm'}) users = [record.user_id] notification_ids = [] if users: notification_ids = [(0, 0, { 'res_partner_id': user.partner_id.id, 'notification_type': 'inbox' } ) for user in users if user.partner_id] if notification_ids: self.env['mail.message'].create({ 'message_type': "notification", 'body': close_reason, 'subject': _("The project %s was automatically Closed by system") % record.display_name, 'partner_ids': [(4, user.partner_id.id) for user in users if users], 'model': record._name, 'res_id': record.id, 'notification_ids': notification_ids, 'author_id': self.env.user.partner_id and self.env.user.partner_id.id }) @api.model def project_invoice_notification(self): """ send notification to PM in invoice date """ projects = self.env['project.invoice'].search([]) for record in projects: if record.plan_date == datetime.now().date() and not record.invoice_id: record.to_invoice = True user_ids = self.env.ref('project.group_project_manager').users self.env['mail.message'].create({'message_type': "notification", 'body': _('This invoice is due and must be issue %s' % (record.name)), 'subject': _("Project Invoice Creation"), 'partner_ids': [(6, 0, user_ids.mapped('partner_id').ids)], 'notification_ids': [(0, 0, {'res_partner_id': user.partner_id.id, 'notification_type': 'inbox'}) for user in user_ids if user_ids], 'model': self._name, 'res_id': record.project_id.id, }) else: record.to_invoice = False def create_invoice(self): self.ensure_one() line_id = self.env['project.invoice'].search( [('project_id', '=', self.id), ('to_invoice', '=', True)], limit=1) return { 'name': _('Invoices'), 'res_model': 'project.invoice', 'type': 'ir.actions.act_window', 'res_id': line_id.id, 'view_mode': 'form', } def update_invoices(self, is_down_payment): InvLine = self.env['project.invoice.line'] for record in self: if is_down_payment: downpayment_line = record.sale_order_id.mapped('order_line').filtered(lambda l: l.is_downpayment) for line in downpayment_line: for inv in record.invoice_ids: if not inv.has_downpayment: values = {'project_invoice_id': inv.id, 'order_line_id': line.id, 'product_id': line.product_id.id, 'product_uom': line.product_uom.id, 'price_unit': line.price_unit, 'product_uom_qty': 0} InvLine.create(values) def create_down_payment(self): ''' Opens a wizard to create down payment ''' self.ensure_one() ctx = { 'active_model': 'sale.order', 'active_ids': self.sale_order_id.ids, 'active_id': self.sale_order_id.id, 'project_id': self.id, } return { 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'sale.advance.payment.inv', 'views': [(False, 'form')], 'view_id': False, 'target': 'new', 'context': ctx, } @api.depends('invoice_ids', 'invoice_ids.to_invoice') def compute_to_invoice(self): for rec in self: invoice_id = rec.invoice_ids.filtered(lambda x: x.to_invoice == True) if invoice_id: rec.to_invoice = True else: rec.to_invoice = False class ProjectCategory(models.Model): _name = "project.category" name = fields.Char(string="Name") company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id) class ProjectHoldReason(models.Model): _name = "project.hold.reason" name = fields.Char(string="Name") company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id) class Stage(models.Model): _inherit = 'project.task.type' color_gantt = fields.Char( string="Color Task Bar", help="Choose your color for Task Bar", default="rgba(170,170,13,0.53)") stage_for = fields.Selection( string='', selection=[('project', 'Project'), ('tasks', 'Tasks'), ], required=True, default='project') company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id) is_default = fields.Boolean(string="Default in new project") class ProjectTeam(models.Model): _name = "project.team" employee_id = fields.Many2one('hr.employee', string="Employee") project_id = fields.Many2one('project.project', string="Project") department_id = fields.Many2one('hr.department', related="employee_id.department_id", string="Department") job_id = fields.Many2one('hr.job', related="employee_id.job_id", string="Job") project_job = fields.Char(string="Job In Project") @api.onchange('job_id') def _onchange_job(self): if not self.project_job: self.project_job = self.job_id and self.job_id.name or ''