Exemple #1
0
class CRMRevealRule(models.Model):
    _name = 'crm.reveal.rule'
    _description = 'CRM Lead Generation Rules'
    _order = 'sequence'

    name = fields.Char(string='Rule Name', required=True)
    active = fields.Boolean(default=True)

    # Website Traffic Filter
    country_ids = fields.Many2many('res.country', string='Countries', help='Only visitors of following countries will be converted into leads/opportunities (using GeoIP).')
    regex_url = fields.Char(string='URL Expression', help='Regex to track website pages. Leave empty to track the entire website, or / to target the homepage. Example: /page* to track all the pages which begin with /page')
    sequence = fields.Integer(help='Used to order the rules with same URL and countries. '
                                   'Rules with a lower sequence number will be processed first.')

    # Company Criteria Filter
    industry_tag_ids = fields.Many2many('crm.reveal.industry', string='Industry Tags', help='Leave empty to always match. Odoo will not create lead if no match')
    company_size_min = fields.Integer(string='Min Company Size', help="Leave it as 0 if you don't want to use this filter.")
    company_size_max = fields.Integer(string='Max Company Size', help="Leave it as 0 if you don't want to use this filter.")

    # Contact Generation Filter
    preferred_role_id = fields.Many2one('crm.reveal.role', string='Preferred Role')
    other_role_ids = fields.Many2many('crm.reveal.role', string='Other Roles')
    seniority_id = fields.Many2one('crm.reveal.seniority', string='Seniority')
    extra_contacts = fields.Integer(string='Extra Contacts', help='This is the number of extra contacts to track if their role and seniority match your criteria.Their details will show up in the history thread of generated leads/opportunities. One credit is consumed per tracked contact.')

    calculate_credits = fields.Integer(compute='_compute_credit_count', string='Credit Used', readonly=True)

    # Lead / Opportunity Data
    lead_for = fields.Selection([('companies', 'Companies'), ('people', 'Companies + Contacts')], string='Data Tracking', required=True, default='companies', help='If you track company data, one credit will be consumed per lead/opportunity created. If you track company and contacts data, two credits will be consumed. Such data will be visible in the lead/opportunity.')
    lead_type = fields.Selection([('lead', 'Lead'), ('opportunity', 'Opportunity')], string='Type', required=True, default='opportunity')
    suffix = fields.Char(string='Suffix', help='This will be appended in name of generated lead so you can identify lead/opportunity is generated with this rule')
    team_id = fields.Many2one('crm.team', string='Sales Channel')
    tag_ids = fields.Many2many('crm.lead.tag', string='Tags')
    user_id = fields.Many2one('res.users', string='Salesperson')
    priority = fields.Selection(crm_stage.AVAILABLE_PRIORITIES, string='Priority')
    lead_ids = fields.One2many('crm.lead', 'reveal_rule_id', string='Generated Lead / Opportunity')
    leads_count = fields.Integer(compute='_compute_leads_count', string='Number of Generated Leads')
    opportunity_count = fields.Integer(compute='_compute_leads_count', string='Number of Generated Opportunity')

    # This limits the number of extra contact.
    # Even if more than 5 extra contacts provided service will return only 5 contacts (see service module for more)
    _sql_constraints = [
        ('limit_extra_contacts', 'check(extra_contacts >= 0 and extra_contacts <= 5)', 'Maximum 5 extra contacts are allowed!'),
    ]

    @api.constrains('regex_url')
    def _check_regex_url(self):
        try:
            if self.regex_url:
                re.compile(self.regex_url)
        except Exception:
            raise ValidationError(_('Enter Valid Regex.'))

    @api.model
    def create(self, vals):
        self.clear_caches() # Clear the cache in order to recompute _get_active_rules
        return super(CRMRevealRule, self).create(vals)

    def write(self, vals):
        fields_set = {
            'country_ids', 'regex_url', 'active'
        }
        if set(vals.keys()) & fields_set:
            self.clear_caches() # Clear the cache in order to recompute _get_active_rules
        return super(CRMRevealRule, self).write(vals)

    def unlink(self):
        self.clear_caches() # Clear the cache in order to recompute _get_active_rules
        return super(CRMRevealRule, self).unlink()

    @api.depends('extra_contacts', 'lead_for')
    def _compute_credit_count(self):
        """ Computes maximum IAP credit can be consumed per lead """
        credit = 1
        if self.lead_for == 'people':
            credit += 1
            if self.extra_contacts:
                credit += self.extra_contacts
        self.calculate_credits = credit

    def _compute_leads_count(self):
        leads = self.env['crm.lead'].read_group([
            ('reveal_rule_id', 'in', self.ids)
        ], fields=['reveal_rule_id', 'type'], groupby=['reveal_rule_id', 'type'], lazy=False)
        mapping = {(lead['reveal_rule_id'][0], lead['type']): lead['__count'] for lead in leads}
        for rule in self:
            rule.leads_count = mapping.get((rule.id, 'lead'), 0)
            rule.opportunity_count = mapping.get((rule.id, 'opportunity'), 0)

    def action_get_lead_tree_view(self):
        action = self.env.ref('crm.crm_lead_all_leads').read()[0]
        action['domain'] = [('id', 'in', self.lead_ids.ids), ('type', '=', 'lead')]
        return action

    def action_get_opportunity_tree_view(self):
        action = self.env.ref('crm.crm_lead_opportunities').read()[0]
        action['domain'] = [('id', 'in', self.lead_ids.ids), ('type', '=', 'opportunity')]
        return action

    @api.model
    @tools.ormcache()
    def _get_active_rules(self):
        """
        Returns informations about the all rules.
        The return is in the form :
        {
            'country_rules': {
                'BE': [0, 1],
                'US': [0]
            },
            'rules': [
            {
                'id': 0,
                'url': ***,
                'country_codes': ['BE', 'US']
            },
            {
                'id': 1,
                'url': ***,
                'country_codes': ['BE']
            }
            ]
        }
        """
        country_rules = {}
        rules_records = self.search([])
        rules = []
        # Fixes for special cases
        for rule in rules_records:
            regex_url = rule['regex_url']
            if not regex_url:
                regex_url = '.*'    # for all pages if url not given
            elif regex_url == '/':
                regex_url = '.*/$'  # for home
            countries = rule.country_ids.mapped('code')
            rules.append({
                'id': rule.id,
                'regex': regex_url,
                'country_codes': countries
            })
            for country in countries:
                country_rules = self._add_to_country(country_rules, country, len(rules) - 1)
        return {
            'country_rules': country_rules,
            'rules': rules,
        }

    def _add_to_country(self, country_rules, country, rule_index):
        """
        Add the rule index to the country code in the country_rules
        """
        if country not in country_rules:
            country_rules[country] = []
        country_rules[country].append(rule_index)
        return country_rules

    def _match_url(self, url, country_code, rules_excluded):
        """
        Return the matching rule based on the country and URL.
        """
        all_rules = self._get_active_rules()
        rules_id = all_rules['country_rules'].get(country_code, [])
        rules_matched = []
        for rule_index in rules_id:
            rule = all_rules['rules'][rule_index]
            if str(rule['id']) not in rules_excluded and re.search(rule['regex'], url):
                rules_matched.append(rule)
        return rules_matched

    @api.model
    def _process_lead_generation(self, autocommit=True):
        """ Cron Job for lead generation from page view """
        _logger.info('Start Reveal Lead Generation')
        self.env['crm.reveal.view']._clean_reveal_views()
        self._unlink_unrelevant_reveal_view()
        reveal_views = self._get_reveal_views_to_process()
        view_count = 0
        while reveal_views:
            view_count += len(reveal_views)
            server_payload = self._prepare_iap_payload(dict(reveal_views))
            enough_credit = self._perform_reveal_service(server_payload)
            if autocommit:
                # auto-commit for batch processing
                self._cr.commit()
            if enough_credit:
                reveal_views = self._get_reveal_views_to_process()
            else:
                reveal_views = False
        _logger.info('End Reveal Lead Generation - %s views processed', view_count)

    @api.model
    def _unlink_unrelevant_reveal_view(self):
        """
        We don't want to create the lead if in past (<6 months) we already
        created lead with given IP. So, we unlink crm.reveal.view with same IP
        as a already created lead.
        """
        months_valid = self.env['ir.config_parameter'].sudo().get_param('reveal.lead_month_valid', DEFAULT_REVEAL_MONTH_VALID)
        try:
            months_valid = int(months_valid)
        except ValueError:
            months_valid = DEFAULT_REVEAL_MONTH_VALID
        domain = []
        domain.append(('reveal_ip', '!=', False))
        domain.append(('create_date', '>', fields.Datetime.to_string(datetime.date.today() - relativedelta(months=months_valid))))
        leads = self.env['crm.lead'].with_context(active_test=False).search(domain)
        self.env['crm.reveal.view'].search([('reveal_ip', 'in', [lead.reveal_ip for lead in leads])]).unlink()

    @api.model
    def _get_reveal_views_to_process(self):
        """ Return list of reveal rule ids grouped by IPs """
        batch_limit = DEFAULT_REVEAL_BATCH_LIMIT
        query = """
            SELECT v.reveal_ip, array_agg(v.reveal_rule_id ORDER BY r.sequence)
            FROM crm_reveal_view v
            INNER JOIN crm_reveal_rule r
            ON v.reveal_rule_id = r.id
            WHERE v.reveal_state='to_process'
            GROUP BY v.reveal_ip
            LIMIT %d
            """ % batch_limit

        self.env.cr.execute(query)
        return self.env.cr.fetchall()

    def _prepare_iap_payload(self, pgv):
        """ This will prepare the page view and returns payload
            Payload sample
            {
                ips: {
                    '192.168.1.1': [1,4],
                    '192.168.1.6': [2,4]
                },
                rules: {
                    1: {rule_data},
                    2: {rule_data},
                    4: {rule_data}
                }
            }
        """
        new_list = list(set(itertools.chain.from_iterable(pgv.values())))
        rule_records = self.browse(new_list)
        return {
            'ips': pgv,
            'rules': rule_records._get_rules_payload()
        }

    def _get_rules_payload(self):
        company_country = self.env.user.company_id.country_id
        rule_payload = {}
        for rule in self:
            data = {
                'rule_id': rule.id,
                'lead_for': rule.lead_for,
                'countries': rule.country_ids.mapped('code'),
                'company_size_min': rule.company_size_min,
                'company_size_max': rule.company_size_max,
                'industry_tags': rule.industry_tag_ids.mapped('reveal_id'),
                'user_country': company_country and company_country.code or False
            }
            if rule.lead_for == 'people':
                data.update({
                    'preferred_role': rule.preferred_role_id.reveal_id or '',
                    'other_roles': rule.other_role_ids.mapped('reveal_id'),
                    'seniority': rule.seniority_id.reveal_id or '',
                    'extra_contacts': rule.extra_contacts
                })
            rule_payload[rule.id] = data
        return rule_payload

    def _perform_reveal_service(self, server_payload):
        result = False
        account_token = self.env['iap.account'].get('reveal')
        endpoint = self.env['ir.config_parameter'].sudo().get_param('reveal.endpoint', DEFAULT_ENDPOINT) + '/iap/clearbit/1/reveal'
        params = {
            'account_token': account_token.account_token,
            'data': server_payload
        }
        result = jsonrpc(endpoint, params=params, timeout=300)
        for res in result.get('reveal_data', []):
            if not res.get('not_found'):
                lead = self._create_lead_from_response(res)
                self.env['crm.reveal.view'].search([('reveal_ip', '=', res['ip'])]).unlink()
            else:
                self.env['crm.reveal.view'].search([('reveal_ip', '=', res['ip'])]).write({
                    'reveal_state': 'not_found'
                })
        if result.get('credit_error'):
            self._notify_no_more_credit()
            return False
        else:
            self.env['ir.config_parameter'].sudo().set_param('reveal.already_notified', False)
        return True

    def _notify_no_more_credit(self):
        """
        Notify about the number of credit.
        In order to avoid to spam people each hour, an ir.config_parameter is set
        """
        already_notified = self.env['ir.config_parameter'].sudo().get_param('reveal.already_notified', False)
        if already_notified:
            return
        mail_template = self.env.ref('crm_reveal.reveal_no_credits')
        iap_account = self.env['iap.account'].search([('service_name', '=', 'reveal')], limit=1)
        # Get the email address of the creators of the Lead Generation Rules
        res = self.env['crm.reveal.rule'].search_read([], ['create_uid'])
        uids = set(r['create_uid'][0] for r in res if r.get('create_uid'))
        res = self.env['res.users'].search_read([('id', 'in', list(uids))], ['email'])
        emails = set(r['email'] for r in res if r.get('email'))

        mail_values = mail_template.generate_email(iap_account.id)
        mail_values['email_to'] = ','.join(emails)
        mail = self.env['mail.mail'].create(mail_values)
        mail.send()
        self.env['ir.config_parameter'].sudo().set_param('reveal.already_notified', True)


    def _create_lead_from_response(self, result):
        """ This method will get response from service and create the lead accordingly """
        if result['rule_id']:
            rule = self.browse(result['rule_id'])
        else:
            # Not create a lead if the information match no rule
            # If there is no match, the service still returns all informations
            # in order to let custom code use it.
            return False
        if not result['clearbit_id']:
            return False
        already_created_lead = self.env['crm.lead'].search([('reveal_id', '=', result['clearbit_id'])])
        if already_created_lead:
            _logger.info('Existing lead for this clearbit_id [%s]', result['clearbit_id'])
            # Does not create a lead if the reveal_id is already known
            return False
        lead_vals = rule._lead_vals_from_response(result)
        lead = self.env['crm.lead'].create(lead_vals)
        lead.message_post_with_view(
            'crm_reveal.lead_message_template',
            values=self._format_data_for_message_post(result),
            subtype_id=self.env.ref('mail.mt_note').id
        )
        return lead

    # Methods responsible for format response data in to valid odoo lead data
    def _lead_vals_from_response(self, result):
        self.ensure_one()
        reveal_data = result['reveal_data']
        people_data = result.get('people_data')
        country_id = self.env['res.country'].search([('code', '=', reveal_data['country_code'])]).id
        website_url = 'https://www.%s' % reveal_data['domain'] if reveal_data['domain'] else False
        lead_vals = {
            # Lead vals from rule itself
            'type': self.lead_type,
            'team_id': self.team_id.id,
            'tag_ids': [(6, 0, self.tag_ids.ids)],
            'priority': self.priority,
            'user_id': self.user_id.id,
            'reveal_ip': result['ip'],
            'reveal_rule_id': self.id,
            'reveal_id': result['clearbit_id'],
            'referred': 'Website Visitor',
            # Lead vals from response
            'name': reveal_data['name'],
            'reveal_iap_credits': result['credit'],
            'partner_name': reveal_data['legal_name'] or reveal_data['name'],
            'email_from': ",".join(reveal_data['email'] or []),
            'phone': reveal_data['phone'] or (reveal_data['phone_numbers'] and reveal_data['phone_numbers'][0]) or '',
            'website': website_url,
            'street': reveal_data['location'],
            'city': reveal_data['city'],
            'zip': reveal_data['postal_code'],
            'country_id': country_id,
            'state_id': self._find_state_id(reveal_data['state_name'], reveal_data['state_code'], country_id),
            'description': self._prepare_lead_description(reveal_data),
        }

        if self.suffix:
            lead_vals['name'] = '%s - %s' % (lead_vals['name'], self.suffix)

        # If type is people then add first contact in lead data
        if people_data:
            lead_vals.update({
                'contact_name': people_data[0]['full_name'],
                'email_from': people_data[0]['email'],
                'function': people_data[0]['title'],
            })
        return lead_vals

    def _find_state_id(self, state_code, state_name, country_id):
        state_id = self.env['res.country.state'].search([('code', '=', state_code), ('country_id', '=', country_id)])
        if state_id:
            return state_id.id
        return False

    def _prepare_lead_description(self, reveal_data):
        description = ''
        if reveal_data['sector']:
            description += reveal_data['sector']
        if reveal_data['website_title']:
            description += '\n' + reveal_data['website_title']
        if reveal_data['twitter_bio']:
            description += '\n' + "Twitter Bio: " + reveal_data['twitter_bio']
        if reveal_data['twitter_followers']:
            description += ('\nTwitter %s followers, %s \n') % (reveal_data['twitter_followers'], reveal_data['twitter_location'] or '')

        numbers = ['raised', 'market_cap', 'employees', 'estimated_annual_revenue']
        millnames = ['', ' K', ' M', ' B', 'T']

        def millify(n):
            try:
                n = float(n)
                millidx = max(0, min(len(millnames) - 1, int(floor(0 if n == 0 else log10(abs(n)) / 3))))
                return '{:.0f}{}'.format(n / 10**(3 * millidx), millnames[millidx])
            except Exception:
                return n

        for key in numbers:
            if reveal_data.get(key):
                description += ' %s : %s,' % (key.replace('_', ' ').title(), millify(reveal_data[key]))
        return description

    def _format_data_for_message_post(self, result):
        reveal_data = result['reveal_data']
        people_data = result.get('people_data')
        log_data = {
            'twitter': reveal_data['twitter'],
            'description': reveal_data['description'],
            'logo': reveal_data['logo'],
            'name': reveal_data['name'],
            'phone_numbers': reveal_data['phone_numbers'],
            'facebook': reveal_data['facebook'],
            'linkedin': reveal_data['linkedin'],
            'crunchbase': reveal_data['crunchbase'],
            'tech': [t.replace('_', ' ').title() for t in reveal_data['tech']],
            'people_data': people_data,
        }
        timezone = result['ip_time_zone'] or reveal_data['timezone']
        if timezone:
            log_data.update({
                'timezone': timezone.replace('_', ' ').title(),
                'timezone_url': reveal_data['timezone_url'],
            })
        return log_data
class GlobalSearchConfig(models.Model):
    _name = 'global.search.config'
    _rec_name = 'model_id'
    _description = 'Global Search Configuration'

    template_id = fields.Many2one('global.search.config.template',
        string="Template", domian="[('id', in, [])]")
    batch_id = fields.Many2one('global.search.config.batch', string="Batch")
    user_id = fields.Many2one('res.users', string='User',
        default=lambda self: self.env.user, copy=False)
    customized = fields.Boolean(string="Customized")
    model_id = fields.Many2one('ir.model', string="Model", required=True)
    field_ids = fields.Many2many('ir.model.fields', string="Fields",
        domain="[('model_id', '=', model_id), ('name', '!=', 'id'), ('selectable', '=', True)]",
        required=True, order="ir_model_fields_id.field_description desc")

    _sql_constraints = [
        ('uniq_template_user',
         "UNIQUE(template_id, user_id)", "The template must be unique per user!"),
    ]

    def write(self, vals):
        '''Override to manage customized boolean'''
        if 'customized' not in vals and ((vals.get('user_id') and len(vals.keys()) > 1) or not vals.get('user_id')):
            vals['customized'] = True
        if 'template_id' in vals and not vals.get('model_id', False):
            vals['model_id'] = self.env['global.search.config.template'].search([('id', '=', vals['template_id'])]).model_id.id
        return super(GlobalSearchConfig, self).write(vals)

    @api.model
    def create(self, vals):
        '''Override check the values'''
        if 'template_id' in vals and not vals.get('model_id', False):
            vals['model_id'] = self.env['global.search.config.template'].search([('id', '=' ,vals['template_id'])]).model_id.id
        return super(GlobalSearchConfig, self).create(vals)

    @api.onchange('user_id')
    def _onchange_user_id(self):
        '''Returns domain to filter template whose model are accessible for selected user.'''
        dom = {'template_id': [('id', 'in', [])]}
        if self.user_id:
            models = self.env['ir.model'].search([('state', '!=', 'manual'), ('transient', '=', False)])
            access_model = self.env['ir.model.access']
            model_ids = [model.id for model in models if access_model.with_user(
                    user=self.env.user.id
                ).check(model.model, 'read', raise_exception=False)]
            dom['template_id'] = [('model_id', 'in', model_ids)]
            dom['model_id'] = [('id', 'in', model_ids)]
        return {'domain': dom}

    @api.onchange('template_id')
    def _onchange_template_id(self):
        '''To set fields as per template selection.'''
        for rec in self:
            rec.set_values_template_batch(rec.template_id)

    @api.onchange('model_id')
    def _onchange_model_id(self):
        if self.template_id:
            self._onchange_template_id()
        else:
            self.field_ids = [(6, 0, [])]

    def set_values_template_batch(self, template_batch_id):
        self.ensure_one()
        self.field_ids = [(6, 0, template_batch_id.field_ids.ids)]
        self.model_id = template_batch_id.model_id.id
        self.customized = False

    def set_default_template_batch(self):
        '''Set default button.
        To set fields as per template or batch selection. '''
        for rec in self:
            rec.set_values_template_batch(rec.batch_id or rec.template_id)

    def _get_search_more_config(self, kw):
        return dict(
            limit=kw['limit'],
            offset=(kw['searched_datas'] + kw['offset']),
            total=kw['total'],
            remaining=max(0, (kw['total'] - (kw['offset'] + kw['limit'])))
        )

    def _process_global_search_data(self, kwargs):
        with api.Environment.manage():
            # As this function is in a new thread, need to open a new cursor, because the old one may be closed
            new_cr = self.pool.cursor()
            self = self.with_env(self.env(cr=new_cr))
            kwargs['search_more_options']['total'] = self.env[kwargs['model']].search_count(kwargs['dom'])
            datas = self.env[kwargs['model']].search_read(kwargs['dom'], list(kwargs['fields'].keys()) + ['display_name'],
                offset=kwargs['search_more_options']['offset'], limit=kwargs['search_more_options']['limit'])
            kwargs['search_more_options']['searched_datas'] = len(datas)
            options = self._get_search_more_config(kwargs['search_more_options'])
            for data in datas:
                for f, v in list(data.items()):
                    if f in ['display_name', 'id']:
                        continue
                    if type(v) is list:
                        if data[f]:
                            x2m_data = self.env[kwargs['fields'][f][1]].browse(data[f]).name_get()
                            x2m_v = ', '.join([d[1] for d in x2m_data if kwargs['search_string'].lower() in d[1].lower()\
                                and d[1] not in NEGATIVE_TERMS])
                            if x2m_v:
                                data[kwargs['fields'][f][0]] = x2m_v
                    elif type(v) is tuple:
                        if data[f] and data[f][1] and kwargs['search_string'].lower() in data[f][1].lower():
                            data[kwargs['fields'][f][0]] = data[f][1]
                    else:
                        if isinstance(data[f], numbers.Number) and type(data[f]) is not bool:
                            data[f] = str(data[f])
                        if data[f] and kwargs['search_string'].lower() in str(data[f]).lower():
                            data[kwargs['fields'][f][0]] = data[f]
                    del data[f]
                    if data.get(kwargs['fields'][f][0]) and kwargs['search_string'].lower() not in str(data[kwargs['fields'][f][0]]).lower():
                        del data[kwargs['fields'][f][0]]
            # Need to close old cursor
            new_cr.close()
            return [datas, options]
Exemple #3
0
class CrmLead(models.Model):
    _inherit = "crm.lead"

    partner_latitude = fields.Float('Geo Latitude', digits=(16, 5))
    partner_longitude = fields.Float('Geo Longitude', digits=(16, 5))
    partner_assigned_id = fields.Many2one(
        'res.partner',
        'Assigned Partner',
        tracking=True,
        domain=
        "['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        help="Partner this case has been forwarded/assigned to.",
        index=True)
    partner_declined_ids = fields.Many2many('res.partner',
                                            'crm_lead_declined_partner',
                                            'lead_id',
                                            'partner_id',
                                            string='Partner not interested')
    date_partner_assign = fields.Date(
        'Partner Assignment Date',
        compute='_compute_date_partner_assign',
        copy=True,
        readonly=False,
        store=True,
        help="Last date this case was forwarded/assigned to a partner")

    def _merge_data(self, fields):
        fields += [
            'partner_latitude', 'partner_longitude', 'partner_assigned_id',
            'date_assign'
        ]
        return super(CrmLead, self)._merge_data(fields)

    @api.onchange("partner_assigned_id")
    def _compute_date_partner_assign(self):
        for lead in self:
            if not lead.partner_assigned_id:
                lead.date_partner_assign = False
            else:
                lead.date_assign = fields.Date.context_today(lead)

    def assign_salesman_of_assigned_partner(self):
        salesmans_leads = {}
        for lead in self:
            if (lead.probability > 0
                    and lead.probability < 100) or lead.stage_id.sequence == 1:
                if lead.partner_assigned_id and lead.partner_assigned_id.user_id != lead.user_id:
                    salesmans_leads.setdefault(
                        lead.partner_assigned_id.user_id.id,
                        []).append(lead.id)

        for salesman_id, leads_ids in salesmans_leads.items():
            leads = self.browse(leads_ids)
            leads.write({'user_id': salesman_id})

    def action_assign_partner(self):
        return self.assign_partner(partner_id=False)

    def assign_partner(self, partner_id=False):
        partner_dict = {}
        res = False
        if not partner_id:
            partner_dict = self.search_geo_partner()
        for lead in self:
            if not partner_id:
                partner_id = partner_dict.get(lead.id, False)
            if not partner_id:
                tag_to_add = self.env.ref(
                    'website_crm_partner_assign.tag_portal_lead_partner_unavailable',
                    False)
                lead.write({'tag_ids': [(4, tag_to_add.id, False)]})
                continue
            lead.assign_geo_localize(lead.partner_latitude,
                                     lead.partner_longitude)
            partner = self.env['res.partner'].browse(partner_id)
            if partner.user_id:
                lead.handle_salesmen_assignment(partner.user_id.ids,
                                                team_id=partner.team_id.id)
            lead.write({'partner_assigned_id': partner_id})
        return res

    def assign_geo_localize(self, latitude=False, longitude=False):
        if latitude and longitude:
            self.write({
                'partner_latitude': latitude,
                'partner_longitude': longitude
            })
            return True
        # Don't pass context to browse()! We need country name in english below
        for lead in self:
            if lead.partner_latitude and lead.partner_longitude:
                continue
            if lead.country_id:
                result = self.env['res.partner']._geo_localize(
                    lead.street, lead.zip, lead.city, lead.state_id.name,
                    lead.country_id.name)
                if result:
                    lead.write({
                        'partner_latitude': result[0],
                        'partner_longitude': result[1]
                    })
        return True

    def search_geo_partner(self):
        Partner = self.env['res.partner']
        res_partner_ids = {}
        self.assign_geo_localize()
        for lead in self:
            partner_ids = []
            if not lead.country_id:
                continue
            latitude = lead.partner_latitude
            longitude = lead.partner_longitude
            if latitude and longitude:
                # 1. first way: in the same country, small area
                partner_ids = Partner.search([
                    ('partner_weight', '>', 0),
                    ('partner_latitude', '>', latitude - 2),
                    ('partner_latitude', '<', latitude + 2),
                    ('partner_longitude', '>', longitude - 1.5),
                    ('partner_longitude', '<', longitude + 1.5),
                    ('country_id', '=', lead.country_id.id),
                    ('id', 'not in', lead.partner_declined_ids.mapped('id')),
                ])

                # 2. second way: in the same country, big area
                if not partner_ids:
                    partner_ids = Partner.search([
                        ('partner_weight', '>', 0),
                        ('partner_latitude', '>', latitude - 4),
                        ('partner_latitude', '<', latitude + 4),
                        ('partner_longitude', '>', longitude - 3),
                        ('partner_longitude', '<', longitude + 3),
                        ('country_id', '=', lead.country_id.id),
                        ('id', 'not in',
                         lead.partner_declined_ids.mapped('id')),
                    ])

                # 3. third way: in the same country, extra large area
                if not partner_ids:
                    partner_ids = Partner.search([
                        ('partner_weight', '>', 0),
                        ('partner_latitude', '>', latitude - 8),
                        ('partner_latitude', '<', latitude + 8),
                        ('partner_longitude', '>', longitude - 8),
                        ('partner_longitude', '<', longitude + 8),
                        ('country_id', '=', lead.country_id.id),
                        ('id', 'not in',
                         lead.partner_declined_ids.mapped('id')),
                    ])

                # 5. fifth way: anywhere in same country
                if not partner_ids:
                    # still haven't found any, let's take all partners in the country!
                    partner_ids = Partner.search([
                        ('partner_weight', '>', 0),
                        ('country_id', '=', lead.country_id.id),
                        ('id', 'not in',
                         lead.partner_declined_ids.mapped('id')),
                    ])

                # 6. sixth way: closest partner whatsoever, just to have at least one result
                if not partner_ids:
                    # warning: point() type takes (longitude, latitude) as parameters in this order!
                    self._cr.execute(
                        """SELECT id, distance
                                  FROM  (select id, (point(partner_longitude, partner_latitude) <-> point(%s,%s)) AS distance FROM res_partner
                                  WHERE active
                                        AND partner_longitude is not null
                                        AND partner_latitude is not null
                                        AND partner_weight > 0
                                        AND id not in (select partner_id from crm_lead_declined_partner where lead_id = %s)
                                        ) AS d
                                  ORDER BY distance LIMIT 1""",
                        (longitude, latitude, lead.id))
                    res = self._cr.dictfetchone()
                    if res:
                        partner_ids = Partner.browse([res['id']])

                total_weight = 0
                toassign = []
                for partner in partner_ids:
                    total_weight += partner.partner_weight
                    toassign.append((partner.id, total_weight))

                random.shuffle(
                    toassign
                )  # avoid always giving the leads to the first ones in db natural order!
                nearest_weight = random.randint(0, total_weight)
                for partner_id, weight in toassign:
                    if nearest_weight <= weight:
                        res_partner_ids[lead.id] = partner_id
                        break
        return res_partner_ids

    def partner_interested(self, comment=False):
        message = _('<p>I am interested by this lead.</p>')
        if comment:
            message += '<p>%s</p>' % html_escape(comment)
        for lead in self:
            lead.message_post(body=message)
            lead.sudo().convert_opportunity(
                lead.partner_id.id)  # sudo required to convert partner data

    def partner_desinterested(self,
                              comment=False,
                              contacted=False,
                              spam=False):
        if contacted:
            message = '<p>%s</p>' % _(
                'I am not interested by this lead. I contacted the lead.')
        else:
            message = '<p>%s</p>' % _(
                'I am not interested by this lead. I have not contacted the lead.'
            )
        partner_ids = self.env['res.partner'].search([
            ('id', 'child_of',
             self.env.user.partner_id.commercial_partner_id.id)
        ])
        self.message_unsubscribe(partner_ids=partner_ids.ids)
        if comment:
            message += '<p>%s</p>' % html_escape(comment)
        self.message_post(body=message)
        values = {
            'partner_assigned_id': False,
        }

        if spam:
            tag_spam = self.env.ref(
                'website_crm_partner_assign.tag_portal_lead_is_spam', False)
            if tag_spam and tag_spam not in self.tag_ids:
                values['tag_ids'] = [(4, tag_spam.id, False)]
        if partner_ids:
            values['partner_declined_ids'] = [(4, p, 0)
                                              for p in partner_ids.ids]
        self.sudo().write(values)

    def update_lead_portal(self, values):
        self.check_access_rights('write')
        for lead in self:
            lead_values = {
                'planned_revenue': values['planned_revenue'],
                'probability': values['probability'],
                'priority': values['priority'],
                'date_deadline': values['date_deadline'] or False,
            }
            # As activities may belong to several users, only the current portal user activity
            # will be modified by the portal form. If no activity exist we create a new one instead
            # that we assign to the portal user.

            user_activity = lead.sudo().activity_ids.filtered(
                lambda activity: activity.user_id == self.env.user)[:1]
            if values['activity_date_deadline']:
                if user_activity:
                    user_activity.sudo().write({
                        'activity_type_id':
                        values['activity_type_id'],
                        'summary':
                        values['activity_summary'],
                        'date_deadline':
                        values['activity_date_deadline'],
                    })
                else:
                    self.env['mail.activity'].sudo().create({
                        'res_model_id':
                        self.env.ref('crm.model_crm_lead').id,
                        'res_id':
                        lead.id,
                        'user_id':
                        self.env.user.id,
                        'activity_type_id':
                        values['activity_type_id'],
                        'summary':
                        values['activity_summary'],
                        'date_deadline':
                        values['activity_date_deadline'],
                    })
            lead.write(lead_values)

    @api.model
    def create_opp_portal(self, values):
        if not (self.env.user.partner_id.grade_id
                or self.env.user.commercial_partner_id.grade_id):
            raise AccessDenied()
        user = self.env.user
        self = self.sudo()
        if not (values['contact_name'] and values['description']
                and values['title']):
            return {'errors': _('All fields are required !')}
        tag_own = self.env.ref(
            'website_crm_partner_assign.tag_portal_lead_own_opp', False)
        values = {
            'contact_name': values['contact_name'],
            'name': values['title'],
            'description': values['description'],
            'priority': '2',
            'partner_assigned_id': user.commercial_partner_id.id,
        }
        if tag_own:
            values['tag_ids'] = [(4, tag_own.id, False)]

        lead = self.create(values)
        lead.assign_salesman_of_assigned_partner()
        lead.convert_opportunity(lead.partner_id.id)
        return {'id': lead.id}

    #
    #   DO NOT FORWARD PORT IN MASTER
    #   instead, crm.lead should implement portal.mixin
    #
    def get_access_action(self, access_uid=None):
        """ Instead of the classic form view, redirect to the online document for
        portal users or if force_website=True in the context. """
        self.ensure_one()

        user, record = self.env.user, self
        if access_uid:
            try:
                record.check_access_rights('read')
                record.check_access_rule("read")
            except AccessError:
                return super(CrmLead, self).get_access_action(access_uid)
            user = self.env['res.users'].sudo().browse(access_uid)
            record = self.with_user(user)
        if user.share or self.env.context.get('force_website'):
            try:
                record.check_access_rights('read')
                record.check_access_rule('read')
            except AccessError:
                pass
            else:
                return {
                    'type': 'ir.actions.act_url',
                    'url': '/my/opportunity/%s' % record.id,
                }
        return super(CrmLead, self).get_access_action(access_uid)
