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]
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()
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 })
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()
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]
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
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
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
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
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')
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
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), } }
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()
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()
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)
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()
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'
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()
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()
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
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)))
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)
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()
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})
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