class DeliveryCarrier(models.Model):
    _name = 'delivery.carrier'
    _description = "Shipping Methods"
    _order = 'sequence, id'
    ''' A Shipping Provider

    In order to add your own external provider, follow these steps:

    1. Create your model MyProvider that _inherit 'delivery.carrier'
    2. Extend the selection of the field "delivery_type" with a pair
       ('<my_provider>', 'My Provider')
    3. Add your methods:
       <my_provider>_rate_shipment
       <my_provider>_send_shipping
       <my_provider>_get_tracking_link
       <my_provider>_cancel_shipment
       _<my_provider>_get_default_custom_package_code
       (they are documented hereunder)
    '''

    # -------------------------------- #
    # Internals for shipping providers #
    # -------------------------------- #

    name = fields.Char('Delivery Method', required=True, translate=True)
    active = fields.Boolean(default=True)
    sequence = fields.Integer(help="Determine the display order", default=10)
    # This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex')
    delivery_type = fields.Selection([('fixed', 'Fixed Price')],
                                     string='Provider',
                                     default='fixed',
                                     required=True)
    integration_level = fields.Selection(
        [('rate', 'Get Rate'),
         ('rate_and_ship', 'Get Rate and Create Shipment')],
        string="Integration Level",
        default='rate_and_ship',
        help="Action while validating Delivery Orders")
    prod_environment = fields.Boolean(
        "Environment",
        help="Set to True if your credentials are certified for production.")
    debug_logging = fields.Boolean(
        'Debug logging', help="Log requests in order to ease debugging")
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 related='product_id.company_id',
                                 store=True,
                                 readonly=False)
    product_id = fields.Many2one('product.product',
                                 string='Delivery Product',
                                 required=True,
                                 ondelete='restrict')

    invoice_policy = fields.Selection(
        [('estimated', 'Estimated cost'), ('real', 'Real cost')],
        string='Invoicing Policy',
        default='estimated',
        required=True,
        help=
        "Estimated Cost: the customer will be invoiced the estimated cost of the shipping.\nReal Cost: the customer will be invoiced the real cost of the shipping, the cost of the shipping will be updated on the SO after the delivery."
    )

    country_ids = fields.Many2many('res.country',
                                   'delivery_carrier_country_rel',
                                   'carrier_id', 'country_id', 'Countries')
    state_ids = fields.Many2many('res.country.state',
                                 'delivery_carrier_state_rel', 'carrier_id',
                                 'state_id', 'States')
    zip_from = fields.Char('Zip From')
    zip_to = fields.Char('Zip To')

    margin = fields.Float(
        help='This percentage will be added to the shipping price.')
    free_over = fields.Boolean(
        'Free if order amount is above',
        help=
        "If the order total amount (shipping excluded) is above or equal to this value, the customer benefits from a free shipping",
        default=False,
        oldname='free_if_more_than')
    amount = fields.Float(
        string='Amount',
        help=
        "Amount of the order to benefit from a free shipping, expressed in the company currency"
    )

    can_generate_return = fields.Boolean(
        compute="_compute_can_generate_return")
    return_label_on_delivery = fields.Boolean(
        string="Generate Return Label",
        help="The return label is automatically generated at the delivery.")
    get_return_label_from_portal = fields.Boolean(
        string="Return Label Accessible from Customer Portal",
        help=
        "The return label can be downloaded by the customer from the customer portal."
    )

    _sql_constraints = [
        ('margin_not_under_100_percent', 'CHECK (margin >= -100)',
         'Margin cannot be lower than -100%'),
    ]

    @api.depends('delivery_type')
    def _compute_can_generate_return(self):
        for carrier in self:
            carrier.can_generate_return = hasattr(
                self, '%s_get_return_label' % carrier.delivery_type)

    def toggle_prod_environment(self):
        for c in self:
            c.prod_environment = not c.prod_environment

    def toggle_debug(self):
        for c in self:
            c.debug_logging = not c.debug_logging

    @api.multi
    def install_more_provider(self):
        return {
            'name':
            'New Providers',
            'view_mode':
            'kanban,form',
            'res_model':
            'ir.module.module',
            'domain': [['name', '=like', 'delivery_%'],
                       ['name', '!=', 'delivery_barcode']],
            'type':
            'ir.actions.act_window',
            'help':
            _('''<p class="o_view_nocontent">
                    Buy Odoo Enterprise now to get more providers.
                </p>'''),
        }

    def available_carriers(self, partner):
        return self.filtered(lambda c: c._match_address(partner))

    def _match_address(self, partner):
        self.ensure_one()
        if self.country_ids and partner.country_id not in self.country_ids:
            return False
        if self.state_ids and partner.state_id not in self.state_ids:
            return False
        if self.zip_from and (partner.zip
                              or '').upper() < self.zip_from.upper():
            return False
        if self.zip_to and (partner.zip or '').upper() > self.zip_to.upper():
            return False
        return True

    @api.onchange('integration_level')
    def _onchange_integration_level(self):
        if self.integration_level == 'rate':
            self.invoice_policy = 'estimated'

    @api.onchange('can_generate_return')
    def _onchange_can_generate_return(self):
        if not self.can_generate_return:
            self.return_label_on_delivery = False

    @api.onchange('return_label_on_delivery')
    def _onchange_return_label_on_delivery(self):
        if not self.return_label_on_delivery:
            self.get_return_label_from_portal = False

    @api.onchange('state_ids')
    def onchange_states(self):
        self.country_ids = [
            (6, 0,
             self.country_ids.ids + self.state_ids.mapped('country_id.id'))
        ]

    @api.onchange('country_ids')
    def onchange_countries(self):
        self.state_ids = [
            (6, 0,
             self.state_ids.filtered(lambda state: state.id in self.country_ids
                                     .mapped('state_ids').ids).ids)
        ]

    # -------------------------- #
    # API for external providers #
    # -------------------------- #

    def rate_shipment(self, order):
        ''' Compute the price of the order shipment

        :param order: record of sale.order
        :return dict: {'success': boolean,
                       'price': a float,
                       'error_message': a string containing an error message,
                       'warning_message': a string containing a warning message}
                       # TODO maybe the currency code?
        '''
        self.ensure_one()
        if hasattr(self, '%s_rate_shipment' % self.delivery_type):
            res = getattr(self, '%s_rate_shipment' % self.delivery_type)(order)
            # apply margin on computed price
            res['price'] = float(res['price']) * (1.0 + (self.margin / 100.0))
            # save the real price in case a free_over rule overide it to 0
            res['carrier_price'] = res['price']
            # free when order is large enough
            if res['success'] and self.free_over and order._compute_amount_total_without_delivery(
            ) >= self.amount:
                res['warning_message'] = _(
                    'The shipping is free since the order amount exceeds %.2f.'
                ) % (self.amount)
                res['price'] = 0.0
            return res

    def send_shipping(self, pickings):
        ''' Send the package to the service provider

        :param pickings: A recordset of pickings
        :return list: A list of dictionaries (one per picking) containing of the form::
                         { 'exact_price': price,
                           'tracking_number': number }
                           # TODO missing labels per package
                           # TODO missing currency
                           # TODO missing success, error, warnings
        '''
        self.ensure_one()
        if hasattr(self, '%s_send_shipping' % self.delivery_type):
            return getattr(self,
                           '%s_send_shipping' % self.delivery_type)(pickings)

    def get_return_label(self,
                         pickings,
                         tracking_number=None,
                         origin_date=None):
        self.ensure_one()
        if self.can_generate_return:
            return getattr(self, '%s_get_return_label' % self.delivery_type)(
                pickings, tracking_number, origin_date)

    def get_return_label_prefix(self):
        return 'ReturnLabel-%s' % self.delivery_type

    def get_tracking_link(self, picking):
        ''' Ask the tracking link to the service provider

        :param picking: record of stock.picking
        :return str: an URL containing the tracking link or False
        '''
        self.ensure_one()
        if hasattr(self, '%s_get_tracking_link' % self.delivery_type):
            return getattr(self, '%s_get_tracking_link' %
                           self.delivery_type)(picking)

    def cancel_shipment(self, pickings):
        ''' Cancel a shipment

        :param pickings: A recordset of pickings
        '''
        self.ensure_one()
        if hasattr(self, '%s_cancel_shipment' % self.delivery_type):
            return getattr(self,
                           '%s_cancel_shipment' % self.delivery_type)(pickings)

    def log_xml(self, xml_string, func):
        self.ensure_one()

        if self.debug_logging:
            db_name = self._cr.dbname

            # Use a new cursor to avoid rollback that could be caused by an upper method
            try:
                db_registry = registry(db_name)
                with db_registry.cursor() as cr:
                    env = api.Environment(cr, SUPERUSER_ID, {})
                    IrLogging = env['ir.logging']
                    IrLogging.sudo().create({
                        'name': 'delivery.carrier',
                        'type': 'server',
                        'dbname': db_name,
                        'level': 'DEBUG',
                        'message': xml_string,
                        'path': self.delivery_type,
                        'func': func,
                        'line': 1
                    })
            except psycopg2.Error:
                pass

    def _get_default_custom_package_code(self):
        """ Some delivery carriers require a prefix to be sent in order to use custom
        packages (ie not official ones). This optional method will return it as a string.
        """
        self.ensure_one()
        if hasattr(self,
                   '_%s_get_default_custom_package_code' % self.delivery_type):
            return getattr(
                self,
                '_%s_get_default_custom_package_code' % self.delivery_type)()
        else:
            return False

    # ------------------------------------------------ #
    # Fixed price shipping, aka a very simple provider #
    # ------------------------------------------------ #

    fixed_price = fields.Float(compute='_compute_fixed_price',
                               inverse='_set_product_fixed_price',
                               store=True,
                               string='Fixed Price')

    @api.depends('product_id.list_price',
                 'product_id.product_tmpl_id.list_price')
    def _compute_fixed_price(self):
        for carrier in self:
            carrier.fixed_price = carrier.product_id.list_price

    def _set_product_fixed_price(self):
        for carrier in self:
            carrier.product_id.list_price = carrier.fixed_price

    def fixed_rate_shipment(self, order):
        carrier = self._match_address(order.partner_shipping_id)
        if not carrier:
            return {
                'success':
                False,
                'price':
                0.0,
                'error_message':
                _('Error: this delivery method is not available for this address.'
                  ),
                'warning_message':
                False
            }
        price = self.fixed_price
        if self.company_id and self.company_id.currency_id.id != order.currency_id.id:
            price = self.env['res.currency']._compute(
                self.company_id.currency_id, order.currency_id, price)
        return {
            'success': True,
            'price': price,
            'error_message': False,
            'warning_message': False
        }

    def fixed_send_shipping(self, pickings):
        res = []
        for p in pickings:
            res = res + [{
                'exact_price': p.carrier_id.fixed_price,
                'tracking_number': False
            }]
        return res

    def fixed_get_tracking_link(self, picking):
        return False

    def fixed_cancel_shipment(self, pickings):
        raise NotImplementedError()
Exemple #5
0
class StoreDatabaseList(models.Model):
    _name = 'store.database.list'
    _inherit = 'mail.thread'

    @api.model
    def _default_template(self):
        return self.env['store.template'].search(
            [('exp_period', '=', 'free_trail')], limit=1)

    def _get_comapny(self):
        return self.env.user.company_id.id

    def _get_default_expire_date(self):
        return fields.Datetime.from_string(
            fields.Datetime.now()) + datetime.timedelta(hours=20)

    def _get_note(self):
        res = """
            Testing
        """
        return res

    def generate_client_id(self):
        return str(uuid.uuid1())

    reference = fields.Char(string='Reference')
    name = fields.Char(string='Store Name', index=True)
    total_sales = fields.Integer(string='Total Sales')
    database_size = fields.Char(compute='_get_database_size',
                                string='Database Size')
    db_name = fields.Char(string="Database name", readonly=True)
    last_update = fields.Datetime(string="Last Activity in DB")
    access_url = fields.Char('Access Url')

    note = fields.Text('Note')
    color = fields.Integer()
    state = fields.Selection([('new', 'New'), ('in_progress', 'In Progress'),
                              ('renew', 'Renew'), ('close', 'Closed'),
                              ('cancel', 'cancelled')],
                             required=True,
                             default='new')

    exp_date = fields.Date('Expiry Date', track_visibility='onchange')
    start_date = fields.Datetime(string="Start Date",
                                 default=fields.Datetime.now)
    is_expired = fields.Boolean('Expired',
                                compute='_compute_is_expired',
                                search='_search_expired_store')
    sale_order_ref = fields.Many2one('sale.order', string='Sale Order Ref.')
    exp_period = fields.Selection([('free_trail', 'Free Trail'),
                                   ('monthly', '1 Month'),
                                   ('quaterly', '3 Months'),
                                   ('half-yearly', '6 Months'),
                                   ('yearly', '12 Months')],
                                  string="EXP Periods",
                                  track_visibility='onchange')
    partner_id = fields.Many2one('res.partner', string="Partner")
    partner_name = fields.Char(related="partner_id.name",
                               string="Partner Name")
    plan_id = fields.Many2one("saas.plan", string="Plan")
    subscription_line_ids = fields.One2many('store.subscription.line',
                                            'store_id',
                                            string="Subscription Line")
    recurring_total = fields.Float(compute='_get_reccuring_amount',
                                   string="Recurring Amount",
                                   store=True)
    currency_id = fields.Many2one(related='company_id.currency_id',
                                  relation='res.currency',
                                  string="Currency")
    db_uuid = fields.Char('Database UUID',
                          default=generate_client_id,
                          copy=False,
                          required=True)
    client_id = fields.Char('Client UUID',
                            default=generate_client_id,
                            copy=False,
                            required=True)
    is_email_verified = fields.Boolean(string="Is Email Verified")
    website_url = fields.Char(
        'Website URL',
        compute='_website_url',
        help='The full URL to access the document through the website.')
    expire_reason = fields.Selection([('trial', 'Trial'), ('renewal', 'Renew'),
                                      ('upsell', 'Upsell')],
                                     string="Expire Reason",
                                     track_visibility='onchange',
                                     default='trial')
    description = fields.Text(string="Description", default=_get_note)
    payment_tx_ids = fields.One2many('payment.transaction',
                                     'store_id',
                                     string="Payments")
    manager_id = fields.Many2one('res.users', string="Sales Representative")
    template_id = fields.Many2one('store.template',
                                  string="Template",
                                  default=_default_template)
    subscription_code_ids = fields.One2many('subscription.code',
                                            'store_id',
                                            string="Subscription Code")
    need_payment = fields.Boolean(string="Need Payment", default=False)
    company_id = fields.Many2one('res.company',
                                 string="Company",
                                 default=_get_comapny)
    store_exp_date_verify = fields.Datetime('Expiry Date',
                                            track_visibility='onchange',
                                            default=_get_default_expire_date)
    is_template_db = fields.Boolean(string='Is Template Database')
    owner_user_id = fields.Many2one('res.users', string='Owner User')

    display_modules_ids = fields.Many2many('ir.module.module',
                                           string='Modules Visible to Clients')
    max_user = fields.Integer(string="Maximum User", default=5)

    update_addons_list = fields.Boolean('Update Addon List', default=True)
    update_addons = fields.Char('Update Addons', size=256)
    install_addons = fields.Char('Install Addons', size=256)
    uninstall_addons = fields.Char('Uninstall Addons', size=256)
    show_addons = fields.Char('Display Addons')
    access_owner_add = fields.Char('Grant access to Owner')
    hide_menus = fields.Char('Hide Menus')
    access_remove = fields.Char(
        'Restrict access',
        help='Restrict access for all users except super-user.\nNote, that ')
    param_ids = fields.One2many('saas.config.param', 'store_id', 'Parameters')
    client_db_action = fields.Selection([
        ('params', 'Config Params'),
        ('update_addons', 'Update Addons'),
        ('install_addons', 'Install Addons'),
        ('uninstall_addons', 'Uninstall Addons'),
        ('show_addons', 'Show Addons'),
        ('access_owner_add', 'Grant Access to Owner'),
        ('access_remove', 'Restrict access'),
        ('hide_menus', 'Hide Menus'),
    ],
                                        string='Client Database Action')
    is_suspended = fields.Boolean()
    ondelete_removedb = fields.Boolean()

    def _search_expired_store(self, operator, value):
        current_date = fields.Date.context_today(self)
        return [('exp_date', '<', current_date)]

    @api.depends('client_id')
    def _website_url(self):
        for store in self:
            store.website_url = '/my/store/contract/%s/%s' % (store.id,
                                                              store.client_id)

    @api.multi
    def _compute_is_expired(self):
        for record in self:
            if record.exp_date:
                current_date = fields.Date.context_today(record)
                record.is_expired = record.exp_date < current_date
            else:
                record.is_expired = False

    @api.multi
    def open_website_url(self):
        return {
            'type': 'ir.actions.act_url',
            'url': self.website_url,
            'target': 'self',
        }

    @api.multi
    def registry(self, new=False, **kwargs):
        self.ensure_one()
        m = odoo.modules.registry.Registry
        return m.new(self.db_name, **kwargs)

    @api.multi
    @api.depends('subscription_line_ids')
    def _get_reccuring_amount(self):
        for store in self:
            total = 0
            for line in store.subscription_line_ids:
                total += line.price_subtotal

            store.recurring_total = total

    @api.multi
    @api.onchange('exp_period')
    def on_change_expire_period(self):
        if self.exp_period:
            month = 0
            if self.exp_period == 'monthly':
                month = 1
            if self.exp_period == 'quaterly':
                month = 3
            if self.exp_period == 'half-yearly':
                month = 6
            if self.exp_period == 'yearly':
                month = 12
            self.exp_date = self.exp_date + relativedelta(months=+month)

    @api.multi
    @api.onchange('exp_date')
    def on_change_expire_date(self):
        if self.exp_date:
            # update the client database expirey date
            registry = self.registry()
            with registry.cursor() as cr:
                env = api.Environment(cr, SUPERUSER_ID, {})
                config = env['ir.config_parameter'].sudo()
                configs = config.search([('key', '=', 'database.expire.date')])
                configs.write({'value': self.exp_date})

                if self.exp_date >= time.strftime('%Y-%m-%d'):
                    self.state = 'in_progress'
                    self.update_clinet_info(self.db_name, {
                        'state': 'in_progress',
                        'exp_date': self.exp_date
                    })

                if self.exp_date < time.strftime('%Y-%m-%d'):
                    self.state = 'renew'
                    self.update_clinet_info(self.db_name, {
                        'state': 'renew',
                        'exp_date': self.exp_date
                    })

    def update_clinet_info(self, dbname, vals):
        if dbname:
            registry = self.registry()
            with registry.cursor() as cr:
                env = api.Environment(cr, SUPERUSER_ID, {})
                store_client = env.ref('store_client.store_client_demo').sudo()
                store_client.state = vals['state']
                store_client.exp_date = vals['exp_date']

    @api.multi
    @api.onchange('template_id')
    def on_change_template(self):
        if not self.template_id:
            return
        self.currency_id = self.template_id.currency_id.id
        self.manager_id = self.template_id.manager_id.id
        self.exp_period = self.template_id.exp_period
        self.subscription_line_ids = [
            (6, 0, self.template_id.subscription_line_ids.ids)
        ]

    @api.one
    def compute_client_info(self):
        registry = self.registry()
        #Update Module List Every Time
        module_list = self._get_modules_list()
        with registry.cursor() as cr:
            env = api.Environment(cr, SUPERUSER_ID, {})
            user_count = env['res.users'].search_count([])
            product_count = env['product.template'].search_count([])
            amounts = sum(env['sale.order'].search([]).mapped('amount_total'))

            self.update({
                'no_of_users': user_count,
                'no_of_products': product_count,
                'total_sales': amounts
            })

            # store_client = env.ref('store_client.store_client_demo').sudo()
            # store_client.update({
            #     'modules_to_display': module_list,
            #     'user_limit': self.user_limit
            # })

    @api.multi
    def _get_database_size(self):
        self.ensure_one()
        self.database_size = "50 MB"
        return
        # TODO: should not be in functional field
        if self.db_name:
            registry = self.registry()
            with registry.cursor() as cr:

                cr.execute("select pg_database_size('%s')" % self.db_name)
                db_storage = cr.fetchone()[0]

            db_storage = int(db_storage / (1024 * 1024))
            mbsize = str(db_storage) + " MB"
            size_total = mbsize
            self.database_size = size_total

    @api.model
    def create(self, vals):
        vals['reference'] = self.env['ir.sequence'].next_by_code(
            'store.database.list')
        if vals.get('exp_period') != 'free_trail':
            vals['state'] = 'renew'
        res = super(StoreDatabaseList, self).create(vals)
        if 'is_template_db' not in vals:
            res.create_client_params()
            if vals.get('exp_period') != 'free_trail':
                res.on_change_template()
                res._create_sale_order()
        res.create_database(template_db=res.plan_id.template_db)
        return res

    @api.multi
    def unlink(self):
        db_to_remove = self.filtered(lambda s: s.ondelete_removedb).mapped(
            'db_name')
        res = super(StoreDatabaseList, self).unlink()
        # if store delete then delete database related to store
        for db_name in db_to_remove:
            database.exp_drop(db_name)
        return res

    @api.multi
    def _create_sale_order(self):
        self.ensure_one()
        order = self.env['sale.order'].sudo().create({
            'partner_id':
            self.partner_id.id,
            'store_id':
            self.id,
            'plan_id':
            self.plan_id.id,
            'order_line': [(0, 0, {
                'product_id': l.product_id.id,
                'product_uom_qty': l.quantity,
                'price_unit': l.price_unit,
                'discount': l.discount
            }) for l in self.subscription_line_ids]
        })
        order.force_quotation_send()

    @api.multi
    def create_database(self, template_db=None, demo=False, lang='en_US'):
        self.ensure_one()
        new_db = self.db_name
        res = {}
        if new_db in database.list_dbs():
            raise UserError(_('Database Already Exist!'))
        if template_db:
            database._drop_conn(self.env.cr, template_db)
            database.exp_duplicate_database(template_db, new_db)
        else:
            # password = random_password()
            # res.update({'superuser_password': password})
            database.exp_create_database(new_db, demo, lang)
        if not self.is_template_db:
            self.upgrade_database({'params': True})
        _logger.info('Database created Successfully')

        return res

    @api.multi
    def upgrade_database(self, action=None):
        self.ensure_one()
        if action is None:
            action = {}

        obj = self[0]
        payload = {
            # TODO: add configure mail server option here
            'update_addons_list': (obj.update_addons_list or ''),
            'update_addons':
            obj.update_addons.split(',') if obj.update_addons and
            (obj.client_db_action == 'update_addons'
             or action.get('update_addons')) else [],
            'install_addons':
            obj.install_addons.split(',') if obj.install_addons and
            (obj.client_db_action == 'install_addons'
             or action.get('install_addons')) else [],
            'uninstall_addons':
            obj.uninstall_addons.split(',') if obj.uninstall_addons and
            (obj.client_db_action == 'uninstall_addons'
             or action.get('uninstall_addons')) else [],
            'access_owner_add':
            obj.access_owner_add.split(',') if obj.access_owner_add and
            (obj.client_db_action == 'access_owner_add'
             or action.get('access_owner_add')) else [],
            'access_remove':
            obj.access_remove.split(',') if obj.access_remove and
            (obj.client_db_action == 'access_remove'
             or action.get('access_remove')) else [],
            'hide_menus':
            obj.access_remove.split(',') if obj.access_remove and
            (obj.client_db_action == 'hide_menus'
             or action.get('access_remove')) else [],
            'params': [{
                'key': x.key,
                'value': x.value,
                'hidden': x.hidden
            } for x in obj.param_ids] if action.get('params')
            or obj.client_db_action == 'params' else [],
        }
        self.upgrade_client_database(payload)

    @api.multi
    def upgrade_client_database(self, payload):
        for record in self:
            with record.registry().cursor() as cr:
                env = api.Environment(cr, SUPERUSER_ID, record._context)
                return record._upgrade_database(env, payload)

    def create_client_params(self):
        config = self.env['saas.config.param']
        config.create({
            'key': 'saas_client.max_users',
            'value': self.max_user,
            'store_id': self.id
        })
        config.create({
            'key': 'saas_client.suspended',
            'value': str(self.is_suspended),
            'store_id': self.id
        })
        #config.create({'key': 'saas_client.total_storage_limit', 'value': self.total_storage_limit or 1000, 'store_id': self.id})
        config.create({
            'key': 'saas_client.database.secret.key',
            'value': self.db_uuid,
            'store_id': self.id
        })
        config.create({
            'key': 'saas_client.client_id',
            'value': self.client_id,
            'store_id': self.id
        })
        config.create({
            'key': 'saas_client.verify.email',
            'value': str(self.is_email_verified),
            'store_id': self.id
        })
        config.create({
            'key':
            'saas_client.expiration_datetime',
            'value':
            datetime.datetime.strftime(self.exp_date,
                                       DEFAULT_SERVER_DATE_FORMAT),
            'store_id':
            self.id
        })

    @api.multi
    def _upgrade_database(self, client_env, data):
        self.ensure_one()
        # "data" comes from saas_portal/models/wizard.py::upgrade_database
        post = data
        module = client_env['ir.module.module']
        _logger.info(('_upgrade_database', data))
        res = {}

        # 0. Update module list
        update_list = post.get('update_addons_list', False)
        if update_list:
            module.update_list()

        # 1. Update addons
        update_addons = post.get('update_addons', [])
        if update_addons:
            module.search([('name', 'in', update_addons)
                           ]).button_immediate_upgrade()

        # 2. Install addons
        install_addons = post.get('install_addons', [])
        if install_addons:
            module.search([('name', 'in', install_addons)
                           ]).button_immediate_install()

        # 3. Uninstall addons
        uninstall_addons = post.get('uninstall_addons', [])
        if uninstall_addons:
            module.search([('name', 'in', uninstall_addons)
                           ]).button_immediate_uninstall()

        # 5. update parameters
        params = post.get('params', [])
        for obj in params:
            if obj['key'] == 'saas_client.expiration_datetime':
                self.expiration_datetime = obj['value']
            if obj['key'] == 'saas_client.trial' and obj['value'] == 'False':
                self.trial = False
            # groups = []
            # if obj.get('hidden'):
            #     groups = ['saas_client.group_saas_support']
            client_env['ir.config_parameter'].sudo().set_param(
                obj['key'], obj['value'] or ' ')

        # 6. Access rights
        access_owner_add = post.get('access_owner_add', [])
        owner_id = client_env['res.users'].search([('is_owner', '=', True)],
                                                  limit=1).id or 0
        owner_id = int(owner_id)
        if not owner_id:
            res['owner_id'] = "Owner's user is not found"
        if access_owner_add and owner_id:
            res['access_owner_add'] = []
            for g_ref in access_owner_add:
                g = client_env.ref(g_ref, raise_if_not_found=False)
                if not g:
                    res['access_owner_add'].append('group not found: %s' %
                                                   g_ref)
                    continue
                print("\n????owner_id", owner_id)
                g.write({'users': [(4, owner_id, 0)]})
        access_remove = post.get('access_remove', [])
        if access_remove:
            res['access_remove'] = []
            for g_ref in access_remove:
                g = client_env.ref(g_ref, raise_if_not_found=False)
                if not g:
                    res['access_remove'].append('group not found: %s' % g_ref)
                    continue
                users = []
                for u in g.users:
                    if u.id not in (SUPERUSER_ID):
                        users.append((3, u.id, 0))
                g.write({'users': users})

        return res

    @api.multi
    def _prepare_owner_user_data(self):
        """
        Prepare the dict of values to update owner user data in client instalnce. This method may be
        overridden to implement custom values (making sure to call super() to establish
        a clean extension chain).
        """
        self.ensure_one()
        owner_user_data = {
            'login': self.owner_user_id.login,
            'name': self.owner_user_id.name,
            'email': self.owner_user_id.email,
            'password': random_password(),
            'is_owner': True,
            'client_id': self.client_id,
        }
        return owner_user_data

    @api.multi
    def create_client_store(self, vals):
        self.ensure_one()
        vals.update(modules_to_display=self._get_modules_list())
        registry = self.registry()
        with registry.cursor() as cr:
            env = api.Environment(cr, SUPERUSER_ID, {})
            res = env.ref('store_client.store_client_demo').sudo().write(vals)
            if res:
                print("\n\n?client Store Created Successfully")

    def _get_modules_list(self):
        self.ensure_one()
        Module = self.env['ir.module.module']
        for module in self.display_modules_ids:
            mods = module.upstream_dependencies(
                exclude_states=('uninstallable', 'to remove'))
            Module |= mods
            Module |= module
        module_list = Module.mapped('name')
        return json.dumps(module_list)

    @api.multi
    def generate_sub_code(self):
        view_id = self.env.ref('store_master.subscription_code_view_form').id
        context = self._context.copy()
        context['default_store_id'] = self.id
        return {
            'name': 'Subscription Code',
            'view_type': 'form',
            'view_mode': 'form',
            'views': [(view_id, 'form')],
            'res_model': 'subscription.code',
            'view_id': view_id,
            'type': 'ir.actions.act_window',
            'target': 'new',
            'context': context,
        }

    @api.model
    def _is_valid_enterprise_code(self, store_id, enterprise_code):
        store = self.browse(store_id or 1)
        for code in store.subscription_code_ids:
            if code.code == enterprise_code and code.is_active:
                return {
                    'valid': True,
                    'start_date': code.start_date,
                    'exp_date': code.end_date
                }
        return {'valid': False}

    @api.multi
    def send_mail_to_client(self, action):
        self.ensure_one()
        ICP = self.env['ir.config_parameter']
        MailTemplate = self.env['mail.template']
        if action == 'activation':
            tmpl = ICP.get_param('saas_portal.notif_store_create_tmpl')
            if tmpl:
                MailTemplate.browse(tmpl).send_mail(self.id,
                                                    force_send=True,
                                                    raise_exception=False)
        elif action == 'payment':
            tmpl = ICP.get_param('saas_portal.payment_tmpl')
            if tmpl:
                MailTemplate.browse(tmpl).send_mail(self.id,
                                                    force_send=True,
                                                    raise_exception=False)
        elif action == 'validity_extend':
            tmpl = ICP.get_param(
                'saas_portal.notif_store_validity_extend_tmpl')
            if tmpl:
                MailTemplate.browse(tmpl).send_mail(self.id,
                                                    force_send=True,
                                                    raise_exception=False)
        elif action == 'plan_change':
            tmpl = ICP.get_param('saas_portal.plan_change_tmpl')
            if tmpl:
                MailTemplate.browse(tmpl).send_mail(self.id,
                                                    force_send=True,
                                                    raise_exception=False)
        elif action == 'extend_yourr_store_validity':
            tmpl = ICP.get_param('saas_portal.notif_before_store_expire_tmpl')
            if tmpl:
                MailTemplate.browse(tmpl).send_mail(self.partner_id.id,
                                                    force_send=True,
                                                    raise_exception=False)
        elif action == '5_day_after_expire':
            tmpl = ICP.get_param('saas_portal.notif_after_store_expire_tmpl')
            if tmpl:
                MailTemplate.browse(tmpl).send_mail(self.partner_id.id,
                                                    force_send=True,
                                                    raise_exception=False)

    @api.model
    def store_expire_notification(self, cron_mode=True):
        today = datetime.date.today()
        remaining_7_days = self.search([
            ('exp_date', '=',
             (today + datetime.timedelta(days=8)).strftime('%Y-%m-%d'))
        ])
        remaining_2_days = self.search([
            ('exp_date', '=',
             (today + datetime.timedelta(days=3)).strftime('%Y-%m-%d'))
        ])
        today_expire = self.search([('exp_date', '=',
                                     today.strftime('%Y-%m-%d'))])

        if remaining_7_days:
            remaining_7_days.send_mail_to_client(
                action='extend_yourr_store_validity')
        elif remaining_2_days:
            remaining_2_days.send_mail_to_client(
                action='extend_yourr_store_validity')
        elif today_expire:
            today_expire.send_mail_to_client(
                action='extend_yourr_store_validity')

    @api.multi
    def send_activation_mail(self):
        self.ensure_one()
        tmpl = self.env['ir.values'].get_default(
            'store.config.settings', 'notif_email_verification_tmpl')
        if tmpl:
            tmpl = self.env['mail.template'].browse(tmpl)
            tmpl.send_mail(self.id, force_send=True, raise_exception=False)

    @api.model
    def make_archive_store(self):
        domain = [('store_exp_date_verify', '<=', fields.Datetime.now()),
                  ('is_email_verified', '=', False)]
        stores = self.search(domain)
        stores.remove_store()

    @api.multi
    def remove_store(self):
        return
        for store in self:
            odoo.service.db.exp_drop(store.db_name)
        partners = self.mapped('partner_id')
        users = partners.mapped('user_ids')
        users.unlink()
        if partners:
            partners.write({'active': False})  # partners.active = False
        self.unlink()

    def get_expire_date(self):
        dt = fields.Datetime.to_string(
            fields.Datetime.context_timestamp(
                self, fields.Datetime.from_string(self.store_exp_date_verify)))

        return dt

    @api.multi
    def extend_store_validity(self):
        self.ensure_one()
        start_date = self.exp_date
        exp_date = ''
        if self.exp_period == 'monthly':
            exp_date = start_date + relativedelta(months=+1)
        if self.exp_period == 'quaterly':
            exp_date = start_date + relativedelta(months=+3)
        if self.exp_period == 'half-yearly':
            exp_date = start_date + relativedelta(months=+6)
        if self.exp_period == 'yearly':
            exp_date = start_date + relativedelta(months=+12)
        self.write({
            'state': 'in_progress',
            'need_payment': False,
            'exp_date': exp_date,
            'start_date': start_date
        })
Exemple #6
0
class Session(models.Model):
    _name = 'openacademy.session'
    _inherit = ['mail.thread', 'ir.needaction_mixin']

    name = fields.Char(string="编号", readonly=True)
    # sequence = fields.Integer(string="编号", index=True, default=1)
    start_date = fields.Date(string="开始日期",
                             default=fields.Date.today,
                             required=True)
    # 持续天数
    duration = fields.Float(digits=(6, 2), string="持续天数")
    # 座位数
    seats = fields.Integer(string="座位数", required=True)
    active = fields.Boolean(string="有效", default=True)
    color = fields.Integer()
    # 教导员
    instructor_id = fields.Many2one('res.partner',
                                    string="教导员",
                                    domain=[
                                        '|', ('instructor', '=', True),
                                        ('category_id.name', 'ilike',
                                         "Teacher")
                                    ])

    # 学科
    course_id = fields.Many2one('openacademy.course',
                                domain=[('state', '=', 'passed')],
                                ondelete='cascade',
                                string="科目",
                                required=True)
    # 参与者
    attendee_ids = fields.Many2many('res.partner', string="参与者")
    # 已预约人数占满额人数的比例
    taken_seats = fields.Float(string="已分配座位", compute='_taken_seats')
    # 学期结束日期
    end_date = fields.Date(string="结束日期",
                           store=True,
                           compute='_get_end_date',
                           inverse='_set_end_date')
    # 小时数
    hours = fields.Float(string="小时数",
                         compute='_get_hours',
                         inverse='_set_hours')
    # 已参加/预约人数
    attendees_count = fields.Integer(string="已参加人数",
                                     compute='_get_attendees_count',
                                     store=True)
    attendance_sheet_ids = fields.One2many('feitas.partner.course.log',
                                           'session_id',
                                           string="考勤表")

    state = fields.Selection([
        ('draft', "草稿"),
        ('confirmed', "已确认"),
        ('done', "完成"),
    ])
    course_log_ids = fields.One2many('openacademy.session.course.log',
                                     'session',
                                     string="授课记录")
    # 关联的销售订单
    sale_order_count = fields.Integer(string='订单数',
                                      compute='compute_sale_order_count')

    @api.multi
    def compute_sale_order_count(self):
        '''
        计算关联的订单数
        '''
        sale_orders = self.env['sale.order'].sudo().search([
            ('partner_id', 'in',
             [attendee_id.id for attendee_id in self.attendee_ids]),
            ('product_id', '=', self.course_id.name)
        ])
        # _logger.info('----------------------------886')
        # _logger.info(len(sale_orders))
        self.sale_order_count = len(sale_orders)

    @api.model
    def create(self, values):
        '''
        重写create方法
        '''
        plans = self.env['openacademy.course'].browse(
            values['course_id']).plan_ids
        for plan in plans:
            value = {'name': plan.name, 'session': self.id}
            self.env['openacademy.session.course.log'].create(value)
        print '------------------111---'
        print values.get('name', 'New')
        # TODO:这里为啥等于False
        # if values.get('name', 'New') == 'New':
        #     values['name'] = self.env['ir.sequence'].next_by_code('openacademy.session') or '/'
        values['name'] = self.env['openacademy.course'].browse(
            values.get('course_id')).code + '-' + self.env[
                'ir.sequence'].next_by_code('openacademy.session') or ''

        session = super(Session, self).create(values)

        return session

    # 设置状态:draft confirmed done
    @api.multi
    def action_draft(self):
        # 学期的状态设为draft
        self.state = 'draft'

    @api.multi
    def action_confirm(self):
        """
        开课状态设为confirmed,确认开课
        """
        self.state = 'confirmed'
        course_name = self.course_id.name
        product = self.env['product.product'].sudo().search([('name', '=',
                                                              course_name)])
        for attendee_id in self.attendee_ids:
            sale_order = self.env['sale.order'].sudo().create(
                {'partner_id': attendee_id.id})
            line_values = {
                'product_id': product.id,
                'price_unit': product.list_price,
                'order_id': sale_order.id
            }
            self.env['sale.order.line'].sudo().create(line_values)
            # TODO: 销售订单直接改状态这种方式是不可以的。复杂的单据一般有一定的业务逻辑
            sale_order.state = 'sale'

            mail_dict = {
                'subject': "开课通知",
                'author_id': self.env.user.id,
                'email_from': "	Administrator <*****@*****.**>",
                'email_to': attendee_id.email,
                'body_html': u"<p>你好,感谢参加课程%s!</p>" % (course_name)
            }
            mail = self.env['mail.mail'].sudo().create(mail_dict)
            mail.send()

    @api.multi
    def action_done(self):
        # 学期的状态设为done
        self.state = 'done'

    @api.depends('seats', 'attendee_ids')
    def _taken_seats(self):
        # 根据参与者计算已被预约的席位的比例
        for r in self:
            if not r.seats:
                r.taken_seats = 0.0
            else:
                r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats

    @api.onchange('seats', 'attendee_ids')
    def _verify_valid_seats(self):
        # 特定条件下显示警告
        if self.seats < 0:
            return {
                'warning': {
                    'title': "Incorrect 'seats' value",
                    'message': "座位数不能小于零",
                },
            }
        if self.seats < len(self.attendee_ids):
            return {
                'warning': {
                    'title': "Too many attendees",
                    'message': "Increase seats or remove excess attendees",
                },
            }

    # start_date和duration发生变化时触发函数
    @api.depends('start_date', 'duration')
    def _get_end_date(self):
        # 计算截止日期
        for r in self:
            if not (r.start_date and r.duration):
                r.end_date = r.start_date
                continue

            # Add duration to start_date, but: Monday + 5 days = Saturday, so
            # subtract one second to get on Friday instead
            start = fields.Datetime.from_string(r.start_date)
            duration = timedelta(days=r.duration, seconds=-1)
            r.end_date = start + duration

    def _set_end_date(self):
        # 计算持续天数
        for r in self:
            if not (r.start_date and r.end_date):
                continue

            # Compute the difference between dates, but: Friday - Monday = 4 days,
            # so add one day to get 5 days instead
            start_date = fields.Datetime.from_string(r.start_date)
            end_date = fields.Datetime.from_string(r.end_date)
            r.duration = (end_date - start_date).days + 1

    @api.depends('duration')
    def _get_hours(self):
        # 计算小时数
        for r in self:
            r.hours = r.duration * 24

    def _set_hours(self):
        # 计算天数
        for r in self:
            r.duration = r.hours / 24

    @api.depends('attendee_ids')
    def _get_attendees_count(self):
        # 计算参与人数
        for r in self:
            r.attendees_count = len(r.attendee_ids)

    # @api.multi
    def to_related_sale_orders(self):
        return {
            'type':
            'ir.actions.act_window',
            'name':
            'see orders',
            'view_type':
            'form',
            'view_mode':
            'tree,form',
            'res_model':
            'sale.order',
            #注意此处m2m字段attendee_ids的用法
            'domain':
            str([('partner_id', 'in',
                  [attendee_id.id for attendee_id in self.attendee_ids]),
                 ('product_id', '=', self.course_id.name)]),
        }

    @api.constrains('instructor_id', 'attendee_ids')
    def _check_instructor_not_in_attendees(self):
        for r in self:
            # 导师不能同时是这个课的学生
            if r.instructor_id and r.instructor_id in r.attendee_ids:
                raise ValidationError("教导员不能同时是参与者")
class ResConfigSettings(models.TransientModel):
    _inherit = 'res.config.settings'

    def _default_website(self):
        return self.env['website'].search(
            [('company_id', '=', self.env.company.id)], limit=1)

    website_id = fields.Many2one('website',
                                 string="website",
                                 default=_default_website,
                                 ondelete='cascade')
    website_name = fields.Char('Website Name',
                               related='website_id.name',
                               readonly=False)
    website_domain = fields.Char('Website Domain',
                                 related='website_id.domain',
                                 readonly=False)
    website_country_group_ids = fields.Many2many(
        related='website_id.country_group_ids', readonly=False)
    website_company_id = fields.Many2one(related='website_id.company_id',
                                         string='Website Company',
                                         readonly=False)
    website_logo = fields.Binary(related='website_id.logo', readonly=False)
    language_ids = fields.Many2many(related='website_id.language_ids',
                                    relation='res.lang',
                                    readonly=False)
    website_language_count = fields.Integer(
        string='Number of languages',
        compute='_compute_website_language_count',
        readonly=True)
    website_default_lang_id = fields.Many2one(
        string='Default language',
        related='website_id.default_lang_id',
        readonly=False)
    website_default_lang_code = fields.Char(
        'Default language code',
        related='website_id.default_lang_id.code',
        readonly=False)
    specific_user_account = fields.Boolean(
        related='website_id.specific_user_account',
        readonly=False,
        help='Are newly created user accounts website specific')
    website_cookies_bar = fields.Boolean(related='website_id.cookies_bar',
                                         readonly=False)

    google_analytics_key = fields.Char(
        'Google Analytics Key',
        related='website_id.google_analytics_key',
        readonly=False)
    google_management_client_id = fields.Char(
        'Google Client ID',
        related='website_id.google_management_client_id',
        readonly=False)
    google_management_client_secret = fields.Char(
        'Google Client Secret',
        related='website_id.google_management_client_secret',
        readonly=False)
    google_search_console = fields.Char(
        'Google Search Console',
        related='website_id.google_search_console',
        readonly=False)

    cdn_activated = fields.Boolean(related='website_id.cdn_activated',
                                   readonly=False)
    cdn_url = fields.Char(related='website_id.cdn_url', readonly=False)
    cdn_filters = fields.Text(related='website_id.cdn_filters', readonly=False)
    auth_signup_uninvited = fields.Selection(compute="_compute_auth_signup",
                                             inverse="_set_auth_signup")

    social_twitter = fields.Char(related='website_id.social_twitter',
                                 readonly=False)
    social_facebook = fields.Char(related='website_id.social_facebook',
                                  readonly=False)
    social_github = fields.Char(related='website_id.social_github',
                                readonly=False)
    social_linkedin = fields.Char(related='website_id.social_linkedin',
                                  readonly=False)
    social_youtube = fields.Char(related='website_id.social_youtube',
                                 readonly=False)
    social_instagram = fields.Char(related='website_id.social_instagram',
                                   readonly=False)

    @api.depends('website_id', 'social_twitter', 'social_facebook',
                 'social_github', 'social_linkedin', 'social_youtube',
                 'social_instagram')
    def has_social_network(self):
        self.has_social_network = self.social_twitter or self.social_facebook or self.social_github \
            or self.social_linkedin or self.social_youtube or self.social_instagram

    def inverse_has_social_network(self):
        if not self.has_social_network:
            self.social_twitter = ''
            self.social_facebook = ''
            self.social_github = ''
            self.social_linkedin = ''
            self.social_youtube = ''
            self.social_instagram = ''

    has_social_network = fields.Boolean("Configure Social Network",
                                        compute=has_social_network,
                                        inverse=inverse_has_social_network)

    favicon = fields.Binary('Favicon',
                            related='website_id.favicon',
                            readonly=False)
    social_default_image = fields.Binary(
        'Default Social Share Image',
        related='website_id.social_default_image',
        readonly=False)

    google_maps_api_key = fields.Char(related='website_id.google_maps_api_key',
                                      readonly=False)
    group_multi_website = fields.Boolean(
        "Multi-website", implied_group="website.group_multi_website")

    @api.onchange('website_id')
    @api.depends('website_id.auth_signup_uninvited')
    def _compute_auth_signup(self):
        self.auth_signup_uninvited = self.website_id.auth_signup_uninvited

    def _set_auth_signup(self):
        for config in self:
            config.website_id.auth_signup_uninvited = config.auth_signup_uninvited

    @api.depends('website_id')
    def has_google_analytics(self):
        self.has_google_analytics = bool(self.google_analytics_key)

    @api.depends('website_id')
    def has_google_analytics_dashboard(self):
        self.has_google_analytics_dashboard = bool(
            self.google_management_client_id)

    @api.depends('website_id')
    def has_google_maps(self):
        self.has_google_maps = bool(self.google_maps_api_key)

    @api.depends('website_id')
    def has_default_share_image(self):
        self.has_default_share_image = bool(self.social_default_image)

    @api.depends('website_id')
    def has_google_search_console(self):
        self.has_google_search_console = bool(self.google_search_console)

    def inverse_has_google_analytics(self):
        if not self.has_google_analytics:
            self.has_google_analytics_dashboard = False
            self.google_analytics_key = False

    def inverse_has_google_maps(self):
        if not self.has_google_maps:
            self.google_maps_api_key = False

    def inverse_has_google_analytics_dashboard(self):
        if not self.has_google_analytics_dashboard:
            self.google_management_client_id = False
            self.google_management_client_secret = False

    def inverse_has_google_search_console(self):
        if not self.has_google_search_console:
            self.google_search_console = False

    def inverse_has_default_share_image(self):
        if not self.has_default_share_image:
            self.social_default_image = False

    has_google_analytics = fields.Boolean("Google Analytics",
                                          compute=has_google_analytics,
                                          inverse=inverse_has_google_analytics)
    has_google_analytics_dashboard = fields.Boolean(
        "Google Analytics Dashboard",
        compute=has_google_analytics_dashboard,
        inverse=inverse_has_google_analytics_dashboard)
    has_google_maps = fields.Boolean("Google Maps",
                                     compute=has_google_maps,
                                     inverse=inverse_has_google_maps)
    has_google_search_console = fields.Boolean(
        "Console Google Search",
        compute=has_google_search_console,
        inverse=inverse_has_google_search_console)
    has_default_share_image = fields.Boolean(
        "Use a image by default for sharing",
        compute=has_default_share_image,
        inverse=inverse_has_default_share_image)

    @api.onchange('language_ids')
    def _onchange_language_ids(self):
        # If current default language is removed from language_ids
        # update the website_default_lang_id
        language_ids = self.language_ids._origin
        if not language_ids:
            self.website_default_lang_id = False
        elif self.website_default_lang_id not in language_ids:
            self.website_default_lang_id = language_ids[0]

    @api.depends('language_ids')
    def _compute_website_language_count(self):
        for config in self:
            config.website_language_count = len(config.language_ids)

    def set_values(self):
        super(ResConfigSettings, self).set_values()

    def open_template_user(self):
        action = self.env["ir.actions.actions"]._for_xml_id(
            "base.action_res_users")
        action['res_id'] = literal_eval(
            self.env['ir.config_parameter'].sudo().get_param(
                'base.template_portal_user_id', 'False'))
        action['views'] = [[self.env.ref('base.view_users_form').id, 'form']]
        return action

    def website_go_to(self):
        self.website_id._force()
        return {
            'type': 'ir.actions.act_url',
            'url': '/',
            'target': 'self',
        }

    def action_website_create_new(self):
        return {
            'view_mode':
            'form',
            'view_id':
            self.env.ref('website.view_website_form_view_themes_modal').id,
            'res_model':
            'website',
            'type':
            'ir.actions.act_window',
            'target':
            'new',
            'res_id':
            False,
        }

    def action_open_robots(self):
        self.website_id._force()
        return {
            'name': _("Robots.txt"),
            'view_mode': 'form',
            'res_model': 'website.robots',
            'type': 'ir.actions.act_window',
            "views": [[False, "form"]],
            'target': 'new',
        }

    def action_ping_sitemap(self):
        if not self.website_id.domain:
            raise UserError(_("You haven't defined your domain"))

        return {
            'type':
            'ir.actions.act_url',
            'url':
            'http://www.google.com/ping?sitemap=%s/sitemap.xml' %
            self.website_id.domain,
            'target':
            'new',
        }

    def install_theme_on_current_website(self):
        self.website_id._force()
        action = self.env["ir.actions.actions"]._for_xml_id(
            "website.theme_install_kanban_action")
        action['target'] = 'main'
        return action
class Lead2OpportunityMassConvert(models.TransientModel):

    _name = 'crm.lead2opportunity.partner.mass'
    _description = 'Convert Lead to Opportunity (in mass)'
    _inherit = 'crm.lead2opportunity.partner'

    @api.model
    def default_get(self, fields):
        res = super(Lead2OpportunityMassConvert, self).default_get(fields)
        if 'partner_id' in fields:  # avoid forcing the partner of the first lead as default
            res['partner_id'] = False
        if 'action' in fields:
            res['action'] = 'each_exist_or_create'
        if 'name' in fields:
            res['name'] = 'convert'
        if 'opportunity_ids' in fields:
            res['opportunity_ids'] = False
        return res

    user_ids = fields.Many2many('res.users', string='Salesmen')
    team_id = fields.Many2one('crm.team', 'Sales Team', index=True)
    deduplicate = fields.Boolean(
        'Apply deduplication',
        default=True,
        help='Merge with existing leads/opportunities of each partner')
    action = fields.Selection(selection_add=[
        ('each_exist_or_create', 'Use existing partner or create'),
    ],
                              string='Related Customer',
                              required=True)
    force_assignation = fields.Boolean(
        'Force assignment',
        help=
        'If unchecked, this will leave the salesman of duplicated opportunities'
    )

    @api.onchange('action')
    def _onchange_action(self):
        if self.action != 'exist':
            self.partner_id = False

    @api.onchange('deduplicate')
    def _onchange_deduplicate(self):
        active_leads = self.env['crm.lead'].browse(self._context['active_ids'])
        partner_ids = [
            (lead.partner_id.id, lead.partner_id and lead.partner_id.email
             or lead.email_from) for lead in active_leads
        ]
        partners_duplicated_leads = {}
        for partner_id, email in partner_ids:
            duplicated_leads = self.env[
                'crm.lead']._get_duplicated_leads_by_emails(partner_id,
                                                            email,
                                                            include_lost=False)
            if len(duplicated_leads) > 1:
                partners_duplicated_leads.setdefault(
                    (partner_id, email), []).extend(duplicated_leads)

        leads_with_duplicates = []
        for lead in active_leads:
            lead_tuple = (lead.partner_id.id, lead.partner_id.email
                          if lead.partner_id else lead.email_from)
            if len(partners_duplicated_leads.get(lead_tuple, [])) > 1:
                leads_with_duplicates.append(lead.id)

        self.opportunity_ids = self.env['crm.lead'].browse(
            leads_with_duplicates)

    def _convert_opportunity(self, vals):
        """ When "massively" (more than one at a time) converting leads to
            opportunities, check the salesteam_id and salesmen_ids and update
            the values before calling super.
        """
        self.ensure_one()
        salesteam_id = self.team_id.id if self.team_id else False
        salesmen_ids = []
        if self.user_ids:
            salesmen_ids = self.user_ids.ids
        vals.update({'user_ids': salesmen_ids, 'team_id': salesteam_id})
        return super(Lead2OpportunityMassConvert,
                     self)._convert_opportunity(vals)

    def mass_convert(self):
        self.ensure_one()
        if self.name == 'convert' and self.deduplicate:
            merged_lead_ids = set()
            remaining_lead_ids = set()
            lead_selected = self._context.get('active_ids', [])
            for lead_id in lead_selected:
                if lead_id not in merged_lead_ids:
                    lead = self.env['crm.lead'].browse(lead_id)
                    duplicated_leads = self.env[
                        'crm.lead']._get_duplicated_leads_by_emails(
                            lead.partner_id.id,
                            lead.partner_id.email
                            if lead.partner_id else lead.email_from,
                            include_lost=False)
                    if len(duplicated_leads) > 1:
                        lead = duplicated_leads.merge_opportunity()
                        merged_lead_ids.update(duplicated_leads.ids)
                        remaining_lead_ids.add(lead.id)
            active_ids = set(self._context.get('active_ids', {}))
            active_ids = (active_ids - merged_lead_ids) | remaining_lead_ids

            self = self.with_context(active_ids=list(
                active_ids))  # only update active_ids when there are set
        no_force_assignation = self._context.get('no_force_assignation',
                                                 not self.force_assignation)
        return self.with_context(
            no_force_assignation=no_force_assignation).action_apply()
Exemple #9
0
class Partner(models.Model):
    """ Update partner to add a field about notification preferences. Add a generic opt-out field that can be used
       to restrict usage of automatic email templates. """
    _name = "res.partner"
    _inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin']
    _mail_flat_thread = False

    message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)
    opt_out = fields.Boolean(
        'Opt-Out', help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
                        "Filter 'Available for Mass Mailing' allows users to filter the partners when performing mass mailing.")
    channel_ids = fields.Many2many('mail.channel', 'mail_channel_partner', 'partner_id', 'channel_id', string='Channels', copy=False)

    @api.multi
    def message_get_suggested_recipients(self):
        recipients = super(Partner, self).message_get_suggested_recipients()
        for partner in self:
            partner._message_add_suggested_recipient(recipients, partner=partner, reason=_('Partner Profile'))
        return recipients

    @api.multi
    def message_get_default_recipients(self):
        return dict((res_id, {'partner_ids': [res_id], 'email_to': False, 'email_cc': False}) for res_id in self.ids)

    @api.model
    def _notify_prepare_template_context(self, message, notif_values):
        # compute signature
        if not notif_values.pop('add_sign', True):
            signature = False
        elif message.author_id and message.author_id.user_ids and message.author_id.user_ids[0].signature:
            signature = message.author_id.user_ids[0].signature
        elif message.author_id:
            signature = "<p>-- <br/>%s</p>" % message.author_id.name
        else:
            signature = ""

        # compute Sent by
        if message.author_id and message.author_id.user_ids:
            user = message.author_id.user_ids[0]
        else:
            user = self.env.user
        if user.company_id.website:
            website_url = 'http://%s' % user.company_id.website if not user.company_id.website.lower().startswith(('http:', 'https:')) else user.company_id.website
        else:
            website_url = False

        model_name = False
        if message.model:
            model_name = self.env['ir.model']._get(message.model).display_name

        record_name = message.record_name

        tracking = []
        for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]):
            tracking.append((tracking_value.field_desc,
                             tracking_value.get_old_display_value()[0],
                             tracking_value.get_new_display_value()[0]))

        is_discussion = message.subtype_id.id == self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')

        record = False
        if message.res_id and message.model in self.env:
            record = self.env[message.model].browse(message.res_id)

        company = user.company_id
        if record and hasattr(record, 'company_id'):
            company = record.company_id

        return {
            'message': message,
            'signature': signature,
            'website_url': website_url,
            'company': company,
            'model_name': model_name,
            'record': record,
            'record_name': record_name,
            'tracking_values': tracking,
            'is_discussion': is_discussion,
            'subtype': message.subtype_id,
        }

    @api.model
    def _notify_prepare_email_values(self, message, notif_values):
        # compute email references
        references = message.parent_id.message_id if message.parent_id else False

        # custom values
        custom_values = dict()
        if message.res_id and message.model:
            custom_values = self.env['mail.thread']._notify_specific_email_values_on_records(message, records=self.env[message.model].browse(message.res_id))

        mail_values = {
            'mail_message_id': message.id,
            'mail_server_id': message.mail_server_id.id,
            'auto_delete': notif_values.pop('mail_auto_delete', True),
            'references': references,
        }
        mail_values.update(custom_values)
        return mail_values

    @api.model
    def _notify_send(self, body, subject, recipients, **mail_values):
        emails = self.env['mail.mail']
        recipients_nbr = len(recipients)
        for email_chunk in split_every(50, recipients.ids):
            # TDE FIXME: missing message parameter. So we will find mail_message_id
            # in the mail_values and browse it. It should already be in the
            # cache so should not impact performances.
            mail_message_id = mail_values.get('mail_message_id')
            message = self.env['mail.message'].browse(mail_message_id) if mail_message_id else None
            tig = self.env[message.model].browse(message.res_id) if message and message.model and message.res_id else False
            recipient_values = self.env['mail.thread']._notify_email_recipients_on_records(message, email_chunk, records=tig)
            create_values = {
                'body_html': body,
                'subject': subject,
            }
            create_values.update(mail_values)
            create_values.update(recipient_values)
            emails |= self.env['mail.mail'].create(create_values)
        return emails, recipients_nbr

    @api.model
    def _notify_udpate_notifications(self, emails):
        for email in emails:
            notifications = self.env['mail.notification'].sudo().search([
                ('mail_message_id', '=', email.mail_message_id.id),
                ('res_partner_id', 'in', email.recipient_ids.ids)])
            notifications.write({
                'mail_id': email.id,
                'is_email': True,
                'is_read': True,  # handle by email discards Inbox notification
                'email_status': 'ready',
            })

    @api.multi
    def _notify(self, message, layout=False, force_send=False, send_after_commit=True, values=None):
        """ Method to send email linked to notified messages. The recipients are
        the recordset on which this method is called.

        :param boolean force_send: send notification emails now instead of letting the scheduler handle the email queue
        :param boolean send_after_commit: send notification emails after the transaction end instead of durign the
                                          transaction; this option is used only if force_send is True
        :param dict values: values used to compute the notification process, containing

         * add_sign: add user signature to notification email, default is True
         * mail_auto_delete: auto delete send emails, default is True
         * other values are given to the context used to render the notification template, allowing customization

        """
        if not self.ids:
            return True
        values = values if values is not None else {}

        template_xmlid = layout if layout else 'mail.message_notification_email'
        try:
            base_template = self.env.ref(template_xmlid, raise_if_not_found=True)
        except ValueError:
            _logger.warning('QWeb template %s not found when sending notification emails. Skipping.' % (template_xmlid))
            return False

        base_template_ctx = self._notify_prepare_template_context(message, values)
        base_mail_values = self._notify_prepare_email_values(message, values)

        # classify recipients: actions / no action
        tig = self.env[message.model].browse(message.res_id) if message.model and message.res_id else False
        recipients = self.env['mail.thread']._notify_classify_recipients_on_records(message, self, records=tig)

        emails = self.env['mail.mail']
        recipients_nbr, recipients_max = 0, 50
        for email_type, recipient_template_values in recipients.items():
            if recipient_template_values['recipients']:
                # generate notification email content
                template_ctx = {**base_template_ctx, **recipient_template_values, **values}  # fixme: set button_unfollow to none
                fol_values = {
                    'subject': message.subject or (message.record_name and 'Re: %s' % message.record_name),
                    'body': base_template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True),
                }
                fol_values['body'] = self.env['mail.thread']._replace_local_links(fol_values['body'])
                # send email
                new_emails, new_recipients_nbr = self._notify_send(fol_values['body'], fol_values['subject'], recipient_template_values['recipients'], **base_mail_values)
                # update notifications
                self._notify_udpate_notifications(new_emails)

                emails |= new_emails
                recipients_nbr += new_recipients_nbr

        # NOTE:
        #   1. for more than 50 followers, use the queue system
        #   2. do not send emails immediately if the registry is not loaded,
        #      to prevent sending email during a simple update of the database
        #      using the command-line.
        test_mode = getattr(threading.currentThread(), 'testing', False)
        if force_send and recipients_nbr < recipients_max and \
                (not self.pool._init or test_mode):
            email_ids = emails.ids
            dbname = self.env.cr.dbname
            _context = self._context

            def send_notifications():
                db_registry = registry(dbname)
                with api.Environment.manage(), db_registry.cursor() as cr:
                    env = api.Environment(cr, SUPERUSER_ID, _context)
                    env['mail.mail'].browse(email_ids).send()

            # unless asked specifically, send emails after the transaction to
            # avoid side effects due to emails being sent while the transaction fails
            if not test_mode and send_after_commit:
                self._cr.after('commit', send_notifications)
            else:
                emails.send()

        return True

    @api.multi
    def _notify_by_chat(self, message):
        """ Broadcast the message to all the partner since """
        if not self:
            return
        message_values = message.message_format()[0]
        notifications = []
        for partner in self:
            notifications.append([(self._cr.dbname, 'ir.needaction', partner.id), dict(message_values)])
        self.env['bus.bus'].sendmany(notifications)

    @api.model
    def get_needaction_count(self):
        """ compute the number of needaction of the current user """
        if self.env.user.partner_id:
            self.env.cr.execute("""
                SELECT count(*) as needaction_count
                FROM mail_message_res_partner_needaction_rel R
                WHERE R.res_partner_id = %s AND (R.is_read = false OR R.is_read IS NULL)""", (self.env.user.partner_id.id,))
            return self.env.cr.dictfetchall()[0].get('needaction_count')
        _logger.error('Call to needaction_count without partner_id')
        return 0

    @api.model
    def get_starred_count(self):
        """ compute the number of starred of the current user """
        if self.env.user.partner_id:
            self.env.cr.execute("""
                SELECT count(*) as starred_count
                FROM mail_message_res_partner_starred_rel R
                WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id,))
            return self.env.cr.dictfetchall()[0].get('starred_count')
        _logger.error('Call to starred_count without partner_id')
        return 0

    @api.model
    def get_static_mention_suggestions(self):
        """ To be overwritten to return the id, name and email of partners used as static mention
            suggestions loaded once at webclient initialization and stored client side. """
        return []

    @api.model
    def get_mention_suggestions(self, search, limit=8):
        """ Return 'limit'-first partners' id, name and email such that the name or email matches a
            'search' string. Prioritize users, and then extend the research to all partners. """
        search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]])
        fields = ['id', 'name', 'email']

        # Search users
        domain = expression.AND([[('user_ids.id', '!=', False)], search_dom])
        users = self.search_read(domain, fields, limit=limit)

        # Search partners if less than 'limit' users found
        partners = []
        if len(users) < limit:
            partners = self.search_read(search_dom, fields, limit=limit)
            # Remove duplicates
            partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])] 

        return [users, partners]
Exemple #10
0
class FSMPerson(models.Model):
    _name = 'fsm.person'
    _inherits = {'res.partner': 'partner_id'}
    _description = 'Field Service Worker'

    partner_id = fields.Many2one('res.partner',
                                 string='Related Partner',
                                 required=True,
                                 ondelete='restrict',
                                 delegate=True,
                                 auto_join=True)
    category_ids = fields.Many2many('fsm.category', string='Categories')
    calendar_id = fields.Many2one('resource.calendar',
                                  string='Working Schedule')
    stage_id = fields.Many2one('fsm.stage',
                               string='Stage',
                               track_visibility='onchange',
                               index=True,
                               copy=False,
                               group_expand='_read_group_stage_ids',
                               default=lambda self: self._default_stage_id())
    hide = fields.Boolean(default=False)
    mobile = fields.Char(string="Mobile")
    territory_ids = fields.Many2many('fsm.territory', string='Territories')

    @api.model
    def _search(self,
                args,
                offset=0,
                limit=None,
                order=None,
                count=False,
                access_rights_uid=None):
        res = super(FSMPerson,
                    self)._search(args=args,
                                  offset=offset,
                                  limit=limit,
                                  order=order,
                                  count=count,
                                  access_rights_uid=access_rights_uid)
        # Check for args first having location_ids as default filter
        for arg in args:
            if isinstance(arg, (list)):
                if arg[0] == 'location_ids':
                    self.env.cr.execute(
                        "SELECT person_id "
                        "FROM fsm_location_person "
                        "WHERE location_id=%s", (arg[2], ))
                    workers_ids = self.env.cr.fetchall()
                    if workers_ids:
                        preferred_workers_list = \
                            [worker[0] for worker in workers_ids]
                        return preferred_workers_list
        return res

    @api.model
    def create(self, vals):
        vals.update({'fsm_person': True})
        return super(FSMPerson, self).create(vals)

    @api.multi
    def get_person_information(self, vals):
        # get person ids
        person_ids = self.search([('id', '!=', 0), ('active', '=', True)])
        person_information_dict = []
        for person in person_ids:
            person_information_dict.append({
                'id': person.id,
                'name': person.name
            })
        return person_information_dict

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        stage_ids = self.env['fsm.stage'].search([('stage_type', '=', 'worker')
                                                  ])
        return stage_ids

    def _default_stage_id(self):
        return self.env['fsm.stage'].search([('stage_type', '=', 'worker'),
                                             ('sequence', '=', '1')])

    def next_stage(self):
        seq = self.stage_id.sequence
        next_stage = self.env['fsm.stage'].search(
            [('stage_type', '=', 'worker'), ('sequence', '>', seq)],
            order="sequence asc")
        if next_stage:
            self.stage_id = next_stage[0]
            self._onchange_stage_id()

    def previous_stage(self):
        seq = self.stage_id.sequence
        prev_stage = self.env['fsm.stage'].search(
            [('stage_type', '=', 'worker'), ('sequence', '<', seq)],
            order="sequence desc")
        if prev_stage:
            self.stage_id = prev_stage[0]
            self._onchange_stage_id()

    @api.onchange('stage_id')
    def _onchange_stage_id(self):
        # get last stage
        heighest_stage = self.env['fsm.stage'].search(
            [('stage_type', '=', 'worker')], order='sequence desc', limit=1)
        if self.stage_id.name == heighest_stage.name:
            self.hide = True
        else:
            self.hide = False
Exemple #11
0
class AccountInvoice(models.Model):
    _inherit = 'account.invoice'

    @api.one
    @api.depends('invoice_line_ids.price_subtotal', 'tax_line_ids.amount',
                 'currency_id', 'company_id')
    def _compute_amount(self):
        super(AccountInvoice, self)._compute_amount()
        lines = self.invoice_line_ids
        self.total_tax = sum(l.price_tax for l in lines)
        self.icms_base = sum(l.icms_base_calculo for l in lines)
        self.icms_value = sum(l.icms_valor for l in lines)
        self.icms_st_base = sum(l.icms_st_base_calculo for l in lines)
        self.icms_st_value = sum(l.icms_st_valor for l in lines)
        self.valor_icms_uf_remet = sum(l.icms_uf_remet for l in lines)
        self.valor_icms_uf_dest = sum(l.icms_uf_dest for l in lines)
        self.valor_icms_fcp_uf_dest = sum(l.icms_fcp_uf_dest for l in lines)
        self.issqn_base = sum(l.issqn_base_calculo for l in lines)
        self.issqn_value = sum(abs(l.issqn_valor) for l in lines)
        self.ipi_base = sum(l.ipi_base_calculo for l in lines)
        self.ipi_value = sum(l.ipi_valor for l in lines)
        self.pis_base = sum(l.pis_base_calculo for l in lines)
        self.pis_value = sum(abs(l.pis_valor) for l in lines)
        self.cofins_base = sum(l.cofins_base_calculo for l in lines)
        self.cofins_value = sum(abs(l.cofins_valor) for l in lines)
        self.ii_base = sum(l.ii_base_calculo for l in lines)
        self.ii_value = sum(l.ii_valor for l in lines)
        self.csll_base = sum(l.csll_base_calculo for l in lines)
        self.csll_value = sum(abs(l.csll_valor) for l in lines)
        self.irrf_base = sum(l.irrf_base_calculo for l in lines)
        self.irrf_value = sum(abs(l.irrf_valor) for l in lines)
        self.inss_base = sum(l.inss_base_calculo for l in lines)
        self.inss_value = sum(abs(l.inss_valor) for l in lines)

        # Retenções
        self.issqn_retention = sum(
            abs(l.issqn_valor) if l.issqn_valor < 0 else 0.0 for l in lines)
        self.pis_retention = sum(
            abs(l.pis_valor) if l.pis_valor < 0 else 0.0 for l in lines)
        self.cofins_retention = sum(
            abs(l.cofins_valor) if l.cofins_valor < 0 else 0.0 for l in lines)
        self.csll_retention = sum(
            abs(l.csll_valor) if l.csll_valor < 0 else 0 for l in lines)
        self.irrf_retention = sum(
            abs(l.irrf_valor) if l.irrf_valor < 0 else 0.0 for l in lines)
        self.inss_retention = sum(
            abs(l.inss_valor) if l.inss_valor < 0 else 0.0 for l in lines)

        self.total_bruto = sum(l.valor_bruto for l in lines)
        self.total_desconto = sum(l.valor_desconto for l in lines)
        self.total_tributos_federais = sum(l.tributos_estimados_federais
                                           for l in lines)
        self.total_tributos_estaduais = sum(l.tributos_estimados_estaduais
                                            for l in lines)
        self.total_tributos_municipais = sum(l.tributos_estimados_municipais
                                             for l in lines)
        self.total_tributos_estimados = sum(l.tributos_estimados
                                            for l in lines)
        # TOTAL
        self.amount_total = self.total_bruto - \
            self.total_desconto + self.total_tax
        sign = self.type in ['in_refund', 'out_refund'] and -1 or 1
        self.amount_total_company_signed = self.amount_total * sign
        self.amount_total_signed = self.amount_total * sign

    @api.one
    @api.depends('move_id.line_ids')
    def _compute_receivables(self):
        self.receivable_move_line_ids = self.move_id.line_ids.filtered(
            lambda m: m.account_id.user_type_id.type == 'receivable').sorted(
                key=lambda m: m.date_maturity)

    @api.one
    @api.depends('move_id.line_ids')
    def _compute_payables(self):
        self.receivable_move_line_ids = self.move_id.line_ids.filtered(
            lambda m: m.account_id.user_type_id.type == 'payable')

    total_tax = fields.Float(string='Impostos ( + )',
                             readonly=True,
                             compute='_compute_amount',
                             digits=dp.get_precision('Account'),
                             store=True)

    receivable_move_line_ids = fields.Many2many('account.move.line',
                                                string='Receivable Move Lines',
                                                compute='_compute_receivables')

    payable_move_line_ids = fields.Many2many('account.move.line',
                                             string='Payable Move Lines',
                                             compute='_compute_payables')

    product_serie_id = fields.Many2one(
        'br_account.document.serie',
        string=u'Série produtos',
        domain="[('fiscal_document_id', '=', product_document_id),\
        ('company_id','=',company_id)]",
        readonly=True,
        states={'draft': [('readonly', False)]})
    product_document_id = fields.Many2one(
        'br_account.fiscal.document',
        string='Documento produtos',
        readonly=True,
        states={'draft': [('readonly', False)]})
    service_serie_id = fields.Many2one(
        'br_account.document.serie',
        string=u'Série serviços',
        domain="[('fiscal_document_id', '=', service_document_id),\
        ('company_id','=',company_id)]",
        readonly=True,
        states={'draft': [('readonly', False)]})
    service_document_id = fields.Many2one(
        'br_account.fiscal.document',
        string='Documento serviços',
        readonly=True,
        states={'draft': [('readonly', False)]})
    fiscal_document_related_ids = fields.One2many(
        'br_account.document.related',
        'invoice_id',
        'Documento Fiscal Relacionado',
        readonly=True,
        states={'draft': [('readonly', False)]})
    fiscal_observation_ids = fields.Many2many(
        'br_account.fiscal.observation',
        string=u"Observações Fiscais",
        readonly=True,
        states={'draft': [('readonly', False)]})
    fiscal_comment = fields.Text(u'Observação Fiscal',
                                 readonly=True,
                                 states={'draft': [('readonly', False)]})

    total_bruto = fields.Float(string='Total Bruto ( = )',
                               store=True,
                               digits=dp.get_precision('Account'),
                               compute='_compute_amount')
    total_desconto = fields.Float(string='Desconto ( - )',
                                  store=True,
                                  digits=dp.get_precision('Account'),
                                  compute='_compute_amount')

    icms_base = fields.Float(string='Base ICMS',
                             store=True,
                             compute='_compute_amount',
                             digits=dp.get_precision('Account'))
    icms_value = fields.Float(string='Valor ICMS',
                              digits=dp.get_precision('Account'),
                              compute='_compute_amount',
                              store=True)
    icms_st_base = fields.Float(string='Base ICMS ST',
                                store=True,
                                compute='_compute_amount',
                                digits=dp.get_precision('Account'))
    icms_st_value = fields.Float(string='Valor ICMS ST',
                                 store=True,
                                 compute='_compute_amount',
                                 digits=dp.get_precision('Account'))
    valor_icms_fcp_uf_dest = fields.Float(
        string="Total ICMS FCP",
        store=True,
        compute='_compute_amount',
        help=u'Total total do ICMS relativo Fundo de Combate à Pobreza (FCP) \
        da UF de destino')
    valor_icms_uf_dest = fields.Float(
        string="ICMS Destino",
        store=True,
        compute='_compute_amount',
        help='Valor total do ICMS Interestadual para a UF de destino')
    valor_icms_uf_remet = fields.Float(
        string="ICMS Remetente",
        store=True,
        compute='_compute_amount',
        help='Valor total do ICMS Interestadual para a UF do Remetente')
    issqn_base = fields.Float(string='Base ISSQN',
                              store=True,
                              digits=dp.get_precision('Account'),
                              compute='_compute_amount')
    issqn_value = fields.Float(string='Valor ISSQN',
                               store=True,
                               digits=dp.get_precision('Account'),
                               compute='_compute_amount')
    issqn_retention = fields.Float(string='ISSQN Retido',
                                   store=True,
                                   digits=dp.get_precision('Account'),
                                   compute='_compute_amount')
    ipi_base = fields.Float(string='Base IPI',
                            store=True,
                            digits=dp.get_precision('Account'),
                            compute='_compute_amount')
    ipi_base_other = fields.Float(string="Base IPI Outras",
                                  store=True,
                                  digits=dp.get_precision('Account'),
                                  compute='_compute_amount')
    ipi_value = fields.Float(string='Valor IPI',
                             store=True,
                             digits=dp.get_precision('Account'),
                             compute='_compute_amount')
    pis_base = fields.Float(string='Base PIS',
                            store=True,
                            digits=dp.get_precision('Account'),
                            compute='_compute_amount')
    pis_value = fields.Float(string='Valor PIS',
                             store=True,
                             digits=dp.get_precision('Account'),
                             compute='_compute_amount')
    pis_retention = fields.Float(string='PIS Retido',
                                 store=True,
                                 digits=dp.get_precision('Account'),
                                 compute='_compute_amount')
    cofins_base = fields.Float(string='Base COFINS',
                               store=True,
                               digits=dp.get_precision('Account'),
                               compute='_compute_amount')
    cofins_value = fields.Float(string='Valor COFINS',
                                store=True,
                                digits=dp.get_precision('Account'),
                                compute='_compute_amount',
                                readonly=True)
    cofins_retention = fields.Float(string='COFINS Retido',
                                    store=True,
                                    digits=dp.get_precision('Account'),
                                    compute='_compute_amount',
                                    readonly=True)
    ii_base = fields.Float(string='Base II',
                           store=True,
                           digits=dp.get_precision('Account'),
                           compute='_compute_amount')
    ii_value = fields.Float(string='Valor II',
                            store=True,
                            digits=dp.get_precision('Account'),
                            compute='_compute_amount')
    csll_base = fields.Float(string='Base CSLL',
                             store=True,
                             digits=dp.get_precision('Account'),
                             compute='_compute_amount')
    csll_value = fields.Float(string='Valor CSLL',
                              store=True,
                              digits=dp.get_precision('Account'),
                              compute='_compute_amount')
    csll_retention = fields.Float(string='CSLL Retido',
                                  store=True,
                                  digits=dp.get_precision('Account'),
                                  compute='_compute_amount')
    irrf_base = fields.Float(string='Base IRRF',
                             store=True,
                             digits=dp.get_precision('Account'),
                             compute='_compute_amount')
    irrf_value = fields.Float(string='Valor IRRF',
                              store=True,
                              digits=dp.get_precision('Account'),
                              compute='_compute_amount')
    irrf_retention = fields.Float(string='IRRF Retido',
                                  store=True,
                                  digits=dp.get_precision('Account'),
                                  compute='_compute_amount')
    inss_base = fields.Float(string='Base INSS',
                             store=True,
                             digits=dp.get_precision('Account'),
                             compute='_compute_amount')
    inss_value = fields.Float(string='Valor INSS',
                              store=True,
                              digits=dp.get_precision('Account'),
                              compute='_compute_amount')
    inss_retention = fields.Float(string='INSS Retido',
                                  store=True,
                                  digits=dp.get_precision('Account'),
                                  compute='_compute_amount')
    total_tributos_federais = fields.Float(string='Total de Tributos Federais',
                                           store=True,
                                           digits=dp.get_precision('Account'),
                                           compute='_compute_amount')
    total_tributos_estaduais = fields.Float(
        string='Total de Tributos Estaduais',
        store=True,
        digits=dp.get_precision('Account'),
        compute='_compute_amount')
    total_tributos_municipais = fields.Float(
        string='Total de Tributos Municipais',
        store=True,
        digits=dp.get_precision('Account'),
        compute='_compute_amount')
    total_tributos_estimados = fields.Float(string='Total de Tributos',
                                            store=True,
                                            digits=dp.get_precision('Account'),
                                            compute='_compute_amount')

    @api.onchange('fiscal_position_id')
    def _onchange_br_account_fiscal_position_id(self):
        if self.fiscal_position_id and self.fiscal_position_id.account_id:
            self.account_id = self.fiscal_position_id.account_id.id
        if self.fiscal_position_id and self.fiscal_position_id.journal_id:
            self.journal_id = self.fiscal_position_id.journal_id

        self.product_serie_id = self.fiscal_position_id.product_serie_id.id
        self.product_document_id = \
            self.fiscal_position_id.product_document_id.id

        self.service_serie_id = self.fiscal_position_id.service_serie_id.id
        self.service_document_id = \
            self.fiscal_position_id.service_document_id.id

        ob_ids = [x.id for x in self.fiscal_position_id.fiscal_observation_ids]
        self.fiscal_observation_ids = [(6, False, ob_ids)]

    @api.multi
    def action_invoice_cancel_paid(self):
        if self.filtered(lambda inv: inv.state not in
                         ['proforma2', 'draft', 'open', 'paid']):
            raise UserError(
                _("Invoice must be in draft, Pro-forma or open \
                              state in order to be cancelled."))
        return self.action_cancel()

    @api.model
    def invoice_line_move_line_get(self):
        res = super(AccountInvoice, self).invoice_line_move_line_get()

        contador = 0

        for line in self.invoice_line_ids:
            if line.quantity == 0:
                continue
            res[contador]['price'] = line.valor_liquido

            price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)

            ctx = line._prepare_tax_context()
            tax_ids = line.invoice_line_tax_ids.with_context(**ctx)

            taxes_dict = tax_ids.compute_all(price,
                                             self.currency_id,
                                             line.quantity,
                                             product=line.product_id,
                                             partner=self.partner_id)
            for tax in line.invoice_line_tax_ids:
                tax_dict = next(x for x in taxes_dict['taxes']
                                if x['id'] == tax.id)
                if tax.price_include and (not tax.account_id
                                          or not tax.deduced_account_id):
                    if tax_dict['amount'] > 0.0:  # Negativo é retido
                        res[contador]['price'] -= tax_dict['amount']

            contador += 1
        return res

    @api.multi
    def finalize_invoice_move_lines(self, move_lines):
        res = super(AccountInvoice, self).\
            finalize_invoice_move_lines(move_lines)
        count = 1
        for invoice_line in res:
            line = invoice_line[2]
            line['ref'] = self.origin
            if line['name'] == '/' or (line['name'] == self.name
                                       and self.name):
                line['name'] = "%02d" % count
                count += 1
        return res

    @api.multi
    def get_taxes_values(self):
        tax_grouped = {}
        for line in self.invoice_line_ids:
            other_taxes = line.invoice_line_tax_ids.filtered(
                lambda x: not x.domain)
            line.invoice_line_tax_ids = other_taxes | line.tax_icms_id | \
                line.tax_ipi_id | line.tax_pis_id | line.tax_cofins_id | \
                line.tax_issqn_id | line.tax_ii_id | line.tax_icms_st_id | \
                line.tax_csll_id | line.tax_irrf_id | \
                line.tax_inss_id

            ctx = line._prepare_tax_context()
            tax_ids = line.invoice_line_tax_ids.with_context(**ctx)

            price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
            taxes = tax_ids.compute_all(price_unit, self.currency_id,
                                        line.quantity, line.product_id,
                                        self.partner_id)['taxes']
            for tax in taxes:
                val = self._prepare_tax_line_vals(line, tax)
                key = self.env['account.tax'].browse(
                    tax['id']).get_grouping_key(val)

                if key not in tax_grouped:
                    tax_grouped[key] = val
                else:
                    tax_grouped[key]['amount'] += round(val['amount'], 2)
                    tax_grouped[key]['base'] += val['base']
        return tax_grouped

    @api.model
    def tax_line_move_line_get(self):
        res = super(AccountInvoice, self).tax_line_move_line_get()

        done_taxes = []
        for tax_line in sorted(self.tax_line_ids, key=lambda x: -x.sequence):
            if tax_line.amount and tax_line.tax_id.deduced_account_id:
                tax = tax_line.tax_id
                done_taxes.append(tax.id)
                res.append({
                    'invoice_tax_line_id':
                    tax_line.id,
                    'tax_line_id':
                    tax_line.tax_id.id,
                    'type':
                    'tax',
                    'name':
                    tax_line.name,
                    'price_unit':
                    tax_line.amount * -1,
                    'quantity':
                    1,
                    'price':
                    tax_line.amount * -1,
                    'account_id':
                    tax_line.tax_id.deduced_account_id.id,
                    'account_analytic_id':
                    tax_line.account_analytic_id.id,
                    'invoice_id':
                    self.id,
                    'tax_ids': [(6, 0, done_taxes)]
                    if tax_line.tax_id.include_base_amount else []
                })
        return res

    @api.model
    def _prepare_refund(self,
                        invoice,
                        date_invoice=None,
                        date=None,
                        description=None,
                        journal_id=None):
        res = super(AccountInvoice,
                    self)._prepare_refund(invoice,
                                          date_invoice=date_invoice,
                                          date=date,
                                          description=description,
                                          journal_id=journal_id)
        docs_related = self._prepare_related_documents(invoice)
        res['fiscal_document_related_ids'] = docs_related
        res['product_document_id'] = invoice.product_document_id.id
        res['product_serie_id'] = invoice.product_serie_id.id
        res['service_document_id'] = invoice.service_document_id.id
        res['service_serie_id'] = invoice.service_serie_id.id
        return res

    def _prepare_related_documents(self, invoice):
        doc_related = self.env['br_account.document.related']
        related_vals = []
        for doc in invoice.invoice_eletronic_ids:
            vals = {
                'invoice_related_id':
                invoice.id,
                'document_type':
                doc_related.translate_document_type(
                    invoice.product_document_id.code),
                'access_key':
                doc.chave_nfe,
                'numero':
                doc.numero
            }
            related = (0, False, vals)
            related_vals.append(related)
        return related_vals
Exemple #12
0
class DemandEstimateWizard(models.TransientModel):
    _name = 'stock.demand.estimate.wizard'
    _description = 'Stock Demand Estimate Wizard'

    date_start = fields.Date(string="Date From", required=True)
    date_end = fields.Date(string="Date To", required=True)
    date_range_type_id = fields.Many2one(string='Date Range Type',
                                         comodel_name='date.range.type',
                                         required=True)
    location_id = fields.Many2one(comodel_name="stock.location",
                                  string="Location",
                                  required=True)
    product_ids = fields.Many2many(comodel_name="product.product",
                                   string="Products")

    @api.onchange('date_range_type_id')
    def _onchange_date_range_type_id(self):
        if self.date_range_type_id.company_id:
            return {
                'domain': {
                    'location_id': [('company_id', '=',
                                     self.date_range_type_id.company_id.id)]
                }
            }
        return {}

    @api.constrains('date_start', 'date_end')
    def _check_start_end_dates(self):
        for rec in self:
            if rec.date_start > rec.date_end:
                raise ValidationError(
                    _('The start date cannot be later than the end date.'))

    @api.multi
    def _prepare_demand_estimate_sheet(self):
        self.ensure_one()
        return {
            'date_start': self.date_start,
            'date_end': self.date_end,
            'date_range_type_id': self.date_range_type_id.id,
            'location_id': self.location_id.id,
        }

    @api.multi
    def create_sheet(self):
        self.ensure_one()
        if not self.product_ids:
            raise UserError(_('You must select at least one product.'))

        context = {
            'default_date_start': self.date_start,
            'default_date_end': self.date_end,
            'default_date_range_type_id': self.date_range_type_id.id,
            'default_location_id': self.location_id.id,
            'product_ids': self.product_ids.ids
        }
        res = {
            'context': context,
            'name': _('Estimate Sheet'),
            'src_model': 'stock.demand.estimate.wizard',
            'view_type': 'form',
            'view_mode': 'form',
            'target': 'new',
            'res_model': 'stock.demand.estimate.sheet',
            'type': 'ir.actions.act_window'
        }

        return res
Exemple #13
0
class StockDemandEstimateSheet(models.TransientModel):
    _name = 'stock.demand.estimate.sheet'
    _description = 'Stock Demand Estimate Sheet'

    def _default_estimate_ids(self):
        date_start = self.env.context.get('default_date_start', False)
        date_end = self.env.context.get('default_date_end', False)
        date_range_type_id = self.env.context.get('default_date_range_type_id',
                                                  False)
        location_id = self.env.context.get('default_location_id', False)
        product_ids = self.env.context.get('product_ids', False)
        domain = [('type_id', '=', date_range_type_id), '|', '&',
                  ('date_start', '>=', date_start),
                  ('date_start', '<=', date_end), '&',
                  ('date_end', '>=', date_start), ('date_end', '<=', date_end)]
        periods = self.env['date.range'].search(domain)
        domain = [('type_id', '=', date_range_type_id),
                  ('date_start', '<=', date_start),
                  ('date_end', '>=', date_start)]
        periods |= self.env['date.range'].search(domain)
        products = self.env['product.product'].browse(product_ids)

        lines = []
        for product in products:
            name_y = ''
            if product.default_code:
                name_y += '[%s] ' % product.default_code
            name_y += product.name
            name_y += ' - %s' % product.uom_id.name
            for period in periods:
                estimates = self.env['stock.demand.estimate'].search([
                    ('product_id', '=', product.id),
                    ('date_range_id', '=', period.id),
                    ('location_id', '=', location_id)
                ])
                if estimates:
                    lines.append((0, 0, {
                        'value_x':
                        period.name,
                        'value_y':
                        name_y,
                        'date_range_id':
                        period.id,
                        'product_id':
                        product.id,
                        'product_uom':
                        estimates[0].product_uom.id,
                        'location_id':
                        location_id,
                        'estimate_id':
                        estimates[0].id,
                        'product_uom_qty':
                        estimates[0].product_uom_qty
                    }))
                else:
                    lines.append((0, 0, {
                        'value_x': period.name,
                        'value_y': name_y,
                        'date_range_id': period.id,
                        'product_id': product.id,
                        'product_uom': product.uom_id.id,
                        'location_id': location_id,
                        'product_uom_qty': 0.0
                    }))
        return lines

    date_start = fields.Date(string="Date From", readonly=True)
    date_end = fields.Date(string="Date From", readonly=True)
    date_range_type_id = fields.Many2one(string='Date Range Type',
                                         comodel_name='date.range.type',
                                         readonly=True)
    location_id = fields.Many2one(comodel_name="stock.location",
                                  string="Location",
                                  readonly=True)
    line_ids = fields.Many2many(
        string="Estimates",
        comodel_name='stock.demand.estimate.sheet.line',
        relation='stock_demand_estimate_line_rel',
        default=_default_estimate_ids)

    @api.model
    def _prepare_estimate_data(self, line):
        return {
            'date_range_id': line.date_range_id.id,
            'product_id': line.product_id.id,
            'location_id': line.location_id.id,
            'product_uom_qty': line.product_uom_qty,
            'product_uom': line.product_id.uom_id.id,
        }

    @api.multi
    def button_validate(self):
        res = []
        for line in self.line_ids:
            if line.estimate_id:
                line.estimate_id.product_uom_qty = line.product_uom_qty
                res.append(line.estimate_id.id)
            else:
                data = self._prepare_estimate_data(line)
                estimate = self.env['stock.demand.estimate'].create(data)
                res.append(estimate.id)
        res = {
            'domain': [('id', 'in', res)],
            'name': _('Stock Demand Estimates'),
            'src_model': 'stock.demand.estimate.wizard',
            'view_type': 'form',
            'view_mode': 'tree',
            'res_model': 'stock.demand.estimate',
            'type': 'ir.actions.act_window'
        }
        return res
Exemple #14
0
class BankReport(models.TransientModel):
    _name = 'bank.report'
    _description = 'Reporte para pago banco'

    stock_quant_id = fields.Char("LFC")
    data = fields.Binary("Archivo")
    data_name = fields.Char("nombre del archivo")
    secuencia = fields.Char("Aplicacion", default='A1')
    aplicacion = fields.Selection(string="tipo de pago",
                                  selection=[('I', 'Inmediata'),
                                             ('M', 'Medio dia'),
                                             ('N', 'Noche')])
    descripcion = fields.Char("Descripcion")
    journal = fields.Many2one('account.journal', string='Diario')
    tipo_pago = fields.Selection(string="tipo de pago",
                                 selection=[('104', 'Pago a Proveedores'),
                                            ('98', 'Pago de Nomina')])
    fecha_aplicacion = fields.Date('Fecha de Aplicacion')
    asientos = fields.Many2many('account.move',
                                string='Asientos',
                                required=True)
    exist_asientos = fields.Boolean(string='Asientos existentes',
                                    compute='get_data_asientos')

    @api.onchange('asientos')
    def get_data_asientos(self):
        if self.asientos:
            self.exist_asientos = True
        else:
            self.exist_asientos = False

    @api.onchange('journal', 'tipo_pago')
    def onchange_journal(self):
        for rec in self:
            return {
                'domain': {
                    'asientos':
                    [('name', 'like', 'CE'),
                     ('journal_id', '=', rec.journal.id), '|',
                     ('partner_id.category_id.id', '=', int(rec.tipo_pago)),
                     ('partner_id.category_id.parent_id', '=',
                      int(rec.tipo_pago))]
                }
            }

    def do_report(self):

        _logger.error("INICIA LA FUNCIÓN GENERAR EL REPORTE ")
        self.make_file()
        return {
            'type':
            'ir.actions.act_url',
            'url':
            '/web/binary/download_document?model=bank.report&field=data&id=%s&filename=%s'
            % (self.id, self.data_name),
            'target':
            'new',
            'nodestroy':
            False,
        }

    def make_file(self):
        _logger.error("INICIA LA FUNCIÓN CONSTRUIR EL ARCHIVO ")

        account = self.asientos

        if not account:
            raise Warning(
                _('!No hay resultados para los datos seleccionados¡'))

        buf = BytesIO()
        wb = xlsxwriter.Workbook(buf)
        ws = wb.add_worksheet('Report')

        # formatos
        title_head = wb.add_format({
            'bold': 1,
            'border': 1,
            'align': 'rigth',
            'fg_color': '#33CCCC',
            'valign': 'vcenter',
        })
        title_head.set_font_name('Arial')
        title_head.set_font_size(10)
        title_head.set_font_color('#ffffff')

        company = self.env['res.company'].search([])

        ws.write(0, 0, 'NIT PAGADOR', title_head)
        ws.write(0, 1, 'TIPO DE PAGO', title_head)
        ws.write(0, 2, 'APLICACIÓN', title_head)
        ws.write(0, 3, 'SECUENCIA DE ENVIO', title_head)
        ws.write(0, 4, 'NRO CUENTA A DEBITAR', title_head)
        ws.write(0, 5, 'TIPO DE CUENTA A DEBITAR', title_head)
        ws.write(0, 6, 'DESCRIPCIÓN DEL PAGO', title_head)

        ws.write(1, 0, '' if not company[0].vat else company[0].vat)
        ws.write(1, 1, self.tipo_pago)
        if self.tipo_pago:
            if self.tipo_pago == '104':
                ws.write(1, 1, '220')
            elif self.tipo_pago == '98':
                ws.write(1, 1, '225')
            else:
                ws.write(1, 1, '')
        else:
            ws.write(1, 1, '')
        ws.write(1, 2, self.aplicacion)
        ws.write(1, 3, self.secuencia)
        ws.write(1, 4, self.journal.bank_account_id.acc_number)
        if self.journal.bank_account_id.account_type:
            if self.journal.bank_account_id.account_type == '1':
                ws.write(1, 5, 'S')
            elif self.journal.bank_account_id.account_type == '2':
                ws.write(1, 5, 'D')
            else:
                ws.write(1, 5, '')
        else:
            ws.write(1, 5, '')
        ws.write(1, 6, self.descripcion)

        ws.write(2, 0, 'Tipo Documento Beneficiario', title_head)
        ws.write(2, 1, 'Nit Beneficiario', title_head)
        ws.write(2, 2, 'Nombre Beneficiario ', title_head)
        ws.write(2, 3, 'Tipo Transaccion', title_head)
        ws.write(2, 4, 'Código Banco', title_head)
        ws.write(2, 5, 'No Cuenta Beneficiario', title_head)
        ws.write(2, 6, 'Email', title_head)
        ws.write(2, 7, 'Documento Autorizado', title_head)
        ws.write(2, 8, 'Referencia', title_head)
        ws.write(2, 9, 'OficinaEntrega', title_head)
        ws.write(2, 10, 'ValorTransaccion', title_head)
        ws.write(2, 11, 'Fecha de aplicación', title_head)

        fila = 3
        for ac in account:
            vat = ac.partner_id.vat
            if ac.partner_id.l10n_co_document_type:
                if ac.partner_id.l10n_co_document_type == 'id_document':
                    ws.write(fila, 0, '1')
                    pos = (ac.partner_id.vat).find("-")
                    if pos != -1:
                        vat = ac.partner_id.vat[0:pos]
                    else:
                        vat = ac.partner_id.vat
                elif ac.partner_id.l10n_co_document_type == 'foreign_id_card':
                    ws.write(fila, 0, '2')
                elif ac.partner_id.l10n_co_document_type == 'rut':
                    ws.write(fila, 0, '3')
                    pos = (ac.partner_id.vat).find("-")
                    if pos != -1:
                        vat = ac.partner_id.vat[0:pos]
                    else:
                        vat = ac.partner_id.vat
                elif ac.partner_id.l10n_co_document_type == 'id_card':
                    ws.write(fila, 0, '4')
                elif ac.partner_id.l10n_co_document_type == 'passport':
                    ws.write(fila, 0, '5')
                else:
                    ws.write(fila, 0, '')
            else:
                ws.write(fila, 0, '')

            ws.write(fila, 1, '' if not vat else vat.replace(".", ""))
            ws.write(fila, 2, ac.partner_id.name)
            if ac.partner_id.bank_ids:
                if ac.partner_id.bank_ids[0].account_type == '1':
                    ws.write(fila, 3, '27')
                elif ac.partner_id.bank_ids[0].account_type == '2':
                    ws.write(fila, 3, '37')
                else:
                    ws.write(fila, 3, '')
            else:
                ws.write(fila, 3, '')
            ws.write(fila, 4, '') if not ac.partner_id.bank_ids else ws.write(
                fila, 4, ac.partner_id.bank_ids[0].bank_id.code_bank)
            ws.write(fila, 5, '') if not ac.partner_id.bank_ids else ws.write(
                fila, 5, ac.partner_id.bank_ids[0].acc_number)
            ws.write(fila, 6, '')
            ws.write(fila, 7, '')
            ws.write(fila, 8, '')
            ws.write(fila, 9, '')
            ws.write(fila, 10, "{:.2f}".format(ac.amount_total))
            ws.write(fila, 11,
                     str(self.fecha_aplicacion.isoformat()).replace("-", ""))
            fila += 1

        try:
            wb.close()
            out = base64.encodestring(buf.getvalue())
            buf.close()
            self.data = out
            self.data_name = 'Reporte pago bancos' + ".xls"
        except ValueError:
            raise Warning('No se pudo generar el archivo')
Exemple #15
0
class TrialBalanceReportAccount(models.TransientModel):
    _name = 'report_trial_balance_account'
    _inherit = 'account_financial_report_abstract'
    _order = 'sequence, code ASC, name'

    report_id = fields.Many2one(comodel_name='report_trial_balance',
                                ondelete='cascade',
                                index=True)
    hide_line = fields.Boolean(compute='_compute_hide_line')
    # Data fields, used to keep link with real object
    sequence = fields.Integer(index=True, default=1)
    level = fields.Integer(index=True, default=1)

    # Data fields, used to keep link with real object
    account_id = fields.Many2one('account.account', index=True)

    account_group_id = fields.Many2one('account.group', index=True)
    parent_id = fields.Many2one('account.group', index=True)
    child_account_ids = fields.Char(string="Accounts")
    compute_account_ids = fields.Many2many('account.account',
                                           string="Accounts",
                                           store=True)

    # Data fields, used for report display
    code = fields.Char()
    name = fields.Char()

    currency_id = fields.Many2one('res.currency')
    initial_balance = fields.Float(digits=(16, 2))
    initial_balance_foreign_currency = fields.Float(digits=(16, 2))
    debit = fields.Float(digits=(16, 2))
    credit = fields.Float(digits=(16, 2))
    period_balance = fields.Float(digits=(16, 2))
    final_balance = fields.Float(digits=(16, 2))
    final_balance_foreign_currency = fields.Float(digits=(16, 2))

    # Data fields, used to browse report data
    partner_ids = fields.One2many(comodel_name='report_trial_balance_partner',
                                  inverse_name='report_account_id')

    @api.depends(
        'currency_id',
        'report_id',
        'report_id.hide_account_at_0',
        'report_id.limit_hierarchy_level',
        'report_id.show_hierarchy_level',
        'initial_balance',
        'final_balance',
        'debit',
        'credit',
    )
    def _compute_hide_line(self):
        for rec in self:
            report = rec.report_id
            r = (rec.currency_id or report.company_id.currency_id).rounding
            if report.hide_account_at_0 and (
                    float_is_zero(rec.initial_balance, precision_rounding=r)
                    and float_is_zero(rec.final_balance, precision_rounding=r)
                    and float_is_zero(rec.debit, precision_rounding=r)
                    and float_is_zero(rec.credit, precision_rounding=r)):
                rec.hide_line = True
            elif report.limit_hierarchy_level and report.show_hierarchy_level:
                if report.hide_parent_hierarchy_level:
                    distinct_level = rec.level != report.show_hierarchy_level
                    if rec.account_group_id and distinct_level:
                        rec.hide_line = True
                    elif rec.level and distinct_level:
                        rec.hide_line = True
                elif not report.hide_parent_hierarchy_level and \
                        rec.level > report.show_hierarchy_level:
                    rec.hide_line = True
Exemple #16
0
class TmsExpenseLine(models.Model):
    _name = 'tms.expense.line'
    _description = 'Expense Line'

    loan_id = fields.Many2one('tms.expense.loan', string='Loan')
    travel_id = fields.Many2one('tms.travel', string='Travel')
    expense_id = fields.Many2one(
        'tms.expense',
        string='Expense',
    )
    product_qty = fields.Float(string='Qty', default=1.0)
    unit_price = fields.Float()
    price_subtotal = fields.Float(
        compute='_compute_price_subtotal',
        string='Subtotal',
    )
    product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure')
    line_type = fields.Selection([('real_expense', 'Real Expense'),
                                  ('made_up_expense', 'Made-up Expense'),
                                  ('salary', 'Salary'), ('fuel', 'Fuel'),
                                  ('fuel_cash', 'Fuel in Cash'),
                                  ('refund', 'Refund'),
                                  ('salary_retention', 'Salary Retention'),
                                  ('salary_discount', 'Salary Discount'),
                                  ('other_income', 'Other Income'),
                                  ('tollstations', 'Toll Stations'),
                                  ('loan', 'Loan')],
                                 compute='_compute_line_type',
                                 store=True,
                                 readonly=True)
    name = fields.Char('Description', required=True)
    sequence = fields.Integer(
        help="Gives the sequence order when displaying a list of "
        "sales order lines.",
        default=10)
    price_total = fields.Float(
        string='Total',
        compute='_compute_price_total',
    )
    tax_amount = fields.Float(compute='_compute_tax_amount', )
    special_tax_amount = fields.Float(string='Special Tax')
    tax_ids = fields.Many2many('account.tax',
                               string='Taxes',
                               domain=[('type_tax_use', '=', 'purchase')])
    notes = fields.Text()
    employee_id = fields.Many2one('hr.employee', string='Driver')
    date = fields.Date()
    state = fields.Char(readonly=True)
    control = fields.Boolean()
    automatic = fields.Boolean(
        help="Check this if you want to create Advances and/or "
        "Fuel Vouchers for this line automatically")
    is_invoice = fields.Boolean(string='Is Invoice?')
    partner_id = fields.Many2one(
        'res.partner',
        string='Supplier',
    )
    invoice_date = fields.Date()
    invoice_number = fields.Char()
    invoice_id = fields.Many2one('account.invoice', string='Supplier Invoice')
    product_id = fields.Many2one(
        'product.product',
        string='Product',
        required=True,
    )
    route_id = fields.Many2one('tms.route',
                               related='travel_id.route_id',
                               string='Route',
                               readonly=True)
    expense_fuel_log = fields.Boolean(readonly=True)

    @api.onchange('product_id')
    def _onchange_product_id(self):
        if self.line_type not in [
                'salary', 'salary_retention', 'salary_discount'
        ]:
            self.tax_ids = self.product_id.supplier_taxes_id
        self.line_type = self.product_id.tms_product_category
        self.product_uom_id = self.product_id.uom_id.id
        self.name = self.product_id.name

    @api.depends('product_id')
    def _compute_line_type(self):
        for rec in self:
            rec.line_type = rec.product_id.tms_product_category

    @api.depends('tax_ids', 'product_qty', 'unit_price')
    def _compute_tax_amount(self):
        for rec in self:
            taxes = rec.tax_ids.compute_all(
                rec.unit_price, rec.expense_id.currency_id, rec.product_qty,
                rec.expense_id.employee_id.address_home_id)
            if taxes['taxes']:
                for tax in taxes['taxes']:
                    rec.tax_amount += tax['amount']
            else:
                rec.tax_amount = 0.0

    @api.depends('product_qty', 'unit_price', 'line_type')
    def _compute_price_subtotal(self):
        for rec in self:
            if rec.line_type in [
                    'salary_retention', 'salary_discount', 'loan'
            ]:
                rec.price_subtotal = rec.product_qty * rec.unit_price * -1
            elif rec.line_type == 'fuel':
                rec.price_subtotal = rec.unit_price
            else:
                rec.price_subtotal = rec.product_qty * rec.unit_price

    @api.depends('price_subtotal', 'tax_ids')
    def _compute_price_total(self):
        for rec in self:
            if rec.line_type == 'fuel':
                rec.price_total = rec.unit_price
            elif rec.line_type in [
                    'salary_retention', 'salary_discount', 'loan'
            ]:
                rec.price_total = rec.price_subtotal - rec.tax_amount
            else:
                rec.price_total = rec.price_subtotal + rec.tax_amount

    @api.model
    def create(self, values):
        expense_line = super(TmsExpenseLine, self).create(values)
        if expense_line.line_type in ('salary_discount', 'salary_retention',
                                      'loan'):
            if expense_line.price_total > 0:
                raise ValidationError(
                    _('This line type needs a '
                      'negative value to continue!'))
        return expense_line
class DinDinMessageTemplate(models.Model):
    _name = 'dindin.message.template'
    _description = "消息模板"
    _rec_name = 'name'

    name = fields.Char(string='模板名', required=True)
    model_id = fields.Many2one(comodel_name='ir.model',
                               string=u'Odoo模型',
                               required=True)
    company_id = fields.Many2one(
        comodel_name='res.company',
        string=u'公司',
        default=lambda self: self.env.user.company_id.id)
    create_send = fields.Boolean(string=u'创建时自动发送消息')
    delete_send = fields.Boolean(string=u'删除时自动发送消息')
    line_ids = fields.One2many(comodel_name='dindin.message.template.line',
                               inverse_name='template_id',
                               string=u'消息字段')
    msg_type = fields.Selection(string=u'接受者',
                                selection=[('00', '员工'), ('01', '部门'),
                                           ('03', '所有人')],
                                required=True,
                                default='00')
    emp_ids = fields.Many2many(
        comodel_name='hr.employee',
        relation='dingding_message_temp_and_employee_rel',
        column1='template_id',
        column2='employee_id',
        string=u'员工',
        domain=[('din_id', '!=', '')])
    dept_ids = fields.Many2many(
        comodel_name='hr.department',
        relation='dingding_message_temp_and_department_rel',
        column1='template_id',
        column2='department_id',
        string=u'部门',
        domain=[('din_id', '!=', '')])

    _sql_constraints = [
        ('model_id_uniq', 'unique (model_id)', 'Odoo模型已存在消息模板,请不要重复创建!'),
    ]

    @api.onchange('model_id')
    def _onchange_model(self):
        for res in self:
            if res.model_id:
                res.name = "{}-消息模板".format(res.model_id.name)

    def check_message_template(self, model, model_type):
        model_id = self.env['ir.model'].sudo().search([('model', '=', model)
                                                       ]).id
        template = self.env['dindin.message.template'].sudo().search([
            ('model_id', '=', model_id)
        ])
        if template:
            if model_type == 'create':
                return True if template.create_send else False
            elif model_type == 'delete':
                return True if template.delete_send else False
        else:
            return False

    def send_message_template(self, model, res_id, model_type):
        """发送消息"""
        model_id = self.env['ir.model'].sudo().search([('model', '=', model)
                                                       ]).id
        template = self.env['dindin.message.template'].sudo().search([
            ('model_id', '=', model_id)
        ])
        document = self.env[model].sudo().browse(res_id).copy_data()  # 当前单据
        message_dict = self.create_message_dict(model_type, template,
                                                document[0])
        logging.info(">>>msg:{}".format(message_dict))
        # 调用消息函数发送
        try:
            if template.msg_type == '03':
                self.env['dindin.work.message'].sudo().send_work_message(
                    toall=True, msg=message_dict)
            elif template.msg_type == '00':
                user_str = ''
                for user in template.emp_ids:
                    if user_str == '':
                        user_str = user_str + "{}".format(user.din_id)
                    else:
                        user_str = user_str + ",{}".format(user.din_id)
                self.env['dindin.work.message'].sudo().send_work_message(
                    userstr=user_str, msg=message_dict)
            elif template.msg_type == '01':
                dept_str = ''
                for dept in template.dept_ids:
                    if dept_str == '':
                        dept_str = dept_str + "{}".format(dept.din_id)
                    else:
                        dept_str = dept_str + ",{}".format(dept.din_id)
                self.env['dindin.work.message'].sudo().send_work_message(
                    deptstr=dept_str, msg=message_dict)
        except Exception as e:
            logging.info("发送消息失败!错误消息为:{}".format(e))

    def create_message_dict(self, model_type, template, res_dict):
        """
        封装为待发送消息的格式
        :param model_type:
        :param template:
        :param res_dict:
        :return: dict()
        """
        msg_text = ''
        if model_type == 'create':
            msg_text = "{}创建了'{}',内容:\n".format(self.env.user.name,
                                                template.model_id.name)
        elif model_type == 'delete':
            msg_text = "{}删除了'{}',内容:\n".format(self.env.user.name,
                                                template.model_id.name)
        for tem_line in template.line_ids:
            # 拼接消息字段
            if tem_line.field_id.ttype == 'many2one':
                doc_model = self.env[tem_line.field_id.relation].sudo().search(
                    [('id', '=', res_dict.get(tem_line.field_id.name))])
                if doc_model:
                    try:
                        msg_text = msg_text + "{}: {}\n".format(
                            tem_line.field_name, doc_model[0].name)
                    except Exception as e:
                        msg_text = msg_text + "{}: {}\n".format(
                            tem_line.field_name, "字段值获取失败!")
            else:
                if res_dict.get(tem_line.field_id.name):
                    msg_text = msg_text + "{}: {}\n".format(
                        tem_line.field_name,
                        res_dict.get(tem_line.field_id.name))
                else:
                    msg_text = msg_text + "{}: {}\n".format(
                        tem_line.field_name, "字段值获取失败!")
        return {
            'msgtype': 'text',
            "text": {
                "content": "{}请注意查看!".format(msg_text),
            }
        }
Exemple #18
0
class AccountCashInOut(models.Model):
    #_inherit = "account.check.deposit"
    _name = "account.cash.inout"

    @api.multi
    @api.depends('check_line_ids')
    def _compute_nrofchecks(self):
        self.ensure_one()
        self.number_of_checks = len(self.check_line_ids)

    @api.multi
    @api.depends('check_line_ids', 'total_amount')
    @api.onchange('check_line_ids', 'total_amount', 'journal_id')
    def onchange_compute_amount(self):
        self.ensure_one()
        types = self.journal_id.inbound_payment_method_ids.mapped(
            'code') + self.journal_id.outbound_payment_method_ids.mapped(
                'code')
        if 'issue_check' in types:
            if self.check_line_ids:
                self.total_amount = sum(self.check_line_ids.mapped('amount'))
            else:
                self.total_amount = 0
            for check in self.check_line_ids:
                if check.amount > 0:
                    check.write({'amount_cash': check.amount})

    @api.multi
    @api.depends('journal_id')
    @api.onchange('journal_id')
    def _compute_journal_has_checks(self):
        self.ensure_one()
        if self.journal_id:
            types = self.journal_id.inbound_payment_method_ids.mapped(
                'code') + self.journal_id.outbound_payment_method_ids.mapped(
                    'code')
            if 'issue_check' in types:
                self.journal_has_checks = True
            else:
                self.journal_has_checks = False
                self.check_line_ids = False

    name = fields.Char(string='Name', size=64, readonly=True, default='/')
    deposit_date = fields.Date(string='Deposit Date',
                               required=True,
                               states={'validated': [('readonly', '=', True)]},
                               default=fields.Date.context_today,
                               translate=True)
    type = fields.Selection([('cash_in', 'Entrada de Caja'),
                             ('cash_out', 'Salida de Caja')],
                            translate=True)
    journal_id = fields.Many2one(
        'account.journal',
        string='Journal',
        domain=[('type', 'in', ['bank', 'cash'])],
        required=True,
        states={'validated': [('readonly', '=', True)]},
        translate=True)
    state = fields.Selection([('draft', 'Draft'), ('validated', 'Validated'),
                              ('cancelled', 'Cancelled')],
                             string='Status',
                             default='draft',
                             readonly=True,
                             translate=True)
    receiptbook_id = fields.Many2one(
        'account.payment.receiptbook',
        'ReceiptBook',
        states={'validated': [('readonly', True)]},
        translate=True)
    paym_account_analytic_id = fields.Many2many(
        'account.analytic.tag',
        string=' Payment Analytic Tag',
        translate=True,
        states={'validated': [('readonly', True)]})
    currency_id = fields.Many2one(
        'res.currency',
        string='Currency',
        required=True,
        states={'validated': [('readonly', '=', True)]},
        translate=True)
    total_amount = fields.Float(string="Total Amount",
                                digits=dp.get_precision('Account'),
                                translate=True,
                                states={'validated': [('readonly', True)]})
    move_id = fields.Many2one('account.move',
                              string='Journal Entry',
                              readonly=True)
    line_ids = fields.One2many('account.move.line',
                               related='move_id.line_ids',
                               string='Lines',
                               readonly=True,
                               translate=True)
    check_line_ids = fields.One2many(
        'account.check',
        'cashin_out_id',
        string='Checks',
        translate=True,
        states={'validated': [('readonly', True)]},
        ondelete="set null")
    number_of_checks = fields.Integer(compute='_compute_nrofchecks',
                                      string="Number of checks",
                                      translate=True)
    benefitiary_type = fields.Selection(
        [('supplier', 'Supplier'), ('employee', 'Employee')],
        string='Benefitiary Type',
        translate=True,
        states={'validated': [('readonly', True)]})
    cash_account_id = fields.Many2one(
        'account.account',
        string='Cash account',
        required=True,
        states={'validated': [('readonly', True)]})
    cash_account_analytic_id = fields.Many2many(
        'account.analytic.tag',
        string='Cash Analytic Tag',
        translate=True,
        states={'validated': [('readonly', True)]})
    benefitiary_id = fields.Many2one(
        'res.partner',
        string="Benefitiary Supplier",
        states={'validated': [('readonly', True)]},
        translate=True)
    employee_id = fields.Many2one('hr.employee',
                                  string="Benefitiary Employee",
                                  states={'validated': [('readonly', True)]},
                                  translate=True)
    journal_has_checks = fields.Boolean(compute="_compute_journal_has_checks")
    company_id = fields.Many2one('res.company', 'Company')
    sub_journal = fields.Many2one('setting.subtype.journal',
                                  translate=True,
                                  string="Sub Type")
    cash_reference = fields.Char(string="Referencia", translate=True)

    @api.multi
    @api.depends('benefitiary_id', 'employee_id')
    @api.onchange('benefitiary_type')
    def onchange_benefitiary_type(self):
        self.ensure_one()
        self.benefitiary_id = False
        self.employee_id = False

    @api.model
    def create(self, vals):
        if vals.get('journal_id'):
            journal = self.env['account.journal'].search([
                ('id', '=', vals.get('journal_id'))
            ])
            if len(journal.outbound_payment_method_ids) > 1:
                raise UserError(
                    _("Journal has more than 1 option in payment method"))
            if len(journal.inbound_payment_method_ids) > 1:
                raise UserError(
                    _("Journal has more than 1 option in debit method"))
            types = journal.inbound_payment_method_ids.mapped(
                'code') + journal.outbound_payment_method_ids.mapped('code')
            if vals.get('type') == 'cash_in' and 'issue_check' in types:
                raise UserError(_("Issue Check Journal not allowed here"))

        if vals.get('name', '/') == '/':
            type = self._context.get('default_type')
            if type == 'cash_out':
                vals['name'] = self.env['ir.sequence'].next_by_code(
                    'account.cash.out')
            else:
                vals['name'] = self.env['ir.sequence'].next_by_code(
                    'account.cash.in')
        res = super(AccountCashInOut, self).create(vals)
        return res

    @api.model
    def default_get(self, fields):
        rec = super(AccountCashInOut, self).default_get(fields)
        rec['name'] = "/"
        type = self._context.get('default_type')
        company = self.env['res.company']._company_default_get(
            'vitt_cashin_cashout')
        if type == 'cash_out':
            domain = [('document_type_id.internal_type', '=', 'cash_out')]
        else:
            domain = [('document_type_id.internal_type', '=', 'cash_in')]
        rec.update({
            'receiptbook_id':
            self.env['account.payment.receiptbook'].search(domain, limit=1).id
        })
        rec.update({
            'currency_id': company.currency_id.id,
            'company_id': company.id,
        })
        return rec

    @api.multi
    def validate_cash(self):
        if self.journal_id.default_debit_account_id.deprecated or self.journal_id.default_debit_account_id.deprecated:
            raise UserError(
                _("Journal account checked as deprecated, not allowed"))
        if self.cash_account_id.deprecated:
            raise UserError(
                _("Cash account checked as deprecated, not allowed"))
        if self.journal_id:
            types = self.journal_id.inbound_payment_method_ids.mapped(
                'code') + self.journal_id.outbound_payment_method_ids.mapped(
                    'code')
            if 'delivered_third_check' in types or 'received_third_check' in types:
                raise UserError(_("journal Third Checks type not allowed"))
            if 'issue_check' in types:
                if self.type == 'cash_in':
                    raise UserError(
                        _("issue check journal type not allowed in cash-in"))
                if not self.benefitiary_type:
                    raise UserError(
                        _("Please, Select a benefitiray type first"))
                else:
                    if self.benefitiary_type == 'supplier' and not self.benefitiary_id:
                        raise UserError(_("Please, Select a Contact first"))
                    if self.benefitiary_type == 'employee' and not self.employee_id:
                        raise UserError(_("Please, Select an Employee first"))
            for check in self.check_line_ids:
                if self.benefitiary_type == 'supplier':
                    if check.partner_id != self.benefitiary_id:
                        raise UserError(
                            _("partner in check %s not the same as cash legder"
                              ) % (check.number))
                if self.benefitiary_type == 'employee':
                    if check.partner_id.employee_id != self.employee_id:
                        raise UserError(
                            _("employee in check %s not the same as cash legder"
                              ) % (check.number))
        self.create_cash_nl()

    @api.multi
    def todraft_cash(self):
        for rec in self:
            for check in rec.check_line_ids:
                if check.state != 'draft':
                    raise UserError(
                        _("check %s is not in draft state, not allowed") %
                        (check.number))
            rec.state = 'draft'

    @api.multi
    def revert_validate(self):
        for rec in self.check_line_ids:
            if rec.state != 'handed':
                raise UserError(_('There are compromised checks, not allowed'))
        self.create_cash_nl(reverse=True)

    @api.multi
    def create_cash_nl(self, reverse=False):
        for cash in self:
            if not reverse:
                credit_account = cash.journal_id.default_debit_account_id
                debit_account = cash.cash_account_id
                cur_factor = cash.currency_id._get_conversion_rate(
                    cash.company_id.currency_id, cash.currency_id)
                if cash.type == 'cash_in':
                    name = _('Entrada de Caja "%s"') % (cash.name)
                    credit_line_vals = {
                        'name':
                        name,
                        'account_id':
                        debit_account.id,
                        'credit':
                        cash.total_amount / cur_factor,
                        'currency_id':
                        cash.currency_id.id,
                        'analytic_tag_ids':
                        [(6, False, cash.paym_account_analytic_id._ids)]
                        # 'ref': ref,
                    }
                    debit_line_vals = {
                        'name':
                        name,
                        'account_id':
                        credit_account.id,
                        'debit':
                        cash.total_amount / cur_factor,
                        'currency_id':
                        cash.currency_id.id,
                        # 'ref': ref,
                        'analytic_tag_ids':
                        [(6, False, cash.paym_account_analytic_id._ids)]
                    }
                    if cash.currency_id.id != self.company_id.currency_id.id:
                        credit_line_vals.update(
                            {'amount_currency': cash.total_amount})
                        debit_line_vals.update(
                            {'amount_currency': -cash.total_amount})
                else:
                    name = _('Salida de Caja "%s"') % (cash.name)
                    debit_line_vals = {
                        'name':
                        name,
                        'account_id':
                        debit_account.id,
                        'debit':
                        cash.total_amount / cur_factor,
                        'currency_id':
                        cash.currency_id.id,
                        # 'ref': ref,
                        'analytic_tag_ids':
                        [(6, False, cash.cash_account_analytic_id._ids)]
                    }
                    credit_line_vals = {
                        'name':
                        name,
                        'account_id':
                        credit_account.id,
                        'credit':
                        cash.total_amount / cur_factor,
                        'currency_id':
                        cash.currency_id.id,
                        # 'ref': ref,
                        'analytic_tag_ids':
                        [(6, False, cash.cash_account_analytic_id._ids)]
                    }
                    if cash.currency_id.id != self.company_id.currency_id.id:
                        debit_line_vals.update(
                            {'amount_currency': cash.total_amount})
                        credit_line_vals.update(
                            {'amount_currency': -cash.total_amount})
                ref = name
                if self.cash_reference:
                    ref = self.cash_reference + '-' + name
                vals = {
                    'ref':
                    ref,
                    'journal_id':
                    cash.journal_id.id,
                    'date':
                    cash.deposit_date,
                    'line_ids': [(0, False, debit_line_vals),
                                 (0, False, credit_line_vals)],
                }
                if cash.benefitiary_id:
                    vals.update({'partner_id': cash.benefitiary_id.id})
                if cash.employee_id:
                    vals.update({
                        'partner_id':
                        self.env['res.partner'].search(
                            [('employee_id', '=', cash.employee_id.id)],
                            limit=1).id
                    })
                move = self.env['account.move'].create(vals)
                move.post()
                cash.write({'move_id': move.id, 'state': 'validated'})
            if reverse:
                move = cash.move_id
                cash.write({'move_id': False, 'state': 'cancelled'})
                move.write({'state': 'draft'})
                move.unlink()
            for check in cash.check_line_ids:
                if reverse:
                    check.write({'state': 'draft'})
                    check._add_operation('draft', cash, None, None, None)
                else:
                    check.write({'state': 'handed'})
                    partner = None
                    # if self.benefitiary_type == 'supplier':
                    #     partner = cash.benefitiary_id
                    # if self.benefitiary_type == 'employee':
                    #     partner = cash.employee_id.partner_id
                    check._add_operation('handed', cash, partner, None,
                                         move.id)

    def copy(self, default=None):
        default = default or {}
        default['name'] = '/'
        res = super(AccountCashInOut, self).copy(default=default)
        return res

    @api.multi
    def unlink(self):
        for rec in self:
            if rec.state not in ('draft', 'cancelled'):
                raise UserError(
                    _("cash record %s is not in draft/canceled state, not allowed"
                      ) % (rec.name))
        return super(AccountCashInOut, self).unlink()
Exemple #19
0
class Rappel(models.Model):

    _inherit = 'rappel'

    brand_ids = fields.Many2many('product.brand', 'rappel_product_brand_rel',
                                 'rappel_id', 'product_brand_id', 'Brand')
    discount_voucher = fields.Boolean()
    pricelist_ids = fields.Many2many('product.pricelist',
                                     'rappel_product_pricelist_rel',
                                     'rappel_id', 'product_pricelist_id',
                                     'Pricelist')
    description = fields.Char(translate=True)
    sequence = fields.Integer(default=100)
    partner_add_conditions = fields.Char('Add partner conditions')

    def get_products(self):
        product_obj = self.env['product.product']
        product_ids = self.env['product.product']
        for rappel in self:
            if not rappel.global_application:
                if rappel.product_id:
                    product_ids += rappel.product_id
                elif rappel.brand_ids:
                    product_ids += product_obj.search([
                        ('product_brand_id', 'in', rappel.brand_ids.ids)
                    ])
                elif rappel.product_categ_id:
                    product_ids += product_obj.search([
                        ('categ_id', '=', rappel.product_categ_id.id)
                    ])
            else:
                product_ids += product_obj.search([])
        return product_ids.ids

    @api.constrains('global_application', 'product_id', 'brand_ids',
                    'product_categ_id')
    def _check_application(self):
        if not self.global_application and not self.product_id \
                and not self.product_categ_id and not self.brand_ids:
            raise exceptions. \
                ValidationError(_('Product, brand and category are empty'))

    @api.model
    def update_partner_rappel_pricelist(self):
        partner_rappel_obj = self.env['res.partner.rappel.rel']

        now = datetime.now()
        now_str = now.strftime("%Y-%m-%d")
        yesterday_str = (now - relativedelta(days=1)).strftime("%Y-%m-%d")
        end_actual_month = now.strftime("%Y-%m") + '-' + str(
            monthrange(now.year, now.month)[1])
        start_next_month = (now +
                            relativedelta(months=1)).strftime("%Y-%m") + '-01'

        discount_voucher_rappels = self.env['rappel'].search([
            ('discount_voucher', '=', True)
        ])

        field = self.env['ir.model.fields'].\
            search([('name', '=', 'property_product_pricelist'),
                    ('model', '=', 'res.partner')], limit=1)

        for rappel in discount_voucher_rappels:
            pricelist_ids = tuple(rappel.pricelist_ids.ids)
            product_rappel = rappel.product_id
            # Clientes que ya pertenecen al rappel:
            partner_rappel_list = tuple(
                partner_rappel_obj.search([('rappel_id', '=', rappel.id),
                                           ('date_start', '<=', now_str), '|',
                                           ('date_end', '=', False),
                                           ('date_end', '>=', now_str)
                                           ]).mapped('partner_id.id'))

            # Clientes que deberian pertenecer al rappel:
            partner_filter = []
            if pricelist_ids:
                # Rappels dependientes de tarifas
                properties = self.env['ir.property']. \
                    search([('fields_id', '=', field.id),
                            ('value_reference', 'in',
                             ['product.pricelist,' +
                              str(x) for x in pricelist_ids]),
                            ('res_id', '!=', False)])

                partner_filter.extend([
                    "('id', 'in', [int(x.res_id.split(',')[1]) for x in properties])"
                ])

            if rappel.partner_add_conditions:
                # Rappels que depende de otros parámetros del cliente
                partner_filter.extend([rappel.partner_add_conditions])

            if product_rappel:
                # Rappel que depende de la compra de un producto concreto
                partner_product = self.env['account.invoice.line'].search([
                    ('product_id', '=', product_rappel.id),
                    ('invoice_id.state', 'in', ['open', 'paid'])
                ]).mapped('invoice_id.partner_id.id')
                partner_filter.extend(["('id', 'in', partner_product)"])

            if partner_filter:
                partner_filter.extend([
                    "('prospective', '=', False), ('active', '=', True), "
                    "('is_company', '=', True), ('parent_id', '=', False)"
                ])
                partner_filter = ', '.join(partner_filter)
                partner_to_check = tuple(
                    eval("self.env['res.partner'].search([" + partner_filter +
                         "])").ids)
            else:
                partner_to_check = tuple()

            # Clientes a los que ya no les corresponde el rappel (cumplen las condiciones anteriores)
            #      - Se actualiza fecha fin con la fecha actual
            remove_partners = set(partner_rappel_list) - set(partner_to_check)
            if remove_partners:
                vals = {'date_end': yesterday_str}
                partner_to_update = partner_rappel_obj.search([
                    ('rappel_id', '=', rappel.id),
                    ('partner_id', 'in', tuple(remove_partners)), '|',
                    ('date_end', '=', False), ('date_end', '>', now),
                    ('date_start', '<=', now_str)
                ])
                partner_to_update.write(vals)

            #  Clientes que faltan en el rappel -> Se crean dos entradas en
            #  el rappel:
            #      - Una para liquidar en el mes actual
            #      - Otra que empiece en fecha 1 del mes siguiente
            add_partners = set(partner_to_check) - set(partner_rappel_list)
            if add_partners:
                new_line1 = {
                    'rappel_id': rappel.id,
                    'periodicity': 'monthly',
                    'date_start': now_str,
                    'date_end': end_actual_month
                }
                new_line2 = {
                    'rappel_id': rappel.id,
                    'periodicity': 'monthly',
                    'date_start': start_next_month
                }
                for partner in add_partners:
                    new_line1.update({'partner_id': partner})
                    partner_rappel_obj.create(new_line1)
                    new_line2.update({'partner_id': partner})
                    partner_rappel_obj.create(new_line2)

    @api.model
    def compute_rappel(self):
        if not self.ids:
            ordered_rappels = self.search([], order='sequence')
        else:
            ordered_rappels = self.sorted(key=lambda x: x.sequence)
        return super(Rappel, ordered_rappels).compute_rappel()
Exemple #20
0
class Invoice(models.Model):
    _inherit = 'account.invoice'

    @api.one
    @api.depends('state', 'pay_order_line.state')
    def _compute_customize_amount(self):
        """
        Calculamos el saldo pendiente de las órdenes de pago
        :return:
        """
        pays = self.pay_order_line.filtered(lambda x: x.state == 'paid')
        if not pays:
            self.state_pay_order = 'no credits'
            self.residual_pay_order = self.residual
        else:
            total = 0.00
            for pay in pays:  # Soló contabilizadas
                total += round(pay.amount, 3)
            self.improved_pay_order = total
            self.residual_pay_order = self.residual
            if float_is_zero(self.residual_pay_order,
                             precision_rounding=0.01) or self.reconciled:
                self.state_pay_order = 'paid'
            else:
                self.state_pay_order = 'partial_payment'

    @api.depends('pay_order_line')
    def _compute_pay_orders(self):
        """
        Calculamos la ordenes de pago relacionadas a la factura y su cantidad
        :return:
        """
        for record in self:
            pays = self.env['account.pay.order'].search([('invoice_ids', 'in',
                                                          record.id)])
            record.pay_order_line = pays
            record.pay_orders_count = len(pays)

    @api.multi
    def action_view_pay_orders(self):
        """
        Ver órdenes de pagos vinculadas a la factura
        :return:
        """
        imd = self.env['ir.model.data']
        action = imd.xmlid_to_object('eliterp_payment.action_pay_order')
        list_view_id = imd.xmlid_to_res_id(
            'eliterp_payment.view_tree_pay_order')
        form_view_id = imd.xmlid_to_res_id(
            'eliterp_payment.view_form_pay_order')
        result = {
            'name': action.name,
            'help': action.help,
            'type': action.type,
            'views': [[list_view_id, 'tree'], [form_view_id, 'form']],
            'target': action.target,
            'context': action.context,
            'res_model': action.res_model,
        }
        if len(self.pay_order_line) > 1:
            result['domain'] = "[('id','in',%s)]" % self.pay_order_line.ids
        elif len(self.pay_order_line) == 1:
            result['views'] = [(form_view_id, 'form')]
            result['res_id'] = self.pay_order_line.ids[0]
        else:
            result = {'type': 'ir.actions.act_window_close'}
        return result

    state_pay_order = fields.Selection([
        ('no credits', 'Sin abonos'),
        ('partial_payment', 'Abono parcial'),
        ('paid', 'Pagado'),
    ],
                                       string="Estado de pago",
                                       compute='_compute_customize_amount',
                                       readonly=True,
                                       copy=False,
                                       store=True)
    improved_pay_order = fields.Float('Abonado',
                                      compute='_compute_customize_amount',
                                      store=True)
    residual_pay_order = fields.Float('Saldo',
                                      compute='_compute_customize_amount',
                                      store=True)
    pay_order_line = fields.Many2many('account.pay.order',
                                      compute='_compute_pay_orders',
                                      store=True,
                                      string='Órdenes de pago')
    pay_orders_count = fields.Integer('# Ordenes de pago',
                                      compute='_compute_pay_orders',
                                      store=True)
Exemple #21
0
class MailDigest(models.Model):
    _name = 'mail.digest'
    _description = 'Mail digest'
    _order = 'create_date desc'

    name = fields.Char(
        string="Name",
        compute="_compute_name",
        readonly=True,
    )
    user_id = fields.Many2one(
        string='User',
        comodel_name='res.users',
        readonly=True,
        required=True,
        ondelete='cascade',
    )
    frequency = fields.Selection(
        related='user_id.digest_frequency',
        readonly=True,
    )
    message_ids = fields.Many2many(
        comodel_name='mail.message',
        string='Messages'
    )
    mail_id = fields.Many2one(
        'mail.mail',
        'Mail',
        ondelete='set null',
    )
    state = fields.Selection(related='mail_id.state', readonly=True)
    # To my future self: never ever change this field to `template_id`.
    # When creating digest records within the context of mail composer
    # (and possibly other contexts) you'll have a `default_template_id`
    # key in the context which is going to override our safe default.
    # This is going to break email generation because the template
    # will be completely wrong. Lesson learned :)
    digest_template_id = fields.Many2one(
        'ir.ui.view',
        'Qweb mail template',
        ondelete='set null',
        default=lambda self: self._default_digest_template_id(),
        domain=[('type', '=', 'qweb')],
    )
    sanitize_msg_body = fields.Boolean(
        string='Sanitize message body',
        help='Collected messages can have different styles applied '
             'on each element. If this flag is enabled (default) '
             'each message content will be sanitized '
             'before generating the email.',
        default=True,
    )

    def _default_digest_template_id(self):
        """Retrieve default template to render digest."""
        return self.env.ref('mail_digest.default_digest_tmpl',
                            raise_if_not_found=False)

    @api.multi
    @api.depends("user_id", "user_id.digest_frequency")
    def _compute_name(self):
        for rec in self:
            rec.name = '{} - {}'.format(
                rec.user_id.name, rec._get_subject())

    @api.model
    def create_or_update(self, partners, message):
        """Create or update digest.

        :param partners: recipients as `res.partner` browse list
        :param message: `mail.message` to include in digest
        """
        for partner in partners:
            digest = self._get_or_create_by_user(partner.real_user_id)
            digest.message_ids |= message
        return True

    @api.model
    def _get_by_user(self, user):
        """Retrieve digest record for given user.

        :param user: `res.users` browse record

        By default we lookup for pending digest without notification yet.
        """
        domain = [
            ('user_id', '=', user.id),
        ]
        return self.search(domain, limit=1)

    @api.model
    def _get_or_create_by_user(self, user):
        """Retrieve digest record or create it by user.

        :param user: `res.users` record to create/get digest for
        """
        existing = self._get_by_user(user)
        if existing:
            return existing
        values = {'user_id': user.id, }
        return self.create(values)

    @api.model
    def _message_group_by_key(self, msg):
        """Return the key to group messages by."""
        return msg.subtype_id.id

    @api.multi
    def _message_group_by(self):
        """Group digest messages.

        A digest can contain several messages.
        To display them in a nice and organized form in your emails
        we group them by subtype by default.
        """
        self.ensure_one()
        grouped = {}
        for msg in self.message_ids:
            grouped.setdefault(self._message_group_by_key(msg), []).append(msg)
        return grouped

    @api.model
    def message_body(self, msg, strip_style=True):
        """Return body message prepared for email content.

        Message's body can contains styles and other stuff
        that can screw the look and feel of digests' mails.

        Here we sanitize it if `sanitize_msg_body` is set on the digest.
        """
        if not self.sanitize_msg_body:
            return msg.body
        return tools.html_sanitize(msg.body or '', strip_style=strip_style)

    def _get_site_name(self):
        """Retrieve site name for meaningful mail subject.

        If you run a website we get website's name
        otherwise we default to current user's company name.
        """
        # default to company
        name = self.env.user.company_id.name
        if 'website' in self.env:
            # TODO: shall we make this configurable at digest or global level?
            # Maybe you have a website but
            # your digest msgs are not related to it at all or partially.
            ws = None
            try:
                ws = self.env['website'].get_current_website()
                name = ws.name
            except RuntimeError:
                # RuntimeError: object unbound -> no website request.
                # Fallback to default website if any.
                ws = self.env['website'].search([], limit=1)
            if ws:
                name = ws.name
        return name

    @api.multi
    def _get_subject(self):
        """Build the full subject for digest's mail."""
        # TODO: shall we move this to computed field?
        self.ensure_one()
        subject = '[{}] '.format(self._get_site_name())
        if self.user_id.digest_frequency == 'daily':
            subject += _('Daily update')
        elif self.user_id.digest_frequency == 'weekly':
            subject += _('Weekly update')
        return subject

    @api.multi
    def _get_template_values(self):
        """Collect variables to render digest's template."""
        self.ensure_one()
        subject = self._get_subject()
        template_values = {
            'digest': self,
            'subject': subject,
            'grouped_messages': self._message_group_by(),
            'base_url':
                self.env['ir.config_parameter'].get_param('web.base.url'),
        }
        return template_values

    @api.multi
    def _get_email_values(self, template=None):
        """Collect variables to create digest's mail message."""
        self.ensure_one()
        template = template or self.digest_template_id
        if not template:
            raise exceptions.UserError(_(
                'You must pass a template or set one on the digest record.'
            ))
        subject = self._get_subject()
        template_values = self._get_template_values()
        values = {
            'email_from': self.env.user.company_id.email,
            'recipient_ids': [(4, self.user_id.partner_id.id)],
            'subject': subject,
            'body_html': template.with_context(
                **self._template_context()
            ).render(template_values),
        }
        return values

    def _create_mail_context(self):
        """Inject context vars.

        By default we make sure that digest's email
        will have only digest's user among recipients.
        """
        return {
            'notify_only_recipients': True,
        }

    @api.multi
    def _template_context(self):
        """Rendering context for digest's template.

        By default we enforce user's language.
        """
        self.ensure_one()
        return {
            'lang': self.user_id.lang,
        }

    @api.multi
    def create_email(self, template=None):
        """Create `mail.message` records for current digests.

        :param template: qweb template instance to override default digest one.
        """
        mail_model = self.env['mail.mail'].with_context(
            **self._create_mail_context())
        created = []
        for item in self:
            if not item.message_ids:
                # useless to create a mail for a digest w/ messages
                # messages could be deleted by admin for instance.
                continue
            values = item.with_context(
                **item._template_context()
            )._get_email_values(template=template)
            item.mail_id = mail_model.create(values)
            created.append(item.id)
        if created:
            logger.info('Create email for digest IDS=%s', str(created))
        return created

    @api.multi
    def action_create_email(self):
        return self.create_email()

    @api.model
    def process(self, frequency='daily', domain=None):
        """Process existing digest records to create emails via cron.

        :param frequency: lookup digest records by users' `digest_frequency`
        :param domain: pass custom domain to lookup only specific digests
        """
        if not domain:
            domain = [
                ('mail_id', '=', False),
                ('user_id.digest_frequency', '=', frequency),
            ]
        self.search(domain).create_email()
Exemple #22
0
class Task(models.Model):
    _name = "project.task"
    _description = "Task"
    _date_name = "date_assign"
    _inherit = ['portal.mixin', 'mail.thread.cc', 'mail.activity.mixin', 'rating.mixin']
    _mail_post_access = 'read'
    _order = "priority desc, sequence, id desc"
    _check_company_auto = True

    @api.model
    def default_get(self, fields_list):
        result = super(Task, self).default_get(fields_list)
        # find default value from parent for the not given ones
        parent_task_id = result.get('parent_id') or self._context.get('default_parent_id')
        if parent_task_id:
            parent_values = self._subtask_values_from_parent(parent_task_id)
            for fname, value in parent_values.items():
                if fname not in result:
                    result[fname] = value
        return result

    @api.model
    def _get_default_partner(self):
        if 'default_project_id' in self.env.context:
            default_project_id = self.env['project.project'].browse(self.env.context['default_project_id'])
            return default_project_id.exists().partner_id

    def _get_default_stage_id(self):
        """ Gives default stage_id """
        project_id = self.env.context.get('default_project_id')
        if not project_id:
            return False
        return self.stage_find(project_id, [('fold', '=', False)])

    @api.model
    def _default_company_id(self):
        if self._context.get('default_project_id'):
            return self.env['project.project'].browse(self._context['default_project_id']).company_id
        return self.env.company

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        search_domain = [('id', 'in', stages.ids)]
        if 'default_project_id' in self.env.context:
            search_domain = ['|', ('project_ids', '=', self.env.context['default_project_id'])] + search_domain

        stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
        return stages.browse(stage_ids)

    active = fields.Boolean(default=True)
    name = fields.Char(string='Title', tracking=True, required=True, index=True)
    description = fields.Html(string='Description')
    priority = fields.Selection([
        ('0', 'Normal'),
        ('1', 'Important'),
    ], default='0', index=True, string="Priority")
    sequence = fields.Integer(string='Sequence', index=True, default=10,
        help="Gives the sequence order when displaying a list of tasks.")
    stage_id = fields.Many2one('project.task.type', string='Stage', ondelete='restrict', tracking=True, index=True,
        default=_get_default_stage_id, group_expand='_read_group_stage_ids',
        domain="[('project_ids', '=', project_id)]", copy=False)
    tag_ids = fields.Many2many('project.tags', string='Tags')
    kanban_state = fields.Selection([
        ('normal', 'Grey'),
        ('done', 'Green'),
        ('blocked', 'Red')], string='Kanban State',
        copy=False, default='normal', required=True)
    kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True)
    create_date = fields.Datetime("Created On", readonly=True, index=True)
    write_date = fields.Datetime("Last Updated On", readonly=True, index=True)
    date_end = fields.Datetime(string='Ending Date', index=True, copy=False)
    date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True)
    date_deadline = fields.Date(string='Deadline', index=True, copy=False, tracking=True)
    date_last_stage_update = fields.Datetime(string='Last Stage Update',
        index=True,
        copy=False,
        readonly=True)
    project_id = fields.Many2one('project.project', string='Project', default=lambda self: self.env.context.get('default_project_id'),
        index=True, tracking=True, check_company=True, change_default=True)
    planned_hours = fields.Float("Planned Hours", help='It is the time planned to achieve the task. If this document has sub-tasks, it means the time needed to achieve this tasks and its childs.',tracking=True)
    subtask_planned_hours = fields.Float("Subtasks", compute='_compute_subtask_planned_hours', help="Computed using sum of hours planned of all subtasks created from main task. Usually these hours are less or equal to the Planned Hours (of main task).")
    user_id = fields.Many2one('res.users',
        string='Assigned to',
        default=lambda self: self.env.uid,
        index=True, tracking=True)
    partner_id = fields.Many2one('res.partner',
        string='Customer',
        default=lambda self: self._get_default_partner(),
        domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    partner_city = fields.Char(related='partner_id.city', readonly=False)
    manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True, related_sudo=False)
    company_id = fields.Many2one('res.company', string='Company', required=True, default=_default_company_id)
    color = fields.Integer(string='Color Index')
    user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False)
    attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments",
        help="Attachment that don't come from message.")
    # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
    displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image')
    legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
    legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
    legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
    parent_id = fields.Many2one('project.task', string='Parent Task', index=True)
    child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", context={'active_test': False})
    subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True)
    subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count')
    email_from = fields.Char(string='Email', help="These people will receive email.", index=True)
    # Computed field about working time elapsed between record creation and assignation/closing.
    working_hours_open = fields.Float(compute='_compute_elapsed', string='Working hours to assign', store=True, group_operator="avg")
    working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg")
    working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg")
    working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg")
    # customer portal: include comment and incoming emails in communication history
    website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])

    def _compute_attachment_ids(self):
        for task in self:
            attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids
            message_attachment_ids = task.mapped('message_ids.attachment_ids').ids  # from mail_thread
            task.attachment_ids = [(6, 0, list(set(attachment_ids) - set(message_attachment_ids)))]

    @api.depends('create_date', 'date_end', 'date_assign')
    def _compute_elapsed(self):
        task_linked_to_calendar = self.filtered(
            lambda task: task.project_id.resource_calendar_id and task.create_date
        )
        for task in task_linked_to_calendar:
            dt_create_date = fields.Datetime.from_string(task.create_date)

            if task.date_assign:
                dt_date_assign = fields.Datetime.from_string(task.date_assign)
                duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_assign, compute_leaves=True)
                task.working_hours_open = duration_data['hours']
                task.working_days_open = duration_data['days']
            else:
                task.working_hours_open = 0.0
                task.working_days_open = 0.0

            if task.date_end:
                dt_date_end = fields.Datetime.from_string(task.date_end)
                duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_end, compute_leaves=True)
                task.working_hours_close = duration_data['hours']
                task.working_days_close = duration_data['days']
            else:
                task.working_hours_close = 0.0
                task.working_days_close = 0.0

        (self - task_linked_to_calendar).update(dict.fromkeys(
            ['working_hours_open', 'working_hours_close', 'working_days_open', 'working_days_close'], 0.0))

    @api.depends('stage_id', 'kanban_state')
    def _compute_kanban_state_label(self):
        for task in self:
            if task.kanban_state == 'normal':
                task.kanban_state_label = task.legend_normal
            elif task.kanban_state == 'blocked':
                task.kanban_state_label = task.legend_blocked
            else:
                task.kanban_state_label = task.legend_done

    def _compute_access_url(self):
        super(Task, self)._compute_access_url()
        for task in self:
            task.access_url = '/my/task/%s' % task.id

    def _compute_access_warning(self):
        super(Task, self)._compute_access_warning()
        for task in self.filtered(lambda x: x.project_id.privacy_visibility != 'portal'):
            task.access_warning = _(
                "The task cannot be shared with the recipient(s) because the privacy of the project is too restricted. Set the privacy of the project to 'Visible by following customers' in order to make it accessible by the recipient(s).")

    @api.depends('child_ids.planned_hours')
    def _compute_subtask_planned_hours(self):
        for task in self:
            task.subtask_planned_hours = sum(task.child_ids.mapped('planned_hours'))

    @api.depends('child_ids')
    def _compute_subtask_count(self):
        """ Note: since we accept only one level subtask, we can use a read_group here """
        task_data = self.env['project.task'].read_group([('parent_id', 'in', self.ids)], ['parent_id'], ['parent_id'])
        mapping = dict((data['parent_id'][0], data['parent_id_count']) for data in task_data)
        for task in self:
            task.subtask_count = mapping.get(task.id, 0)

    @api.onchange('partner_id')
    def _onchange_partner_id(self):
        self.email_from = self.partner_id.email

    @api.onchange('parent_id')
    def _onchange_parent_id(self):
        if self.parent_id:
            for field_name, value in self._subtask_values_from_parent(self.parent_id.id).items():
                if not self[field_name]:
                    self[field_name] = value

    @api.onchange('project_id')
    def _onchange_project(self):
        if self.project_id:
            # find partner
            if self.project_id.partner_id:
                self.partner_id = self.project_id.partner_id
            # find stage
            if self.project_id not in self.stage_id.project_ids:
                self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)])
            # keep multi company consistency
            self.company_id = self.project_id.company_id
        else:
            self.stage_id = False

    @api.constrains('parent_id', 'child_ids')
    def _check_subtask_level(self):
        for task in self:
            if task.parent_id and task.child_ids:
                raise ValidationError(_('Task %s cannot have several subtask levels.' % (task.name,)))

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        if default is None:
            default = {}
        if not default.get('name'):
            default['name'] = _("%s (copy)") % self.name
        return super(Task, self).copy(default)

    @api.constrains('parent_id')
    def _check_parent_id(self):
        for task in self:
            if not task._check_recursion():
                raise ValidationError(_('Error! You cannot create recursive hierarchy of task(s).'))

    @api.model
    def get_empty_list_help(self, help):
        tname = _("task")
        project_id = self.env.context.get('default_project_id', False)
        if project_id:
            name = self.env['project.project'].browse(project_id).label_tasks
            if name: tname = name.lower()

        self = self.with_context(
            empty_list_help_id=self.env.context.get('default_project_id'),
            empty_list_help_model='project.project',
            empty_list_help_document_name=tname,
        )
        return super(Task, self).get_empty_list_help(help)

    # ----------------------------------------
    # Case management
    # ----------------------------------------

    def stage_find(self, section_id, domain=[], order='sequence'):
        """ Override of the base.stage method
            Parameter of the stage search taken from the lead:
            - section_id: if set, stages must belong to this section or
              be a default stage; if not set, stages must be default
              stages
        """
        # collect all section_ids
        section_ids = []
        if section_id:
            section_ids.append(section_id)
        section_ids.extend(self.mapped('project_id').ids)
        search_domain = []
        if section_ids:
            search_domain = [('|')] * (len(section_ids) - 1)
            for section_id in section_ids:
                search_domain.append(('project_ids', '=', section_id))
        search_domain += list(domain)
        # perform search, return the first found
        return self.env['project.task.type'].search(search_domain, order=order, limit=1).id

    # ------------------------------------------------
    # CRUD overrides
    # ------------------------------------------------

    @api.model
    def create(self, vals):
        # context: no_log, because subtype already handle this
        context = dict(self.env.context)
        # for default stage
        if vals.get('project_id') and not context.get('default_project_id'):
            context['default_project_id'] = vals.get('project_id')
        # user_id change: update date_assign
        if vals.get('user_id'):
            vals['date_assign'] = fields.Datetime.now()
        # Stage change: Update date_end if folded stage and date_last_stage_update
        if vals.get('stage_id'):
            vals.update(self.update_date_end(vals['stage_id']))
            vals['date_last_stage_update'] = fields.Datetime.now()
        # substask default values
        if vals.get('parent_id'):
            for fname, value in self._subtask_values_from_parent(vals['parent_id']).items():
                if fname not in vals:
                    vals[fname] = value
        task = super(Task, self.with_context(context)).create(vals)
        return task

    def write(self, vals):
        now = fields.Datetime.now()
        # stage change: update date_last_stage_update
        if 'stage_id' in vals:
            vals.update(self.update_date_end(vals['stage_id']))
            vals['date_last_stage_update'] = now
            # reset kanban state when changing stage
            if 'kanban_state' not in vals:
                vals['kanban_state'] = 'normal'
        # user_id change: update date_assign
        if vals.get('user_id') and 'date_assign' not in vals:
            vals['date_assign'] = now

        result = super(Task, self).write(vals)
        # rating on stage
        if 'stage_id' in vals and vals.get('stage_id'):
            self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_task_rating_mail(force_send=True)
        return result

    def update_date_end(self, stage_id):
        project_task_type = self.env['project.task.type'].browse(stage_id)
        if project_task_type.fold:
            return {'date_end': fields.Datetime.now()}
        return {'date_end': False}

    # ---------------------------------------------------
    # Subtasks
    # ---------------------------------------------------

    def _subtask_default_fields(self):
        """ Return the list of field name for default value when creating a subtask """
        return ['partner_id', 'email_from']

    def _subtask_values_from_parent(self, parent_id):
        """ Get values for substask implied field of the given"""
        result = {}
        parent_task = self.env['project.task'].browse(parent_id)
        for field_name in self._subtask_default_fields():
            result[field_name] = parent_task[field_name]
        # special case for the subtask default project
        result['project_id'] = parent_task.project_id.subtask_project_id
        return self._convert_to_write(result)

    # ---------------------------------------------------
    # Mail gateway
    # ---------------------------------------------------

    def _track_template(self, changes):
        res = super(Task, self)._track_template(changes)
        test_task = self[0]
        if 'stage_id' in changes and test_task.stage_id.mail_template_id:
            res['stage_id'] = (test_task.stage_id.mail_template_id, {
                'auto_delete_message': True,
                'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
                'email_layout_xmlid': 'mail.mail_notification_light'
            })
        return res

    def _creation_subtype(self):
        return self.env.ref('project.mt_task_new')

    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'kanban_state_label' in init_values and self.kanban_state == 'blocked':
            return self.env.ref('project.mt_task_blocked')
        elif 'kanban_state_label' in init_values and self.kanban_state == 'done':
            return self.env.ref('project.mt_task_ready')
        elif 'stage_id' in init_values:
            return self.env.ref('project.mt_task_stage')
        return super(Task, self)._track_subtype(init_values)

    def _notify_get_groups(self):
        """ Handle project users and managers recipients that can assign
        tasks and create new one directly from notification emails. Also give
        access button to portal users and portal customers. If they are notified
        they should probably have access to the document. """
        groups = super(Task, self)._notify_get_groups()

        self.ensure_one()

        project_user_group_id = self.env.ref('project.group_project_user').id
        new_group = (
            'group_project_user',
            lambda pdata: pdata['type'] == 'user' and project_user_group_id in pdata['groups'],
            {},
        )

        if not self.user_id and not self.stage_id.fold:
            take_action = self._notify_get_action_link('assign')
            project_actions = [{'url': take_action, 'title': _('I take it')}]
            new_group[2]['actions'] = project_actions

        groups = [new_group] + groups

        for group_name, group_method, group_data in groups:
            if group_name != 'customer':
                group_data['has_button_access'] = True

        return groups

    def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
        """ Override to set alias of tasks to their project if any. """
        aliases = self.sudo().mapped('project_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
        res = {task.id: aliases.get(task.project_id.id) for task in self}
        leftover = self.filtered(lambda rec: not rec.project_id)
        if leftover:
            res.update(super(Task, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
        return res

    def email_split(self, msg):
        email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or ''))
        # check left-part is not already an alias
        aliases = self.mapped('project_id.alias_name')
        return [x for x in email_list if x.split('@')[0] not in aliases]

    @api.model
    def message_new(self, msg, custom_values=None):
        """ Overrides mail_thread message_new that is called by the mailgateway
            through message_process.
            This override updates the document according to the email.
        """
        # remove default author when going through the mail gateway. Indeed we
        # do not want to explicitly set user_id to False; however we do not
        # want the gateway user to be responsible if no other responsible is
        # found.
        create_context = dict(self.env.context or {})
        create_context['default_user_id'] = False
        if custom_values is None:
            custom_values = {}
        defaults = {
            'name': msg.get('subject') or _("No Subject"),
            'email_from': msg.get('from'),
            'planned_hours': 0.0,
            'partner_id': msg.get('author_id')
        }
        defaults.update(custom_values)

        task = super(Task, self.with_context(create_context)).message_new(msg, custom_values=defaults)
        email_list = task.email_split(msg)
        partner_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(email_list, records=task, force_create=False) if p]
        task.message_subscribe(partner_ids)
        return task

    def message_update(self, msg, update_vals=None):
        """ Override to update the task according to the email. """
        email_list = self.email_split(msg)
        partner_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(email_list, records=self, force_create=False) if p]
        self.message_subscribe(partner_ids)
        return super(Task, self).message_update(msg, update_vals=update_vals)

    def _message_get_suggested_recipients(self):
        recipients = super(Task, self)._message_get_suggested_recipients()
        for task in self:
            if task.partner_id:
                reason = _('Customer Email') if task.partner_id.email else _('Customer')
                task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason)
            elif task.email_from:
                task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email'))
        return recipients

    def _notify_email_header_dict(self):
        headers = super(Task, self)._notify_email_header_dict()
        if self.project_id:
            current_objects = [h for h in headers.get('X-Odoo-Objects', '').split(',') if h]
            current_objects.insert(0, 'project.project-%s, ' % self.project_id.id)
            headers['X-Odoo-Objects'] = ','.join(current_objects)
        if self.tag_ids:
            headers['X-Odoo-Tags'] = ','.join(self.tag_ids.mapped('name'))
        return headers

    def _message_post_after_hook(self, message, msg_vals):
        if self.email_from and not self.partner_id:
            # we consider that posting a message with a specified recipient (not a follower, a specific one)
            # on a document without customer means that it was created through the chatter using
            # suggested recipients. This heuristic allows to avoid ugly hacks in JS.
            new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email_from)
            if new_partner:
                self.search([
                    ('partner_id', '=', False),
                    ('email_from', '=', new_partner.email),
                    ('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id})
        return super(Task, self)._message_post_after_hook(message, msg_vals)

    def action_assign_to_me(self):
        self.write({'user_id': self.env.user.id})

    def action_open_parent_task(self):
        return {
            'name': _('Parent Task'),
            'view_mode': 'form',
            'res_model': 'project.task',
            'res_id': self.parent_id.id,
            'type': 'ir.actions.act_window',
            'context': dict(self._context, create=False)
        }

    def action_subtask(self):
        action = self.env.ref('project.project_task_action_sub_task').read()[0]

        # only display subtasks of current task
        action['domain'] = [('id', 'child_of', self.id), ('id', '!=', self.id)]

        # update context, with all default values as 'quick_create' does not contains all field in its view
        if self._context.get('default_project_id'):
            default_project = self.env['project.project'].browse(self.env.context['default_project_id'])
        else:
            default_project = self.project_id.subtask_project_id or self.project_id
        ctx = dict(self.env.context)
        ctx.update({
            'default_name': self.env.context.get('name', self.name) + ':',
            'default_parent_id': self.id,  # will give default subtask field in `default_get`
            'default_company_id': default_project.company_id.id if default_project else self.env.company.id,
            'search_default_parent_id': self.id,
        })
        parent_values = self._subtask_values_from_parent(self.id)
        for fname, value in parent_values.items():
            if 'default_' + fname not in ctx:
                ctx['default_' + fname] = value
        action['context'] = ctx

        return action

    # ---------------------------------------------------
    # Rating business
    # ---------------------------------------------------

    def _send_task_rating_mail(self, force_send=False):
        for task in self:
            rating_template = task.stage_id.rating_template_id
            if rating_template:
                task.rating_send_request(rating_template, lang=task.partner_id.lang, force_send=force_send)

    def rating_get_partner_id(self):
        res = super(Task, self).rating_get_partner_id()
        if not res and self.project_id.partner_id:
            return self.project_id.partner_id
        return res

    def rating_apply(self, rate, token=None, feedback=None, subtype=None):
        return super(Task, self).rating_apply(rate, token=token, feedback=feedback, subtype="project.mt_task_rating")

    def _rating_get_parent_field_name(self):
        return 'project_id'
Exemple #23
0
class SaleOrder(models.Model):
    _inherit = "sale.order"

    applied_coupon_ids = fields.One2many('coupon.coupon',
                                         'sales_order_id',
                                         string="Applied Coupons",
                                         copy=False)
    generated_coupon_ids = fields.One2many('coupon.coupon',
                                           'order_id',
                                           string="Offered Coupons",
                                           copy=False)
    reward_amount = fields.Float(compute='_compute_reward_total')
    no_code_promo_program_ids = fields.Many2many(
        'coupon.program',
        string="Applied Immediate Promo Programs",
        domain=
        "[('promo_code_usage', '=', 'no_code_needed'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        copy=False)
    code_promo_program_id = fields.Many2one(
        'coupon.program',
        string="Applied Promo Program",
        domain=
        "[('promo_code_usage', '=', 'code_needed'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        copy=False)
    promo_code = fields.Char(related='code_promo_program_id.promo_code',
                             help="Applied program code",
                             readonly=False)

    @api.depends('order_line')
    def _compute_reward_total(self):
        for order in self:
            order.reward_amount = sum(
                [line.price_subtotal for line in order._get_reward_lines()])

    def _get_no_effect_on_threshold_lines(self):
        self.ensure_one()
        lines = self.env['sale.order.line']
        return lines

    def recompute_coupon_lines(self):
        for order in self:
            order._remove_invalid_reward_lines()
            order._create_new_no_code_promo_reward_lines()
            order._update_existing_reward_lines()

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        order = super(SaleOrder, self).copy(default)
        reward_line = order._get_reward_lines()
        if reward_line:
            reward_line.unlink()
            order._create_new_no_code_promo_reward_lines()
        return order

    def action_confirm(self):
        self.generated_coupon_ids.write({'state': 'new'})
        self.applied_coupon_ids.write({'state': 'used'})
        self._send_reward_coupon_mail()
        return super(SaleOrder, self).action_confirm()

    def action_cancel(self):
        res = super(SaleOrder, self).action_cancel()
        self.generated_coupon_ids.write({'state': 'expired'})
        self.applied_coupon_ids.write({'state': 'new'})
        self.applied_coupon_ids.sales_order_id = False
        self.recompute_coupon_lines()
        return res

    def action_draft(self):
        res = super(SaleOrder, self).action_draft()
        self.generated_coupon_ids.write({'state': 'reserved'})
        return res

    def _get_reward_lines(self):
        self.ensure_one()
        return self.order_line.filtered(lambda line: line.is_reward_line)

    def _is_reward_in_order_lines(self, program):
        self.ensure_one()
        order_quantity = sum(
            self.order_line.filtered(lambda line: line.product_id == program.
                                     reward_product_id).mapped(
                                         'product_uom_qty'))
        return order_quantity >= program.reward_product_quantity

    def _is_global_discount_already_applied(self):
        applied_programs = self.no_code_promo_program_ids + \
                           self.code_promo_program_id + \
                           self.applied_coupon_ids.mapped('program_id')
        return applied_programs.filtered(
            lambda program: program._is_global_discount_program())

    def _get_reward_values_product(self, program):
        price_unit = self.order_line.filtered(
            lambda line: program.reward_product_id == line.product_id
        )[0].price_reduce

        order_lines = (self.order_line - self._get_reward_lines()).filtered(
            lambda x: program._get_valid_products(x.product_id))
        max_product_qty = sum(order_lines.mapped('product_uom_qty')) or 1
        total_qty = sum(
            self.order_line.filtered(
                lambda x: x.product_id == program.reward_product_id).mapped(
                    'product_uom_qty'))
        # Remove needed quantity from reward quantity if same reward and rule product
        if program._get_valid_products(program.reward_product_id):
            # number of times the program should be applied
            program_in_order = max_product_qty // (
                program.rule_min_quantity + program.reward_product_quantity)
            # multipled by the reward qty
            reward_product_qty = program.reward_product_quantity * program_in_order
            # do not give more free reward than products
            reward_product_qty = min(reward_product_qty, total_qty)
            if program.rule_minimum_amount:
                order_total = sum(order_lines.mapped('price_total')) - (
                    program.reward_product_quantity *
                    program.reward_product_id.lst_price)
                reward_product_qty = min(
                    reward_product_qty,
                    order_total // program.rule_minimum_amount)
        else:
            program_in_order = max_product_qty // program.rule_min_quantity
            reward_product_qty = min(
                program.reward_product_quantity * program_in_order, total_qty)

        reward_qty = min(
            int(
                int(max_product_qty / program.rule_min_quantity) *
                program.reward_product_quantity), reward_product_qty)
        # Take the default taxes on the reward product, mapped with the fiscal position
        taxes = program.reward_product_id.taxes_id.filtered(
            lambda t: t.company_id.id == self.company_id.id)
        taxes = self.fiscal_position_id.map_tax(taxes)
        return {
            'product_id': program.discount_line_product_id.id,
            'price_unit': -price_unit,
            'product_uom_qty': reward_qty,
            'is_reward_line': True,
            'name': _("Free Product") + " - " + program.reward_product_id.name,
            'product_uom': program.reward_product_id.uom_id.id,
            'tax_id': [(4, tax.id, False) for tax in taxes],
        }

    def _get_paid_order_lines(self):
        """ Returns the sale order lines that are not reward lines.
            It will also return reward lines being free product lines. """
        free_reward_product = self.env['coupon.program'].search([
            ('reward_type', '=', 'product')
        ]).mapped('discount_line_product_id')
        return self.order_line.filtered(lambda x: not x._is_not_sellable_line(
        ) or x.product_id in free_reward_product)

    def _get_base_order_lines(self, program):
        """ Returns the sale order lines not linked to the given program.
        """
        return self.order_line.filtered(lambda x: not x._is_not_sellable_line(
        ) or (x.is_reward_line and x.product_id != program.
              discount_line_product_id))

    def _get_reward_values_discount_fixed_amount(self, program):
        total_amount = sum(
            self._get_base_order_lines(program).mapped('price_total'))
        fixed_amount = program._compute_program_amount('discount_fixed_amount',
                                                       self.currency_id)
        if total_amount < fixed_amount:
            return total_amount
        else:
            return fixed_amount

    def _get_cheapest_line(self):
        # Unit prices tax included
        return min(self.order_line.filtered(
            lambda x: not x._is_not_sellable_line() and x.price_reduce > 0),
                   key=lambda x: x['price_reduce'])

    def _get_reward_values_discount_percentage_per_line(self, program, line):
        discount_amount = line.product_uom_qty * line.price_reduce * (
            program.discount_percentage / 100)
        return discount_amount

    def _get_reward_values_discount(self, program):
        if program.discount_type == 'fixed_amount':
            product_taxes = program.discount_line_product_id.taxes_id.filtered(
                lambda tax: tax.company_id == self.company_id)
            taxes = self.fiscal_position_id.map_tax(product_taxes)
            return [{
                'name':
                _("Discount: %s", program.name),
                'product_id':
                program.discount_line_product_id.id,
                'price_unit':
                -self._get_reward_values_discount_fixed_amount(program),
                'product_uom_qty':
                1.0,
                'product_uom':
                program.discount_line_product_id.uom_id.id,
                'is_reward_line':
                True,
                'tax_id': [(4, tax.id, False) for tax in taxes],
            }]
        reward_dict = {}
        lines = self._get_paid_order_lines()
        amount_total = sum(
            self._get_base_order_lines(program).mapped('price_subtotal'))
        if program.discount_apply_on == 'cheapest_product':
            line = self._get_cheapest_line()
            if line:
                discount_line_amount = min(
                    line.price_reduce * (program.discount_percentage / 100),
                    amount_total)
                if discount_line_amount:
                    taxes = self.fiscal_position_id.map_tax(line.tax_id)

                    reward_dict[line.tax_id] = {
                        'name':
                        _("Discount: %s", program.name),
                        'product_id':
                        program.discount_line_product_id.id,
                        'price_unit':
                        -discount_line_amount
                        if discount_line_amount > 0 else 0,
                        'product_uom_qty':
                        1.0,
                        'product_uom':
                        program.discount_line_product_id.uom_id.id,
                        'is_reward_line':
                        True,
                        'tax_id': [(4, tax.id, False) for tax in taxes],
                    }
        elif program.discount_apply_on in ['specific_products', 'on_order']:
            if program.discount_apply_on == 'specific_products':
                # We should not exclude reward line that offer this product since we need to offer only the discount on the real paid product (regular product - free product)
                free_product_lines = self.env['coupon.program'].search([
                    ('reward_type', '=', 'product'),
                    ('reward_product_id', 'in',
                     program.discount_specific_product_ids.ids)
                ]).mapped('discount_line_product_id')
                lines = lines.filtered(lambda x: x.product_id in
                                       (program.discount_specific_product_ids |
                                        free_product_lines))

            # when processing lines we should not discount more than the order remaining total
            currently_discounted_amount = 0
            for line in lines:
                discount_line_amount = min(
                    self._get_reward_values_discount_percentage_per_line(
                        program, line),
                    amount_total - currently_discounted_amount)

                if discount_line_amount:

                    if line.tax_id in reward_dict:
                        reward_dict[
                            line.tax_id]['price_unit'] -= discount_line_amount
                    else:
                        taxes = self.fiscal_position_id.map_tax(line.tax_id)

                        reward_dict[line.tax_id] = {
                            'name':
                            _(
                                "Discount: %(program)s - On product with following taxes: %(taxes)s",
                                program=program.name,
                                taxes=", ".join(taxes.mapped('name')),
                            ),
                            'product_id':
                            program.discount_line_product_id.id,
                            'price_unit':
                            -discount_line_amount
                            if discount_line_amount > 0 else 0,
                            'product_uom_qty':
                            1.0,
                            'product_uom':
                            program.discount_line_product_id.uom_id.id,
                            'is_reward_line':
                            True,
                            'tax_id': [(4, tax.id, False) for tax in taxes],
                        }
                        currently_discounted_amount += discount_line_amount

        # If there is a max amount for discount, we might have to limit some discount lines or completely remove some lines
        max_amount = program._compute_program_amount('discount_max_amount',
                                                     self.currency_id)
        if max_amount > 0:
            amount_already_given = 0
            for val in list(reward_dict):
                amount_to_discount = amount_already_given + reward_dict[val][
                    "price_unit"]
                if abs(amount_to_discount) > max_amount:
                    reward_dict[val]["price_unit"] = -(
                        max_amount - abs(amount_already_given))
                    add_name = formatLang(self.env,
                                          max_amount,
                                          currency_obj=self.currency_id)
                    reward_dict[val]["name"] += "( " + _(
                        "limited to ") + add_name + ")"
                amount_already_given += reward_dict[val]["price_unit"]
                if reward_dict[val]["price_unit"] == 0:
                    del reward_dict[val]
        return reward_dict.values()

    def _get_reward_line_values(self, program):
        self.ensure_one()
        self = self.with_context(lang=self.partner_id.lang)
        program = program.with_context(lang=self.partner_id.lang)
        if program.reward_type == 'discount':
            return self._get_reward_values_discount(program)
        elif program.reward_type == 'product':
            return [self._get_reward_values_product(program)]

    def _create_reward_line(self, program):
        self.write({
            'order_line': [(0, False, value)
                           for value in self._get_reward_line_values(program)]
        })

    def _create_reward_coupon(self, program):
        # if there is already a coupon that was set as expired, reactivate that one instead of creating a new one
        coupon = self.env['coupon.coupon'].search([
            ('program_id', '=', program.id),
            ('state', '=', 'expired'),
            ('partner_id', '=', self.partner_id.id),
            ('order_id', '=', self.id),
            ('discount_line_product_id', '=',
             program.discount_line_product_id.id),
        ],
                                                  limit=1)
        if coupon:
            coupon.write({'state': 'reserved'})
        else:
            coupon = self.env['coupon.coupon'].sudo().create({
                'program_id':
                program.id,
                'state':
                'reserved',
                'partner_id':
                self.partner_id.id,
                'order_id':
                self.id,
                'discount_line_product_id':
                program.discount_line_product_id.id
            })
        self.generated_coupon_ids |= coupon
        return coupon

    def _send_reward_coupon_mail(self):
        template = self.env.ref('sale_coupon.mail_template_sale_coupon',
                                raise_if_not_found=False)
        if template:
            for order in self:
                for coupon in order.generated_coupon_ids:
                    order.message_post_with_template(
                        template.id,
                        composition_mode='comment',
                        model='coupon.coupon',
                        res_id=coupon.id,
                        email_layout_xmlid='mail.mail_notification_light',
                    )

    def _get_applicable_programs(self):
        """
        This method is used to return the valid applicable programs on given order.
        """
        self.ensure_one()
        programs = self.env['coupon.program'].with_context(
            no_outdated_coupons=True, ).search(
                [
                    ('company_id', 'in', [self.company_id.id, False]),
                    '|',
                    ('rule_date_from', '=', False),
                    ('rule_date_from', '<=', self.date_order),
                    '|',
                    ('rule_date_to', '=', False),
                    ('rule_date_to', '>=', self.date_order),
                ],
                order="id")._filter_programs_from_common_rules(self)
        # no impact code...
        # should be programs = programs.filtered if we really want to filter...
        # if self.promo_code:
        #     programs._filter_promo_programs_with_code(self)
        return programs

    def _get_applicable_no_code_promo_program(self):
        self.ensure_one()
        programs = self.env['coupon.program'].with_context(
            no_outdated_coupons=True,
            applicable_coupon=True,
        ).search([
            ('promo_code_usage', '=', 'no_code_needed'),
            '|',
            ('rule_date_from', '=', False),
            ('rule_date_from', '<=', self.date_order),
            '|',
            ('rule_date_to', '=', False),
            ('rule_date_to', '>=', self.date_order),
            '|',
            ('company_id', '=', self.company_id.id),
            ('company_id', '=', False),
        ])._filter_programs_from_common_rules(self)
        return programs

    def _get_valid_applied_coupon_program(self):
        self.ensure_one()
        # applied_coupon_ids's coupons might be coming from:
        #   * a coupon generated from a previous order that benefited from a promotion_program that rewarded the next sale order.
        #     In that case requirements to benefit from the program (Quantity and price) should not be checked anymore
        #   * a coupon_program, in that case the promo_applicability is always for the current order and everything should be checked (filtered)
        programs = self.applied_coupon_ids.mapped('program_id').filtered(
            lambda p: p.promo_applicability == 'on_next_order'
        )._filter_programs_from_common_rules(self, True)
        programs += self.applied_coupon_ids.mapped('program_id').filtered(
            lambda p: p.promo_applicability == 'on_current_order'
        )._filter_programs_from_common_rules(self)
        return programs

    def _create_new_no_code_promo_reward_lines(self):
        '''Apply new programs that are applicable'''
        self.ensure_one()
        order = self
        programs = order._get_applicable_no_code_promo_program()
        programs = programs._keep_only_most_interesting_auto_applied_global_discount_program(
        )
        for program in programs:
            # VFE REF in master _get_applicable_no_code_programs already filters programs
            # why do we need to reapply this bunch of checks in _check_promo_code ????
            # We should only apply a little part of the checks in _check_promo_code...
            error_status = program._check_promo_code(order, False)
            if not error_status.get('error'):
                if program.promo_applicability == 'on_next_order':
                    order.state != 'cancel' and order._create_reward_coupon(
                        program)
                elif program.discount_line_product_id.id not in self.order_line.mapped(
                        'product_id').ids:
                    self.write({
                        'order_line':
                        [(0, False, value)
                         for value in self._get_reward_line_values(program)]
                    })
                order.no_code_promo_program_ids |= program

    def _update_existing_reward_lines(self):
        '''Update values for already applied rewards'''
        def update_line(order, lines, values):
            '''Update the lines and return them if they should be deleted'''
            lines_to_remove = self.env['sale.order.line']
            # Check commit 6bb42904a03 for next if/else
            # Remove reward line if price or qty equal to 0
            if values['product_uom_qty'] and values['price_unit']:
                lines.write(values)
            else:
                if program.reward_type != 'free_shipping':
                    # Can't remove the lines directly as we might be in a recordset loop
                    lines_to_remove += lines
                else:
                    values.update(price_unit=0.0)
                    lines.write(values)
            return lines_to_remove

        self.ensure_one()
        order = self
        applied_programs = order._get_applied_programs_with_rewards_on_current_order(
        )
        for program in applied_programs:
            values = order._get_reward_line_values(program)
            lines = order.order_line.filtered(lambda line: line.product_id ==
                                              program.discount_line_product_id)
            if program.reward_type == 'discount' and program.discount_type == 'percentage':
                lines_to_remove = lines
                # Values is what discount lines should really be, lines is what we got in the SO at the moment
                # 1. If values & lines match, we should update the line (or delete it if no qty or price?)
                # 2. If the value is not in the lines, we should add it
                # 3. if the lines contains a tax not in value, we should remove it
                for value in values:
                    value_found = False
                    for line in lines:
                        # Case 1.
                        if not len(
                                set(line.tax_id.mapped(
                                    'id')).symmetric_difference(
                                        set([v[1] for v in value['tax_id']]))):
                            value_found = True
                            # Working on Case 3.
                            lines_to_remove -= line
                            lines_to_remove += update_line(order, line, value)
                            continue
                    # Case 2.
                    if not value_found:
                        order.write({'order_line': [(0, False, value)]})
                # Case 3.
                lines_to_remove.unlink()
            else:
                update_line(order, lines, values[0]).unlink()

    def _remove_invalid_reward_lines(self):
        """ Find programs & coupons that are not applicable anymore.
            It will then unlink the related reward order lines.
            It will also unset the order's fields that are storing
            the applied coupons & programs.
            Note: It will also remove a reward line coming from an archive program.
        """
        self.ensure_one()
        order = self

        applied_programs = order._get_applied_programs()
        applicable_programs = self.env['coupon.program']
        if applied_programs:
            applicable_programs = order._get_applicable_programs(
            ) + order._get_valid_applied_coupon_program()
            applicable_programs = applicable_programs._keep_only_most_interesting_auto_applied_global_discount_program(
            )
        programs_to_remove = applied_programs - applicable_programs

        reward_product_ids = applied_programs.discount_line_product_id.ids
        # delete reward line coming from an archived coupon (it will never be updated/removed when recomputing the order)
        invalid_lines = order.order_line.filtered(
            lambda line: line.is_reward_line and line.product_id.id not in
            reward_product_ids)

        if programs_to_remove:
            product_ids_to_remove = programs_to_remove.discount_line_product_id.ids

            if product_ids_to_remove:
                # Invalid generated coupon for which we are not eligible anymore ('expired' since it is specific to this SO and we may again met the requirements)
                self.generated_coupon_ids.filtered(
                    lambda coupon: coupon.program_id.discount_line_product_id.
                    id in product_ids_to_remove).write({'state': 'expired'})

            # Reset applied coupons for which we are not eligible anymore ('valid' so it can be use on another )
            coupons_to_remove = order.applied_coupon_ids.filtered(
                lambda coupon: coupon.program_id in programs_to_remove)
            coupons_to_remove.write({'state': 'new'})

            # Unbind promotion and coupon programs which requirements are not met anymore
            order.no_code_promo_program_ids -= programs_to_remove
            order.code_promo_program_id -= programs_to_remove

            if coupons_to_remove:
                order.applied_coupon_ids -= coupons_to_remove

            # Remove their reward lines
            if product_ids_to_remove:
                invalid_lines |= order.order_line.filtered(
                    lambda line: line.product_id.id in product_ids_to_remove)

        invalid_lines.unlink()

    def _get_applied_programs_with_rewards_on_current_order(self):
        # Need to add filter on current order. Indeed, it has always been calculating reward line even if on next order (which is useless and do calculation for nothing)
        # This problem could not be noticed since it would only update or delete existing lines related to that program, it would not find the line to update since not in the order
        # But now if we dont find the reward line in the order, we add it (since we can now have multiple line per  program in case of discount on different vat), thus the bug
        # mentionned ahead will be seen now
        return self.no_code_promo_program_ids.filtered(lambda p: p.promo_applicability == 'on_current_order') + \
               self.applied_coupon_ids.mapped('program_id') + \
               self.code_promo_program_id.filtered(lambda p: p.promo_applicability == 'on_current_order')

    def _get_applied_programs_with_rewards_on_next_order(self):
        return self.no_code_promo_program_ids.filtered(lambda p: p.promo_applicability == 'on_next_order') + \
            self.code_promo_program_id.filtered(lambda p: p.promo_applicability == 'on_next_order')

    def _get_applied_programs(self):
        """Returns all applied programs on current order:

        Expected to return same result than:

            self._get_applied_programs_with_rewards_on_current_order()
            +
            self._get_applied_programs_with_rewards_on_next_order()
        """
        return self.code_promo_program_id + self.no_code_promo_program_ids + self.applied_coupon_ids.mapped(
            'program_id')

    def _get_invoice_status(self):
        # Handling of a specific situation: an order contains
        # a product invoiced on delivery and a promo line invoiced
        # on order. We would avoid having the invoice status 'to_invoice'
        # if the created invoice will only contain the promotion line
        super()._get_invoice_status()
        for order in self.filtered(
                lambda order: order.invoice_status == 'to invoice'):
            paid_lines = order._get_paid_order_lines()
            if not any(line.invoice_status == 'to invoice'
                       for line in paid_lines):
                order.invoice_status = 'no'

    def _get_invoiceable_lines(self, final=False):
        """ Ensures we cannot invoice only reward lines.

        Since promotion lines are specified with service products,
        those lines are directly invoiceable when the order is confirmed
        which can result in invoices containing only promotion lines.

        To avoid those cases, we allow the invoicing of promotion lines
        iff at least another 'basic' lines is also invoiceable.
        """
        invoiceable_lines = super()._get_invoiceable_lines(final)
        reward_lines = self._get_reward_lines()
        if invoiceable_lines <= reward_lines:
            return self.env['sale.order.line'].browse()
        return invoiceable_lines

    def update_prices(self):
        """Recompute coupons/promotions after pricelist prices reset."""
        super().update_prices()
        if any(line.is_reward_line for line in self.order_line):
            self.recompute_coupon_lines()
Exemple #24
0
class Project(models.Model):
    _name = "project.project"
    _description = "Project"
    _inherit = ['portal.mixin', 'mail.alias.mixin', 'mail.thread', 'rating.parent.mixin']
    _order = "sequence, name, id"
    _period_number = 5
    _rating_satisfaction_days = False  # takes all existing ratings
    _check_company_auto = True

    def get_alias_model_name(self, vals):
        return vals.get('alias_model', 'project.task')

    def get_alias_values(self):
        values = super(Project, self).get_alias_values()
        values['alias_defaults'] = {'project_id': self.id}
        return values

    def _compute_attached_docs_count(self):
        Attachment = self.env['ir.attachment']
        for project in self:
            project.doc_count = Attachment.search_count([
                '|',
                '&',
                ('res_model', '=', 'project.project'), ('res_id', '=', project.id),
                '&',
                ('res_model', '=', 'project.task'), ('res_id', 'in', project.task_ids.ids)
            ])

    def _compute_task_count(self):
        task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids), '|', ('stage_id.fold', '=', False), ('stage_id', '=', False)], ['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 = result.get(project.id, 0)

    def attachment_tree_view(self):
        self.ensure_one()
        domain = [
            '|',
            '&', ('res_model', '=', 'project.project'), ('res_id', 'in', self.ids),
            '&', ('res_model', '=', 'project.task'), ('res_id', 'in', self.task_ids.ids)]
        return {
            'name': _('Attachments'),
            'domain': domain,
            'res_model': 'ir.attachment',
            'type': 'ir.actions.act_window',
            'view_id': False,
            'view_mode': 'kanban,tree,form',
            'help': _('''<p class="o_view_nocontent_smiling_face">
                        Documents are attached to the tasks of your project.</p><p>
                        Send messages or log internal notes with attachments to link
                        documents to your project.
                    </p>'''),
            'limit': 80,
            'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, self.id)
        }

    @api.model
    def activate_sample_project(self):
        """ Unarchives the sample project 'project.project_project_data' and
            reloads the project dashboard """
        # Unarchive sample project
        project = self.env.ref('project.project_project_data', False)
        if project:
            project.write({'active': True})

        cover_image = self.env.ref('project.msg_task_data_14_attach', False)
        cover_task = self.env.ref('project.project_task_data_14', False)
        if cover_image and cover_task:
            cover_task.write({'displayed_image_id': cover_image.id})

        # Change the help message on the action (no more activate project)
        action = self.env.ref('project.open_view_project_all', False)
        action_data = None
        if action:
            action.sudo().write({
                "help": _('''<p class="o_view_nocontent_smiling_face">
                    Create a new project</p>''')
            })
            action_data = action.read()[0]
        # Reload the dashboard
        return action_data

    def _compute_is_favorite(self):
        for project in self:
            project.is_favorite = self.env.user in project.favorite_user_ids

    def _inverse_is_favorite(self):
        favorite_projects = not_fav_projects = self.env['project.project'].sudo()
        for project in self:
            if self.env.user in project.favorite_user_ids:
                favorite_projects |= project
            else:
                not_fav_projects |= project

        # Project User has no write access for project.
        not_fav_projects.write({'favorite_user_ids': [(4, self.env.uid)]})
        favorite_projects.write({'favorite_user_ids': [(3, self.env.uid)]})

    def _get_default_favorite_user_ids(self):
        return [(6, 0, [self.env.uid])]

    name = fields.Char("Name", index=True, required=True, tracking=True)
    active = fields.Boolean(default=True,
        help="If the active field is set to False, it will allow you to hide the project without removing it.")
    sequence = fields.Integer(default=10, help="Gives the sequence order when displaying a list of Projects.")
    partner_id = fields.Many2one('res.partner', string='Customer', auto_join=True, tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
    currency_id = fields.Many2one('res.currency', related="company_id.currency_id", string="Currency", readonly=True)
    analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account", copy=False, ondelete='set null',
        domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", check_company=True,
        help="Analytic account to which this project is linked for financial management. "
             "Use an analytic account to record cost and revenue on your project.")

    favorite_user_ids = fields.Many2many(
        'res.users', 'project_favorite_user_rel', 'project_id', 'user_id',
        default=_get_default_favorite_user_ids,
        string='Members')
    is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite', string='Show Project on dashboard',
        help="Whether this project should be displayed on your dashboard.")
    label_tasks = fields.Char(string='Use Tasks as', default='Tasks', help="Label used for the tasks of the project.")
    tasks = fields.One2many('project.task', 'project_id', string="Task Activities")
    resource_calendar_id = fields.Many2one(
        'resource.calendar', string='Working Time',
        default=lambda self: self.env.company.resource_calendar_id.id,
        domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        help="Timetable working hours to adjust the gantt diagram report")
    type_ids = fields.Many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', string='Tasks Stages')
    task_count = fields.Integer(compute='_compute_task_count', string="Task Count")
    task_ids = fields.One2many('project.task', 'project_id', string='Tasks',
                               domain=['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
    color = fields.Integer(string='Color Index')
    user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True)
    alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True,
        help="Internal email associated with this project. Incoming emails are automatically synchronized "
             "with Tasks (or optionally Issues if the Issue Tracker module is installed).")
    privacy_visibility = fields.Selection([
            ('followers', 'Invited employees'),
            ('employees', 'All employees'),
            ('portal', 'Portal users and all employees'),
        ],
        string='Visibility', required=True,
        default='portal',
        help="Defines the visibility of the tasks of the project:\n"
                "- Invited employees: employees may only see the followed project and tasks.\n"
                "- All employees: employees may see all project and tasks.\n"
                "- Portal users and all employees: employees may see everything."
                "   Portal users may see project and tasks followed by.\n"
                "   them or by someone of their company.")
    doc_count = fields.Integer(compute='_compute_attached_docs_count', string="Number of documents attached")
    date_start = fields.Date(string='Start Date')
    date = fields.Date(string='Expiration Date', index=True, tracking=True)
    subtask_project_id = fields.Many2one('project.project', string='Sub-task Project', ondelete="restrict",
        help="Project in which sub-tasks of the current project will be created. It can be the current project itself.")

    # rating fields
    rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True)
    rating_status = fields.Selection([('stage', 'Rating when changing stage'), ('periodic', 'Periodical Rating'), ('no','No rating')], 'Customer(s) Ratings', help="How to get customer feedback?\n"
                    "- Rating when changing stage: an email will be sent when a task is pulled in another stage.\n"
                    "- Periodical Rating: email will be sent periodically.\n\n"
                    "Don't forget to set up the mail templates on the stages for which you want to get the customer's feedbacks.", default="no", required=True)
    rating_status_period = fields.Selection([
        ('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'),
        ('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly')
    ], 'Rating Frequency')

    portal_show_rating = fields.Boolean('Rating visible publicly', copy=False)

    _sql_constraints = [
        ('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.')
    ]

    def _compute_access_url(self):
        super(Project, self)._compute_access_url()
        for project in self:
            project.access_url = '/my/project/%s' % project.id

    def _compute_access_warning(self):
        super(Project, self)._compute_access_warning()
        for project in self.filtered(lambda x: x.privacy_visibility != 'portal'):
            project.access_warning = _(
                "The project cannot be shared with the recipient(s) because the privacy of the project is too restricted. Set the privacy to 'Visible by following customers' in order to make it accessible by the recipient(s).")

    @api.depends('rating_status', 'rating_status_period')
    def _compute_rating_request_deadline(self):
        periods = {'daily': 1, 'weekly': 7, 'bimonthly': 15, 'monthly': 30, 'quarterly': 90, 'yearly': 365}
        for project in self:
            project.rating_request_deadline = fields.datetime.now() + timedelta(days=periods.get(project.rating_status_period, 0))

    @api.model
    def _map_tasks_default_valeus(self, task, project):
        """ get the default value for the copied task on project duplication """
        return {
            'stage_id': task.stage_id.id,
            'name': task.name,
            'company_id': project.company_id.id,
        }

    def map_tasks(self, new_project_id):
        """ copy and map tasks from old to new project """
        project = self.browse(new_project_id)
        tasks = self.env['project.task']
        # We want to copy archived task, but do not propagate an active_test context key
        task_ids = self.env['project.task'].with_context(active_test=False).search([('project_id', '=', self.id)], order='parent_id').ids
        old_to_new_tasks = {}
        for task in self.env['project.task'].browse(task_ids):
            # preserve task name and stage, normally altered during copy
            defaults = self._map_tasks_default_valeus(task, project)
            if task.parent_id:
                # set the parent to the duplicated task
                defaults['parent_id'] = old_to_new_tasks.get(task.parent_id.id, False)
            new_task = task.copy(defaults)
            old_to_new_tasks[task.id] = new_task.id
            tasks += new_task

        return project.write({'tasks': [(6, 0, tasks.ids)]})

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        if default is None:
            default = {}
        if not default.get('name'):
            default['name'] = _("%s (copy)") % (self.name)
        project = super(Project, self).copy(default)
        if self.subtask_project_id == self:
            project.subtask_project_id = project
        for follower in self.message_follower_ids:
            project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids)
        if 'tasks' not in default:
            self.map_tasks(project.id)
        return project

    @api.model
    def create(self, vals):
        # Prevent double project creation
        self = self.with_context(mail_create_nosubscribe=True)
        project = super(Project, self).create(vals)
        if not vals.get('subtask_project_id'):
            project.subtask_project_id = project.id
        if project.privacy_visibility == 'portal' and project.partner_id:
            project.message_subscribe(project.partner_id.ids)
        return project

    def write(self, vals):
        # directly compute is_favorite to dodge allow write access right
        if 'is_favorite' in vals:
            vals.pop('is_favorite')
            self._fields['is_favorite'].determine_inverse(self)
        res = super(Project, self).write(vals) if vals else True
        if 'active' in vals:
            # archiving/unarchiving a project does it on its tasks, too
            self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']})
        if vals.get('partner_id') or vals.get('privacy_visibility'):
            for project in self.filtered(lambda project: project.privacy_visibility == 'portal'):
                project.message_subscribe(project.partner_id.ids)
        return res

    def unlink(self):
        # Check project is empty
        for project in self.with_context(active_test=False):
            if project.tasks:
                raise UserError(_('You cannot delete a project containing tasks. You can either archive it or first delete all of its tasks.'))
        # Delete the empty related analytic account
        analytic_accounts_to_delete = self.env['account.analytic.account']
        for project in self:
            if project.analytic_account_id and not project.analytic_account_id.line_ids:
                analytic_accounts_to_delete |= project.analytic_account_id
        result = super(Project, self).unlink()
        analytic_accounts_to_delete.unlink()
        return result

    def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None):
        """ Subscribe to all existing active tasks when subscribing to a project """
        res = super(Project, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids)
        project_subtypes = self.env['mail.message.subtype'].browse(subtype_ids) if subtype_ids else None
        task_subtypes = project_subtypes.mapped('parent_id').ids if project_subtypes else None
        if not subtype_ids or task_subtypes:
            self.mapped('tasks').message_subscribe(
                partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=task_subtypes)
        return res

    def message_unsubscribe(self, partner_ids=None, channel_ids=None):
        """ Unsubscribe from all tasks when unsubscribing from a project """
        self.mapped('tasks').message_unsubscribe(partner_ids=partner_ids, channel_ids=channel_ids)
        return super(Project, self).message_unsubscribe(partner_ids=partner_ids, channel_ids=channel_ids)

    # ---------------------------------------------------
    #  Actions
    # ---------------------------------------------------

    def toggle_favorite(self):
        favorite_projects = not_fav_projects = self.env['project.project'].sudo()
        for project in self:
            if self.env.user in project.favorite_user_ids:
                favorite_projects |= project
            else:
                not_fav_projects |= project

        # Project User has no write access for project.
        not_fav_projects.write({'favorite_user_ids': [(4, self.env.uid)]})
        favorite_projects.write({'favorite_user_ids': [(3, self.env.uid)]})

    def open_tasks(self):
        ctx = dict(self._context)
        ctx.update({'search_default_project_id': self.id})
        action = self.env['ir.actions.act_window'].for_xml_id('project', 'act_project_project_2_project_task_all')
        return dict(action, context=ctx)

    def action_view_account_analytic_line(self):
        """ return the action to see all the analytic lines of the project's analytic account """
        action = self.env.ref('analytic.account_analytic_line_action').read()[0]
        action['context'] = {'default_account_id': self.analytic_account_id.id}
        action['domain'] = [('account_id', '=', self.analytic_account_id.id)]
        return action

    def action_view_all_rating(self):
        """ return the action to see all the rating of the project, and activate default filters """
        if self.portal_show_rating:
            return {
                'type': 'ir.actions.act_url',
                'name': "Redirect to the Website Projcet Rating Page",
                'target': 'self',
                'url': "/project/rating/%s" % (self.id,)
            }
        action = self.env['ir.actions.act_window'].for_xml_id('project', 'rating_rating_action_view_project_rating')
        action['name'] = _('Ratings of %s') % (self.name,)
        action_context = safe_eval(action['context']) if action['context'] else {}
        action_context.update(self._context)
        action_context['search_default_parent_res_name'] = self.name
        action_context.pop('group_by', None)
        return dict(action, context=action_context)

    # ---------------------------------------------------
    #  Business Methods
    # ---------------------------------------------------

    @api.model
    def _create_analytic_account_from_values(self, values):
        analytic_account = self.env['account.analytic.account'].create({
            'name': values.get('name', _('Unknown Analytic Account')),
            'company_id': values.get('company_id') or self.env.company.id,
            'partner_id': values.get('partner_id'),
            'active': True,
        })
        return analytic_account

    def _create_analytic_account(self):
        for project in self:
            analytic_account = self.env['account.analytic.account'].create({
                'name': project.name,
                'company_id': project.company_id.id,
                'partner_id': project.partner_id.id,
                'active': True,
            })
            project.write({'analytic_account_id': analytic_account.id})

    # ---------------------------------------------------
    # Rating business
    # ---------------------------------------------------

    # This method should be called once a day by the scheduler
    @api.model
    def _send_rating_all(self):
        projects = self.search([('rating_status', '=', 'periodic'), ('rating_request_deadline', '<=', fields.Datetime.now())])
        projects.mapped('task_ids')._send_task_rating_mail()
        projects._compute_rating_request_deadline()
Exemple #25
0
class SchoolTeacher(models.Model):
    ''' Defining a Teacher information '''
    _name = 'school.teacher'
    _description = 'Informasi Guru'

    employee_id = fields.Many2one('hr.employee',
                                  'ID Pegawai',
                                  ondelete="cascade",
                                  delegate=True,
                                  required=True)
    standard_id = fields.Many2one('school.standard',
                                  "Mengajar pada tingkat",
                                  help="Tingkat kelas yang diajar oleh\
                                  guur bersangkutan.")
    stand_id = fields.Many2one('standard.standard',
                               "Kelas",
                               related="standard_id.standard_id",
                               store=True)
    subject_id = fields.Many2many('subject.subject', 'subject_teacher_rel',
                                  'teacher_id', 'subject_id',
                                  'Mata Pelajaran ')
    school_id = fields.Many2one('school.school',
                                "Jenjang",
                                related="standard_id.school_id",
                                store=True)
    department_id = fields.Many2one('hr.department', 'Departemen')
    is_parent = fields.Boolean('Is Parent')
    stu_parent_id = fields.Many2one('school.parent', 'Orang Tua dari')
    student_id = fields.Many2many('student.student',
                                  'students_teachers_parent_rel', 'teacher_id',
                                  'student_id', 'Anak')
    phone_numbers = fields.Char("No Telp/HP")

    @api.onchange('is_parent')
    def _onchange_isparent(self):
        if self.is_parent:
            self.stu_parent_id = False
            self.student_id = [(6, 0, [])]

    @api.onchange('stu_parent_id')
    def _onchangestudent_parent(self):
        stud_list = []
        if self.stu_parent_id and self.stu_parent_id.student_id:
            for student in self.stu_parent_id.student_id:
                stud_list.append(student.id)
            self.student_id = [(6, 0, stud_list)]

    @api.model
    def create(self, vals):
        teacher_id = super(SchoolTeacher, self).create(vals)
        user_obj = self.env['res.users']
        user_vals = {
            'name': teacher_id.name,
            'login': teacher_id.work_email,
            'email': teacher_id.work_email,
        }
        ctx_vals = {
            'teacher_create': True,
            'school_id': teacher_id.school_id.company_id.id
        }
        user_id = user_obj.with_context(ctx_vals).create(user_vals)
        teacher_id.employee_id.write({'user_id': user_id.id})
        if vals.get('is_parent'):
            self.parent_crt(teacher_id)
        return teacher_id

    def parent_crt(self, manager_id):
        stu_parent = []
        if manager_id.stu_parent_id:
            stu_parent = manager_id.stu_parent_id
        if not stu_parent:
            emp_user = manager_id.employee_id
            students = [stu.id for stu in manager_id.student_id]
            parent_vals = {
                'name': manager_id.name,
                'email': emp_user.work_email,
                'parent_create_mng': 'parent',
                'user_ids': [(6, 0, [emp_user.user_id.id])],
                'partner_id': emp_user.user_id.partner_id.id,
                'student_id': [(6, 0, students)]
            }
            stu_parent = self.env['school.parent'].create(parent_vals)
            manager_id.write({'stu_parent_id': stu_parent.id})
        user = stu_parent.user_ids
        user_rec = user[0]
        parent_grp_id = self.env.ref('school.group_school_parent')
        groups = parent_grp_id
        if user_rec.groups_id:
            groups = user_rec.groups_id
            groups += parent_grp_id
        group_ids = [group.id for group in groups]
        user_rec.write({'groups_id': [(6, 0, group_ids)]})

    def write(self, vals):
        if vals.get('is_parent'):
            self.parent_crt(self)
        if vals.get('student_id'):
            self.stu_parent_id.write({'student_id': vals.get('student_id')})
        if not vals.get('is_parent'):
            user_rec = self.employee_id.user_id
            ir_obj = self.env['ir.model.data']
            parent_grp_id = ir_obj.get_object('school', 'group_school_parent')
            groups = parent_grp_id
            if user_rec.groups_id:
                groups = user_rec.groups_id
                groups -= parent_grp_id
            group_ids = [group.id for group in groups]
            user_rec.write({'groups_id': [(6, 0, group_ids)]})
        return super(SchoolTeacher, self).write(vals)

    @api.onchange('address_id')
    def onchange_address_id(self):
        self.work_phone = False
        self.mobile_phone = False
        if self.address_id:
            self.work_phone = self.address_id.phone,
            self.mobile_phone = self.address_id.mobile

    @api.onchange('department_id')
    def onchange_department_id(self):
        if self.department_id:
            self.parent_id = (self.department_id
                              and self.department_id.manager_id
                              and self.department_id.manager_id.id) or False

    @api.onchange('user_id')
    def onchange_user(self):
        if self.user_id:
            self.name = self.name or self.user_id.name
            self.work_email = self.user_id.email
            self.image = self.image or self.user_id.image

    @api.onchange('school_id')
    def onchange_school(self):
        self.address_id = False
        self.mobile_phone = False
        self.work_location = False
        self.work_email = False
        self.work_phone = False
        if self.school_id:
            self.address_id = self.school_id.company_id.partner_id.id
            self.mobile_phone = self.school_id.company_id.partner_id.mobile
            self.work_location = self.school_id.company_id.partner_id.city
            self.work_email = self.school_id.company_id.partner_id.email
            phone = self.school_id.company_id.partner_id.phone
            self.work_phone = phone
            self.phone_numbers = phone
            phone = self.school_id.company_id.partner_id.phone
Exemple #26
0
class FSMOrder(models.Model):
    _name = 'fsm.order'
    _description = 'Field Service Order'
    _inherit = ['mail.thread', 'mail.activity.mixin']

    def _default_stage_id(self):
        stage_ids = self.env['fsm.stage'].\
            search([('stage_type', '=', 'order'),
                    ('is_default', '=', True),
                    ('company_id', 'in', (self.env.user.company_id.id,
                                          False))],
                   order='sequence asc', limit=1)
        if stage_ids:
            return stage_ids[0]
        else:
            raise ValidationError(
                _("You must create an FSM order stage first."))

    def _default_team_id(self):
        team_ids = self.env['fsm.team'].\
            search([('company_id', 'in', (self.env.user.company_id.id,
                                          False))],
                   order='sequence asc', limit=1)
        if team_ids:
            return team_ids[0]
        else:
            raise ValidationError(_("You must create an FSM team first."))

    @api.depends('date_start', 'date_end')
    def _compute_duration(self):
        for rec in self:
            if rec.date_start and rec.date_end:
                start = fields.Datetime.from_string(rec.date_start)
                end = fields.Datetime.from_string(rec.date_end)
                delta = end - start
                rec.duration = delta.total_seconds() / 3600

    @api.depends('stage_id')
    def _get_stage_color(self):
        """ Get stage color"""
        self.custom_color = self.stage_id.custom_color or '#FFFFFF'

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'stage_id' in init_values:
            if self.stage_id.id == self.env.\
                    ref('fieldservice.fsm_stage_completed').id:
                return 'fieldservice.mt_order_completed'
            if self.stage_id.id == self.env.\
                    ref('fieldservice.fsm_stage_cancelled').id:
                return 'fieldservice.mt_order_cancelled'
        return super()._track_subtype(init_values)

    stage_id = fields.Many2one('fsm.stage',
                               string='Stage',
                               track_visibility='onchange',
                               index=True,
                               copy=False,
                               group_expand='_read_group_stage_ids',
                               default=lambda self: self._default_stage_id())
    priority = fields.Selection(fsm_stage.AVAILABLE_PRIORITIES,
                                string='Priority',
                                index=True,
                                default=fsm_stage.AVAILABLE_PRIORITIES[0][0])
    tag_ids = fields.Many2many('fsm.tag',
                               'fsm_order_tag_rel',
                               'fsm_order_id',
                               'tag_id',
                               string='Tags',
                               help="Classify and analyze your orders")
    color = fields.Integer('Color Index', default=0)
    team_id = fields.Many2one('fsm.team',
                              string='Team',
                              default=lambda self: self._default_team_id(),
                              index=True,
                              required=True,
                              track_visibility='onchange')

    # Request
    name = fields.Char(string='Name',
                       required=True,
                       index=True,
                       copy=False,
                       default=lambda self: _('New'))

    location_id = fields.Many2one('fsm.location',
                                  string='Location',
                                  index=True,
                                  required=True)
    location_directions = fields.Char(string='Location Directions')
    request_early = fields.Datetime(string='Earliest Request Date',
                                    default=datetime.now())
    color = fields.Integer('Color Index')
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 required=True,
                                 index=True,
                                 default=lambda self: self.env.user.company_id,
                                 help="Company related to this order")

    def _compute_request_late(self, vals):
        if vals.get('priority') == '0':
            if vals.get('request_early'):
                vals['request_late'] = fields.Datetime.\
                    from_string(vals.get('request_early')) + timedelta(days=3)
            else:
                vals['request_late'] = datetime.now() + timedelta(days=3)
        elif vals.get('priority') == '1':
            vals['request_late'] = fields.Datetime.\
                from_string(vals.get('request_early')) + timedelta(days=2)
        elif vals.get('priority') == '2':
            vals['request_late'] = fields.Datetime.\
                from_string(vals.get('request_early')) + timedelta(days=1)
        elif vals.get('priority') == '3':
            vals['request_late'] = fields.Datetime.\
                from_string(vals.get('request_early')) + timedelta(hours=8)
        return vals

    request_late = fields.Datetime(string='Latest Request Date')
    description = fields.Text(string='Description')

    person_ids = fields.Many2many('fsm.person', string='Field Service Workers')

    @api.onchange('location_id')
    def _onchange_location_id_customer(self):
        if self.company_id.auto_populate_equipments_on_order:
            fsm_equipment_rec = self.env['fsm.equipment'].search([
                ('current_location_id', '=', self.location_id.id)
            ])
            self.equipment_ids = [(6, 0, fsm_equipment_rec.ids)]

    # Planning
    person_id = fields.Many2one('fsm.person', string='Assigned To', index=True)
    person_phone = fields.Char(related="person_id.phone",
                               string="Worker Phone")
    scheduled_date_start = fields.Datetime(string='Scheduled Start (ETA)')
    scheduled_duration = fields.Float(string='Scheduled duration',
                                      help='Scheduled duration of the work in'
                                      ' hours')
    scheduled_date_end = fields.Datetime(string="Scheduled End")
    sequence = fields.Integer(string='Sequence', default=10)
    todo = fields.Text(string='Instructions')

    # Execution
    resolution = fields.Text(string='Resolution',
                             placeholder="Resolution of the order")
    date_start = fields.Datetime(string='Actual Start')
    date_end = fields.Datetime(string='Actual End')
    duration = fields.Float(string='Actual duration',
                            compute=_compute_duration,
                            help='Actual duration in hours')
    current_date = fields.Datetime(default=fields.datetime.now(), store=True)

    # Location
    territory_id = fields.Many2one('fsm.territory',
                                   string="Territory",
                                   related='location_id.territory_id',
                                   store=True)
    branch_id = fields.Many2one('fsm.branch',
                                string='Branch',
                                related='location_id.branch_id',
                                store=True)
    district_id = fields.Many2one('fsm.district',
                                  string='District',
                                  related='location_id.district_id',
                                  store=True)
    region_id = fields.Many2one('fsm.region',
                                string='Region',
                                related='location_id.region_id',
                                store=True)

    # Fields for Geoengine Identify
    display_name = fields.Char(related="name", string="Order")
    street = fields.Char(related="location_id.street")
    street2 = fields.Char(related="location_id.street2")
    zip = fields.Char(related="location_id.zip")
    city = fields.Char(related="location_id.city", string="City")
    state_name = fields.Char(related="location_id.state_id.name",
                             string='State',
                             ondelete='restrict')
    country_name = fields.Char(related="location_id.country_id.name",
                               string='Country',
                               ondelete='restrict')
    phone = fields.Char(related="location_id.phone", string="Location Phone")
    mobile = fields.Char(related="location_id.mobile")

    stage_name = fields.Char(related="stage_id.name", string="Stage Name")
    # Field for Stage Color
    custom_color = fields.Char(related="stage_id.custom_color",
                               string='Stage Color')

    # Template
    template_id = fields.Many2one('fsm.template', string="Template")
    category_ids = fields.Many2many('fsm.category', string="Categories")

    # Equipment used for Maintenance and Repair Orders
    equipment_id = fields.Many2one('fsm.equipment', string='Equipment')

    # Equipment used for all other Service Orders
    equipment_ids = fields.Many2many('fsm.equipment', string='Equipments')
    type = fields.Many2one('fsm.order.type', string="Type")

    internal_type = fields.Selection(string='Internal Type',
                                     related='type.internal_type')

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        search_domain = [('stage_type', '=', 'order')]
        if self.env.context.get('default_team_id'):
            search_domain = [
                '&', ('team_ids', 'in', self.env.context['default_team_id'])
            ] + search_domain
        return stages.search(search_domain, order=order)

    @api.model
    def create(self, vals):
        if vals.get('name', _('New')) == _('New'):
            vals['name'] = self.env['ir.sequence'].next_by_code('fsm.order') \
                or _('New')
        if vals.get('request_early',
                    False) and not vals.get('scheduled_date_start'):
            req_date = fields.Datetime.from_string(vals['request_early'])
            # Round scheduled date start
            req_date = req_date.replace(minute=0, second=0)
            vals.update({
                'scheduled_date_start': str(req_date),
                'request_early': str(req_date)
            })
        vals.update({
            'scheduled_date_end':
            self._context.get('default_scheduled_date_end') or False
        })
        self._calc_scheduled_dates(vals)
        if not vals.get('request_late'):
            if vals.get('priority') == '0':
                if vals.get('request_early'):
                    vals['request_late'] = \
                        fields.Datetime.from_string(vals.get('request_early'))\
                        + timedelta(days=3)
                else:
                    vals['request_late'] = datetime.now() + timedelta(days=3)
            elif vals.get('priority') == '1':
                vals['request_late'] = fields.Datetime.\
                    from_string(vals.get('request_early')) + timedelta(days=2)
            elif vals.get('priority') == '2':
                vals['request_late'] = fields.Datetime.\
                    from_string(vals.get('request_early')) + timedelta(days=1)
            elif vals.get('priority') == '3':
                vals['request_late'] = fields.Datetime.\
                    from_string(vals.get('request_early')) + timedelta(hours=8)
        return super(FSMOrder, self).create(vals)

    is_button = fields.Boolean(default=False)

    @api.multi
    def write(self, vals):
        if vals.get('stage_id', False) and vals.get('is_button', False):
            vals['is_button'] = False
        else:
            stage_id = self.env['fsm.stage'].browse(vals.get('stage_id'))
            if stage_id == self.env.ref('fieldservice.fsm_stage_completed'):
                raise UserError(_('Cannot move to completed from Kanban'))
        self._calc_scheduled_dates(vals)
        res = super(FSMOrder, self).write(vals)
        return res

    def can_unlink(self):
        """:return True if the order can be deleted, False otherwise"""
        return self.stage_id == self._default_stage_id()

    @api.multi
    def unlink(self):
        for order in self:
            if order.can_unlink():
                return super(FSMOrder, order).unlink()
            else:
                raise ValidationError(_("You cannot delete this order."))

    def _calc_scheduled_dates(self, vals):
        """Calculate scheduled dates and duration"""

        if (vals.get('scheduled_duration') or vals.get('scheduled_date_start')
                or vals.get('scheduled_date_end')):

            if (vals.get('scheduled_date_start')
                    and vals.get('scheduled_date_end')):
                new_date_start = fields.Datetime.from_string(
                    vals.get('scheduled_date_start', False))
                new_date_end = fields.Datetime.from_string(
                    vals.get('scheduled_date_end', False))
                hours = new_date_end.replace(
                    second=0) - new_date_start.replace(second=0)
                hrs = hours.total_seconds() / 3600
                vals['scheduled_duration'] = float(hrs)

            elif vals.get('scheduled_date_end'):
                hrs = vals.get('scheduled_duration',
                               False) or self.scheduled_duration or 0
                date_to_with_delta = fields.Datetime.from_string(
                    vals.get('scheduled_date_end',
                             False)) - timedelta(hours=hrs)
                vals['scheduled_date_start'] = str(date_to_with_delta)

            elif (vals.get('scheduled_duration', False)
                  or (vals.get('scheduled_date_start', False) and
                      (self.scheduled_date_start != vals.get(
                          'scheduled_date_start', False)))):
                hours = vals.get('scheduled_duration', False)
                start_date_val = vals.get('scheduled_date_start',
                                          self.scheduled_date_start)
                start_date = fields.Datetime.from_string(start_date_val)
                date_to_with_delta = start_date + timedelta(hours=hours)
                vals['scheduled_date_end'] = str(date_to_with_delta)

    def action_complete(self):
        return self.write({
            'stage_id':
            self.env.ref('fieldservice.fsm_stage_completed').id,
            'is_button':
            True
        })

    def action_cancel(self):
        return self.write(
            {'stage_id': self.env.ref('fieldservice.fsm_stage_cancelled').id})

    @api.onchange('scheduled_date_end')
    def onchange_scheduled_date_end(self):
        if self.scheduled_date_end:
            date_to_with_delta = fields.Datetime.from_string(
                self.scheduled_date_end) - \
                timedelta(hours=self.scheduled_duration)
            self.date_start = str(date_to_with_delta)

    @api.onchange('scheduled_duration')
    def onchange_scheduled_duration(self):
        if (self.scheduled_duration and self.scheduled_date_start):
            date_to_with_delta = fields.Datetime.from_string(
                self.scheduled_date_start) + \
                timedelta(hours=self.scheduled_duration)
            self.scheduled_date_end = str(date_to_with_delta)

    def copy_notes(self):
        old_desc = self.description
        self.description = ""
        self.location_directions = ""
        if self.type and self.type.name not in ['repair', 'maintenance']:
            for equipment_id in self.equipment_ids:
                if equipment_id:
                    if equipment_id.notes:
                        if self.description:
                            self.description = (self.description +
                                                equipment_id.notes + '\n ')
                        else:
                            self.description = (equipment_id.notes + '\n ')
        else:
            if self.equipment_id:
                if self.equipment_id.notes:
                    if self.description:
                        self.description = (self.description +
                                            self.equipment_id.notes + '\n ')
                    else:
                        self.description = (self.equipment_id.notes + '\n ')
        if self.location_id:
            self.location_directions = self.\
                _get_location_directions(self.location_id)
        if self.template_id:
            self.todo = self.template_id.instructions
        if self.description:
            self.description += '\n' + old_desc
        else:
            self.description = old_desc

    @api.onchange('location_id')
    def onchange_location_id(self):
        if self.location_id:
            self.territory_id = self.location_id.territory_id or False
            self.branch_id = self.location_id.branch_id or False
            self.district_id = self.location_id.district_id or False
            self.region_id = self.location_id.region_id or False
            self.copy_notes()

    @api.onchange('equipment_ids')
    def onchange_equipment_ids(self):
        self.copy_notes()

    @api.onchange('template_id')
    def _onchange_template_id(self):
        if self.template_id:
            self.category_ids = self.template_id.category_ids
            self.scheduled_duration = self.template_id.hours
            self.copy_notes()
            if self.template_id.type_id:
                self.type = self.template_id.type_id
            if self.template_id.team_id:
                self.team_id = self.template_id.team_id

    def _get_location_directions(self, location_id):
        self.location_directions = ""
        s = self.location_id.direction or ""
        parent_location = self.location_id.fsm_parent_id
        # ps => Parent Location Directions
        # s => String to Return
        while parent_location.id is not False:
            ps = parent_location.direction
            if ps:
                s += parent_location.direction
            parent_location = parent_location.fsm_parent_id
        return s

    @api.constrains('scheduled_date_start')
    def check_day(self):
        for rec in self:
            if rec.scheduled_date_start:
                holidays = self.env['resource.calendar.leaves'].search([
                    ('date_from', '>=', rec.scheduled_date_start),
                    ('date_to', '<=', rec.scheduled_date_start),
                ])
                if holidays:
                    raise ValidationError(
                        _("%s is a holiday (%s)." %
                          (rec.scheduled_date_start.date(), holidays[0].name)))
Exemple #27
0
class TechnicalSupportOrder(models.Model):
    _name = 'technical_support.order'
    _description = 'Technical Support Order'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'name desc'

    STATE_SELECTION = [('draft', 'DRAFT'), ('released', 'WAITING PARTS'),
                       ('consulting', 'CONSULTING FACTORY'),
                       ('ready', 'IN PROCESS'), ('done', 'DONE'),
                       ('cancel', 'CANCELED')]

    MAINTENANCE_TYPE_SELECTION = [('pm', 'Preventive'), ('cm', 'Corrective'),
                                  ('in', 'Instalación'), ('cbm', 'Predictive'),
                                  ('din', 'Uninstall'), ('fco', 'FCO')]

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'state' in init_values and self.state == 'ready':
            return 'technical_support.mt_order_confirmed'
        return super(TechnicalSupportOrder, self)._track_subtype(init_values)

    name = fields.Char('Reference', size=64)
    description = fields.Char(related='ticket_id.name',
                              string='Description',
                              size=64,
                              readonly=True,
                              track_visibility='onchange')
    origin = fields.Char(
        'Source Document',
        size=64,
        states={
            'done': [('readonly', True)],
            'cancel': [('readonly', True)]
        },
        help=
        "Reference of the document that generated this Technical Support Order.",
        track_visibility='onchange')
    state = fields.Selection(
        STATE_SELECTION,
        'Status',
        readonly=True,
        track_visibility='onchange',
        help=
        "When the maintenance order is created the status is set to 'Draft'.\n\
        If the order is confirmed the status is set to 'Waiting Parts'.\n\
        If the order is confirmed the status is set to 'Consulting Factory'.\n\
        If the stock is available then the status is set to 'Ready to Maintenance'.\n\
        When the maintenance is over, the status is set to 'Done'.",
        default='draft')
    maintenance_type = fields.Selection(MAINTENANCE_TYPE_SELECTION,
                                        'Maintenance Type',
                                        required=True,
                                        states={
                                            'done': [('readonly', True)],
                                            'cancel': [('readonly', True)]
                                        },
                                        default='cm',
                                        track_visibility='onchange')
    ticket_type_id = fields.Many2one('helpdesk.ticket.type',
                                     string="Ticket Type",
                                     track_visibility='onchange',
                                     states={
                                         'done': [('readonly', True)],
                                         'cancel': [('readonly', True)]
                                     })

    date_planned = fields.Datetime('Planned Date',
                                   required=True,
                                   readonly=True,
                                   states={'draft': [('readonly', False)]},
                                   default=time.strftime('%Y-%m-%d %H:%M:%S'),
                                   track_visibility='onchange')
    date_scheduled = fields.Datetime(
        'Start Date',
        required=True,
        states={
            'done': [('readonly', True)],
            'cancel': [('readonly', True)]
        },
        default=time.strftime('%Y-%m-%d %H:%M:%S'),
        track_visibility='onchange')
    date_execution = fields.Datetime(
        'Execution Date',
        required=True,
        states={
            'done': [('readonly', True)],
            'cancel': [('readonly', True)],
            'ready': [('readonly', True)]
        },
        default=time.strftime('%Y-%m-%d %H:%M:%S'),
        track_visibility='onchange')
    date_finish = fields.Datetime('Finish Date',
                                  required=True,
                                  states={
                                      'done': [('readonly', True)],
                                      'cancel': [('readonly', True)]
                                  },
                                  default=time.strftime('%Y-%m-%d %H:%M:%S'),
                                  track_visibility='onchange')

    tools_description = fields.Text('Tools Description',
                                    states={
                                        'done': [('readonly', True)],
                                        'cancel': [('readonly', True)]
                                    })
    labor_description = fields.Text('Labor Description',
                                    states={
                                        'done': [('readonly', True)],
                                        'cancel': [('readonly', True)]
                                    })
    operations_description = fields.Text('Operations Description',
                                         states={
                                             'done': [('readonly', True)],
                                             'cancel': [('readonly', True)]
                                         })
    documentation_description = fields.Text('Documentation Description',
                                            states={
                                                'done': [('readonly', True)],
                                                'cancel': [('readonly', True)]
                                            })
    problem_description = fields.Text(related='ticket_id.description',
                                      string='Problem Description',
                                      readonly=True,
                                      store=True,
                                      track_visibility='onchange')

    ticket_id = fields.Many2one('helpdesk.ticket',
                                string='Ticket',
                                track_visibility='onchange',
                                states={
                                    'done': [('readonly', True)],
                                    'cancel': [('readonly', True)]
                                })
    task_id = fields.Many2one('technical_support.task',
                              'Task',
                              states={
                                  'done': [('readonly', True)],
                                  'cancel': [('readonly', True)]
                              },
                              domain="[('model_id', '=', model_id)]")
    equipment_id = fields.Many2one('equipment.equipment',
                                   string='Equipment',
                                   required=True,
                                   readonly=True,
                                   states={'draft': [('readonly', False)]})
    user_id = fields.Many2one('res.users',
                              'Responsible',
                              track_visibility='onchange',
                              default=lambda self: self._uid,
                              states={
                                  'done': [('readonly', True)],
                                  'cancel': [('readonly', True)]
                              })
    company_id = fields.Many2one(
        'res.company',
        'Company',
        required=True,
        readonly=True,
        states={'draft': [('readonly', False)]},
        default=lambda self: self.env['res.company']._company_default_get(
            'technical_support.order'))
    procurement_group_id = fields.Many2one('procurement.group',
                                           'Procurement group',
                                           copy=False)
    category_ids = fields.Many2many(related='equipment_id.category_ids',
                                    string='equipment Category',
                                    readonly=True)
    wo_id = fields.Many2one('technical_support.workorder',
                            'Work Order',
                            ondelete='cascade')
    request_id = fields.Many2one('technical_support.request',
                                 'Request',
                                 ondelete='cascade')
    client_id = fields.Many2one('res.partner',
                                related='equipment_id.client_id',
                                string='Client',
                                store=True,
                                readonly=True)
    brand_id = fields.Many2one('equipment.brand',
                               related='equipment_id.brand_id',
                               string='Brand',
                               readonly=True)
    zone_id = fields.Many2one('equipment.zone',
                              related='equipment_id.zone_id',
                              string='Zone',
                              readonly=True)
    model_id = fields.Many2one('equipment.model',
                               related='equipment_id.model_id',
                               string='Model',
                               store=True,
                               readonly=True)
    parent_id = fields.Many2one('equipment.equipment',
                                related='equipment_id.parent_id',
                                string='Equipment Relation',
                                readonly=True)
    modality_id = fields.Many2one('equipment.modality',
                                  related='equipment_id.modality_id',
                                  string='Modality',
                                  store=True,
                                  readonly=True)
    order_id = fields.Many2one('technical_support.checklist.history',
                               string='Control List')
    equipment_state_id = fields.Many2one(
        'equipment.state',
        related='equipment_id.maintenance_state_id',
        string='Equipment State',
        domain=[('team', '=', '3')],
        readonly=True,
        store=True)

    parts_lines = fields.One2many('technical_support.order.parts.line',
                                  'maintenance_id',
                                  'Planned Parts',
                                  track_visibility='onchange',
                                  states={
                                      'done': [('readonly', True)],
                                      'cancel': [('readonly', True)]
                                  })
    assets_lines = fields.One2many('technical_support.order.assets.line',
                                   'maintenance_id',
                                   'Planned Tools',
                                   track_visibility='onchange',
                                   states={
                                       'done': [('readonly', True)],
                                       'cancel': [('readonly', True)]
                                   })
    checklist_lines = fields.One2many('technical_support.order.checklist.line',
                                      'maintenance_id',
                                      'CheckList',
                                      track_visibility='onchange',
                                      states={
                                          'done': [('readonly', True)],
                                          'cancel': [('readonly', True)]
                                      })
    signature_lines = fields.One2many('technical_support.order.signature.line',
                                      'maintenance_id',
                                      'Users',
                                      track_visibility='onchange')
    signature_client_lines = fields.One2many(
        'technical_support.order.signature.client.line',
        'maintenance_id',
        'Clients',
        track_visibility='onchange')

    serial = fields.Char(related='equipment_id.serial',
                         string='Serial',
                         readonly=True)
    equipment_number = fields.Char(related='equipment_id.equipment_number',
                                   string='N° de Equipo',
                                   readonly=True)
    location = fields.Char(related='equipment_id.location',
                           string='Location',
                           readonly=True)

    active = fields.Boolean(default=True)
    signature = fields.Binary('Signature',
                              help='Signature received through the portal.',
                              copy=False,
                              attachment=True)
    require_signature = fields.Boolean(
        'Online Signature',
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'sent': [('readonly', False)]
        },
        help=
        'Request a online signature to the customer in order to confirm orders automatically.'
    )
    signed_by = fields.Char('Signed by',
                            help='Name of the person that signed the SO.',
                            copy=False)
    wait_time = fields.Float(help="Wait Time in hours and minutes.",
                             track_visibility='onchange')
    transportation_time = fields.Float(
        help="Transportation Time in hours and minutes.",
        track_visibility='onchange')
    duration = fields.Float('Real Duration', store=True)

    detail_cause = fields.Text('Detail Causa', readonly=True)
    cause_reason = fields.Many2one('helpdesk.ticket.cause.reason',
                                   string='cause Reason',
                                   index=True,
                                   track_visibility='onchange')
    remote = fields.Boolean('Remote Attention', copy=False)
    close_order = fields.Boolean('Close Order Only', copy=False)
    close_ticket = fields.Boolean('Close Order and Ticket', copy=False)
    observation = fields.Boolean('Observation', copy=False)

    @api.onchange('equipment_id', 'maintenance_type')
    def onchange_equipment(self):
        if self.equipment_id:
            self.model_id = self.equipment_id.model_id
        return {
            'domain': {
                'task_id': [('model_id', 'in', self.model_id.ids),
                            ('maintenance_type', '=', self.maintenance_type)]
            }
        }

    @api.onchange('date_planned')
    def onchange_planned_date(self):
        self.date_scheduled = self.date_planned

    @api.onchange('date_scheduled')
    def onchange_scheduled_date(self):
        self.date_execution = self.date_scheduled


#    @api.onchange('date_execution')
#    def onchange_execution_date(self):
#        if self.state == 'draft':
#            self.date_planned = self.date_execution
#        else:
#            self.date_scheduled = self.date_execution

    @api.onchange('task_id')
    def onchange_task(self):
        task = self.task_id
        new_checklist_lines = []
        for line in task.checklist_lines:
            new_checklist_lines.append([
                0, 0, {
                    'name': line.name,
                    'question_id': line.question_id.id,
                    'answer': line.answer,
                }
            ])
        self.checklist_lines = new_checklist_lines
        self.description = task.name
        self.tools_description = task.tools_description
        self.labor_description = task.labor_description
        self.operations_description = task.operations_description
        self.documentation_description = task.documentation_description

    @api.onchange('ticket_id')
    def onchange_ticket(self):
        self.equipment_id = self.ticket_id.equipment_id
        self.user_id = self.ticket_id.user_id

    def test_ready(self):
        res = True
        for order in self:
            if order.parts_lines and order.procurement_group_id:
                states = []
                for procurement in order.procurement_group_id.procurement_ids:
                    states += [
                        move.state != 'assigned'
                        for move in procurement.move_ids
                        if move.location_dest_id.id ==
                        order.equipment_id.property_stock_equipment.id
                    ]
                if any(states) or len(states) == 0: res = False
        return res

    def test_if_parts(self):
        res = True
        for order in self:
            order.parts_lines.write({'state': 'released'})
            if not order.parts_lines:
                res = False
        return res

    # ACTIONS
    def action_confirm(self):
        self.write({'state': 'ready'})
        self.ticket_id.write({'stage_id': 2})
        self.request_id.write({'state': 'run'})
        return True

    def action_ready(self):
        self.write({'state': 'ready'})
        return True

    def action_done(self):
        self.request_id.write({'state': 'done'})
        for order in self:
            if order.test_if_parts():
                order.write({
                    'state':
                    'done',
                    'date_execution':
                    time.strftime('%Y-%m-%d %H:%M:%S')
                })
            else:
                order.write({
                    'state':
                    'done',
                    'date_execution':
                    time.strftime('%Y-%m-%d %H:%M:%S')
                })
        return 0

    def action_cancel(self):
        self.write({'state': 'cancel'})
        return True

    def ticket_done(self):
        for order in self:
            if order.ticket_id:
                order.ticket_id.write({'stage_id': 3})
                order.ticket_id.remote = order.remote
                order.ticket_id.observation = order.observation
                order.ticket_id.detail_cause = order.detail_cause
                order.ticket_id.cause_reason = order.cause_reason.id
        return True

    def action_change_equipment_ticket(self):
        for order in self:
            if order.ticket_id:
                order.ticket_id.equipment_id = order.equipment_id
        return True

    def action_change_equipment_tsr(self):
        for order in self:
            if order.request_id:
                order.request_id.equipment_id = order.equipment_id
        return True

    def _track_subtype(self, init_values):
        # init_values contains the modified fields' values before the changes
        #
        # the applied values can be accessed on the record as they are already
        # in cache
        self.ensure_one()
        if 'state' in init_values and self.state == 'done':
            return 'technical_support.mt_state_change'  # Full external id
        return super(TechnicalSupportOrder, self)._track_subtype(init_values)

    # CRUD
    @api.model
    def create(self, vals):
        if vals.get('name', '/') == '/':
            vals['name'] = self.env['ir.sequence'].next_by_code(
                'technical_support.order') or '/'
        request = super(TechnicalSupportOrder, self).create(vals)
        request.activity_update()
        return request

    @api.multi
    def write(self, vals):
        if vals.get('date_execution') and not vals.get('state'):
            # constraint for calendar view
            for order in self:
                if order.state == 'draft':
                    vals['date_planned'] = vals['date_execution']
                    vals['date_scheduled'] = vals['date_execution']
                elif order.state in ('released', 'ready'):
                    vals['date_scheduled'] = vals['date_execution']
                else:
                    del vals['date_execution']
        res = super(TechnicalSupportOrder, self).write(vals)
        if 'state' in vals:
            self.filtered(lambda m: m.state == 'ready')
            self.activity_feedback(
                ['technical_support.mail_act_technical_support_order'])
        if vals.get('user_id') or vals.get('date_planned'):
            self.activity_update()
        if vals.get('equipment_id'):
            # need to change description of activity also so unlink old and create new activity
            self.activity_unlink(
                ['technical_support.mail_act_technical_support_order'])
            self.activity_update()
        return res

    def activity_update(self):
        """ Update maintenance activities based on current record set state.
        It reschedule, unlink or create maintenance request activities. """
        self.filtered(
            lambda request: not request.date_planned).activity_unlink(
                ['technical_support.mail_act_technical_support_order'])
        for request in self.filtered(lambda request: request.date_planned):
            date_dl = fields.Datetime.from_string(request.date_planned).date()
            updated = request.activity_reschedule(
                ['technical_support.mail_act_technical_support_order'],
                date_deadline=date_dl,
                new_user_id=request.user_id.id or self.env.uid)
            if not updated:
                if request.equipment_id:
                    note = _(
                        'Request planned for <a href="#" data-oe-model="%s" data-oe-id="%s">%s</a>'
                    ) % (request.equipment_id._name, request.equipment_id.id,
                         request.equipment_id.display_name)
                else:
                    note = False
                request.activity_schedule(
                    'technical_support.mail_act_technical_support_order',
                    fields.Datetime.from_string(request.date_planned).date(),
                    note=note,
                    user_id=request.user_id.id or self.env.uid)

    @api.multi
    def action_send_mail(self):
        self.ensure_one()
        template_id = self.env.ref(
            'technical_support.mail_template_technical_support_consulting').id
        ctx = {
            'default_model': 'technical_support.order',
            'default_res_id': self.id,
            'default_use_template': bool(template_id),
            'default_template_id': template_id,
            'default_composition_mode': 'comment',
            'custom_layout': 'mail.mail_notification_light',
            'mark_consulting_as_sent': True,
        }
        return {
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'mail.compose.message',
            'target': 'new',
            'context': ctx,
        }

    @api.multi
    @api.returns('mail.message', lambda value: value.id)
    def message_post(self, **kwargs):
        if self.env.context.get('mark_consulting_as_sent'):
            self.filtered(lambda o: o.state == 'ready').write(
                {'state': 'consulting'})
        return super(TechnicalSupportOrder,
                     self.with_context(
                         mail_post_autofollow=True)).message_post(**kwargs)
Exemple #28
0
class MergePartnerAutomatic(models.TransientModel):
    """
        The idea behind this wizard is to create a list of potential partners to
        merge. We use two objects, the first one is the wizard for the end-user.
        And the second will contain the partner list to merge.
    """

    _name = 'base.partner.merge.automatic.wizard'

    @api.model
    def default_get(self, fields):
        res = super(MergePartnerAutomatic, self).default_get(fields)
        active_ids = self.env.context.get('active_ids')
        if self.env.context.get(
                'active_model') == 'res.partner' and active_ids:
            res['state'] = 'selection'
            res['partner_ids'] = active_ids
            res['dst_partner_id'] = self._get_ordered_partner(
                active_ids)[-1].id
        return res

    # Group by
    group_by_email = fields.Boolean('Email')
    group_by_name = fields.Boolean('Name')
    group_by_is_company = fields.Boolean('Is Company')
    group_by_vat = fields.Boolean('VAT')
    group_by_parent_id = fields.Boolean('Parent Company')

    state = fields.Selection([('option', 'Option'), ('selection', 'Selection'),
                              ('finished', 'Finished')],
                             readonly=True,
                             required=True,
                             string='State',
                             default='option')

    number_group = fields.Integer('Group of Contacts', readonly=True)
    current_line_id = fields.Many2one('base.partner.merge.line',
                                      string='Current Line')
    line_ids = fields.One2many('base.partner.merge.line',
                               'wizard_id',
                               string='Lines')
    partner_ids = fields.Many2many('res.partner', string='Contacts')
    dst_partner_id = fields.Many2one('res.partner',
                                     string='Destination Contact')

    exclude_contact = fields.Boolean('A user associated to the contact')
    exclude_journal_item = fields.Boolean(
        'Journal Items associated to the contact')
    maximum_group = fields.Integer('Maximum of Group of Contacts')

    # ----------------------------------------
    # Update method. Core methods to merge steps
    # ----------------------------------------

    def _get_fk_on(self, table):
        """ return a list of many2one relation with the given table.
            :param table : the name of the sql table to return relations
            :returns a list of tuple 'table name', 'column name'.
        """
        query = """
            SELECT cl1.relname as table, att1.attname as column
            FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, pg_attribute as att1, pg_attribute as att2
            WHERE con.conrelid = cl1.oid
                AND con.confrelid = cl2.oid
                AND array_lower(con.conkey, 1) = 1
                AND con.conkey[1] = att1.attnum
                AND att1.attrelid = cl1.oid
                AND cl2.relname = %s
                AND att2.attname = 'id'
                AND array_lower(con.confkey, 1) = 1
                AND con.confkey[1] = att2.attnum
                AND att2.attrelid = cl2.oid
                AND con.contype = 'f'
        """
        self._cr.execute(query, (table, ))
        return self._cr.fetchall()

    @api.model
    def _update_foreign_keys(self, src_partners, dst_partner):
        """ Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated.
            :param src_partners : merge source res.partner recordset (does not include destination one)
            :param dst_partner : record of destination res.partner
        """
        _logger.debug(
            '_update_foreign_keys for dst_partner: %s for src_partners: %s',
            dst_partner.id, str(src_partners.ids))

        # find the many2one relation to a partner
        Partner = self.env['res.partner']
        relations = self._get_fk_on('res_partner')

        for table, column in relations:
            if 'base_partner_merge_' in table:  # ignore two tables
                continue

            # get list of columns of current table (exept the current fk column)
            query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % (
                table)
            self._cr.execute(query, ())
            columns = []
            for data in self._cr.fetchall():
                if data[0] != column:
                    columns.append(data[0])

            # do the update for the current table/column in SQL
            query_dic = {
                'table': table,
                'column': column,
                'value': columns[0],
            }
            if len(columns) <= 1:
                # unique key treated
                query = """
                    UPDATE "%(table)s" as ___tu
                    SET %(column)s = %%s
                    WHERE
                        %(column)s = %%s AND
                        NOT EXISTS (
                            SELECT 1
                            FROM "%(table)s" as ___tw
                            WHERE
                                %(column)s = %%s AND
                                ___tu.%(value)s = ___tw.%(value)s
                        )""" % query_dic
                for partner in src_partners:
                    self._cr.execute(
                        query, (dst_partner.id, partner.id, dst_partner.id))
            else:
                try:
                    with mute_logger('odoo.sql_db'), self._cr.savepoint():
                        query = 'UPDATE "%(table)s" SET %(column)s = %%s WHERE %(column)s IN %%s' % query_dic
                        self._cr.execute(query, (
                            dst_partner.id,
                            tuple(src_partners.ids),
                        ))

                        # handle the recursivity with parent relation
                        if column == Partner._parent_name and table == 'res_partner':
                            query = """
                                WITH RECURSIVE cycle(id, parent_id) AS (
                                        SELECT id, parent_id FROM res_partner
                                    UNION
                                        SELECT  cycle.id, res_partner.parent_id
                                        FROM    res_partner, cycle
                                        WHERE   res_partner.id = cycle.parent_id AND
                                                cycle.id != cycle.parent_id
                                )
                                SELECT id FROM cycle WHERE id = parent_id AND id = %s
                            """
                            self._cr.execute(query, (dst_partner.id, ))
                            # NOTE JEM : shouldn't we fetch the data ?
                except psycopg2.Error:
                    # updating fails, most likely due to a violated unique constraint
                    # keeping record with nonexistent partner_id is useless, better delete it
                    query = 'DELETE FROM "%(table)s" WHERE "%(column)s" IN %%s' % query_dic
                    self._cr.execute(query, (tuple(src_partners.ids), ))

    @api.model
    def _update_reference_fields(self, src_partners, dst_partner):
        """ Update all reference fields from the src_partner to dst_partner.
            :param src_partners : merge source res.partner recordset (does not include destination one)
            :param dst_partner : record of destination res.partner
        """
        _logger.debug(
            '_update_reference_fields for dst_partner: %s for src_partners: %r',
            dst_partner.id, src_partners.ids)

        def update_records(model, src, field_model='model', field_id='res_id'):
            Model = self.env[model] if model in self.env else None
            if Model is None:
                return
            records = Model.sudo().search([(field_model, '=', 'res.partner'),
                                           (field_id, '=', src.id)])
            try:
                with mute_logger('odoo.sql_db'), self._cr.savepoint():
                    return records.sudo().write({field_id: dst_partner.id})
            except psycopg2.Error:
                # updating fails, most likely due to a violated unique constraint
                # keeping record with nonexistent partner_id is useless, better delete it
                return records.sudo().unlink()

        update_records = functools.partial(update_records)

        for partner in src_partners:
            update_records('calendar',
                           src=partner,
                           field_model='model_id.model')
            update_records('ir.attachment',
                           src=partner,
                           field_model='res_model')
            update_records('mail.followers',
                           src=partner,
                           field_model='res_model')
            update_records('mail.message', src=partner)
            update_records('marketing.campaign.workitem',
                           src=partner,
                           field_model='object_id.model')
            update_records('ir.model.data', src=partner)

        records = self.env['ir.model.fields'].search([('ttype', '=',
                                                       'reference')])
        for record in records.sudo():
            try:
                Model = self.env[record.model]
                field = Model._fields[record.name]
            except KeyError:
                # unknown model or field => skip
                continue

            if field.compute is not None:
                continue

            for partner in src_partners:
                records_ref = Model.sudo().search([
                    (record.name, '=', 'res.partner,%d' % partner.id)
                ])
                values = {
                    record.name: 'res.partner,%d' % dst_partner.id,
                }
                records_ref.sudo().write(values)

    @api.model
    def _update_values(self, src_partners, dst_partner):
        """ Update values of dst_partner with the ones from the src_partners.
            :param src_partners : recordset of source res.partner
            :param dst_partner : record of destination res.partner
        """
        _logger.debug(
            '_update_values for dst_partner: %s for src_partners: %r',
            dst_partner.id, src_partners.ids)

        model_fields = dst_partner.fields_get().keys()

        def write_serializer(item):
            if isinstance(item, models.BaseModel):
                return item.id
            else:
                return item

        # get all fields that are not computed or x2many
        values = dict()
        for column in model_fields:
            field = dst_partner._fields[column]
            if field.type not in ('many2many',
                                  'one2many') and field.compute is None:
                for item in itertools.chain(src_partners, [dst_partner]):
                    if item[column]:
                        values[column] = write_serializer(item[column])
        # remove fields that can not be updated (id and parent_id)
        values.pop('id', None)
        parent_id = values.pop('parent_id', None)
        dst_partner.write(values)
        # try to update the parent_id
        if parent_id and parent_id != dst_partner.id:
            try:
                dst_partner.write({'parent_id': parent_id})
            except ValidationError:
                _logger.info(
                    'Skip recursive partner hierarchies for parent_id %s of partner: %s',
                    parent_id, dst_partner.id)

    def _merge(self, partner_ids, dst_partner=None):
        """ private implementation of merge partner
            :param partner_ids : ids of partner to merge
            :param dst_partner : record of destination res.partner
        """
        Partner = self.env['res.partner']
        partner_ids = Partner.browse(partner_ids).exists()
        if len(partner_ids) < 2:
            return

        if len(partner_ids) > 3:
            raise UserError(
                _("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed."
                  ))

        # check if the list of partners to merge contains child/parent relation
        child_ids = self.env['res.partner']
        for partner_id in partner_ids:
            child_ids |= Partner.search([('id', 'child_of', [partner_id.id])
                                         ]) - partner_id
        if partner_ids & child_ids:
            raise UserError(
                _("You cannot merge a contact with one of his parent."))

        # check only admin can merge partners with different emails
        if SUPERUSER_ID != self.env.uid and len(
                set(partner.email for partner in partner_ids)) > 1:
            raise UserError(
                _("All contacts must have the same email. Only the Administrator can merge contacts with different emails."
                  ))

        # remove dst_partner from partners to merge
        if dst_partner and dst_partner in partner_ids:
            src_partners = partner_ids - dst_partner
        else:
            ordered_partners = self._get_ordered_partner(partner_ids.ids)
            dst_partner = ordered_partners[-1]
            src_partners = ordered_partners[:-1]
        _logger.info("dst_partner: %s", dst_partner.id)

        # FIXME: is it still required to make and exception for account.move.line since accounting v9.0 ?
        if SUPERUSER_ID != self.env.uid and 'account.move.line' in self.env and self.env[
                'account.move.line'].sudo().search([('partner_id', 'in', [
                    partner.id for partner in src_partners
                ])]):
            raise UserError(
                _("Only the destination contact may be linked to existing Journal Items. Please ask the Administrator if you need to merge several contacts linked to existing Journal Items."
                  ))

        # call sub methods to do the merge
        self._update_foreign_keys(src_partners, dst_partner)
        self._update_reference_fields(src_partners, dst_partner)
        self._update_values(src_partners, dst_partner)

        _logger.info('(uid = %s) merged the partners %r with %s', self._uid,
                     src_partners.ids, dst_partner.id)
        dst_partner.message_post(body='%s %s' %
                                 (_("Merged with the following partners:"),
                                  ", ".join('%s <%s> (ID %s)' %
                                            (p.name, p.email or 'n/a', p.id)
                                            for p in src_partners)))

        # delete source partner, since they are merged
        src_partners.unlink()

    # ----------------------------------------
    # Helpers
    # ----------------------------------------

    @api.model
    def _generate_query(self, fields, maximum_group=100):
        """ Build the SQL query on res.partner table to group them according to given criteria
            :param fields : list of column names to group by the partners
            :param maximum_group : limit of the query
        """
        # make the list of column to group by in sql query
        sql_fields = []
        for field in fields:
            if field in ['email', 'name']:
                sql_fields.append('lower(%s)' % field)
            elif field in ['vat']:
                sql_fields.append("replace(%s, ' ', '')" % field)
            else:
                sql_fields.append(field)
        group_fields = ', '.join(sql_fields)

        # where clause : for given group by columns, only keep the 'not null' record
        filters = []
        for field in fields:
            if field in ['email', 'name', 'vat']:
                filters.append((field, 'IS NOT', 'NULL'))
        criteria = ' AND '.join('%s %s %s' % (field, operator, value)
                                for field, operator, value in filters)

        # build the query
        text = [
            "SELECT min(id), array_agg(id)",
            "FROM res_partner",
        ]

        if criteria:
            text.append('WHERE %s' % criteria)

        text.extend([
            "GROUP BY %s" % group_fields,
            "HAVING COUNT(*) >= 2",
            "ORDER BY min(id)",
        ])

        if maximum_group:
            text.append("LIMIT %s" % maximum_group, )

        return ' '.join(text)

    @api.model
    def _compute_selected_groupby(self):
        """ Returns the list of field names the partner can be grouped (as merge
            criteria) according to the option checked on the wizard
        """
        groups = []
        group_by_prefix = 'group_by_'

        for field_name in self._fields:
            if field_name.startswith(group_by_prefix):
                if getattr(self, field_name, False):
                    groups.append(field_name[len(group_by_prefix):])

        if not groups:
            raise UserError(
                _("You have to specify a filter for your selection"))

        return groups

    @api.model
    def _partner_use_in(self, aggr_ids, models):
        """ Check if there is no occurence of this group of partner in the selected model
            :param aggr_ids : stringified list of partner ids separated with a comma (sql array_agg)
            :param models : dict mapping a model name with its foreign key with res_partner table
        """
        return any(self.env[model].search_count([(field, 'in', aggr_ids)])
                   for model, field in models.iteritems())

    @api.model
    def _get_ordered_partner(self, partner_ids):
        """ Helper : returns a `res.partner` recordset ordered by create_date/active fields
            :param partner_ids : list of partner ids to sort
        """
        return self.env['res.partner'].browse(partner_ids).sorted(
            key=lambda p: (p.active, p.create_date),
            reverse=True,
        )

    @api.multi
    def _compute_models(self):
        """ Compute the different models needed by the system if you want to exclude some partners. """
        model_mapping = {}
        if self.exclude_contact:
            model_mapping['res.users'] = 'partner_id'
        if 'account.move.line' in self.env and self.exclude_journal_item:
            model_mapping['account.move.line'] = 'partner_id'
        return model_mapping

    # ----------------------------------------
    # Actions
    # ----------------------------------------

    @api.multi
    def action_skip(self):
        """ Skip this wizard line. Don't compute any thing, and simply redirect to the new step."""
        if self.current_line_id:
            self.current_line_id.unlink()
        return self._action_next_screen()

    @api.multi
    def _action_next_screen(self):
        """ return the action of the next screen ; this means the wizard is set to treat the
            next wizard line. Each line is a subset of partner that can be merged together.
            If no line left, the end screen will be displayed (but an action is still returned).
        """
        self.invalidate_cache()  # FIXME: is this still necessary?
        values = {}
        if self.line_ids:
            # in this case, we try to find the next record.
            current_line = self.line_ids[0]
            current_partner_ids = literal_eval(current_line.aggr_ids)
            values.update({
                'current_line_id':
                current_line.id,
                'partner_ids': [(6, 0, current_partner_ids)],
                'dst_partner_id':
                self._get_ordered_partner(current_partner_ids)[-1].id,
                'state':
                'selection',
            })
        else:
            values.update({
                'current_line_id': False,
                'partner_ids': [],
                'state': 'finished',
            })

        self.write(values)

        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

    @api.multi
    def _process_query(self, query):
        """ Execute the select request and write the result in this wizard
            :param query : the SQL query used to fill the wizard line
        """
        self.ensure_one()
        model_mapping = self._compute_models()

        # group partner query
        self._cr.execute(query)

        counter = 0
        for min_id, aggr_ids in self._cr.fetchall():
            # To ensure that the used partners are accessible by the user
            partners = self.env['res.partner'].search([('id', 'in', aggr_ids)])
            if len(partners) < 2:
                continue

            # exclude partner according to options
            if model_mapping and self._partner_use_in(partners.ids,
                                                      model_mapping):
                continue

            self.env['base.partner.merge.line'].create({
                'wizard_id':
                self.id,
                'min_id':
                min_id,
                'aggr_ids':
                partners.ids,
            })
            counter += 1

        self.write({
            'state': 'selection',
            'number_group': counter,
        })

        _logger.info("counter: %s", counter)

    @api.multi
    def action_start_manual_process(self):
        """ Start the process 'Merge with Manual Check'. Fill the wizard according to the group_by and exclude
            options, and redirect to the first step (treatment of first wizard line). After, for each subset of
            partner to merge, the wizard will be actualized.
                - Compute the selected groups (with duplication)
                - If the user has selected the 'exclude_xxx' fields, avoid the partners
        """
        self.ensure_one()
        groups = self._compute_selected_groupby()
        query = self._generate_query(groups, self.maximum_group)
        self._process_query(query)
        return self._action_next_screen()

    @api.multi
    def action_start_automatic_process(self):
        """ Start the process 'Merge Automatically'. This will fill the wizard with the same mechanism as 'Merge
            with Manual Check', but instead of refreshing wizard with the current line, it will automatically process
            all lines by merging partner grouped according to the checked options.
        """
        self.ensure_one()
        self.action_start_manual_process(
        )  # here we don't redirect to the next screen, since it is automatic process
        self.invalidate_cache()  # FIXME: is this still necessary?

        for line in self.line_ids:
            partner_ids = literal_eval(line.aggr_ids)
            self._merge(partner_ids)
            line.unlink()
            self._cr.commit()  # TODO JEM : explain why

        self.write({'state': 'finished'})
        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

    @api.multi
    def parent_migration_process_cb(self):
        self.ensure_one()

        query = """
            SELECT
                min(p1.id),
                array_agg(DISTINCT p1.id)
            FROM
                res_partner as p1
            INNER join
                res_partner as p2
            ON
                p1.email = p2.email AND
                p1.name = p2.name AND
                (p1.parent_id = p2.id OR p1.id = p2.parent_id)
            WHERE
                p2.id IS NOT NULL
            GROUP BY
                p1.email,
                p1.name,
                CASE WHEN p1.parent_id = p2.id THEN p2.id
                    ELSE p1.id
                END
            HAVING COUNT(*) >= 2
            ORDER BY
                min(p1.id)
        """

        self._process_query(query)

        for line in self.line_ids:
            partner_ids = literal_eval(line.aggr_ids)
            self._merge(partner_ids)
            line.unlink()
            self._cr.commit()

        self.write({'state': 'finished'})

        self._cr.execute("""
            UPDATE
                res_partner
            SET
                is_company = NULL,
                parent_id = NULL
            WHERE
                parent_id = id
        """)

        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

    @api.multi
    def action_update_all_process(self):
        self.ensure_one()
        self.parent_migration_process_cb()

        # NOTE JEM : seems louche to create a new wizard instead of reuse the current one with updated options.
        # since it is like this from the initial commit of this wizard, I don't change it. yet ...
        wizard = self.create({
            'group_by_vat': True,
            'group_by_email': True,
            'group_by_name': True
        })
        wizard.action_start_automatic_process()

        # NOTE JEM : no idea if this query is usefull
        self._cr.execute("""
            UPDATE
                res_partner
            SET
                is_company = NULL
            WHERE
                parent_id IS NOT NULL AND
                is_company IS NOT NULL
        """)

        return self._action_next_screen()

    @api.multi
    def action_merge(self):
        """ Merge Contact button. Merge the selected partners, and redirect to
            the end screen (since there is no other wizard line to process.
        """
        if not self.partner_ids:
            self.write({'state': 'finished'})
            return {
                'type': 'ir.actions.act_window',
                'res_model': self._name,
                'res_id': self.id,
                'view_mode': 'form',
                'target': 'new',
            }

        self._merge(self.partner_ids.ids, self.dst_partner_id)

        if self.current_line_id:
            self.current_line_id.unlink()

        return self._action_next_screen()
Exemple #29
0
class Applicant(models.Model):
    _name = "hr.applicant"
    _description = "Applicant"
    _order = "priority desc, id desc"
    _inherit = ['mail.thread', 'ir.needaction_mixin', 'utm.mixin']
    _mail_mass_mailing = _('Applicants')

    def _default_stage_id(self):
        if self._context.get('default_job_id'):
            ids = self.env['hr.recruitment.stage'].search([
                '|', ('job_id', '=', False),
                ('job_id', '=', self._context['default_job_id']),
                ('fold', '=', False)
            ],
                                                          order='sequence asc',
                                                          limit=1).ids
            if ids:
                return ids[0]
        return False

    def _default_company_id(self):
        company_id = False
        if self._context.get('default_department_id'):
            department = self.env['hr.department'].browse(
                self._context['default_department_id'])
            company_id = department.company_id.id
        if not company_id:
            company_id = self.env['res.company']._company_default_get(
                'hr.applicant')
        return company_id

    name = fields.Char("Subject / Application Name", required=True)
    active = fields.Boolean(
        "Active",
        default=True,
        help=
        "If the active field is set to false, it will allow you to hide the case without removing it."
    )
    description = fields.Text("Description")
    email_from = fields.Char("Email",
                             size=128,
                             help="These people will receive email.")
    email_cc = fields.Text(
        "Watchers Emails",
        size=252,
        help=
        "These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"
    )
    probability = fields.Float("Probability")
    partner_id = fields.Many2one('res.partner', "Contact")
    create_date = fields.Datetime("Creation Date", readonly=True, index=True)
    write_date = fields.Datetime("Update Date", readonly=True)
    stage_id = fields.Many2one(
        'hr.recruitment.stage',
        'Stage',
        track_visibility='onchange',
        domain="['|', ('job_id', '=', False), ('job_id', '=', job_id)]",
        copy=False,
        index=True,
        group_expand='_read_group_stage_ids',
        default=_default_stage_id)
    last_stage_id = fields.Many2one(
        'hr.recruitment.stage',
        "Last Stage",
        help=
        "Stage of the applicant before being in the current stage. Used for lost cases analysis."
    )
    categ_ids = fields.Many2many('hr.applicant.category', string="Tags")
    company_id = fields.Many2one('res.company',
                                 "Company",
                                 default=_default_company_id)
    user_id = fields.Many2one('res.users',
                              "Responsible",
                              track_visibility="onchange",
                              default=lambda self: self.env.uid)
    date_closed = fields.Datetime("Closed", readonly=True, index=True)
    date_open = fields.Datetime("Assigned", readonly=True, index=True)
    date_last_stage_update = fields.Datetime("Last Stage Update",
                                             index=True,
                                             default=fields.Datetime.now)
    date_action = fields.Date("Next Action Date")
    title_action = fields.Char("Next Action", size=64)
    priority = fields.Selection(AVAILABLE_PRIORITIES,
                                "Appreciation",
                                default='0')
    job_id = fields.Many2one('hr.job', "Applied Job")
    salary_proposed_extra = fields.Char(
        "Proposed Salary Extra",
        help="Salary Proposed by the Organisation, extra advantages")
    salary_expected_extra = fields.Char(
        "Expected Salary Extra",
        help="Salary Expected by Applicant, extra advantages")
    salary_proposed = fields.Float("Proposed Salary",
                                   help="Salary Proposed by the Organisation")
    salary_expected = fields.Float("Expected Salary",
                                   help="Salary Expected by Applicant")
    availability = fields.Date(
        "Availability",
        help=
        "The date at which the applicant will be available to start working")
    partner_name = fields.Char("Applicant's Name")
    partner_phone = fields.Char("Phone", size=32)
    partner_mobile = fields.Char("Mobile", size=32)
    type_id = fields.Many2one('hr.recruitment.degree', "Degree")
    department_id = fields.Many2one('hr.department', "Department")
    reference = fields.Char("Referred By")
    day_open = fields.Float(compute='_compute_day', string="Days to Open")
    day_close = fields.Float(compute='_compute_day', string="Days to Close")
    color = fields.Integer("Color Index", default=0)
    emp_id = fields.Many2one('hr.employee',
                             string="Employee",
                             track_visibility="onchange",
                             help="Employee linked to the applicant.")
    user_email = fields.Char(related='user_id.email',
                             type="char",
                             string="User Email",
                             readonly=True)
    attachment_number = fields.Integer(compute='_get_attachment_number',
                                       string="Number of Attachments")
    employee_name = fields.Char(related='emp_id.name', string="Employee Name")
    attachment_ids = fields.One2many('ir.attachment',
                                     'res_id',
                                     domain=[('res_model', '=', 'hr.applicant')
                                             ],
                                     string='Attachments')

    @api.depends('date_open', 'date_closed')
    @api.one
    def _compute_day(self):
        if self.date_open:
            date_create = datetime.strptime(
                self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
            date_open = datetime.strptime(self.date_open,
                                          tools.DEFAULT_SERVER_DATETIME_FORMAT)
            self.day_open = (date_open - date_create).total_seconds() / (24.0 *
                                                                         3600)

        if self.date_closed:
            date_create = datetime.strptime(
                self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
            date_closed = datetime.strptime(
                self.date_closed, tools.DEFAULT_SERVER_DATETIME_FORMAT)
            self.day_close = (date_closed -
                              date_create).total_seconds() / (24.0 * 3600)

    @api.multi
    def _get_attachment_number(self):
        read_group_res = self.env['ir.attachment'].read_group(
            [('res_model', '=', 'hr.applicant'),
             ('res_id', 'in', self.ids)], ['res_id'], ['res_id'])
        attach_data = dict(
            (res['res_id'], res['res_id_count']) for res in read_group_res)
        for record in self:
            record.attachment_number = attach_data.get(record.id, 0)

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        # retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
        job_id = self._context.get('default_job_id')
        search_domain = [('job_id', '=', False)]
        if job_id:
            search_domain = ['|', ('job_id', '=', job_id)] + search_domain
        if stages:
            search_domain = ['|', ('id', 'in', stages.ids)] + search_domain

        stage_ids = stages._search(search_domain,
                                   order=order,
                                   access_rights_uid=SUPERUSER_ID)
        return stages.browse(stage_ids)

    @api.onchange('job_id')
    def onchange_job_id(self):
        vals = self._onchange_job_id_internal(self.job_id.id)
        self.department_id = vals['value']['department_id']
        self.user_id = vals['value']['user_id']
        self.stage_id = vals['value']['stage_id']

    def _onchange_job_id_internal(self, job_id):
        department_id = False
        user_id = False
        stage_id = self.stage_id.id
        if job_id:
            job = self.env['hr.job'].browse(job_id)
            department_id = job.department_id.id
            user_id = job.user_id.id
            if not self.stage_id:
                stage_ids = self.env['hr.recruitment.stage'].search(
                    [
                        '|', ('job_id', '=', False), ('job_id', '=', job.id),
                        ('fold', '=', False)
                    ],
                    order='sequence asc',
                    limit=1).ids
                stage_id = stage_ids[0] if stage_ids else False

        return {
            'value': {
                'department_id': department_id,
                'user_id': user_id,
                'stage_id': stage_id
            }
        }

    @api.onchange('partner_id')
    def onchange_partner_id(self):
        self.partner_phone = self.partner_id.phone
        self.partner_mobile = self.partner_id.mobile
        self.email_from = self.partner_id.email

    @api.onchange('stage_id')
    def onchange_stage_id(self):
        vals = self._onchange_stage_id_internal(self.stage_id.id)
        if vals['value'].get('date_closed'):
            self.date_closed = vals['value']['date_closed']

    def _onchange_stage_id_internal(self, stage_id):
        if not stage_id:
            return {'value': {}}
        stage = self.env['hr.recruitment.stage'].browse(stage_id)
        if stage.fold:
            return {'value': {'date_closed': fields.datetime.now()}}
        return {'value': {'date_closed': False}}

    @api.model
    def create(self, vals):
        if vals.get('department_id'
                    ) and not self._context.get('default_department_id'):
            self = self.with_context(
                default_department_id=vals.get('department_id'))
        if vals.get('job_id') or self._context.get('default_job_id'):
            job_id = vals.get('job_id') or self._context.get('default_job_id')
            for key, value in self._onchange_job_id_internal(
                    job_id)['value'].iteritems():
                if key not in vals:
                    vals[key] = value
        if vals.get('user_id'):
            vals['date_open'] = fields.Datetime.now()
        if 'stage_id' in vals:
            vals.update(
                self._onchange_stage_id_internal(
                    vals.get('stage_id'))['value'])
        return super(Applicant,
                     self.with_context(mail_create_nolog=True)).create(vals)

    @api.multi
    def write(self, vals):
        # user_id change: update date_open
        if vals.get('user_id'):
            vals['date_open'] = fields.Datetime.now()
        # stage_id: track last stage before update
        if 'stage_id' in vals:
            vals['date_last_stage_update'] = fields.Datetime.now()
            vals.update(
                self._onchange_stage_id_internal(
                    vals.get('stage_id'))['value'])
            for applicant in self:
                vals['last_stage_id'] = applicant.stage_id.id
                res = super(Applicant, self).write(vals)
        else:
            res = super(Applicant, self).write(vals)
        return res

    @api.model
    def get_empty_list_help(self, help):
        return super(
            Applicant,
            self.with_context(
                empty_list_help_model='hr.job',
                empty_list_help_id=self.env.context.get('default_job_id'),
                empty_list_help_document_name=_(
                    "job applicants"))).get_empty_list_help(help)

    @api.multi
    def action_get_created_employee(self):
        self.ensure_one()
        action = self.env['ir.actions.act_window'].for_xml_id(
            'hr', 'open_view_employee_list')
        action['res_id'] = self.mapped('emp_id').ids[0]
        return action

    @api.multi
    def action_makeMeeting(self):
        """ This opens Meeting's calendar view to schedule meeting on current applicant
            @return: Dictionary value for created Meeting view
        """
        self.ensure_one()
        partners = self.partner_id | self.user_id.partner_id | self.department_id.manager_id.user_id.partner_id

        category = self.env.ref('hr_recruitment.categ_meet_interview')
        res = self.env['ir.actions.act_window'].for_xml_id(
            'calendar', 'action_calendar_event')
        res['context'] = {
            'search_default_partner_ids': self.partner_id.name,
            'default_partner_ids': partners.ids,
            'default_user_id': self.env.uid,
            'default_name': self.name,
            'default_categ_ids': category and [category.id] or False,
        }
        return res

    @api.multi
    def action_get_attachment_tree_view(self):
        attachment_action = self.env.ref('base.action_attachment')
        action = attachment_action.read()[0]
        action['context'] = {
            'default_res_model': self._name,
            'default_res_id': self.ids[0]
        }
        action['domain'] = str(
            ['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)])
        action['search_view_id'] = (self.env.ref(
            'hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').
                                    id, )
        return action

    @api.multi
    def _track_template(self, tracking):
        res = super(Applicant, self)._track_template(tracking)
        applicant = self[0]
        changes, dummy = tracking[applicant.id]
        if 'stage_id' in changes and applicant.stage_id.template_id:
            res['stage_id'] = (applicant.stage_id.template_id, {
                'composition_mode': 'mass_mail'
            })
        return res

    @api.multi
    def _track_subtype(self, init_values):
        record = self[0]
        if 'emp_id' in init_values and record.emp_id:
            return 'hr_recruitment.mt_applicant_hired'
        elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1:
            return 'hr_recruitment.mt_applicant_new'
        elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence > 1:
            return 'hr_recruitment.mt_applicant_stage_changed'
        return super(Applicant, self)._track_subtype(init_values)

    @api.model
    def message_get_reply_to(self, ids, default=None):
        """ Override to get the reply_to of the parent project. """
        applicants = self.sudo().browse(ids)
        aliases = self.env['hr.job'].message_get_reply_to(
            applicants.mapped('job_id').ids, default=default)
        return dict(
            (applicant.id,
             aliases.get(applicant.job_id and applicant.job_id.id or 0, False))
            for applicant in applicants)

    @api.multi
    def message_get_suggested_recipients(self):
        recipients = super(Applicant, self).message_get_suggested_recipients()
        for applicant in self:
            if applicant.partner_id:
                applicant._message_add_suggested_recipient(
                    recipients,
                    partner=applicant.partner_id,
                    reason=_('Contact'))
            elif applicant.email_from:
                applicant._message_add_suggested_recipient(
                    recipients,
                    email=applicant.email_from,
                    reason=_('Contact Email'))
        return recipients

    @api.model
    def message_new(self, msg, custom_values=None):
        """ Overrides mail_thread message_new that is called by the mailgateway
            through message_process.
            This override updates the document according to the email.
        """
        # remove default author when going through the mail gateway. Indeed we
        # do not want to explicitly set user_id to False; however we do not
        # want the gateway user to be responsible if no other responsible is
        # found.
        self = self.with_context(default_user_id=False)
        val = msg.get('from').split('<')[0]
        defaults = {
            'name': msg.get('subject') or _("No Subject"),
            'partner_name': val,
            'email_from': msg.get('from'),
            'email_cc': msg.get('cc'),
            'partner_id': msg.get('author_id', False),
        }
        if msg.get('priority'):
            defaults['priority'] = msg.get('priority')
        if custom_values:
            defaults.update(custom_values)
        return super(Applicant, self).message_new(msg, custom_values=defaults)

    @api.multi
    def create_employee_from_applicant(self):
        """ Create an hr.employee from the hr.applicants """
        employee = False
        for applicant in self:
            address_id = contact_name = False
            if applicant.partner_id:
                address_id = applicant.partner_id.address_get(['contact'
                                                               ])['contact']
                contact_name = applicant.partner_id.name_get()[0][1]
            if applicant.job_id and (applicant.partner_name or contact_name):
                applicant.job_id.write({
                    'no_of_hired_employee':
                    applicant.job_id.no_of_hired_employee + 1
                })
                employee = self.env['hr.employee'].create({
                    'name':
                    applicant.partner_name or contact_name,
                    'job_id':
                    applicant.job_id.id,
                    'address_home_id':
                    address_id,
                    'department_id':
                    applicant.department_id.id or False,
                    'address_id':
                    applicant.company_id and applicant.company_id.partner_id
                    and applicant.company_id.partner_id.id or False,
                    'work_email':
                    applicant.department_id
                    and applicant.department_id.company_id
                    and applicant.department_id.company_id.email or False,
                    'work_phone':
                    applicant.department_id
                    and applicant.department_id.company_id
                    and applicant.department_id.company_id.phone or False
                })
                applicant.write({'emp_id': employee.id})
                applicant.job_id.message_post(
                    body=_('New Employee %s Hired') % applicant.partner_name
                    if applicant.partner_name else applicant.name,
                    subtype="hr_recruitment.mt_job_applicant_hired")
                employee._broadcast_welcome()
            else:
                raise UserError(
                    _('You must define an Applied Job and a Contact Name for this applicant.'
                      ))

        employee_action = self.env.ref('hr.open_view_employee_list')
        dict_act_window = employee_action.read([])[0]
        if employee:
            dict_act_window['res_id'] = employee.id
        dict_act_window['view_mode'] = 'form,tree'
        return dict_act_window

    @api.multi
    def archive_applicant(self):
        self.write({'active': False})

    @api.multi
    def reset_applicant(self):
        """ Reinsert the applicant into the recruitment pipe in the first stage"""
        default_stage_id = self._default_stage_id()
        self.write({'active': True, 'stage_id': default_stage_id})
Exemple #30
0
class MailActivityType(models.Model):
    """ Activity Types are used to categorize activities. Each type is a different
    kind of activity e.g. call, mail, meeting. An activity can be generic i.e.
    available for all models using activities; or specific to a model in which
    case res_model_id field should be used. """
    _name = 'mail.activity.type'
    _description = 'Activity Type'
    _rec_name = 'name'
    _order = 'sequence, id'

    @api.model
    def default_get(self, fields):
        if not self.env.context.get(
                'default_res_model_id') and self.env.context.get(
                    'default_res_model'):
            self = self.with_context(
                default_res_model_id=self.env['ir.model']._get(
                    self.env.context.get('default_res_model')))
        return super(MailActivityType, self).default_get(fields)

    name = fields.Char('Name', required=True, translate=True)
    summary = fields.Char('Summary', translate=True)
    sequence = fields.Integer('Sequence', default=10)
    active = fields.Boolean(default=True)
    delay_count = fields.Integer(
        'After',
        default=0,
        oldname='days',
        help=
        'Number of days/week/month before executing the action. It allows to plan the action deadline.'
    )
    delay_unit = fields.Selection([('days', 'days'), ('weeks', 'weeks'),
                                   ('months', 'months')],
                                  string="Delay units",
                                  help="Unit of delay",
                                  required=True,
                                  default='days')
    delay_from = fields.Selection(
        [('current_date', 'after validation date'),
         ('previous_activity', 'after previous activity deadline')],
        string="Delay Type",
        help="Type of delay",
        required=True,
        default='previous_activity')
    icon = fields.Char('Icon', help="Font awesome icon e.g. fa-tasks")
    decoration_type = fields.Selection(
        [('warning', 'Alert'), ('danger', 'Error')],
        string="Decoration Type",
        help=
        "Change the background color of the related activities of this type.")
    res_model_id = fields.Many2one(
        'ir.model',
        'Model',
        index=True,
        domain=['&', ('is_mail_thread', '=', True), ('transient', '=', False)],
        help='Specify a model if the activity should be specific to a model'
        ' and not available when managing activities for other models.')
    default_next_type_id = fields.Many2one(
        'mail.activity.type',
        'Default Next Activity',
        domain=
        "['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]"
    )
    force_next = fields.Boolean("Auto Schedule Next Activity", default=False)
    next_type_ids = fields.Many2many(
        'mail.activity.type',
        'mail_activity_rel',
        'activity_id',
        'recommended_id',
        domain=
        "['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]",
        string='Recommended Next Activities')
    previous_type_ids = fields.Many2many(
        'mail.activity.type',
        'mail_activity_rel',
        'recommended_id',
        'activity_id',
        domain=
        "['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]",
        string='Preceding Activities')
    category = fields.Selection(
        [('default', 'Other')],
        default='default',
        string='Category',
        help=
        'Categories may trigger specific behavior like opening calendar view')
    mail_template_ids = fields.Many2many('mail.template',
                                         string='Mails templates')

    #Fields for display purpose only
    initial_res_model_id = fields.Many2one(
        'ir.model',
        'Initial model',
        compute="_compute_initial_res_model_id",
        store=False,
        help=
        'Technical field to keep trace of the model at the beginning of the edition for UX related behaviour'
    )
    res_model_change = fields.Boolean(
        string="Model has change",
        help="Technical field for UX related behaviour",
        default=False,
        store=False)

    @api.onchange('res_model_id')
    def _onchange_res_model_id(self):
        self.mail_template_ids = self.mail_template_ids.filtered(
            lambda template: template.model_id == self.res_model_id)
        self.res_model_change = self.initial_res_model_id and self.initial_res_model_id != self.res_model_id

    def _compute_initial_res_model_id(self):
        for activity_type in self:
            activity_type.initial_res_model_id = activity_type.res_model_id