class MergeOpportunity(models.TransientModel): """ Merge opportunities together. If we're talking about opportunities, it's just because it makes more sense to merge opps than leads, because the leads are more ephemeral objects. But since opportunities are leads, it's also possible to merge leads together (resulting in a new lead), or leads and opps together (resulting in a new opp). """ _name = 'crm.merge.opportunity' _description = 'Merge Opportunities' @api.model def default_get(self, fields): """ Use active_ids from the context to fetch the leads/opps to merge. In order to get merged, these leads/opps can't be in 'Dead' or 'Closed' """ record_ids = self._context.get('active_ids') result = super(MergeOpportunity, self).default_get(fields) if record_ids: if 'opportunity_ids' in fields: opp_ids = self.env['crm.lead'].browse(record_ids).filtered( lambda opp: opp.probability < 100).ids result['opportunity_ids'] = opp_ids return result opportunity_ids = fields.Many2many('crm.lead', 'merge_opportunity_rel', 'merge_id', 'opportunity_id', string='Leads/Opportunities') user_id = fields.Many2one('res.users', 'Salesperson', index=True) team_id = fields.Many2one('crm.team', 'Sales Team', index=True) def action_merge(self): self.ensure_one() merge_opportunity = self.opportunity_ids.merge_opportunity( self.user_id.id, self.team_id.id) return merge_opportunity.redirect_lead_opportunity_view() @api.onchange('user_id') def _onchange_user(self): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ team_id = False if self.user_id: user_in_team = False if self.team_id: user_in_team = self.env['crm.team'].search_count([ ('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id) ]) if not user_in_team: team_id = self.env['crm.team'].search([ '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id) ], limit=1) self.team_id = team_id
class IrRule(models.Model): _name = 'ir.rule' _description = 'Record Rule' _order = 'model_id DESC' _MODES = ['read', 'write', 'create', 'unlink'] name = fields.Char(index=True) active = fields.Boolean(default=True, help="If you uncheck the active field, it will disable the record rule without deleting it (if you delete a native record rule, it may be re-created when you reload the module).") model_id = fields.Many2one('ir.model', string='Object', index=True, required=True, ondelete="cascade") groups = fields.Many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id') domain_force = fields.Text(string='Domain') perm_read = fields.Boolean(string='Apply for Read', default=True) perm_write = fields.Boolean(string='Apply for Write', default=True) perm_create = fields.Boolean(string='Apply for Create', default=True) perm_unlink = fields.Boolean(string='Apply for Delete', default=True) _sql_constraints = [ ('no_access_rights', 'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)', 'Rule must have at least one checked access right !'), ] def _eval_context_for_combinations(self): """Returns a dictionary to use as evaluation context for ir.rule domains, when the goal is to obtain python lists that are easier to parse and combine, but not to actually execute them.""" return {'user': tools.unquote('user'), 'time': tools.unquote('time')} @api.model def _eval_context(self): """Returns a dictionary to use as evaluation context for ir.rule domains.""" return {'user': self.env.user, 'time': time} @api.depends('groups') def _compute_global(self): for rule in self: rule['global'] = not rule.groups @api.constrains('model_id') def _check_model_transience(self): if any(self.env[rule.model_id.model].is_transient() for rule in self): raise ValidationError(_('Rules can not be applied on Transient models.')) @api.constrains('model_id') def _check_model_name(self): # Don't allow rules on rules records (this model). if any(rule.model_id.model == self._name for rule in self): raise ValidationError(_('Rules can not be applied on the Record Rules model.')) def _compute_domain_keys(self): """ Return the list of context keys to use for caching ``_compute_domain``. """ return [] @api.model @tools.conditional( 'xml' not in config['dev_mode'], tools.ormcache('self._uid', 'model_name', 'mode', 'tuple(self._context.get(k) for k in self._compute_domain_keys())'), ) def _compute_domain(self, model_name, mode="read"): if mode not in self._MODES: raise ValueError('Invalid mode: %r' % (mode,)) if self._uid == SUPERUSER_ID: return None query = """ SELECT r.id FROM ir_rule r JOIN ir_model m ON (r.model_id=m.id) WHERE m.model=%s AND r.active AND r.perm_{mode} AND (r.id IN (SELECT rule_group_id FROM rule_group_rel rg JOIN res_groups_users_rel gu ON (rg.group_id=gu.gid) WHERE gu.uid=%s) OR r.global) """.format(mode=mode) self._cr.execute(query, (model_name, self._uid)) rule_ids = [row[0] for row in self._cr.fetchall()] if not rule_ids: return [] # browse user and rules as SUPERUSER_ID to avoid access errors! eval_context = self._eval_context() user_groups = self.env.user.groups_id global_domains = [] # list of domains group_domains = [] # list of domains for rule in self.browse(rule_ids).sudo(): # evaluate the domain for the current user dom = safe_eval(rule.domain_force, eval_context) if rule.domain_force else [] dom = expression.normalize_domain(dom) if not rule.groups: global_domains.append(dom) elif rule.groups & user_groups: group_domains.append(dom) # combine global domains and group domains if not group_domains: return expression.AND(global_domains) return expression.AND(global_domains + [expression.OR(group_domains)]) @api.model def clear_cache(self): """ Deprecated, use `clear_caches` instead. """ self.clear_caches() @api.model def domain_get(self, model_name, mode='read'): dom = self._compute_domain(model_name, mode) if dom: # _where_calc is called as superuser. This means that rules can # involve objects on which the real uid has no acces rights. # This means also there is no implicit restriction (e.g. an object # references another object the user can't see). query = self.env[model_name].sudo()._where_calc(dom, active_test=False) return query.where_clause, query.where_clause_params, query.tables return [], [], ['"%s"' % self.env[model_name]._table] @api.multi def unlink(self): res = super(IrRule, self).unlink() self.clear_caches() return res @api.model_create_multi def create(self, vals_list): res = super(IrRule, self).create(vals_list) self.clear_caches() return res @api.multi def write(self, vals): res = super(IrRule, self).write(vals) self.clear_caches() return res
class KsDashboardNinjaBoard(models.Model): _name = 'ks_dashboard_ninja.board' name = fields.Char(string="Dashboard Name", required=True, size=35) ks_dashboard_items_ids = fields.One2many('ks_dashboard_ninja.item', 'ks_dashboard_ninja_board_id', string='Dashboard Items') ks_dashboard_menu_name = fields.Char(string="Menu Name") ks_dashboard_top_menu_id = fields.Many2one('ir.ui.menu', domain="[('parent_id','=',False)]", string="Show Under Menu") ks_dashboard_client_action_id = fields.Many2one('ir.actions.client') ks_dashboard_menu_id = fields.Many2one('ir.ui.menu') ks_dashboard_state = fields.Char() ks_dashboard_active = fields.Boolean(string="Active", default=True) ks_dashboard_group_access = fields.Many2many('res.groups', string="Group Access") # DateFilter Fields ks_dashboard_start_date = fields.Datetime() ks_dashboard_end_date = fields.Datetime() ks_date_filter_selection = fields.Selection([ ('l_none', 'All Time'), ('l_day', 'Today'), ('t_week', 'This Week'), ('t_month', 'This Month'), ('t_quarter', 'This Quarter'), ('t_year', 'This Year'), ('ls_day', 'Last Day'), ('ls_week', 'Last Week'), ('ls_month', 'Last Month'), ('ls_quarter', 'Last Quarter'), ('ls_year', 'Last Year'), ('l_week', 'Last 7 days'), ('l_month', 'Last 30 days'), ('l_quarter', 'Last 90 days'), ('l_year', 'Last 365 days'), ('l_custom', 'Custom Filter'), ], default='l_none') ks_gridstack_config = fields.Char('Item Configurations') ks_dashboard_default_template = fields.Many2one('ks_dashboard_ninja.board_template', default=lambda self: self.env.ref('ks_dashboard_ninja.ks_blank', False), string="Dashboard Template", required=True) ks_set_interval = fields.Selection([ (15000, '15 Seconds'), (30000, '30 Seconds'), (45000, '45 Seconds'), (60000, '1 minute'), (120000, '2 minute'), (300000, '5 minute'), (600000, '10 minute'), ], string="Update Interval") @api.model def create(self, vals): record = super(KsDashboardNinjaBoard, self).create(vals) if 'ks_dashboard_top_menu_id' in vals and 'ks_dashboard_menu_name' in vals: action_id = { 'name': vals['ks_dashboard_menu_name'] + " Action", 'res_model': 'ks_dashboard_ninja.board', 'tag': 'ks_dashboard_ninja', 'params': {'ks_dashboard_id': record.id}, } record.ks_dashboard_client_action_id = self.env['ir.actions.client'].sudo().create(action_id) record.ks_dashboard_menu_id = self.env['ir.ui.menu'].sudo().create({ 'name': vals['ks_dashboard_menu_name'], 'active': vals.get('ks_dashboard_active', True), 'parent_id': vals['ks_dashboard_top_menu_id'], 'action': "ir.actions.client," + str(record.ks_dashboard_client_action_id.id), 'groups_id': vals.get('ks_dashboard_group_access', False), }) if record.ks_dashboard_default_template.ks_item_count: ks_gridstack_config = {} template_data = json.loads(record.ks_dashboard_default_template.ks_gridstack_config) for item_data in template_data: dashboard_item = self.env.ref(item_data['item_id']).copy({'ks_dashboard_ninja_board_id': record.id}) ks_gridstack_config[dashboard_item.id] = item_data['data'] record.ks_gridstack_config = json.dumps(ks_gridstack_config) return record @api.multi def write(self, vals): record = super(KsDashboardNinjaBoard, self).write(vals) for rec in self: if 'ks_dashboard_menu_name' in vals: if self.env.ref('ks_dashboard_ninja.ks_my_default_dashboard_board') and self.env.ref( 'ks_dashboard_ninja.ks_my_default_dashboard_board').sudo().id == rec.id: if self.env.ref('ks_dashboard_ninja.board_menu_root', False): self.env.ref('ks_dashboard_ninja.board_menu_root').sudo().name = vals['ks_dashboard_menu_name'] else: rec.ks_dashboard_menu_id.sudo().name = vals['ks_dashboard_menu_name'] if 'ks_dashboard_group_access' in vals: if self.env.ref('ks_dashboard_ninja.ks_my_default_dashboard_board').id == rec.id: if self.env.ref('ks_dashboard_ninja.board_menu_root', False): self.env.ref('ks_dashboard_ninja.board_menu_root').groups_id = vals['ks_dashboard_group_access'] else: rec.ks_dashboard_menu_id.sudo().groups_id = vals['ks_dashboard_group_access'] if 'ks_dashboard_active' in vals and rec.ks_dashboard_menu_id: rec.ks_dashboard_menu_id.sudo().active = vals['ks_dashboard_active'] if 'ks_dashboard_top_menu_id' in vals: rec.ks_dashboard_menu_id.write( {'parent_id': vals['ks_dashboard_top_menu_id']} ) return record @api.multi def unlink(self): if self.env.ref('ks_dashboard_ninja.ks_my_default_dashboard_board').id in self.ids: raise ValidationError(_("Default Dashboard can't be deleted.")) else: for rec in self: rec.ks_dashboard_client_action_id.sudo().unlink() rec.ks_dashboard_menu_id.sudo().unlink() res = super(KsDashboardNinjaBoard, self).unlink() return res @api.model def ks_fetch_dashboard_data(self, ks_dashboard_id): self.ks_set_date(ks_dashboard_id) has_group_ks_dashboard_manager = self.env.user.has_group('ks_dashboard_ninja.ks_dashboard_ninja_group_manager') dashboard_data = { 'name': self.browse(ks_dashboard_id).name, 'ks_dashboard_manager': has_group_ks_dashboard_manager, 'ks_dashboard_list': self.search_read([], ['id', 'name']), 'ks_dashboard_start_date': self.browse(ks_dashboard_id).ks_dashboard_start_date, 'ks_dashboard_end_date': self.browse(ks_dashboard_id).ks_dashboard_end_date, 'ks_date_filter_selection': self.browse(ks_dashboard_id).ks_date_filter_selection, 'ks_gridstack_config': self.browse(ks_dashboard_id).ks_gridstack_config, 'ks_set_interval': self.browse(ks_dashboard_id).ks_set_interval, } if len(self.browse(ks_dashboard_id).ks_dashboard_items_ids) < 1: dashboard_data['ks_item_data'] = False else: items = self.ks_fetch_item(self.browse(ks_dashboard_id).ks_dashboard_items_ids.ids) dashboard_data['ks_item_data'] = items return dashboard_data @api.model def ks_fetch_item(self, item_list): """ :rtype: object :param item_list: list of item ids. :return: {'id':[item_data]} """ items = {} item_model = self.env['ks_dashboard_ninja.item'] for item_id in item_list: item = self.ks_fetch_item_data(item_model.browse(item_id)) items[item['id']] = item return items # fetching Item info (Divided to make function inherit easily) def ks_fetch_item_data(self, rec): """ :rtype: object :param item_id: item object :return: object with formatted item data """ item = { 'name': rec.name if rec.name else rec.ks_model_id.name if rec.ks_model_id else "Name", 'ks_background_color': rec.ks_background_color, 'ks_font_color': rec.ks_font_color, # 'ks_domain': rec.ks_domain.replace('"%UID"', str( # self.env.user.id)) if rec.ks_domain and "%UID" in rec.ks_domain else rec.ks_domain, 'ks_domain': rec.ks_convert_into_proper_domain(rec.ks_domain,rec), 'ks_icon': rec.ks_icon, 'ks_model_id': rec.ks_model_id.id, 'ks_model_name': rec.ks_model_name, 'ks_model_display_name': rec.ks_model_id.name, 'ks_record_count_type': rec.ks_record_count_type, 'ks_record_count': rec.ks_record_count, 'id': rec.id, 'ks_layout': rec.ks_layout, 'ks_icon_select': rec.ks_icon_select, 'ks_default_icon': rec.ks_default_icon, 'ks_default_icon_color': rec.ks_default_icon_color, # Pro Fields 'ks_dashboard_item_type': rec.ks_dashboard_item_type, 'ks_chart_item_color': rec.ks_chart_item_color, 'ks_chart_groupby_type': rec.ks_chart_groupby_type, 'ks_chart_relation_groupby': rec.ks_chart_relation_groupby.id, 'ks_chart_relation_groupby_name': rec.ks_chart_relation_groupby.name, 'ks_chart_date_groupby': rec.ks_chart_date_groupby, 'ks_record_field': rec.ks_record_field.id if rec.ks_record_field else False, 'ks_chart_data': rec.ks_chart_data, 'ks_list_view_data': rec.ks_list_view_data, 'ks_chart_data_count_type': rec.ks_chart_data_count_type, 'ks_bar_chart_stacked': rec.ks_bar_chart_stacked, 'ks_semi_circle_chart': rec.ks_semi_circle_chart, 'ks_list_view_type': rec.ks_list_view_type, 'ks_list_view_group_fields': rec.ks_list_view_group_fields.ids if rec.ks_list_view_group_fields else False, 'ks_previous_period': rec.ks_previous_period, 'ks_kpi_data': rec.ks_kpi_data, 'ks_goal_enable': rec.ks_goal_enable, 'ks_model_id_2': rec.ks_model_id_2.id, 'ks_record_field_2': rec.ks_record_field_2.id, 'ks_data_comparison': rec.ks_data_comparison, 'ks_target_view': rec.ks_target_view, 'ks_date_filter_selection': rec.ks_date_filter_selection, } return item def ks_set_date(self, ks_dashboard_id): ks_date_filter_selection = self.browse(ks_dashboard_id).ks_date_filter_selection if ks_date_filter_selection not in ['l_custom', 'l_none']: ks_date_data = ks_get_date(ks_date_filter_selection) self.browse(ks_dashboard_id).write({'ks_dashboard_end_date': ks_date_data["selected_end_date"], 'ks_dashboard_start_date': ks_date_data["selected_start_date"]}) @api.multi def load_previous_data(self): for rec in self: if rec.ks_dashboard_menu_id and rec.ks_dashboard_menu_id.action._table == 'ir_act_window': action_id = { 'name': rec['ks_dashboard_menu_name'] + " Action", 'res_model': 'ks_dashboard_ninja.board', 'tag': 'ks_dashboard_ninja', 'params': {'ks_dashboard_id': rec.id}, } rec.ks_dashboard_client_action_id = self.env['ir.actions.client'].sudo().create(action_id) rec.ks_dashboard_menu_id.write( {'action': "ir.actions.client," + str(rec.ks_dashboard_client_action_id.id)}) # fetching Item info (Divided to make function inherit easily) def ks_export_item_data(self, rec): ks_chart_measure_field = [] ks_chart_measure_field_2 = [] for res in rec.ks_chart_measure_field: ks_chart_measure_field.append(res.name) for res in rec.ks_chart_measure_field_2: ks_chart_measure_field_2.append(res.name) ks_list_view_group_fields = [] for res in rec.ks_list_view_group_fields: ks_list_view_group_fields.append(res.name) ks_goal_lines = [] for res in rec.ks_goal_lines: goal_line = { 'ks_goal_date': datetime.datetime.strftime(res.ks_goal_date, '%b %d, %Y'), 'ks_goal_value': res.ks_goal_value } ks_goal_lines.append(goal_line) ks_list_view_field = [] for res in rec.ks_list_view_fields: ks_list_view_field.append(res.name) item = { 'name': rec.name if rec.name else rec.ks_model_id.name if rec.ks_model_id else "Name", 'ks_background_color': rec.ks_background_color, 'ks_font_color': rec.ks_font_color, 'ks_domain': rec.ks_domain, 'ks_icon': rec.ks_icon, 'ks_id': rec.id, 'ks_model_id': rec.ks_model_name, 'ks_record_count': rec.ks_record_count, 'ks_layout': rec.ks_layout, 'ks_icon_select': rec.ks_icon_select, 'ks_default_icon': rec.ks_default_icon, 'ks_default_icon_color': rec.ks_default_icon_color, 'ks_record_count_type': rec.ks_record_count_type, # Pro Fields 'ks_dashboard_item_type': rec.ks_dashboard_item_type, 'ks_chart_item_color': rec.ks_chart_item_color, 'ks_chart_groupby_type': rec.ks_chart_groupby_type, 'ks_chart_relation_groupby': rec.ks_chart_relation_groupby.name, 'ks_chart_date_groupby': rec.ks_chart_date_groupby, 'ks_record_field': rec.ks_record_field.name, 'ks_chart_sub_groupby_type': rec.ks_chart_sub_groupby_type, 'ks_chart_relation_sub_groupby': rec.ks_chart_relation_sub_groupby.name, 'ks_chart_date_sub_groupby': rec.ks_chart_date_sub_groupby, 'ks_chart_data_count_type': rec.ks_chart_data_count_type, 'ks_chart_measure_field': ks_chart_measure_field, 'ks_chart_measure_field_2': ks_chart_measure_field_2, 'ks_list_view_fields': ks_list_view_field, 'ks_list_view_group_fields': ks_list_view_group_fields, 'ks_list_view_type': rec.ks_list_view_type, 'ks_record_data_limit': rec.ks_record_data_limit, 'ks_sort_by_order': rec.ks_sort_by_order, 'ks_sort_by_field': rec.ks_sort_by_field.name, 'ks_date_filter_field': rec.ks_date_filter_field.name, 'ks_goal_enable': rec.ks_goal_enable, 'ks_standard_goal_value': rec.ks_standard_goal_value, 'ks_goal_liness': ks_goal_lines, 'ks_date_filter_selection': rec.ks_date_filter_selection, 'ks_item_start_date': datetime.datetime.strftime(rec.ks_item_start_date, '%b %d, %Y') if rec.ks_item_start_date else False, 'ks_item_end_date': datetime.datetime.strftime(rec.ks_item_end_date, '%b %d, %Y') if rec.ks_item_end_date else False, 'ks_date_filter_selection_2': rec.ks_date_filter_selection_2, 'ks_item_start_date_2': datetime.datetime.strftime(rec.ks_item_start_date_2, '%b %d, %Y') if rec.ks_item_start_date_2 else False, 'ks_item_end_date_2': datetime.datetime.strftime(rec.ks_item_end_date_2, '%b %d, %Y') if rec.ks_item_end_date_2 else False, 'ks_previous_period': rec.ks_previous_period, 'ks_target_view': rec.ks_target_view, 'ks_data_comparison': rec.ks_data_comparison, 'ks_record_count_type_2': rec.ks_record_count_type_2, 'ks_record_field_2': rec.ks_record_field_2.name, 'ks_model_id_2': rec.ks_model_id_2.model, 'ks_date_filter_field_2': rec.ks_date_filter_field_2.name, } return item @api.model def ks_dashboard_export(self, ks_dashboard_ids): ks_dashboard_data = [] ks_dashboard_export_data = {} ks_dashboard_ids = json.loads(ks_dashboard_ids) for ks_dashboard_id in ks_dashboard_ids: dashboard_data = { 'name': self.browse(ks_dashboard_id).name, 'ks_dashboard_menu_name': self.browse(ks_dashboard_id).ks_dashboard_menu_name, 'ks_gridstack_config': self.browse(ks_dashboard_id).ks_gridstack_config, } if len(self.browse(ks_dashboard_id).ks_dashboard_items_ids) < 1: dashboard_data['ks_item_data'] = False else: items = [] for rec in self.browse(ks_dashboard_id).ks_dashboard_items_ids: item = self.ks_export_item_data(rec) items.append(item) dashboard_data['ks_item_data'] = items ks_dashboard_data.append(dashboard_data) ks_dashboard_export_data = { 'ks_file_format': 'ks_dashboard_ninja_export_file', 'ks_dashboard_data': ks_dashboard_data } return ks_dashboard_export_data @api.model def ks_import_dashboard(self, file): try: # ks_dashboard_data = json.loads(file) ks_dashboard_file_read = json.loads(file) except: raise ValidationError(_("This file is not supported")) if 'ks_file_format' in ks_dashboard_file_read and ks_dashboard_file_read[ 'ks_file_format'] == 'ks_dashboard_ninja_export_file': ks_dashboard_data = ks_dashboard_file_read['ks_dashboard_data'] else: raise ValidationError(_("Current Json File is not properly formatted according to Dashboard Ninja Model.")) ks_dashboard_key = ['name', 'ks_dashboard_menu_name', 'ks_gridstack_config'] ks_dashboard_item_key = ['ks_model_id', 'ks_chart_measure_field', 'ks_list_view_fields', 'ks_record_field', 'ks_chart_relation_groupby', 'ks_id'] # Fetching dashboard model info for data in ks_dashboard_data: if not all(key in data for key in ks_dashboard_key): raise ValidationError( _("Current Json File is not properly formatted according to Dashboard Ninja Model.")) vals = { 'name': data['name'], 'ks_dashboard_menu_name': data['ks_dashboard_menu_name'], 'ks_dashboard_top_menu_id': self.env.ref("ks_dashboard_ninja.board_menu_root").id, 'ks_dashboard_active': True, 'ks_gridstack_config': data['ks_gridstack_config'], 'ks_dashboard_default_template': self.env.ref("ks_dashboard_ninja.ks_blank").id, 'ks_dashboard_group_access': False, } # Creating Dashboard dashboard_id = self.create(vals) if data['ks_gridstack_config']: ks_gridstack_config = eval(data['ks_gridstack_config']) ks_grid_stack_config = {} if data['ks_item_data']: # Fetching dashboard item info for item in data['ks_item_data']: if not all(key in item for key in ks_dashboard_item_key): raise ValidationError( _("Current Json File is not properly formatted according to Dashboard Ninja Model.")) ks_model = item['ks_model_id'].replace(".", "_") ks_measure_field_ids = [] ks_measure_field_2_ids = [] model = self.env['ir.model'].search([('model', '=', item['ks_model_id'])]) if not model: raise ValidationError(_( "Please Install the Module which contains the following Model : %s " % item['ks_model_id'])) if item['ks_model_id_2']: model_2 = self.env['ir.model'].search([('model', '=', item['ks_model_id_2'])]) if not model_2: raise ValidationError(_( "Please Install the Module which contains the following Model : %s " % item['ks_model_id_2'])) for ks_measure in item['ks_chart_measure_field']: for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): measure_id = x + '.field_' + ks_model + "__" + ks_measure ks_measure_id = self.env.ref(measure_id, False) if ks_measure_id: ks_measure_field_ids.append(ks_measure_id.id) item['ks_chart_measure_field'] = [(6, 0, ks_measure_field_ids)] for ks_measure in item['ks_chart_measure_field_2']: for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): measure_id = x + '.field_' + ks_model + "__" + ks_measure ks_measure_id = self.env.ref(measure_id, False) if ks_measure_id: ks_measure_field_2_ids.append(ks_measure_id.id) item['ks_chart_measure_field_2'] = [(6, 0, ks_measure_field_2_ids)] ks_list_view_group_fields = [] for ks_measure in item['ks_list_view_group_fields']: for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): measure_id = x + '.field_' + ks_model + "__" + ks_measure ks_measure_id = self.env.ref(measure_id, False) if ks_measure_id: ks_list_view_group_fields.append(ks_measure_id.id) item['ks_list_view_group_fields'] = [(6, 0, ks_list_view_group_fields)] ks_list_view_field_ids = [] for ks_list_field in item['ks_list_view_fields']: for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): list_field_id = x + '.field_' + ks_model + "__" + ks_list_field ks_list_field_id = self.env.ref(list_field_id, False) if ks_list_field_id: ks_list_view_field_ids.append(ks_list_field_id.id) item['ks_list_view_fields'] = [(6, 0, ks_list_view_field_ids)] if item['ks_record_field']: ks_record_field = item['ks_record_field'] for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): record_id = x + '.field_' + ks_model + "__" + ks_record_field ks_record_id = self.env.ref(record_id, False) if ks_record_id: item['ks_record_field'] = ks_record_id.id if item['ks_date_filter_field']: ks_date_filter_field = item['ks_date_filter_field'] for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): record_id = x + '.field_' + ks_model + "__" + ks_date_filter_field ks_record_id = self.env.ref(record_id, False) if ks_record_id: item['ks_date_filter_field'] = ks_record_id.id if item['ks_chart_relation_groupby']: ks_group_by = item['ks_chart_relation_groupby'] for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): field_id = x + '.field_' + ks_model + "__" + ks_group_by ks_chart_relation_groupby = self.env.ref(field_id, False) if ks_chart_relation_groupby: item['ks_chart_relation_groupby'] = ks_chart_relation_groupby.id if item['ks_chart_relation_sub_groupby']: ks_group_by = item['ks_chart_relation_sub_groupby'] for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): field_id = x + '.field_' + ks_model + "__" + ks_group_by ks_chart_relation_sub_groupby = self.env.ref(field_id, False) if ks_chart_relation_sub_groupby: item['ks_chart_relation_sub_groupby'] = ks_chart_relation_sub_groupby.id # Sort by field : Many2one Entery if item['ks_sort_by_field']: ks_group_by = item['ks_sort_by_field'] for x in self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).modules.split(", "): field_id = x + '.field_' + ks_model + "__" + ks_group_by ks_sort_by_field = self.env.ref(field_id, False) if ks_sort_by_field: item['ks_sort_by_field'] = ks_sort_by_field.id ks_model_id = self.env['ir.model'].search([('model', '=', item['ks_model_id'])]).id if(item['ks_model_id_2']): ks_model_2 = item['ks_model_id_2'].replace(".", "_") ks_model_id_2 = self.env['ir.model'].search([('model', '=', item['ks_model_id_2'])]).id if item['ks_record_field_2']: ks_record_field = item['ks_record_field_2'] for x in self.env['ir.model'].search([('model', '=', item['ks_model_id_2'])]).modules.split( ", "): record_id = x + '.field_' + ks_model_2 + "__" + ks_record_field ks_record_id = self.env.ref(record_id, False) if ks_record_id: item['ks_record_field_2'] = ks_record_id.id if item['ks_date_filter_field_2']: ks_date_filter_field = item['ks_date_filter_field_2'] for x in self.env['ir.model'].search([('model', '=', item['ks_model_id_2'])]).modules.split( ", "): record_id = x + '.field_' + ks_model_2 + "__" + ks_date_filter_field ks_record_id = self.env.ref(record_id, False) if ks_record_id: item['ks_date_filter_field_2'] = ks_record_id.id item['ks_model_id_2'] = ks_model_id_2 else: item['ks_date_filter_field_2'] = False item['ks_record_field_2'] = False item['ks_model_id'] = ks_model_id item['ks_dashboard_ninja_board_id'] = dashboard_id.id ks_goal_lines = item['ks_goal_liness'].copy() if item.get('ks_goal_liness', False) else False item['ks_goal_liness'] = False item['ks_item_start_date'] = datetime.datetime.strptime(item['ks_item_start_date'], '%b %d, %Y') if \ item[ 'ks_item_start_date'] else False item['ks_item_end_date'] = datetime.datetime.strptime(item['ks_item_end_date'], '%b %d, %Y') if \ item[ 'ks_item_end_date'] else False item['ks_item_start_date_2'] = datetime.datetime.strptime(item['ks_item_start_date_2'], '%b %d, %Y') if \ item[ 'ks_item_start_date_2'] else False item['ks_item_end_date_2'] = datetime.datetime.strptime(item['ks_item_end_date_2'], '%b %d, %Y') if \ item[ 'ks_item_end_date_2'] else False # Creating dashboard items ks_item = self.env['ks_dashboard_ninja.item'].create(item) if ks_goal_lines and len(ks_goal_lines) != 0: for line in ks_goal_lines: line['ks_goal_date'] = datetime.datetime.strptime(line['ks_goal_date'], '%b %d, %Y') line['ks_dashboard_item'] = ks_item.id self.env['ks_dashboard_ninja.item_goal'].create(line) if data['ks_gridstack_config'] and str(item['ks_id']) in ks_gridstack_config: ks_grid_stack_config[str(ks_item.id)] = ks_gridstack_config[str(item['ks_id'])] self.browse(dashboard_id.id).write({ 'ks_gridstack_config': json.dumps(ks_grid_stack_config) }) return "Success"
class ReRegistrationResponceStudent(models.Model): _name = 're.reg.waiting.responce.student' _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Re-Registration Student" @api.depends('fees_line_ids') def compute_amount(self): for record in self: amount = 0.00 for fees_record in record.fees_line_ids: amount += fees_record.amount record.total_amount = amount @api.depends('total_amount', 'total_paid_amount') def compute_residual_amount_student(self): for record in self: record.residual = record.total_amount - record.total_paid_amount code = fields.Char('Code') name = fields.Many2one('res.partner', 'Name') reg_no = fields.Char('Registration') batch_id = fields.Many2one("batch", "Current Academic Year") course_id = fields.Many2one('course', 'Current Admission To Class') next_year_batch_id = fields.Many2one("batch", "Next Academic Year") next_year_course_id = fields.Many2one('course', 'Next Year Admission To Class') re_reg_parents = fields.Many2one('re.reg.waiting.responce.parents', 'Parent') parents_re_reg = fields.Many2one('re.reg.waiting.responce.parents', 'Reg Parent') confirm = fields.Boolean("Confirm Student") fee_status = fields.Selection([('re_unpaid', 'UnPaid'), ('re_partially_paid', 'Partially Paid'), ('re_Paid', 'Paid')], 'Fee Status', track_visibility='onchange') fees_line_ids = fields.Many2many('fees.line', 're_reg_fee_table', 're_reg_sid', 'fee_id', 'Fees Line') state = fields.Selection([('awaiting_response', 'Awaiting Response'), ('awaiting_re_registration_fee', 'Awaiting Re-Registration Fee'), \ ('re_registration_confirmed', 'Re-Registration Confirmed'), ('tc_expected', 'TC Expected')], track_visibility='onchange') total_amount = fields.Float('Total Amount', compute='compute_amount') total_paid_amount = fields.Float('Total Paid Amount') residual = fields.Float('Balance', compute='compute_residual_amount_student', readonly='1') confirmation_date = fields.Date('Confirmed On') user_id = fields.Many2one('res.users', 'Confirmed By') response = fields.Boolean("Response", default=False) @api.multi def re_send_payfort_payment_link_student(self): if self.residual > 0.00: stud_table_data = '<tr><td>%s</td><td>%s</td><td>Yes</td><td>%s</td></tr>' % ( self.name.name, self.next_year_course_id.name, self.total_amount) parent_rec = self.re_reg_parents parent_rec.send_re_registration_payment_link( parent_record=parent_rec, child_data_table=stud_table_data) @api.model def create(self, vals): vals['code'] = self.env['ir.sequence'].get( 're.reg.student.form') or '/' res = super(ReRegistrationResponceStudent, self).create(vals) return res @api.multi def come_tc_expected_to_waiting_fee(self): fees_structure_obj = self.env['fees.structure'] child_data_table = '' for re_reg_stud_rec in self: fee_line_id_list = [] fee_record = fees_structure_obj.search( [('academic_year_id', '=', re_reg_stud_rec.next_year_batch_id.id), ('course_id', '=', re_reg_stud_rec.next_year_course_id.id), ('type', '=', 're_reg')], limit=1) if not fee_record.id: raise except_orm(_('Warning!'), _("Re-Registration Fee is Not Define !")) for fee_line_rec in fee_record.fee_line_ids: fee_line_id_list.append(fee_line_rec.id) re_reg_stud_rec.write({ 'confirm': True, 're_reg_parents': re_reg_stud_rec.parents_re_reg.id, 'state': 'awaiting_re_registration_fee', 'fee_status': 're_unpaid', }) for fee_line_id in fee_line_id_list: re_reg_stud_rec.fees_line_ids = [(4, fee_line_id)] # # if student have already paid advance amount # student_advance_paid = re_reg_stud_rec.name.credit # if student_advance_paid < 0.00: # student_advance_paid = abs(student_advance_paid) # s_payble_amount = 0.00 # is_full_paid = False # if re_reg_stud_rec.residual > student_advance_paid: # s_payble_amount = student_advance_paid # else: # s_payble_amount = re_reg_stud_rec.residual # is_full_paid = True # if s_payble_amount > 0: # # self.re_reg_parents.re_reg_fee_reconcile_stud_advance(re_reg_partner_rec=re_reg_stud_rec, # # amount=s_payble_amount) # re_reg_stud_rec.total_paid_amount = s_payble_amount # if is_full_paid: # re_reg_stud_rec.write({ # 'fee_status': 're_Paid', # 're_reg_next_academic_year': 'yes', }) # else: # re_reg_stud_rec.fee_status = 're_partially_paid' child_data_table += '<tr><td>%s</td><td>%s</td><td>Yes</td><td>%s</td></tr>' % ( re_reg_stud_rec.name.name, re_reg_stud_rec.next_year_course_id.name, re_reg_stud_rec.total_amount) # send mail for fee reminder re_reg_stud_rec.parents_re_reg.send_re_registration_payment_link( parent_record=re_reg_stud_rec.parents_re_reg, child_data_table=child_data_table)
class BlogPost(models.Model): _name = "blog.post" _description = "Blog Post" _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.multi.mixin' ] _order = 'id DESC' _mail_post_access = 'read' @api.multi def _compute_website_url(self): super(BlogPost, self)._compute_website_url() for blog_post in self: blog_post.website_url = "/blog/%s/post/%s" % (slug( blog_post.blog_id), slug(blog_post)) @api.multi @api.depends('post_date', 'visits') def _compute_ranking(self): res = {} for blog_post in self: if blog_post.id: # avoid to rank one post not yet saved and so withtout post_date in case of an onchange. age = datetime.now() - fields.Datetime.from_string( blog_post.post_date) res[blog_post.id] = blog_post.visits * ( 0.5 + random.random()) / max(3, age.days) return res def _default_content(self): return ''' <section class="s_text_block"> <div class="container"> <div class="row"> <div class="col-lg-12 mb16 mt16"> <p class="o_default_snippet_text">''' + _( "Start writing here...") + '''</p> </div> </div> </div> </section> ''' name = fields.Char('Title', required=True, translate=True, default='') subtitle = fields.Char('Sub Title', translate=True) author_id = fields.Many2one('res.partner', 'Author', default=lambda self: self.env.user.partner_id) active = fields.Boolean('Active', default=True) cover_properties = fields.Text( 'Cover Properties', default= '{"background-image": "none", "background-color": "oe_black", "opacity": "0.2", "resize_class": ""}' ) blog_id = fields.Many2one('blog.blog', 'Blog', required=True, ondelete='cascade') tag_ids = fields.Many2many('blog.tag', string='Tags') content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize=False) teaser = fields.Text('Teaser', compute='_compute_teaser', inverse='_set_teaser') teaser_manual = fields.Text(string='Teaser Content') website_message_ids = fields.One2many( domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment')]) # creation / update stuff create_date = fields.Datetime('Created on', index=True, readonly=True) published_date = fields.Datetime('Published Date') post_date = fields.Datetime( 'Publishing date', compute='_compute_post_date', inverse='_set_post_date', store=True, help= "The blog post will be visible for your visitors as of this date on the website if it is set as published." ) create_uid = fields.Many2one('res.users', 'Created by', index=True, readonly=True) write_date = fields.Datetime('Last Updated on', index=True, readonly=True) write_uid = fields.Many2one('res.users', 'Last Contributor', index=True, readonly=True) author_avatar = fields.Binary(related='author_id.image_small', string="Avatar", readonly=False) visits = fields.Integer('No of Views', copy=False) ranking = fields.Float(compute='_compute_ranking', string='Ranking') website_id = fields.Many2one(related='blog_id.website_id', readonly=True) @api.multi @api.depends('content', 'teaser_manual') def _compute_teaser(self): for blog_post in self: if blog_post.teaser_manual: blog_post.teaser = blog_post.teaser_manual else: content = html2plaintext(blog_post.content).replace('\n', ' ') blog_post.teaser = content[:150] + '...' @api.multi def _set_teaser(self): for blog_post in self: blog_post.teaser_manual = blog_post.teaser @api.multi @api.depends('create_date', 'published_date') def _compute_post_date(self): for blog_post in self: if blog_post.published_date: blog_post.post_date = blog_post.published_date else: blog_post.post_date = blog_post.create_date @api.multi def _set_post_date(self): for blog_post in self: blog_post.published_date = blog_post.post_date if not blog_post.published_date: blog_post._write(dict(post_date=blog_post.create_date) ) # dont trigger inverse function def _check_for_publication(self, vals): if vals.get('website_published'): for post in self: post.blog_id.message_post_with_view( 'website_blog.blog_post_template_new_post', subject=post.name, values={'post': post}, subtype_id=self.env['ir.model.data'].xmlid_to_res_id( 'website_blog.mt_blog_blog_published')) return True return False @api.model def create(self, vals): post_id = super(BlogPost, self.with_context(mail_create_nolog=True)).create(vals) post_id._check_for_publication(vals) return post_id @api.multi def write(self, vals): result = True for post in self: copy_vals = dict(vals) if ('website_published' in vals and 'published_date' not in vals and (not post.published_date or post.published_date <= fields.Datetime.now())): copy_vals['published_date'] = vals[ 'website_published'] and fields.Datetime.now() or False result &= super(BlogPost, self).write(copy_vals) self._check_for_publication(vals) return result @api.multi def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to the post on website directly if user is an employee or if the post is published. """ self.ensure_one() user = access_uid and self.env['res.users'].sudo().browse( access_uid) or self.env.user if user.share and not self.sudo().website_published: return super(BlogPost, self).get_access_action(access_uid) return { 'type': 'ir.actions.act_url', 'url': self.website_url, 'target': 'self', 'target_type': 'public', 'res_id': self.id, } @api.multi def _notify_get_groups(self, message, groups): """ Add access button to everyone if the document is published. """ groups = super(BlogPost, self)._notify_get_groups(message, groups) if self.website_published: for group_name, group_method, group_data in groups: group_data['has_button_access'] = True return groups @api.multi def _notify_customize_recipients(self, message, msg_vals, recipients_vals): """ Override to avoid keeping all notified recipients of a comment. We avoid tracking needaction on post comments. Only emails should be sufficient. """ msg_type = msg_vals.get('message_type') or message.message_type if msg_type == 'comment': return {'needaction_partner_ids': []} return {} def _default_website_meta(self): res = super(BlogPost, self)._default_website_meta() res['default_opengraph']['og:description'] = res['default_twitter'][ 'twitter:description'] = self.subtitle blog_post_cover_properties = json.loads(self.cover_properties) res['default_opengraph']['og:image'] = res['default_twitter'][ 'twitter:image'] = blog_post_cover_properties.get( 'background-image', 'none')[4:-1] res['default_opengraph']['og:title'] = res['default_twitter'][ 'twitter:title'] = self.name return res
class HrEmployeeAttachment(models.Model): _inherit = 'ir.attachment' doc_attach_rel = fields.Many2many('education.documents', 'doc_attachment_id', 'attach_id3', 'doc_id', string="Attachment", invisible=1)
class ResPartner(models.Model): _inherit = 'res.partner' next_course_ids = fields.One2many(comodel_name='education.course.change', inverse_name='school_id', string='Next Courses') prev_course_ids = fields.One2many(comodel_name='education.course.change', inverse_name='next_school_id', string='Previous Courses') alumni_center_id = fields.Many2one(comodel_name='res.partner', string='Last Education Center', domain=[('educational_category', '=', 'school')]) alumni_academic_year_id = fields.Many2one( comodel_name='education.academic_year', string='Last Academic Year') alumni_member = fields.Boolean(string='Alumni Association Member') student_group_ids = fields.Many2many( comodel_name='education.group', relation='edu_group_student', column1='student_id', column2='group_id', string='Education Groups', readonly=True, domain="[('group_type_id.type', '=', 'official')]") current_group_id = fields.Many2one(comodel_name='education.group', string='Current Group', compute='_compute_current_group_id') current_center_id = fields.Many2one( comodel_name='res.partner', string='Current Education Center', compute='_compute_current_group_id', search='_search_current_center_id', domain="[('educational_category', '=', 'school')]") current_course_id = fields.Many2one(comodel_name='education.course', string='Current Course', compute='_compute_current_group_id', search='_search_current_course_id') childs_current_center_ids = fields.Many2many( comodel_name='res.partner', string='Children\'s Current Education Centers', compute='_compute_child_current_group_ids', search='_search_parent_current_center_id') childs_current_course_ids = fields.Many2many( comodel_name='education.course', string='Children\'s Current Courses', compute='_compute_child_current_group_ids', search='_search_parent_current_course_id') @api.depends('student_group_ids') def _compute_current_group_id(self): today = fields.Date.context_today(self) current_year = self.env['education.academic_year'].search( [('date_start', '<=', today), ('date_end', '>=', today)], limit=1) for partner in self.filtered( lambda p: p.educational_category == 'student'): groups = partner.student_group_ids.filtered( lambda g: g.group_type_id.type == 'official' and g. academic_year_id == current_year) partner.current_group_id = groups[:1] partner.current_center_id = partner.current_group_id.center_id partner.current_course_id = partner.current_group_id.course_id @api.multi def _search_current_center_id(self, operator, value): if operator == '=': centers = self.browse(value) else: centers = self.search([ (self._rec_name, operator, value), ('educational_category', '=', 'school'), ]) groups = self._search_current_groups().filtered( lambda g: g.center_id in centers) return [('id', 'in', groups.mapped('student_ids').ids)] @api.multi def _search_current_course_id(self, operator, value): course_obj = self.env['education.course'] if operator == '=': courses = course_obj.browse(value) else: courses = course_obj.search([ (course_obj._rec_name, operator, value), ]) groups = self._search_current_groups().filtered( lambda g: g.course_id in courses) return [('id', 'in', groups.mapped('student_ids').ids)] @api.depends('child_ids', 'child_ids.current_group_id') def _compute_child_current_group_ids(self): for partner in self.filtered( lambda p: p.educational_category == 'family'): childs_groups = partner.mapped('child_ids.current_group_id') partner.childs_current_center_ids = [ (6, 0, childs_groups.mapped('center_id').ids) ] partner.childs_current_course_ids = [ (6, 0, childs_groups.mapped('course_id').ids) ] @api.multi def _search_parent_current_center_id(self, operator, value): if operator == '=': centers = self.browse(value) else: centers = self.search([ (self._rec_name, operator, value), ('educational_category', '=', 'school'), ]) groups = self._search_current_groups().filtered( lambda g: g.center_id in centers) return [('id', 'in', groups.mapped('student_ids.parent_id').ids)] @api.multi def _search_parent_current_course_id(self, operator, value): course_obj = self.env['education.course'] if operator == '=': courses = course_obj.browse(value) else: courses = course_obj.search([ (course_obj._rec_name, operator, value), ]) groups = self._search_current_groups().filtered( lambda g: g.course_id in courses) return [('id', 'in', groups.mapped('student_ids.parent_id').ids)] @api.multi def _search_current_groups(self): today = fields.Date.context_today(self) current_year = self.env['education.academic_year'].search( [('date_start', '<=', today), ('date_end', '>=', today)], limit=1) official_type = self.env['education.group_type'].search([('type', '=', 'official')]) return self.env['education.group'].search([ ('academic_year_id', '=', current_year.id), ('group_type_id', 'in', official_type.ids) ])
class MailComposer(models.TransientModel): """ Generic message composition wizard. You may inherit from this wizard at model and view levels to provide specific features. The behavior of the wizard depends on the composition_mode field: - 'comment': post on a record. The wizard is pre-populated via ``get_record_data`` - 'mass_mail': wizard in mass mailing mode where the mail details can contain template placeholders that will be merged with actual data before being sent to each recipient. """ _name = 'mail.compose.message' _inherit = 'mail.message' _description = 'Email composition wizard' _log_access = True _batch_size = 500 @api.model def default_get(self, fields): """ Handle composition mode. Some details about context keys: - comment: default mode, model and ID of a record the user comments - default_model or active_model - default_res_id or active_id - reply: active_id of a message the user replies to - default_parent_id or message_id or active_id: ID of the mail.message we reply to - message.res_model or default_model - message.res_id or default_res_id - mass_mail: model and IDs of records the user mass-mails - active_ids: record IDs - default_model or active_model """ result = super(MailComposer, self).default_get(fields) # v6.1 compatibility mode result['composition_mode'] = result.get( 'composition_mode', self._context.get('mail.compose.message.mode', 'comment')) result['model'] = result.get('model', self._context.get('active_model')) result['res_id'] = result.get('res_id', self._context.get('active_id')) result['parent_id'] = result.get('parent_id', self._context.get('message_id')) if 'no_auto_thread' not in result and ( result['model'] not in self.env or not hasattr(self.env[result['model']], 'message_post')): result['no_auto_thread'] = True # default values according to composition mode - NOTE: reply is deprecated, fall back on comment if result['composition_mode'] == 'reply': result['composition_mode'] = 'comment' vals = {} if 'active_domain' in self._context: # not context.get() because we want to keep global [] domains vals['active_domain'] = '%s' % self._context.get('active_domain') if result['composition_mode'] == 'comment': vals.update(self.get_record_data(result)) for field in vals: if field in fields: result[field] = vals[field] # TDE HACK: as mailboxes used default_model='res.users' and default_res_id=uid # (because of lack of an accessible pid), creating a message on its own # profile may crash (res_users does not allow writing on it) # Posting on its own profile works (res_users redirect to res_partner) # but when creating the mail.message to create the mail.compose.message # access rights issues may rise # We therefore directly change the model and res_id if result['model'] == 'res.users' and result['res_id'] == self._uid: result['model'] = 'res.partner' result['res_id'] = self.env.user.partner_id.id if fields is not None: [ result.pop(field, None) for field in list(result) if field not in fields ] return result @api.model def _get_composition_mode_selection(self): return [('comment', 'Post on a document'), ('mass_mail', 'Email Mass Mailing'), ('mass_post', 'Post on Multiple Documents')] composition_mode = fields.Selection( selection=_get_composition_mode_selection, string='Composition mode', default='comment') partner_ids = fields.Many2many('res.partner', 'mail_compose_message_res_partner_rel', 'wizard_id', 'partner_id', 'Additional Contacts') use_active_domain = fields.Boolean('Use active domain') active_domain = fields.Text('Active domain', readonly=True) attachment_ids = fields.Many2many( 'ir.attachment', 'mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id', 'Attachments') is_log = fields.Boolean( 'Log an Internal Note', help='Whether the message is an internal note (comment mode only)') subject = fields.Char(default=False) # mass mode options notify = fields.Boolean( 'Notify followers', help='Notify followers of the document (mass post only)') auto_delete = fields.Boolean('Delete Emails', help='Delete sent emails (mass mailing only)') auto_delete_message = fields.Boolean( 'Delete Message Copy', help= 'Do not keep a copy of the email in the document communication history (mass mailing only)' ) template_id = fields.Many2one('mail.template', 'Use template', index=True, domain="[('model', '=', model)]") # mail_message updated fields message_type = fields.Selection(default="comment") subtype_id = fields.Many2one(default=lambda self: self.env['ir.model.data'] .xmlid_to_res_id('mail.mt_comment')) @api.multi def check_access_rule(self, operation): """ Access rules of mail.compose.message: - create: if - model, no res_id, I create a message in mass mail mode - then: fall back on mail.message acces rules """ # Author condition (CREATE (mass_mail)) if operation == 'create' and self._uid != SUPERUSER_ID: # read mail_compose_message.ids to have their values message_values = {} self._cr.execute( 'SELECT DISTINCT id, model, res_id FROM "%s" WHERE id = ANY (%%s) AND res_id = 0' % self._table, (self.ids, )) for mid, rmod, rid in self._cr.fetchall(): message_values[mid] = {'model': rmod, 'res_id': rid} # remove from the set to check the ids that mail_compose_message accepts author_ids = [ mid for mid, message in message_values.items() if message.get('model') and not message.get('res_id') ] self = self.browse(list(set(self.ids) - set(author_ids))) # not sure slef = ... return super(MailComposer, self).check_access_rule(operation) @api.multi def _notify(self, **kwargs): """ Override specific notify method of mail.message, because we do not want that feature in the wizard. """ return @api.model def get_record_data(self, values): """ Returns a defaults-like dict with initial values for the composition wizard when sending an email related a previous email (parent_id) or a document (model, res_id). This is based on previously computed default values. """ result, subject = {}, False if values.get('parent_id'): parent = self.env['mail.message'].browse(values.get('parent_id')) result['record_name'] = parent.record_name, subject = tools.ustr(parent.subject or parent.record_name or '') if not values.get('model'): result['model'] = parent.model if not values.get('res_id'): result['res_id'] = parent.res_id partner_ids = values.get('partner_ids', list()) + [ (4, id) for id in parent.partner_ids.ids ] if self._context.get( 'is_private' ) and parent.author_id: # check message is private then add author also in partner list. partner_ids += [(4, parent.author_id.id)] result['partner_ids'] = partner_ids elif values.get('model') and values.get('res_id'): doc_name_get = self.env[values.get('model')].browse( values.get('res_id')).name_get() result['record_name'] = doc_name_get and doc_name_get[0][1] or '' subject = tools.ustr(result['record_name']) re_prefix = _('Re:') if subject and not (subject.startswith('Re:') or subject.startswith(re_prefix)): subject = "%s %s" % (re_prefix, subject) result['subject'] = subject return result #------------------------------------------------------ # Wizard validation and send #------------------------------------------------------ # action buttons call with positionnal arguments only, so we need an intermediary function # to ensure the context is passed correctly @api.multi def action_send_mail(self): self.send_mail() return {'type': 'ir.actions.act_window_close', 'infos': 'mail_sent'} @api.multi def send_mail(self, auto_commit=False): """ Process the wizard content and proceed with sending the related email(s), rendering any template patterns on the fly if needed. """ notif_layout = self._context.get('custom_layout') # Several custom layouts make use of the model description at rendering, e.g. in the # 'View <document>' button. Some models are used for different business concepts, such as # 'purchase.order' which is used for a RFQ and and PO. To avoid confusion, we must use a # different wording depending on the state of the object. # Therefore, we can set the description in the context from the beginning to avoid falling # back on the regular display_name retrieved in '_notify_prepare_template_context'. model_description = self._context.get('model_description') for wizard in self: # Duplicate attachments linked to the email.template. # Indeed, basic mail.compose.message wizard duplicates attachments in mass # mailing mode. But in 'single post' mode, attachments of an email template # also have to be duplicated to avoid changing their ownership. if wizard.attachment_ids and wizard.composition_mode != 'mass_mail' and wizard.template_id: new_attachment_ids = [] for attachment in wizard.attachment_ids: if attachment in wizard.template_id.attachment_ids: new_attachment_ids.append( attachment.copy({ 'res_model': 'mail.compose.message', 'res_id': wizard.id }).id) else: new_attachment_ids.append(attachment.id) wizard.write({'attachment_ids': [(6, 0, new_attachment_ids)]}) # Mass Mailing mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post') Mail = self.env['mail.mail'] ActiveModel = self.env[wizard.model] if wizard.model and hasattr( self.env[wizard.model], 'message_post') else self.env['mail.thread'] if wizard.composition_mode == 'mass_post': # do not send emails directly but use the queue instead # add context key to avoid subscribing the author ActiveModel = ActiveModel.with_context( mail_notify_force_send=False, mail_create_nosubscribe=True) # wizard works in batch mode: [res_id] or active_ids or active_domain if mass_mode and wizard.use_active_domain and wizard.model: res_ids = self.env[wizard.model].search( safe_eval(wizard.active_domain)).ids elif mass_mode and wizard.model and self._context.get( 'active_ids'): res_ids = self._context['active_ids'] else: res_ids = [wizard.res_id] batch_size = int(self.env['ir.config_parameter'].sudo().get_param( 'mail.batch_size')) or self._batch_size sliced_res_ids = [ res_ids[i:i + batch_size] for i in range(0, len(res_ids), batch_size) ] if wizard.composition_mode == 'mass_mail' or wizard.is_log or ( wizard.composition_mode == 'mass_post' and not wizard.notify): # log a note: subtype is False subtype_id = False elif wizard.subtype_id: subtype_id = wizard.subtype_id.id else: subtype_id = self.env['ir.model.data'].xmlid_to_res_id( 'mail.mt_comment') for res_ids in sliced_res_ids: batch_mails = Mail all_mail_values = wizard.get_mail_values(res_ids) for res_id, mail_values in all_mail_values.items(): if wizard.composition_mode == 'mass_mail': batch_mails |= Mail.create(mail_values) else: post_params = dict( message_type=wizard.message_type, subtype_id=subtype_id, notif_layout=notif_layout, add_sign=not bool(wizard.template_id), mail_auto_delete=wizard.template_id.auto_delete if wizard.template_id else False, model_description=model_description, **mail_values) if ActiveModel._name == 'mail.thread' and wizard.model: post_params['model'] = wizard.model ActiveModel.browse(res_id).message_post(**post_params) if wizard.composition_mode == 'mass_mail': batch_mails.send(auto_commit=auto_commit) @api.multi def get_mail_values(self, res_ids): """Generate the values that will be used by send_mail to create mail_messages or mail_mails. """ self.ensure_one() results = dict.fromkeys(res_ids, False) rendered_values = {} mass_mail_mode = self.composition_mode == 'mass_mail' # render all template-based value at once if mass_mail_mode and self.model: rendered_values = self.render_message(res_ids) # compute alias-based reply-to in batch reply_to_value = dict.fromkeys(res_ids, None) if mass_mail_mode and not self.no_auto_thread: records = self.env[self.model].browse(res_ids) reply_to_value = self.env[ 'mail.thread']._notify_get_reply_to_on_records( default=self.email_from, records=records) blacklisted_rec_ids = [] if mass_mail_mode and hasattr(self.env[self.model], "_primary_email"): BL_sudo = self.env['mail.blacklist'].sudo() blacklist = set(BL_sudo.search([]).mapped('email')) if blacklist: [email_field] = self.env[self.model]._primary_email targets = self.env[self.model].browse(res_ids).read( [email_field]) # First extract email from recipient before comparing with blacklist for target in targets: sanitized_email = self.env[ 'mail.blacklist']._sanitize_email( target.get(email_field)) if sanitized_email and sanitized_email in blacklist: blacklisted_rec_ids.append(target['id']) for res_id in res_ids: # static wizard (mail.message) values mail_values = { 'subject': self.subject, 'body': self.body or '', 'parent_id': self.parent_id and self.parent_id.id, 'partner_ids': [partner.id for partner in self.partner_ids], 'attachment_ids': [attach.id for attach in self.attachment_ids], 'author_id': self.author_id.id, 'email_from': self.email_from, 'reply_to': self.reply_to, 'record_name': self.record_name, 'no_auto_thread': self.no_auto_thread, 'mail_server_id': self.mail_server_id.id, 'mail_activity_type_id': self.mail_activity_type_id.id, } # mass mailing: rendering override wizard static values if mass_mail_mode and self.model: mail_values.update( self.env['mail.thread']. _notify_specific_email_values_on_records( False, records=self.env[self.model].browse(res_id))) # keep a copy unless specifically requested, reset record name (avoid browsing records) mail_values.update(notification=not self.auto_delete_message, model=self.model, res_id=res_id, record_name=False) # auto deletion of mail_mail if self.auto_delete or self.template_id.auto_delete: mail_values['auto_delete'] = True # rendered values using template email_dict = rendered_values[res_id] mail_values['partner_ids'] += email_dict.pop('partner_ids', []) mail_values.update(email_dict) if not self.no_auto_thread: mail_values.pop('reply_to') if reply_to_value.get(res_id): mail_values['reply_to'] = reply_to_value[res_id] if self.no_auto_thread and not mail_values.get('reply_to'): mail_values['reply_to'] = mail_values['email_from'] # mail_mail values: body -> body_html, partner_ids -> recipient_ids mail_values['body_html'] = mail_values.get('body', '') mail_values['recipient_ids'] = [ (4, id) for id in mail_values.pop('partner_ids', []) ] # process attachments: should not be encoded before being processed by message_post / mail_mail create mail_values['attachments'] = [ (name, base64.b64decode(enc_cont)) for name, enc_cont in email_dict.pop( 'attachments', list()) ] attachment_ids = [] for attach_id in mail_values.pop('attachment_ids'): new_attach_id = self.env['ir.attachment'].browse( attach_id).copy({ 'res_model': self._name, 'res_id': self.id }) attachment_ids.append(new_attach_id.id) mail_values['attachment_ids'] = self.env[ 'mail.thread']._message_post_process_attachments( mail_values.pop('attachments', []), attachment_ids, { 'model': 'mail.message', 'res_id': 0 }) # Filter out the blacklisted records by setting the mail state to cancel -> Used for Mass Mailing stats if res_id in blacklisted_rec_ids: mail_values['state'] = 'cancel' # Do not post the mail into the recipient's chatter mail_values['notification'] = False results[res_id] = mail_values return results #------------------------------------------------------ # Template methods #------------------------------------------------------ @api.multi @api.onchange('template_id') def onchange_template_id_wrapper(self): self.ensure_one() values = self.onchange_template_id(self.template_id.id, self.composition_mode, self.model, self.res_id)['value'] for fname, value in values.items(): setattr(self, fname, value) @api.multi def onchange_template_id(self, template_id, composition_mode, model, res_id): """ - mass_mailing: we cannot render, so return the template values - normal mode: return rendered values /!\ for x2many field, this onchange return command instead of ids """ if template_id and composition_mode == 'mass_mail': template = self.env['mail.template'].browse(template_id) fields = [ 'subject', 'body_html', 'email_from', 'reply_to', 'mail_server_id' ] values = dict((field, getattr(template, field)) for field in fields if getattr(template, field)) if template.attachment_ids: values['attachment_ids'] = [ att.id for att in template.attachment_ids ] if template.mail_server_id: values['mail_server_id'] = template.mail_server_id.id if template.user_signature and 'body_html' in values: signature = self.env.user.signature values['body_html'] = tools.append_content_to_html( values['body_html'], signature, plaintext=False) elif template_id: values = self.generate_email_for_composer(template_id, [res_id])[res_id] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send attachment_ids = [] Attachment = self.env['ir.attachment'] for attach_fname, attach_datas in values.pop('attachments', []): data_attach = { 'name': attach_fname, 'datas': attach_datas, 'datas_fname': attach_fname, 'res_model': 'mail.compose.message', 'res_id': 0, 'type': 'binary', # override default_type from context, possibly meant for another model! } attachment_ids.append(Attachment.create(data_attach).id) if values.get('attachment_ids', []) or attachment_ids: values['attachment_ids'] = [(5, )] + values.get( 'attachment_ids', []) + attachment_ids else: default_values = self.with_context( default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get([ 'composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id' ]) values = dict((key, default_values[key]) for key in [ 'subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id' ] if key in default_values) if values.get('body_html'): values['body'] = values.pop('body_html') # This onchange should return command instead of ids for x2many field. # ORM handle the assignation of command list on new onchange (api.v8), # this force the complete replacement of x2many field with # command and is compatible with onchange api.v7 values = self._convert_to_write(values) return {'value': values} @api.multi def save_as_template(self): """ hit save as template button: current form value will be a new template attached to the current document. """ for record in self: model = self.env['ir.model']._get(record.model or 'mail.message') model_name = model.name or '' template_name = "%s: %s" % (model_name, tools.ustr(record.subject)) values = { 'name': template_name, 'subject': record.subject or False, 'body_html': record.body or False, 'model_id': model.id or False, 'attachment_ids': [(6, 0, [att.id for att in record.attachment_ids])], } template = self.env['mail.template'].create(values) # generate the saved template record.write({'template_id': template.id}) record.onchange_template_id_wrapper() return _reopen(self, record.id, record.model, context=self._context) #------------------------------------------------------ # Template rendering #------------------------------------------------------ @api.multi def render_message(self, res_ids): """Generate template-based values of wizard, for the document records given by res_ids. This method is meant to be inherited by email_template that will produce a more complete dictionary, using Jinja2 templates. Each template is generated for all res_ids, allowing to parse the template once, and render it multiple times. This is useful for mass mailing where template rendering represent a significant part of the process. Default recipients are also computed, based on mail_thread method message_get_default_recipients. This allows to ensure a mass mailing has always some recipients specified. :param browse wizard: current mail.compose.message browse record :param list res_ids: list of record ids :return dict results: for each res_id, the generated template values for subject, body, email_from and reply_to """ self.ensure_one() multi_mode = True if isinstance(res_ids, pycompat.integer_types): multi_mode = False res_ids = [res_ids] subjects = self.env['mail.template']._render_template( self.subject, self.model, res_ids) bodies = self.env['mail.template']._render_template(self.body, self.model, res_ids, post_process=True) emails_from = self.env['mail.template']._render_template( self.email_from, self.model, res_ids) replies_to = self.env['mail.template']._render_template( self.reply_to, self.model, res_ids) default_recipients = {} if not self.partner_ids: default_recipients = self.env[ 'mail.thread'].message_get_default_recipients( res_model=self.model, res_ids=res_ids) results = dict.fromkeys(res_ids, False) for res_id in res_ids: results[res_id] = { 'subject': subjects[res_id], 'body': bodies[res_id], 'email_from': emails_from[res_id], 'reply_to': replies_to[res_id], } results[res_id].update(default_recipients.get(res_id, dict())) # generate template-based values if self.template_id: template_values = self.generate_email_for_composer( self.template_id.id, res_ids, fields=[ 'email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id' ]) else: template_values = {} for res_id in res_ids: if template_values.get(res_id): # recipients are managed by the template results[res_id].pop('partner_ids') results[res_id].pop('email_to') results[res_id].pop('email_cc') # remove attachments from template values as they should not be rendered template_values[res_id].pop('attachment_ids', None) else: template_values[res_id] = dict() # update template values by composer values template_values[res_id].update(results[res_id]) return multi_mode and template_values or template_values[res_ids[0]] @api.model def generate_email_for_composer(self, template_id, res_ids, fields=None): """ Call email_template.generate_email(), get fields relevant for mail.compose.message, transform email_cc and email_to into partner_ids """ multi_mode = True if isinstance(res_ids, pycompat.integer_types): multi_mode = False res_ids = [res_ids] if fields is None: fields = [ 'subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id' ] returned_fields = fields + ['partner_ids', 'attachments'] values = dict.fromkeys(res_ids, False) template_values = self.env['mail.template'].with_context( tpl_partners_only=True).browse(template_id).generate_email( res_ids, fields=fields) for res_id in res_ids: res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field)) res_id_values['body'] = res_id_values.pop('body_html', '') values[res_id] = res_id_values return multi_mode and values or values[res_ids[0]]
class MergePartnerAutomatic(models.TransientModel): """ The idea behind this wizard is to create a list of potential partners to merge. We use two objects, the first one is the wizard for the end-user. And the second will contain the partner list to merge. """ _name = 'base.partner.merge.automatic.wizard' _description = 'Merge Partner 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'] = [(6, 0, 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') self.flush() 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('eagle.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), )) self.invalidate_cache() @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('eagle.sql_db'), self._cr.savepoint( ), self.env.clear_upon_failure(): records.sudo().write({field_id: dst_partner.id}) records.flush() except psycopg2.Error: # updating fails, most likely due to a violated unique constraint # keeping record with nonexistent partner_id is useless, better delete it 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('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) self.flush() def _get_summable_fields(self): """ Returns the list of fields that should be summed when merging partners """ return [] @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() summable_fields = self._get_summable_fields() 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]: if column in summable_fields and values.get(column): values[column] += write_serializer(item[column]) else: 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, extra_checks=True): """ private implementation of merge partner :param partner_ids : ids of partner to merge :param dst_partner : record of destination res.partner :param extra_checks: pass False to bypass extra sanity check (e.g. email address) """ # super-admin can be used to bypass extra checks if self.env.is_admin(): extra_checks = False 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.")) if extra_checks 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 extra_checks 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." )) # Make the company of all related users consistent for user in partner_ids.user_ids: user.sudo().write({ 'company_ids': [(6, 0, [dst_partner.company_id.id])], 'company_id': dst_partner.company_id.id }) # 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) self._log_merge_operation(src_partners, dst_partner) # delete source partner, since they are merged src_partners.unlink() def _log_merge_operation(self, src_partners, dst_partner): _logger.info('(uid = %s) merged the partners %r with %s', self._uid, src_partners.ids, dst_partner.id) # ---------------------------------------- # 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.items()) @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 or datetime.datetime(1970, 1, 1))), reverse=True, ) 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 # ---------------------------------------- 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() 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', } 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) 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() 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', } 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', } 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() 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 IrActionsReport(models.Model): _name = 'ir.actions.report' _description = 'Report Action' _inherit = 'ir.actions.actions' _table = 'ir_act_report_xml' _sequence = 'ir_actions_id_seq' _order = 'name' name = fields.Char(translate=True) type = fields.Char(default='ir.actions.report') binding_type = fields.Selection(default='report') model = fields.Char(required=True, string='Model Name') model_id = fields.Many2one('ir.model', string='Model', compute='_compute_model_id', search='_search_model_id') report_type = fields.Selection( [ ('qweb-html', 'HTML'), ('qweb-pdf', 'PDF'), ('qweb-text', 'Text'), ], required=True, default='qweb-pdf', help= 'The type of the report that will be rendered, each one having its own' ' rendering method. HTML means the report will be opened directly in your' ' browser PDF means the report will be rendered using Wkhtmltopdf and' ' downloaded by the user.') report_name = fields.Char( string='Template Name', required=True, help= "For QWeb reports, name of the template used in the rendering. The method 'render_html' of the model 'report.template_name' will be called (if any) to give the html. For RML reports, this is the LocalService name." ) report_file = fields.Char( string='Report File', required=False, readonly=False, store=True, help= "The path to the main report file (depending on Report Type) or empty if the content is in another field" ) groups_id = fields.Many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', string='Groups') multi = fields.Boolean( string='On Multiple Doc.', help= "If set to true, the action will not be displayed on the right toolbar of a form view." ) paperformat_id = fields.Many2one('report.paperformat', 'Paper Format') print_report_name = fields.Char( 'Printed Report Name', help= "This is the filename of the report going to download. Keep empty to not change the report filename. You can use a python expression with the 'object' and 'time' variables." ) attachment_use = fields.Boolean( string='Reload from Attachment', help= 'If you check this, then the second time the user prints with same attachment name, it returns the previous report.' ) attachment = fields.Char( string='Save as Attachment Prefix', help= 'This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.' ) @api.depends('model') def _compute_model_id(self): for action in self: action.model_id = self.env['ir.model']._get(action.model).id def _search_model_id(self, operator, value): ir_model_ids = None if isinstance(value, str): names = self.env['ir.model'].name_search(value, operator=operator) ir_model_ids = [n[0] for n in names] elif isinstance(value, Iterable): ir_model_ids = value elif isinstance(value, int) and not isinstance(value, bool): ir_model_ids = [value] if ir_model_ids: operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' ir_model = self.env['ir.model'].browse(ir_model_ids) return [('model', operator, ir_model.mapped('model'))] elif isinstance(value, bool) or value is None: return [('model', operator, value)] else: return FALSE_DOMAIN @api.multi def associated_view(self): """Used in the ir.actions.report form view in order to search naively after the view(s) used in the rendering. """ self.ensure_one() action_ref = self.env.ref('base.action_ui_view') if not action_ref or len(self.report_name.split('.')) < 2: return False action_data = action_ref.read()[0] action_data['domain'] = [('name', 'ilike', self.report_name.split('.')[1]), ('type', '=', 'qweb')] return action_data @api.multi def create_action(self): """ Create a contextual action for each report. """ for report in self: model = self.env['ir.model']._get(report.model) report.write({ 'binding_model_id': model.id, 'binding_type': 'report' }) return True @api.multi def unlink_action(self): """ Remove the contextual actions created for the reports. """ self.check_access_rights('write', raise_exception=True) self.filtered('binding_model_id').write({'binding_model_id': False}) return True #-------------------------------------------------------------------------- # Main report methods #-------------------------------------------------------------------------- @api.multi def retrieve_attachment(self, record): '''Retrieve an attachment for a specific record. :param record: The record owning of the attachment. :param attachment_name: The optional name of the attachment. :return: A recordset of length <=1 or None ''' attachment_name = safe_eval(self.attachment, { 'object': record, 'time': time }) if not attachment_name: return None return self.env['ir.attachment'].search( [('datas_fname', '=', attachment_name), ('res_model', '=', self.model), ('res_id', '=', record.id)], limit=1) @api.multi def postprocess_pdf_report(self, record, buffer): '''Hook to handle post processing during the pdf report generation. The basic behavior consists to create a new attachment containing the pdf base64 encoded. :param record_id: The record that will own the attachment. :param pdf_content: The optional name content of the file to avoid reading both times. :return: A modified buffer if the previous one has been modified, None otherwise. ''' attachment_name = safe_eval(self.attachment, { 'object': record, 'time': time }) if not attachment_name: return None attachment_vals = { 'name': attachment_name, 'datas': base64.encodestring(buffer.getvalue()), 'datas_fname': attachment_name, 'res_model': self.model, 'res_id': record.id, } try: self.env['ir.attachment'].create(attachment_vals) except AccessError: _logger.info("Cannot save PDF report %r as attachment", attachment_vals['name']) else: _logger.info('The PDF document %s is now saved in the database', attachment_vals['name']) return buffer @api.model def get_wkhtmltopdf_state(self): '''Get the current state of wkhtmltopdf: install, ok, upgrade, workers or broken. * install: Starting state. * upgrade: The binary is an older version (< 0.12.0). * ok: A binary was found with a recent version (>= 0.12.0). * workers: Not enough workers found to perform the pdf rendering process (< 2 workers). * broken: A binary was found but not responding. :return: wkhtmltopdf_state ''' return wkhtmltopdf_state @api.model def get_paperformat(self): return self.paperformat_id or self.env.user.company_id.paperformat_id @api.model def _build_wkhtmltopdf_args(self, paperformat_id, landscape, specific_paperformat_args=None, set_viewport_size=False): '''Build arguments understandable by wkhtmltopdf bin. :param paperformat_id: A report.paperformat record. :param landscape: Force the report orientation to be landscape. :param specific_paperformat_args: A dictionary containing prioritized wkhtmltopdf arguments. :param set_viewport_size: Enable a viewport sized '1024x1280' or '1280x1024' depending of landscape arg. :return: A list of string representing the wkhtmltopdf process command args. ''' if landscape is None and specific_paperformat_args and specific_paperformat_args.get( 'data-report-landscape'): landscape = specific_paperformat_args.get('data-report-landscape') command_args = ['--disable-local-file-access'] if set_viewport_size: command_args.extend( ['--viewport-size', landscape and '1024x1280' or '1280x1024']) # Passing the cookie to wkhtmltopdf in order to resolve internal links. try: if request: command_args.extend( ['--cookie', 'session_id', request.session.sid]) except AttributeError: pass # Less verbose error messages command_args.extend(['--quiet']) # Build paperformat args if paperformat_id: if paperformat_id.format and paperformat_id.format != 'custom': command_args.extend(['--page-size', paperformat_id.format]) if paperformat_id.page_height and paperformat_id.page_width and paperformat_id.format == 'custom': command_args.extend( ['--page-width', str(paperformat_id.page_width) + 'mm']) command_args.extend( ['--page-height', str(paperformat_id.page_height) + 'mm']) if specific_paperformat_args and specific_paperformat_args.get( 'data-report-margin-top'): command_args.extend([ '--margin-top', str(specific_paperformat_args['data-report-margin-top']) ]) else: command_args.extend( ['--margin-top', str(paperformat_id.margin_top)]) dpi = None if specific_paperformat_args and specific_paperformat_args.get( 'data-report-dpi'): dpi = int(specific_paperformat_args['data-report-dpi']) elif paperformat_id.dpi: if os.name == 'nt' and int(paperformat_id.dpi) <= 95: _logger.info( "Generating PDF on Windows platform require DPI >= 96. Using 96 instead." ) dpi = 96 else: dpi = paperformat_id.dpi if dpi: command_args.extend(['--dpi', str(dpi)]) if wkhtmltopdf_dpi_zoom_ratio: command_args.extend(['--zoom', str(96.0 / dpi)]) if specific_paperformat_args and specific_paperformat_args.get( 'data-report-header-spacing'): command_args.extend([ '--header-spacing', str(specific_paperformat_args['data-report-header-spacing'] ) ]) elif paperformat_id.header_spacing: command_args.extend( ['--header-spacing', str(paperformat_id.header_spacing)]) command_args.extend( ['--margin-left', str(paperformat_id.margin_left)]) command_args.extend( ['--margin-bottom', str(paperformat_id.margin_bottom)]) command_args.extend( ['--margin-right', str(paperformat_id.margin_right)]) if not landscape and paperformat_id.orientation: command_args.extend( ['--orientation', str(paperformat_id.orientation)]) if paperformat_id.header_line: command_args.extend(['--header-line']) if landscape: command_args.extend(['--orientation', 'landscape']) return command_args @api.multi def _prepare_html(self, html): '''Divide and recreate the header/footer html by merging all found in html. The bodies are extracted and added to a list. Then, extract the specific_paperformat_args. The idea is to put all headers/footers together. Then, we will use a javascript trick (see minimal_layout template) to set the right header/footer during the processing of wkhtmltopdf. This allows the computation of multiple reports in a single call to wkhtmltopdf. :param html: The html rendered by render_qweb_html. :type: bodies: list of string representing each one a html body. :type header: string representing the html header. :type footer: string representing the html footer. :type specific_paperformat_args: dictionary of prioritized paperformat values. :return: bodies, header, footer, specific_paperformat_args ''' IrConfig = self.env['ir.config_parameter'].sudo() base_url = IrConfig.get_param('report.url') or IrConfig.get_param( 'web.base.url') # Return empty dictionary if 'web.minimal_layout' not found. layout = self.env.ref('web.minimal_layout', False) if not layout: return {} layout = self.env['ir.ui.view'].browse( self.env['ir.ui.view'].get_view_id('web.minimal_layout')) root = lxml.html.fromstring(html) match_klass = "//div[contains(concat(' ', normalize-space(@class), ' '), ' {} ')]" header_node = etree.Element('div', id='minimal_layout_report_headers') footer_node = etree.Element('div', id='minimal_layout_report_footers') bodies = [] res_ids = [] body_parent = root.xpath('//main')[0] # Retrieve headers for node in root.xpath(match_klass.format('header')): body_parent = node.getparent() node.getparent().remove(node) header_node.append(node) # Retrieve footers for node in root.xpath(match_klass.format('footer')): body_parent = node.getparent() node.getparent().remove(node) footer_node.append(node) # Retrieve bodies for node in root.xpath(match_klass.format('article')): layout_with_lang = layout # set context language to body language if node.get('data-oe-lang'): layout_with_lang = layout_with_lang.with_context( lang=node.get('data-oe-lang')) body = layout_with_lang.render( dict(subst=False, body=lxml.html.tostring(node), base_url=base_url)) bodies.append(body) if node.get('data-oe-model') == self.model: res_ids.append(int(node.get('data-oe-id', 0))) else: res_ids.append(None) if not bodies: body = bytearray().join( [lxml.html.tostring(c) for c in body_parent.getchildren()]) bodies.append(body) # Get paperformat arguments set in the root html tag. They are prioritized over # paperformat-record arguments. specific_paperformat_args = {} for attribute in root.items(): if attribute[0].startswith('data-report-'): specific_paperformat_args[attribute[0]] = attribute[1] header = layout.render( dict(subst=True, body=lxml.html.tostring(header_node), base_url=base_url)) footer = layout.render( dict(subst=True, body=lxml.html.tostring(footer_node), base_url=base_url)) return bodies, res_ids, header, footer, specific_paperformat_args @api.model def _run_wkhtmltopdf(self, bodies, header=None, footer=None, landscape=False, specific_paperformat_args=None, set_viewport_size=False): '''Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf document. :param bodies: The html bodies of the report, one per page. :param header: The html header of the report containing all headers. :param footer: The html footer of the report containing all footers. :param landscape: Force the pdf to be rendered under a landscape format. :param specific_paperformat_args: dict of prioritized paperformat arguments. :param set_viewport_size: Enable a viewport sized '1024x1280' or '1280x1024' depending of landscape arg. :return: Content of the pdf as a string ''' paperformat_id = self.get_paperformat() # Build the base command args for wkhtmltopdf bin command_args = self._build_wkhtmltopdf_args( paperformat_id, landscape, specific_paperformat_args=specific_paperformat_args, set_viewport_size=set_viewport_size) files_command_args = [] temporary_files = [] if header: head_file_fd, head_file_path = tempfile.mkstemp( suffix='.html', prefix='report.header.tmp.') with closing(os.fdopen(head_file_fd, 'wb')) as head_file: head_file.write(header) temporary_files.append(head_file_path) files_command_args.extend(['--header-html', head_file_path]) if footer: foot_file_fd, foot_file_path = tempfile.mkstemp( suffix='.html', prefix='report.footer.tmp.') with closing(os.fdopen(foot_file_fd, 'wb')) as foot_file: foot_file.write(footer) temporary_files.append(foot_file_path) files_command_args.extend(['--footer-html', foot_file_path]) paths = [] for i, body in enumerate(bodies): prefix = '%s%d.' % ('report.body.tmp.', i) body_file_fd, body_file_path = tempfile.mkstemp(suffix='.html', prefix=prefix) with closing(os.fdopen(body_file_fd, 'wb')) as body_file: body_file.write(body) paths.append(body_file_path) temporary_files.append(body_file_path) pdf_report_fd, pdf_report_path = tempfile.mkstemp(suffix='.pdf', prefix='report.tmp.') os.close(pdf_report_fd) temporary_files.append(pdf_report_path) try: wkhtmltopdf = [ _get_wkhtmltopdf_bin() ] + command_args + files_command_args + paths + [pdf_report_path] process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = process.communicate() if process.returncode not in [0, 1]: if process.returncode == -11: message = _( 'Wkhtmltopdf failed (error code: %s). Memory limit too low or maximum file number of subprocess reached. Message : %s' ) else: message = _( 'Wkhtmltopdf failed (error code: %s). Message: %s') raise UserError(message % (str(process.returncode), err[-1000:])) else: if err: _logger.warning('wkhtmltopdf: %s' % err) except: raise with open(pdf_report_path, 'rb') as pdf_document: pdf_content = pdf_document.read() # Manual cleanup of the temporary files for temporary_file in temporary_files: try: os.unlink(temporary_file) except (OSError, IOError): _logger.error('Error when trying to remove file %s' % temporary_file) return pdf_content @api.model def _get_report_from_name(self, report_name): """Get the first record of ir.actions.report having the ``report_name`` as value for the field report_name. """ report_obj = self.env['ir.actions.report'] conditions = [('report_name', '=', report_name)] context = self.env['res.users'].context_get() return report_obj.with_context(context).search(conditions, limit=1) @api.model def barcode(self, barcode_type, value, width=600, height=100, humanreadable=0): if barcode_type == 'UPCA' and len(value) in (11, 12, 13): barcode_type = 'EAN13' if len(value) in (11, 12): value = '0%s' % value try: width, height, humanreadable = int(width), int(height), bool( int(humanreadable)) barcode = createBarcodeDrawing(barcode_type, value=value, format='png', width=width, height=height, humanReadable=humanreadable) return barcode.asString('png') except (ValueError, AttributeError): if barcode_type == 'Code128': raise ValueError("Cannot convert into barcode.") else: return self.barcode('Code128', value, width=width, height=height, humanreadable=humanreadable) @api.multi def render_template(self, template, values=None): """Allow to render a QWeb template python-side. This function returns the 'ir.ui.view' render but embellish it with some variables/methods used in reports. :param values: additionnal methods/variables used in the rendering :returns: html representation of the template """ if values is None: values = {} context = dict(self.env.context, inherit_branding=values.get('enable_editor')) # Browse the user instead of using the sudo self.env.user user = self.env['res.users'].browse(self.env.uid) website = None if request and hasattr(request, 'website'): if request.website is not None: website = request.website context = dict(context, translatable=context.get('lang') != request.env['ir.http']._get_default_lang().code) view_obj = self.env['ir.ui.view'].with_context(context) values.update( time=time, context_timestamp=lambda t: fields.Datetime.context_timestamp( self.with_context(tz=user.tz), t), editable=values.get('enable_editor'), user=user, res_company=user.company_id, website=website, web_base_url=self.env['ir.config_parameter'].sudo().get_param( 'web.base.url', default=''), ) return view_obj.render_template(template, values) @api.multi def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None): '''Merge the existing attachments by adding one by one the content of the attachments and then, we add the pdf_content if exists. Create the attachments for each record individually if required. :param save_in_attachment: The retrieved attachments as map record.id -> attachment_id. :param pdf_content: The pdf content newly generated by wkhtmltopdf. :param res_ids: the ids of record to allow postprocessing. :return: The pdf content of the merged pdf. ''' def close_streams(streams): for stream in streams: try: stream.close() except Exception: pass # Check special case having only one record with existing attachment. if len(save_in_attachment) == 1 and not pdf_content: return base64.decodestring( list(save_in_attachment.values())[0].datas) # Create a list of streams representing all sub-reports part of the final result # in order to append the existing attachments and the potentially modified sub-reports # by the postprocess_pdf_report calls. streams = [] # In wkhtmltopdf has been called, we need to split the pdf in order to call the postprocess method. if pdf_content: pdf_content_stream = io.BytesIO(pdf_content) # Build a record_map mapping id -> record record_map = { r.id: r for r in self.env[self.model].browse( [res_id for res_id in res_ids if res_id]) } # If no value in attachment or no record specified, only append the whole pdf. if not record_map or not self.attachment: streams.append(pdf_content_stream) else: if len(res_ids) == 1: # Only one record, so postprocess directly and append the whole pdf. if res_ids[0] in record_map and not res_ids[ 0] in save_in_attachment: new_stream = self.postprocess_pdf_report( record_map[res_ids[0]], pdf_content_stream) # If the buffer has been modified, mark the old buffer to be closed as well. if new_stream and new_stream != pdf_content_stream: close_streams([pdf_content_stream]) pdf_content_stream = new_stream streams.append(pdf_content_stream) else: # In case of multiple docs, we need to split the pdf according the records. # To do so, we split the pdf based on outlines computed by wkhtmltopdf. # An outline is a <h?> html tag found on the document. To retrieve this table, # we look on the pdf structure using pypdf to compute the outlines_pages that is # an array like [0, 3, 5] that means a new document start at page 0, 3 and 5. reader = PdfFileReader(pdf_content_stream) if reader.trailer['/Root'].get('/Dests'): outlines_pages = sorted([ outline.getObject()[0] for outline in reader.trailer['/Root']['/Dests'].values() ]) assert len(outlines_pages) == len(res_ids) for i, num in enumerate(outlines_pages): to = outlines_pages[i + 1] if i + 1 < len( outlines_pages) else reader.numPages attachment_writer = PdfFileWriter() for j in range(num, to): attachment_writer.addPage(reader.getPage(j)) stream = io.BytesIO() attachment_writer.write(stream) if res_ids[i] and res_ids[ i] not in save_in_attachment: new_stream = self.postprocess_pdf_report( record_map[res_ids[i]], stream) # If the buffer has been modified, mark the old buffer to be closed as well. if new_stream and new_stream != stream: close_streams([stream]) stream = new_stream streams.append(stream) close_streams([pdf_content_stream]) else: # If no outlines available, do not save each record streams.append(pdf_content_stream) # If attachment_use is checked, the records already having an existing attachment # are not been rendered by wkhtmltopdf. So, create a new stream for each of them. if self.attachment_use: for attachment_id in save_in_attachment.values(): content = base64.decodestring(attachment_id.datas) streams.append(io.BytesIO(content)) # Build the final pdf. # If only one stream left, no need to merge them (and then, preserve embedded files). if len(streams) == 1: result = streams[0].getvalue() else: writer = PdfFileWriter() for stream in streams: reader = PdfFileReader(stream) writer.appendPagesFromReader(reader) result_stream = io.BytesIO() streams.append(result_stream) writer.write(result_stream) result = result_stream.getvalue() # We have to close the streams after PdfFileWriter's call to write() close_streams(streams) return result @api.multi def render_qweb_pdf(self, res_ids=None, data=None): if not data: data = {} data.setdefault('report_type', 'pdf') # remove editor feature in pdf generation data.update(enable_editor=False) # In case of test environment without enough workers to perform calls to wkhtmltopdf, # fallback to render_html. if (tools.config['test_enable'] or tools.config['test_file'] ) and not self.env.context.get('force_report_rendering'): return self.render_qweb_html(res_ids, data=data) # As the assets are generated during the same transaction as the rendering of the # templates calling them, there is a scenario where the assets are unreachable: when # you make a request to read the assets while the transaction creating them is not done. # Indeed, when you make an asset request, the controller has to read the `ir.attachment` # table. # This scenario happens when you want to print a PDF report for the first time, as the # assets are not in cache and must be generated. To workaround this issue, we manually # commit the writes in the `ir.attachment` table. It is done thanks to a key in the context. context = dict(self.env.context) if not config['test_enable']: context['commit_assetsbundle'] = True # Disable the debug mode in the PDF rendering in order to not split the assets bundle # into separated files to load. This is done because of an issue in wkhtmltopdf # failing to load the CSS/Javascript resources in time. # Without this, the header/footer of the reports randomly disapear # because the resources files are not loaded in time. # https://github.com/wkhtmltopdf/wkhtmltopdf/issues/2083 context['debug'] = False # The test cursor prevents the use of another environnment while the current # transaction is not finished, leading to a deadlock when the report requests # an asset bundle during the execution of test scenarios. In this case, return # the html version. if isinstance(self.env.cr, TestCursor): return self.with_context(context).render_qweb_html(res_ids, data=data)[0] save_in_attachment = OrderedDict() if res_ids: # Dispatch the records by ones having an attachment and ones requesting a call to # wkhtmltopdf. Model = self.env[self.model] record_ids = Model.browse(res_ids) wk_record_ids = Model if self.attachment: for record_id in record_ids: attachment_id = self.retrieve_attachment(record_id) if attachment_id: save_in_attachment[record_id.id] = attachment_id if not self.attachment_use or not attachment_id: wk_record_ids += record_id else: wk_record_ids = record_ids res_ids = wk_record_ids.ids # A call to wkhtmltopdf is mandatory in 2 cases: # - The report is not linked to a record. # - The report is not fully present in attachments. if save_in_attachment and not res_ids: _logger.info('The PDF report has been generated from attachments.') return self._post_pdf(save_in_attachment), 'pdf' if self.get_wkhtmltopdf_state() == 'install': # wkhtmltopdf is not installed # the call should be catched before (cf /report/check_wkhtmltopdf) but # if get_pdf is called manually (email template), the check could be # bypassed raise UserError( _("Unable to find Wkhtmltopdf on this system. The PDF can not be created." )) html = self.with_context(context).render_qweb_html(res_ids, data=data)[0] # Ensure the current document is utf-8 encoded. html = html.decode('utf-8') bodies, html_ids, header, footer, specific_paperformat_args = self.with_context( context)._prepare_html(html) if self.attachment and set(res_ids) != set(html_ids): raise UserError( _("The report's template '%s' is wrong, please contact your administrator. \n\n" "Can not separate file to save as attachment because the report's template does not contains the attributes 'data-oe-model' and 'data-oe-id' on the div with 'article' classname." ) % self.name) pdf_content = self._run_wkhtmltopdf( bodies, header=header, footer=footer, landscape=context.get('landscape'), specific_paperformat_args=specific_paperformat_args, set_viewport_size=context.get('set_viewport_size'), ) if res_ids: _logger.info('The PDF report has been generated for records %s.' % (str(res_ids))) return self._post_pdf(save_in_attachment, pdf_content=pdf_content, res_ids=html_ids), 'pdf' return pdf_content, 'pdf' @api.model def render_qweb_text(self, docids, data=None): if not data: data = {} data.setdefault('report_type', 'text') data = self._get_rendering_context(docids, data) return self.render_template(self.report_name, data), 'text' @api.model def render_qweb_html(self, docids, data=None): """This method generates and returns html version of a report. """ if not data: data = {} data.setdefault('report_type', 'html') data = self._get_rendering_context(docids, data) return self.render_template(self.report_name, data), 'html' @api.model def _get_rendering_context_model(self): report_model_name = 'report.%s' % self.report_name return self.env.get(report_model_name) @api.model def _get_rendering_context(self, docids, data): # If the report is using a custom model to render its html, we must use it. # Otherwise, fallback on the generic html rendering. report_model = self._get_rendering_context_model() data = data and dict(data) or {} if report_model is not None: data.update(report_model._get_report_values(docids, data=data)) else: docs = self.env[self.model].browse(docids) data.update({ 'doc_ids': docids, 'doc_model': self.model, 'docs': docs, }) return data @api.multi def render(self, res_ids, data=None): report_type = self.report_type.lower().replace('-', '_') render_func = getattr(self, 'render_' + report_type, None) if not render_func: return None return render_func(res_ids, data=data) @api.noguess def report_action(self, docids, data=None, config=True): """Return an action of type ir.actions.report. :param docids: id/ids/browserecord of the records to print (if not used, pass an empty list) :param report_name: Name of the template to generate an action for """ discard_logo_check = self.env.context.get('discard_logo_check') if (self.env.user._is_admin()) and ( (not self.env.user.company_id.external_report_layout_id) or (not discard_logo_check and not self.env.user.company_id.logo)) and config: template = self.env.ref('base.view_company_report_form_with_print' ) if self.env.context.get( 'from_transient_model', False) else self.env.ref( 'base.view_company_report_form') return { 'name': _('Choose Your Document Layout'), 'type': 'ir.actions.act_window', 'context': { 'default_report_name': self.report_name, 'discard_logo_check': True }, 'view_type': 'form', 'view_mode': 'form', 'res_id': self.env.user.company_id.id, 'res_model': 'res.company', 'views': [(template.id, 'form')], 'view_id': template.id, 'target': 'new', } context = self.env.context if docids: if isinstance(docids, models.Model): active_ids = docids.ids elif isinstance(docids, int): active_ids = [docids] elif isinstance(docids, list): active_ids = docids context = dict(self.env.context, active_ids=active_ids) return { 'context': context, 'data': data, 'type': 'ir.actions.report', 'report_name': self.report_name, 'report_type': self.report_type, 'report_file': self.report_file, 'name': self.name, }
class SaasPlans(models.Model): _name = "saas.plan" _order = "id desc" _description = 'Class for managing SaaS subscription plans.' @api.depends('name') def _compute_db_template_name(self): for obj in self: if obj.name and type(obj.id) != NewId and not obj.db_template: template_name = obj.name.lower().replace(" ", "_") obj.db_template = "{}_tid_{}".format(template_name, obj.id) def _default_saas_server(self): saas_servers = self.env['saas.server'].search([]) if saas_servers: return saas_servers[0].id return False def _get_contract_count(self): for obj in self: contracts = self.env['saas.contract'].search([('plan_id', '=', obj.id)]) obj.contract_count = len(contracts) def action_view_contracts(self): contracts = self.env['saas.contract'].search([('plan_id', '=', self.id) ]) action = self.env.ref('eagle_saas_kit.saas_contract_action').read()[0] if len(contracts) > 1: action['domain'] = [('id', 'in', contracts.ids)] elif len(contracts) == 1: action['views'] = [ (self.env.ref('eagle_saas_kit.saas_contract_form_view').id, 'form') ] action['res_id'] = contracts.ids[0] else: action = {'type': 'ir.actions.act_window_close'} return action @api.onchange('server_id') def server_id_change(self): for obj in self: obj.saas_base_url = obj.server_id.server_domain name = fields.Char(string='Plan', required=True) saas_base_url = fields.Char(string="SaaS Domain(Base URL)", required=True) image = fields.Binary(string='Image') summary = fields.Char(string="Plan Summary") expiration = fields.Integer('Expiration (hours)', help='time to delete database. Use for demo') grace_period = fields.Integer('Grace period (days)', help='initial days before expiration') product_template_ids = fields.One2many(comodel_name="product.template", string="Linked Products", inverse_name="saas_plan_id") use_specific_user_template = fields.Boolean( string="Use Specific User Template", help= """Select if you want to provide some specific permissions to your user for acessing its eagle instance which is going to be created by this plan.""" ) template_user_id = fields.Char( string="Database Template User ID", help= """Enter the user_id of User which you have created in the DB Template with some specific permissions or whose permission you want to grant to the user of eagle instances which is going to be created by this plan.""" ) saas_module_ids = fields.Many2many(comodel_name="saas.module", relation="saas_plan_module_relation", column1="plan_id", column2="module_id", string="Related Modules") description = fields.Text('Plan Description') recurring_interval = fields.Integer(default=1, string='Default Billing Cycle') recurring_rule_type = fields.Selection([ ('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('monthlylastday', 'Month(s) last day'), ('yearly', 'Year(s)'), ], default='monthly', string='Recurrence', readonly=True) # total_cycles = fields.Integer(string="Number of Cycles", default=1) trial_period = fields.Integer(string="Complimentary(Free) days", default=0) server_id = fields.Many2one(comodel_name="saas.server", string="SaaS Server", default=_default_saas_server, domain=[('state', '=', 'confirm')]) db_template = fields.Char( compute='_compute_db_template_name', string="DB Template Name", store=True, help= "Enter a uniquie name to create a DB associated to this plan or leave it blank and let eagle to give it a unique name." ) container_id = fields.Char(string="Instance ID") state = fields.Selection(selection=STATE, string="States", default="draft") contract_count = fields.Integer(string='Contract Count', compute='_get_contract_count', readonly=True) billing_criteria = fields.Selection(selection=BILLING_CRITERIA, string="Default Billing Criteria", required=True, default="fixed") per_user_pricing = fields.Boolean( string="User Based Pricing", help="Used to enable the per user costing of end user's instance") user_cost = fields.Float(help="PUPC(Per User Per Cycle cost)") min_users = fields.Integer( string="Min. No. of user", help= "Minimum number of users whose cost client have to pay either created or not", default="1") max_users = fields.Integer( string="Max. No. of user", help= "End user is not allowed to create user more than Maximum number of user limit. Enter -1 to allow user to create infinte number of user.", default="1") due_users_price = fields.Float(string="Due users price", default="1.0") user_product = fields.Many2one( comodel_name="product.product", string="Product for user calculation", help="Select a product for calculation costing user pricing.", domain="[('is_user_pricing', '=', True)]") @api.onchange('max_users') def check_max_user(self): for obj in self: if obj.max_users != -1 and obj.max_users < obj.min_users: raise UserError( "Max. No. of users must be greater than or Equal to Min. no. of users" ) else: obj.max_users = obj.max_users @api.onchange('min_users') def check_min_users(self): for obj in self: if obj.min_users < 1: raise UserError("Min. No. of users can't be less than 1") if obj.min_users > obj.max_users: raise UserError( "Max. No. of users must be greater than or Equal to Min. no. of users" ) def reset_to_draft(self): for obj in self: contracts = self.env['saas.contract'].search([('plan_id', '=', obj.id)]) if contracts: raise UserError( "This plan has some conracts associated with it!") obj.state = 'draft' def login_to_db_template(self): for obj in self: host_server, db_server = obj.server_id.get_server_details() response = query.get_credentials(obj.db_template, host_server=host_server, db_server=db_server) if response: login = response[0][0] password = response[0][1] login_url = "http://db13_templates.{}/saas/login?db={}&login={}&passwd={}".format( obj.saas_base_url, obj.db_template, login, password) _logger.info("$$$$$$$$$$$$$$%r", login_url) return { 'type': 'ir.actions.act_url', 'url': login_url, 'target': 'new', } else: raise UserError("Unknown Error!") def restart_db_template(self): for obj in self: host_server, db_server = obj.server_id.get_server_details() response_flag = containers.action(operation="restart", container_id=obj.container_id, host_server=host_server, db_server=db_server) if not response_flag: raise UserError("Operation Failed! Unknown Error!") def force_confirm(self): for obj in self: response = None if not obj.container_id: _, db_server = obj.server_id.get_server_details() response = query.is_db_exist(obj.db_template, db_server=db_server) if not response: raise Warning("Please create DB Template First!") obj.state = 'confirm' def create_db_template(self): for obj in self: if not obj.db_template: raise UserError("Please select the DB template name first.") if re.match("^template_", obj.db_template): raise UserError( "Couldn't Create DB. Please try again with some other Template Name!" ) db_template_name = "template_{}".format(obj.db_template) modules = [module.technical_name for module in obj.saas_module_ids] config_path = get_module_resource('eagle_saas_kit') modules.append('wk_saas_tool') try: host_server, db_server = obj.server_id.get_server_details() response = saas.create_db_template( db_template=db_template_name, modules=modules, config_path=config_path, host_server=host_server, db_server=db_server) except Exception as e: _logger.info("--------DB-TEMPLATE-CREATION-EXCEPTION-------%r", e) raise UserError(e) else: if response: if response.get('status', False): obj.db_template = db_template_name obj.state = 'confirm' obj.container_id = response.get('container_id', False) else: msg = response.get('msg', False) if msg: raise UserError(msg) else: raise UserError( "Unknown Error. Please try again later with some different Template Name" ) else: raise UserError( "No Response. Please try again later with some different Template Name" ) def unlink(self): for obj in self: if obj.contract_count: raise UserError( "Error: You must delete the associated SaaS Contracts first!" ) return super(SaasPlans, self).unlink() @api.model def create(self, vals): if vals.get('recurring_interval', 0) <= 0: raise Warning("Default Billing Cycle can't be less than 1") res = super(SaasPlans, self).create(vals) for obj in res: if obj.name and not obj.db_template: template_name = obj.name.lower().replace(" ", "_") obj.db_template = "{}_tid_{}".format(template_name, res.id) return res def write(self, vals): if vals.get('recurring_interval', False) and vals['recurring_interval'] <= 0: raise Warning("Default Billing Cycle can't be less than 1") res = super(SaasPlans, self).write(vals) return res
class EducationGroup(models.Model): _name = 'education.group' _inherit = 'education.data' _description = 'Education Group' _rec_name = 'description' _order = 'academic_year_id, education_code' academic_year_id = fields.Many2one( comodel_name='education.academic_year', string='Academic Year', required=True, copy=False) center_id = fields.Many2one( comodel_name='res.partner', string='Education Center', required=True) plan_id = fields.Many2one( comodel_name='education.plan', string='Plan') level_id = fields.Many2one( comodel_name='education.level', string='Level', domain="[('plan_id', '=', plan_id)]", required=True) field_id = fields.Many2one( comodel_name='education.field', string='Study Field') classroom_id = fields.Many2one( comodel_name='education.classroom', string='Classroom', domain="[('center_id', '=', center_id)]") shift_id = fields.Many2one( comodel_name='education.shift', string='Shift') course_id = fields.Many2one( comodel_name='education.course', string='Course', domain="[('plan_id', '=', plan_id), ('level_id', '=', level_id)," "('field_id', '=', field_id), ('shift_id', '=', shift_id)]") model_id = fields.Many2one( comodel_name='education.model', string='Educational Model') group_type_id = fields.Many2one( comodel_name='education.group_type', string='Educational Group Type') calendar_id = fields.Many2one( comodel_name='resource.calendar', string='Calendar', domain="[('center_id', '=', center_id)]") comments = fields.Text(string='Comments') teacher_ids = fields.One2many( comodel_name='education.group.teacher', inverse_name='group_id', string='Teachers') session_ids = fields.One2many( comodel_name='education.group.session', inverse_name='group_id', string='Sessions') student_ids = fields.Many2many( comodel_name='res.partner', relation='edu_group_student', column1='group_id', column2='student_id', string='Students') student_count = fields.Integer( string='Student Number', compute='_compute_student_count', store=True) parent_id = fields.Many2one( comodel_name='education.group', string='Parent Group', domain="[('academic_year_id', '=', academic_year_id)," "('center_id', '=', center_id)," "('course_id', '=', course_id)," "('group_type_id.type', '=', 'official')]") schedule_ids = fields.Many2many( comodel_name='education.schedule', string='Education Schedule', relation='edu_schedule_group', column2='schedule_id', column1='group_id', readonly=True) schedule_count = fields.Integer( compute='_compute_schedule_count', string='Schedule Number') _sql_constraints = [ ('education_code_unique', 'unique(education_code,center_id,academic_year_id)', 'Education code must be unique per center and academic year!'), ] @api.constrains('parent_id') def _check_group_recursion(self): if not self._check_recursion(): raise ValidationError(_('You cannot create recursive groups.')) @api.depends('student_ids') def _compute_student_count(self): for record in self: record.student_count = len(record.student_ids) @api.depends('schedule_ids') def _compute_schedule_count(self): for record in self: record.schedule_count = len(record.schedule_ids) @api.multi def button_open_schedule(self): action = self.env.ref('education.action_education_schedule_from_group') action_dict = action.read()[0] if action else {} domain = expression.AND([ [('id', 'in', self.mapped('schedule_ids').ids)], safe_eval(action.domain or '[]')]) action_dict.update({ 'domain': domain, }) return action_dict @api.multi def button_open_students(self): action = self.env.ref('base.action_partner_form') action_dict = action.read()[0] if action else {} domain = expression.AND([ [('id', 'in', self.mapped('student_ids').ids)], safe_eval(action.domain or '[]') ]) context = safe_eval(action.context or '[]') context.pop('search_default_customer') action_dict.update({ 'display_name': _('Students'), 'domain': domain, 'context': context, }) return action_dict
class EagleeduSyllabus(models.Model): _name = 'eagleedu.syllabus' _description = "Syllabus " _rec_name = 'display' _order = 'sequence' name = fields.Char(string='Name', help="Enter the Name of the Syllabus") # syllabus_code = fields.Char(string='Syllabus Code', compute="_get_code") display = fields.Char('Syllabus Display', help="This is printed on the marksheet as Subject") class_id = fields.Many2one('eagleedu.class', string='Class ID') subject_id = fields.Many2one('eagleedu.subject', string='Subject', copy=False) academic_year = fields.Many2one('eagleedu.academic.year', string='Academic Year') code = fields.Char('Code', compute="_get_code") sequence = fields.Integer( default=0, help="Gives the sequence order when displaying a list.") #for copy all the value as product template # attribute_line_ids = fields.One2many('eagleedu.syllabus', 'class_id', copy=True) # has_group=fields.Integer(related='class_id.division_count') divisional = fields.Boolean("Grouping ?") division_id = fields.Many2one('eagleedu.group_division', string='Group') paper = fields.Char(string='Paper') active = fields.Boolean('Active?', related='academic_year.active') compulsory_for = fields.Many2many('eagleedu.class.history', 'eagleedu_syllabus_class_history_rel', 'compulsory_for', 'compulsory_subjects', 'compulsory for') selective_for = fields.Many2many('eagleedu.class.history', 'eagleedu_syllabus_class_history_1_rel', 'selective_for', 'selective_subjects', 'selective for') optional_for = fields.Many2many( 'eagleedu.class.history', 'eagleedu_syllabus_class_history_optional_rel', 'optional_for', 'optional_subjects', 'Optional for') subject_type = fields.Selection([('theory', 'Theory'), ('practical', 'Practical'), ('both', 'Both'), ('other', 'Other')], 'Subject Type', default="theory", required=True) selection_type = fields.Selection([('compulsory', 'Compulsory'), ('elective', 'Elective')], 'Selection Type', default="compulsory", required=True) evaluation_type = fields.Selection([('general', 'General'), ('extra', 'Extra')], 'Evaluation Type', default="general", required=True) # total_hours = fields.Float(string='Total Hours') total_mark = fields.Integer('Total') pass_mark = fields.Integer('Pass') tut_mark = fields.Integer('Tutorial') tut_pass = fields.Integer('pass') subj_mark = fields.Integer('Subjective') subj_pass = fields.Integer('pass') obj_mark = fields.Integer('Objective') obj_pass = fields.Integer('pass') prac_mark = fields.Integer('Practical') prac_pass = fields.Integer('pass') description = fields.Text(string='Syllabus Modules') @api.onchange('academic_year', 'class_id', 'division_id', 'subject_id', 'paper') def _get_code(self): for rec in self: recname = '' reccode = '' if rec.paper and rec.subject_id: recname = rec.subject_id.name + '-' + rec.paper reccode = rec.subject_id.code + '-' + rec.paper elif rec.subject_id: recname = rec.subject_id.name reccode = rec.subject_id.code rec.display = recname if recname != '': if rec.class_id: if rec.academic_year: if rec.divisional == True: recname = recname + '-' + rec.class_id.name + '-' + rec.academic_year.name # +' ('+rec.division_id.name +')' reccode = reccode + '-' + rec.class_id.code + '-' + rec.academic_year.ay_code # +' ('+rec.division_id.code +')' else: recname = recname + '-' + rec.class_id.name + '-' + rec.academic_year.name reccode = reccode + '-' + rec.class_id.code + '-' + rec.academic_year.ay_code rec.division_id = False rec.name = recname rec.code = reccode @api.model @api.onchange('tut_mark', 'subj_mark', 'obj_mark', 'prac_mark', 'tut_pass', 'subj_pass', 'obj_pass', 'prac_pass') def calculate_total_mark(self): for rec in self: rec.total_mark = rec.tut_mark + rec.subj_mark + rec.obj_mark + rec.prac_mark rec.pass_mark = rec.tut_pass + rec.subj_pass + rec.obj_pass + rec.prac_pass
class BaseLanguageExport(models.TransientModel): _name = "base.language.export" _description = 'Language Export' @api.model def _get_languages(self): langs = self.env['res.lang'].search([('translatable', '=', True)]) return [(NEW_LANG_KEY, _('New Language (Empty translation template)'))] + \ [(lang.code, lang.name) for lang in langs] name = fields.Char('File Name', readonly=True) lang = fields.Selection(_get_languages, string='Language', required=True, default=NEW_LANG_KEY) format = fields.Selection([('csv', 'CSV File'), ('po', 'PO File'), ('tgz', 'TGZ Archive')], string='File Format', required=True, default='csv') modules = fields.Many2many('ir.module.module', 'rel_modules_langexport', 'wiz_id', 'module_id', string='Apps To Export', domain=[('state', '=', 'installed')]) data = fields.Binary('File', readonly=True) state = fields.Selection( [('choose', 'choose'), ('get', 'get')], # choose language or get the file default='choose') @api.multi def act_getfile(self): this = self[0] lang = this.lang if this.lang != NEW_LANG_KEY else False mods = sorted(this.mapped('modules.name')) or ['all'] with contextlib.closing(io.BytesIO()) as buf: tools.trans_export(lang, mods, buf, this.format, self._cr) out = base64.encodestring(buf.getvalue()) filename = 'new' if lang: filename = tools.get_iso_codes(lang) elif len(mods) == 1: filename = mods[0] extension = this.format if not lang and extension == 'po': extension = 'pot' name = "%s.%s" % (filename, extension) this.write({'state': 'get', 'data': out, 'name': name}) return { 'type': 'ir.actions.act_window', 'res_model': 'base.language.export', 'view_mode': 'form', 'view_type': 'form', 'res_id': this.id, 'views': [(False, 'form')], 'target': 'new', }
class Track(models.Model): _name = "event.track" _description = 'Event Track' _order = 'priority, date' _inherit = [ 'mail.thread', 'mail.activity.mixin', 'website.seo.metadata', 'website.published.mixin' ] @api.model def _get_default_stage_id(self): return self.env['event.track.stage'].search([], limit=1).id name = fields.Char('Title', required=True, translate=True) active = fields.Boolean(default=True) user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.user) partner_id = fields.Many2one('res.partner', 'Speaker') partner_name = fields.Char('Speaker Name') partner_email = fields.Char('Speaker Email') partner_phone = fields.Char('Speaker Phone') partner_biography = fields.Html('Speaker Biography') tag_ids = fields.Many2many('event.track.tag', string='Tags') stage_id = fields.Many2one('event.track.stage', string='Stage', ondelete='restrict', index=True, copy=False, default=_get_default_stage_id, group_expand='_read_group_stage_ids', required=True, track_visibility='onchange') kanban_state = fields.Selection( [('normal', 'Grey'), ('done', 'Green'), ('blocked', 'Red')], string='Kanban State', copy=False, default='normal', required=True, track_visibility='onchange', help= "A track's kanban state indicates special situations affecting it:\n" " * Grey is the default situation\n" " * Red indicates something is preventing the progress of this track\n" " * Green indicates the track is ready to be pulled to the next stage") description = fields.Html('Track Description', translate=html_translate, sanitize_attributes=False) date = fields.Datetime('Track Date') duration = fields.Float('Duration', default=1.5) location_id = fields.Many2one('event.track.location', 'Room') event_id = fields.Many2one('event.event', 'Event', required=True) color = fields.Integer('Color Index') priority = fields.Selection([('0', 'Low'), ('1', 'Medium'), ('2', 'High'), ('3', 'Highest')], 'Priority', required=True, default='1') image = fields.Binary('Image', related='partner_id.image_medium', store=True, attachment=True, readonly=False) @api.multi @api.depends('name') def _compute_website_url(self): super(Track, self)._compute_website_url() for track in self: if not isinstance(track.id, models.NewId): track.website_url = '/event/%s/track/%s' % (slug( track.event_id), slug(track)) @api.onchange('partner_id') def _onchange_partner_id(self): if self.partner_id: self.partner_name = self.partner_id.name self.partner_email = self.partner_id.email self.partner_phone = self.partner_id.phone self.partner_biography = self.partner_id.website_description @api.model def create(self, vals): track = super(Track, self).create(vals) track.event_id.message_post_with_view( 'website_event_track.event_track_template_new', values={'track': track}, subject=track.name, subtype_id=self.env.ref('website_event_track.mt_event_track').id, ) return track @api.multi def write(self, vals): if 'stage_id' in vals and 'kanban_state' not in vals: vals['kanban_state'] = 'normal' res = super(Track, self).write(vals) if vals.get('partner_id'): self.message_subscribe([vals['partner_id']]) return res @api.model def _read_group_stage_ids(self, stages, domain, order): """ Always display all stages """ return stages.search([], order=order) @api.multi def _track_template(self, tracking): res = super(Track, self)._track_template(tracking) track = self[0] changes, tracking_value_ids = tracking[track.id] if 'stage_id' in changes and track.stage_id.mail_template_id: res['stage_id'] = (track.stage_id.mail_template_id, { 'composition_mode': 'comment', 'auto_delete_message': True, 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), 'notif_layout': 'mail.mail_notification_light' }) return res @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'kanban_state' in init_values and self.kanban_state == 'blocked': return 'website_event_track.mt_track_blocked' elif 'kanban_state' in init_values and self.kanban_state == 'done': return 'website_event_track.mt_track_ready' return super(Track, self)._track_subtype(init_values) @api.multi def message_get_suggested_recipients(self): recipients = super(Track, self).message_get_suggested_recipients() for track in self: if track.partner_email != track.partner_id.email: track._message_add_suggested_recipient( recipients, email=track.partner_email, reason=_('Speaker Email')) return recipients def _message_post_after_hook(self, message, *args, **kwargs): if self.partner_email 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.partner_email) if new_partner: self.search([ ('partner_id', '=', False), ('partner_email', '=', new_partner.email), ('stage_id.is_cancel', '=', False), ]).write({'partner_id': new_partner.id}) return super(Track, self)._message_post_after_hook(message, *args, **kwargs) @api.multi def open_track_speakers_list(self): return { 'name': _('Speakers'), 'domain': [('id', 'in', self.mapped('partner_id').ids)], 'view_type': 'form', 'view_mode': 'kanban,form', 'res_model': 'res.partner', 'view_id': False, 'type': 'ir.actions.act_window', }
class EducationFaculty(models.Model): _name = 'education.faculty' _inherit = ['mail.thread'] _description = 'Faculty Record' @api.multi def create_employee(self): """Creating the employee for the faculty""" for rec in self: values = { 'name': rec.name + rec.last_name, 'gender': rec.gender, 'birthday': rec.date_of_birth, 'image': rec.image, 'work_phone': rec.phone, 'work_mobile': rec.mobile, 'work_email': rec.email, } emp_id = self.env['hr.employee'].create(values) rec.employee_id = emp_id.id @api.model def create(self, vals): """Over riding the create method to assign the sequence for newly creating records""" vals['faculty_id'] = self.env['ir.sequence'].next_by_code( 'education.faculty') res = super(EducationFaculty, self).create(vals) return res name = fields.Char(string='Name', required=True, help="Enter the first name") faculty_id = fields.Char(string="ID", readonly=True) last_name = fields.Char(string='Last Name', help="Enter the last name") image = fields.Binary(string="Image") email = fields.Char(string="Email", help="Enter the Email for contact purpose") phone = fields.Char(string="Phone", help="Enter the Phone for contact purpose") mobile = fields.Char(string="Mobile", help="Enter the Mobile for contact purpose") date_of_birth = fields.Date(string="Date Of birth", help="Enter the DOB") guardian_name = fields.Char(string="Guardian", help="Your guardian is ") father_name = fields.Char(string="Father", help="Your Father name is ") mother_name = fields.Char(string="Mother", help="Your Mother name is ") subject_lines = fields.Many2many('education.subject', string='Subject Lines') employee_id = fields.Many2one('hr.employee', string="Related Employee") degree = fields.Many2one('hr.recruitment.degree', string="Degree", Help="Select your Highest degree") gender = fields.Selection([('male', 'Male'), ('female', 'Female'), ('other', 'Other')], string='Gender', required=True, default='male', track_visibility='onchange') blood_group = fields.Selection([('a+', 'A+'), ('a-', 'A-'), ('b+', 'B+'), ('o+', 'O+'), ('o-', 'O-'), ('ab-', 'AB-'), ('ab+', 'AB+')], string='Blood Group', required=True, default='a+', track_visibility='onchange')
class EducationDocuments(models.Model): _name = 'education.documents' _description = "Student Documents" _inherit = ['mail.thread'] @api.model def create(self, vals): """Over riding the create method to assign the sequence for newly creating records""" if vals.get('name', _('New')) == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code('education.documents') or _('New') res = super(EducationDocuments, self).create(vals) return res @api.multi def verify_document(self): """Return the state to done if the documents are perfect""" for rec in self: rec.write({ 'verified_by': self.env.uid, 'verified_date': datetime.datetime.now().strftime("%Y-%m-%d"), 'state': 'done' }) @api.multi def need_correction(self): """Return the state to correction if the documents are not perfect""" for rec in self: rec.write({ 'state': 'correction' }) @api.multi def hard_copy_returned(self): """Records who return the documents and when is it returned""" for rec in self: if rec.state == 'done': rec.write({ 'state': 'returned', 'returned_by': self.env.uid, 'returned_date': datetime.datetime.now().strftime("%Y-%m-%d") }) name = fields.Char(string='Serial Number', copy=False, default=lambda self: _('New')) document_name = fields.Many2one('document.document', string='Document Type', required=True, help="Choose the type of the Document") description = fields.Text(string='Description', copy=False, help="Enter a description about the document") has_hard_copy = fields.Boolean(string="Hard copy Received", help="Tick the field if the hard copy is provided") location_id = fields.Many2one('stock.location', 'Location', domain="[('usage', '=', 'internal')]", help="Location where which the hard copy is stored") location_note = fields.Char(string="Location Note", help="Enter some notes about the location") submitted_date = fields.Date(string="Submitted Date", default=date.today(), help="Documents are submitted on") received_by = fields.Many2one('hr.employee', string="Received By", help="The Documents are received by") returned_by = fields.Many2one('hr.employee', string="Returned By", help="The Documents are returned by") verified_date = fields.Date(string="Verified Date", help="Date at the verification is done") returned_date = fields.Date(string="Returned Date", help="Returning date") reference = fields.Char(string='Document Number', required=True, copy=False) responsible_verified = fields.Many2one('hr.employee', string="Responsible") responsible_returned = fields.Many2one('hr.employee', string="Responsible") verified_by = fields.Many2one('res.users', string='Verified by') application_ref = fields.Many2one('education.application', invisible=1, copy=False) doc_attachment_id = fields.Many2many('ir.attachment', 'doc_attach_rel', 'doc_id', 'attach_id3', string="Attachment", help='You can attach the copy of your document', copy=False) state = fields.Selection([('draft', 'Draft'), ('correction', 'Correction'), ('done', 'Done'), ('returned', 'Returned')], string='State', required=True, default='draft', track_visibility='onchange')
class AccountReconcileModel(models.Model): _name = 'account.reconcile.model' _description = 'Preset to create journal entries during a invoices and payments matching' _order = 'sequence, id' # Base fields. name = fields.Char(string='Name', required=True) sequence = fields.Integer(required=True, default=10) company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) rule_type = fields.Selection(selection=[ ('writeoff_button', _('Manually create a write-off on clicked button.')), ('writeoff_suggestion', _('Suggest counterpart values.')), ('invoice_matching', _('Match existing invoices/bills.')) ], string='Type', default='writeoff_button', required=True) auto_reconcile = fields.Boolean( string='Auto-validate', help= 'Validate the statement line automatically (reconciliation based on your rule).' ) # ===== Conditions ===== match_journal_ids = fields.Many2many( 'account.journal', string='Journals', domain="[('type', 'in', ('bank', 'cash'))]", help= 'The reconciliation model will only be available from the selected journals.' ) match_nature = fields.Selection( selection=[('amount_received', 'Amount Received'), ('amount_paid', 'Amount Paid'), ('both', 'Amount Paid/Received')], string='Amount Nature', required=True, default='both', help= '''The reconciliation model will only be applied to the selected transaction type: * Amount Received: Only applied when receiving an amount. * Amount Paid: Only applied when paying an amount. * Amount Paid/Received: Applied in both cases.''') match_amount = fields.Selection( selection=[ ('lower', 'Is Lower Than'), ('greater', 'Is Greater Than'), ('between', 'Is Between'), ], string='Amount', help= 'The reconciliation model will only be applied when the amount being lower than, greater than or between specified amount(s).' ) match_amount_min = fields.Float(string='Amount Min Parameter') match_amount_max = fields.Float(string='Amount Max Parameter') match_label = fields.Selection( selection=[ ('contains', 'Contains'), ('not_contains', 'Not Contains'), ('match_regex', 'Match Regex'), ], string='Label', help='''The reconciliation model will only be applied when the label: * Contains: The proposition label must contains this string (case insensitive). * Not Contains: Negation of "Contains". * Match Regex: Define your own regular expression.''') match_label_param = fields.Char(string='Label Parameter') match_same_currency = fields.Boolean( string='Same Currency Matching', default=True, help= 'Restrict to propositions having the same currency as the statement line.' ) match_total_amount = fields.Boolean( string='Amount Matching', default=True, help= 'The sum of total residual amount propositions matches the statement line amount.' ) match_total_amount_param = fields.Float( string='Amount Matching %', default=100, help= 'The sum of total residual amount propositions matches the statement line amount under this percentage.' ) match_partner = fields.Boolean( string='Partner Is Set', help= 'The reconciliation model will only be applied when a customer/vendor is set.' ) match_partner_ids = fields.Many2many( 'res.partner', string='Restrict Partners to', help= 'The reconciliation model will only be applied to the selected customers/vendors.' ) match_partner_category_ids = fields.Many2many( 'res.partner.category', string='Restrict Partner Categories to', help= 'The reconciliation model will only be applied to the selected customer/vendor categories.' ) # ===== Write-Off ===== # First part fields. account_id = fields.Many2one('account.account', string='Account', ondelete='cascade', domain=[('deprecated', '=', False)]) journal_id = fields.Many2one( 'account.journal', string='Journal', ondelete='cascade', help="This field is ignored in a bank statement reconciliation.") label = fields.Char(string='Journal Item Label') amount_type = fields.Selection([('fixed', 'Fixed'), ('percentage', 'Percentage of balance')], required=True, default='percentage') is_tax_price_included = fields.Boolean( string='Is Tax Included in Price', related='tax_id.price_include', help= 'Technical field used inside the view to make the force_tax_included field readonly if the tax is already price included.' ) tax_amount_type = fields.Selection( string='Tax Amount Type', related='tax_id.amount_type', help= 'Technical field used inside the view to make the force_tax_included field invisible if the tax is a group.' ) force_tax_included = fields.Boolean( string='Tax Included in Price', help='Force the tax to be managed as a price included tax.') amount = fields.Float( string='Write-off Amount', digits=0, required=True, default=100.0, help= "Fixed amount will count as a debit if it is negative, as a credit if it is positive." ) tax_id = fields.Many2one('account.tax', string='Tax', ondelete='restrict') analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', ondelete='set null') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') # Second part fields. has_second_line = fields.Boolean(string='Add a second line', default=False) second_account_id = fields.Many2one('account.account', string='Second Account', ondelete='cascade', domain=[('deprecated', '=', False)]) second_journal_id = fields.Many2one( 'account.journal', string='Second Journal', ondelete='cascade', help="This field is ignored in a bank statement reconciliation.") second_label = fields.Char(string='Second Journal Item Label') second_amount_type = fields.Selection( [('fixed', 'Fixed'), ('percentage', 'Percentage of amount')], string="Second Amount type", required=True, default='percentage') is_second_tax_price_included = fields.Boolean( string='Is Second Tax Included in Price', related='second_tax_id.price_include', help= 'Technical field used inside the view to make the force_second_tax_included field readonly if the tax is already price included.' ) second_tax_amount_type = fields.Selection( string='Second Tax Amount Type', related='second_tax_id.amount_type', help= 'Technical field used inside the view to make the force_second_tax_included field invisible if the tax is a group.' ) force_second_tax_included = fields.Boolean( string='Second Tax Included in Price', help='Force the second tax to be managed as a price included tax.') second_amount = fields.Float( string='Second Write-off Amount', digits=0, required=True, default=100.0, help= "Fixed amount will count as a debit if it is negative, as a credit if it is positive." ) second_tax_id = fields.Many2one('account.tax', string='Second Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'purchase')]) second_analytic_account_id = fields.Many2one( 'account.analytic.account', string='Second Analytic Account', ondelete='set null') second_analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Second Analytic Tags') @api.onchange('name') def onchange_name(self): self.label = self.name @api.onchange('tax_id') def _onchange_tax_id(self): if self.tax_id: self.force_tax_included = self.tax_id.price_include @api.onchange('second_tax_id') def _onchange_second_tax_id(self): if self.second_tax_id: self.force_second_tax_included = self.second_tax_id.price_include @api.onchange('match_total_amount_param') def _onchange_match_total_amount_param(self): if self.match_total_amount_param < 0 or self.match_total_amount_param > 100: self.match_total_amount_param = min( max(0, self.match_total_amount_param), 100) #################################################### # RECONCILIATION PROCESS #################################################### @api.model def _get_taxes_move_lines_dict(self, tax, base_line_dict): ''' Get move.lines dict (to be passed to the create()) corresponding to a tax. :param tax: An account.tax record. :param base_line_dict: A dict representing the move.line containing the base amount. :return: A list of dict representing move.lines to be created corresponding to the tax. ''' balance = base_line_dict['debit'] - base_line_dict['credit'] currency = base_line_dict.get( 'currency_id') and self.env['res.currency'].browse( base_line_dict['currency_id']) res = tax.compute_all(balance, currency=currency) new_aml_dicts = [] for tax_res in res['taxes']: tax = self.env['account.tax'].browse(tax_res['id']) new_aml_dicts.append({ 'account_id': tax.account_id and tax.account_id.id or base_line_dict['account_id'], 'name': tax.name, 'partner_id': base_line_dict.get('partner_id'), 'debit': tax_res['amount'] > 0 and tax_res['amount'] or 0, 'credit': tax_res['amount'] < 0 and -tax_res['amount'] or 0, 'analytic_account_id': tax.analytic and base_line_dict['analytic_account_id'], 'analytic_tag_ids': tax.analytic and base_line_dict['analytic_tag_ids'], 'tax_exigible': tax.tax_exigibility == 'on_payment', 'tax_line_id': tax.id, }) # Handle price included taxes. base_line_dict['debit'] = tax_res['base'] > 0 and tax_res[ 'base'] or base_line_dict['debit'] base_line_dict['credit'] = tax_res[ 'base'] < 0 and -tax_res['base'] or base_line_dict['credit'] return new_aml_dicts @api.multi def _get_write_off_move_lines_dict(self, st_line, move_lines=None): ''' Get move.lines dict (to be passed to the create()) corresponding to the reconciliation model's write-off lines. :param st_line: An account.bank.statement.line record. :param move_lines: An account.move.line recordset. :return: A list of dict representing move.lines to be created corresponding to the write-off lines. ''' self.ensure_one() if self.rule_type == 'invoice_matching' and ( not self.match_total_amount or (self.match_total_amount_param == 100)): return [] line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id total_residual = move_lines and sum( aml.currency_id and aml.amount_residual_currency or aml.amount_residual for aml in move_lines) or 0.0 balance = total_residual - line_residual if not self.account_id or float_is_zero( balance, precision_rounding=line_currency.rounding): return [] if self.amount_type == 'percentage': line_balance = balance * (self.amount / 100.0) else: line_balance = self.amount * (1 if balance > 0.0 else -1) new_aml_dicts = [] # First write-off line. writeoff_line = { 'name': self.label or st_line.name, 'account_id': self.account_id.id, 'analytic_account_id': self.analytic_account_id.id, 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)], 'debit': line_balance > 0 and line_balance or 0, 'credit': line_balance < 0 and -line_balance or 0, } new_aml_dicts.append(writeoff_line) if self.tax_id: writeoff_line['tax_ids'] = [(6, None, [self.tax_id.id])] tax = self.tax_id if self.force_tax_included: tax = tax.with_context(force_price_include=True) new_aml_dicts += self._get_taxes_move_lines_dict( tax, writeoff_line) # Second write-off line. if self.has_second_line and self.second_account_id: line_balance = balance - sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts) second_writeoff_line = { 'name': self.second_label or st_line.name, 'account_id': self.second_account_id.id, 'analytic_account_id': self.second_analytic_account_id.id, 'analytic_tag_ids': [(6, 0, self.second_analytic_tag_ids.ids)], 'debit': line_balance > 0 and line_balance or 0, 'credit': line_balance < 0 and -line_balance or 0, } new_aml_dicts.append(second_writeoff_line) if self.second_tax_id: second_writeoff_line['tax_ids'] = [(6, None, [self.second_tax_id.id])] tax = self.second_tax_id if self.force_second_tax_included: tax = tax.with_context(force_price_include=True) new_aml_dicts += self._get_taxes_move_lines_dict( tax, second_writeoff_line) return new_aml_dicts @api.multi def _prepare_reconciliation(self, st_line, move_lines=None, partner=None): ''' Reconcile the statement line with some move lines using this reconciliation model. :param st_line: An account.bank.statement.line record. :param move_lines: An account.move.line recordset. :param partner_id: An optional res.partner record. If not set, st_line.partner_id will be used. :return: Counterpart account.moves. ''' self.ensure_one() # Create counterpart_aml_dicts + payment_aml_rec. counterpart_aml_dicts = [] payment_aml_rec = self.env['account.move.line'] if move_lines: for aml in move_lines: if aml.account_id.internal_type == 'liquidity': payment_aml_rec |= aml else: amount = aml.currency_id and aml.amount_residual_currency or aml.amount_residual counterpart_aml_dicts.append({ 'name': aml.name if aml.name != '/' else aml.move_id.name, 'debit': amount < 0 and -amount or 0, 'credit': amount > 0 and amount or 0, 'move_line': aml, }) # Create new_aml_dicts. new_aml_dicts = self._get_write_off_move_lines_dict( st_line, move_lines=move_lines) line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id total_residual = move_lines and sum( aml.currency_id and aml.amount_residual_currency or aml.amount_residual for aml in move_lines) or 0.0 total_residual -= sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts) # Create open_balance_dict open_balance_dict = None if float_compare(line_residual, total_residual, precision_rounding=line_currency.rounding) != 0: if not partner and not st_line.partner_id: open_balance_dict = False else: balance = total_residual - line_residual partner = partner or st_line.partner_id open_balance_dict = { 'name': '%s : %s' % (st_line.name, _('Open Balance')), 'account_id': balance < 0 and partner.property_account_payable_id.id or partner.property_account_receivable_id.id, 'debit': balance > 0 and balance or 0, 'credit': balance < 0 and -balance or 0, } return { 'counterpart_aml_dicts': counterpart_aml_dicts, 'payment_aml_rec': payment_aml_rec, 'new_aml_dicts': new_aml_dicts, 'open_balance_dict': open_balance_dict } #################################################### # RECONCILIATION CRITERIA #################################################### @api.multi def _apply_conditions(self, query, params): self.ensure_one() rule = self # Filter on journals. if rule.match_journal_ids: query += ' AND st_line.journal_id IN %s' params += [tuple(rule.match_journal_ids.ids)] # Filter on amount nature. if rule.match_nature == 'amount_received': query += ' AND st_line.amount >= 0.0' elif rule.match_nature == 'amount_paid': query += ' AND st_line.amount <= 0.0' # Filter on amount. if rule.match_amount: query += ' AND ROUND(ABS(st_line.amount), jnl_precision.dp) ' if rule.match_amount == 'lower': query += '< %s' params += [self.match_amount_max] elif rule.match_amount == 'greater': query += '> %s' params += [self.match_amount_min] else: # if self.match_amount == 'between' query += 'BETWEEN %s AND %s' params += [rule.match_amount_min, rule.match_amount_max] # Filter on label. if rule.match_label == 'contains': query += ' AND st_line.name ILIKE %s' params += ['%%%s%%' % rule.match_label_param] elif rule.match_label == 'not_contains': query += ' AND st_line.name NOT ILIKE %s' params += ['%%%s%%' % rule.match_label_param] elif rule.match_label == 'match_regex': query += ' AND st_line.name ~ %s' params += [rule.match_label_param] # Filter on partners. if rule.match_partner: query += ' AND line_partner.partner_id != 0' if rule.match_partner_ids: query += ' AND line_partner.partner_id IN %s' params += [tuple(rule.match_partner_ids.ids)] if rule.match_partner_category_ids: query += ''' AND line_partner.partner_id IN ( SELECT DISTINCT categ.partner_id FROM res_partner_res_partner_category_rel categ WHERE categ.category_id IN %s ) ''' params += [tuple(rule.match_partner_category_ids.ids)] return query, params @api.multi def _get_with_tables(self, st_lines, partner_map=None): with_tables = ''' WITH jnl_precision AS ( SELECT j.id AS journal_id, currency.decimal_places AS dp FROM account_journal j LEFT JOIN res_company c ON j.company_id = c.id LEFT JOIN res_currency currency ON COALESCE(j.currency_id, c.currency_id) = currency.id WHERE j.type IN ('bank', 'cash') )''' # Compute partners values table. # This is required since some statement line's partners could be shadowed in the reconciliation widget. partners_list = [] for line in st_lines: partner_id = partner_map and partner_map.get( line.id) or line.partner_id.id or 0 partners_list.append('(%d, %d)' % (line.id, partner_id)) partners_table = 'SELECT * FROM (VALUES %s) AS line_partner (line_id, partner_id)' % ','.join( partners_list) with_tables += ', partners_table AS (' + partners_table + ')' return with_tables @api.multi def _get_invoice_matching_query(self, st_lines, excluded_ids=None, partner_map=None): ''' Get the query applying all rules trying to match existing entries with the given statement lines. :param st_lines: Account.bank.statement.lines recordset. :param excluded_ids: Account.move.lines to exclude. :param partner_map: Dict mapping each line with new partner eventually. :return: (query, params) ''' if any(m.rule_type != 'invoice_matching' for m in self): raise UserError( _('Programmation Error: Can\'t call _get_invoice_matching_query() for different rules than \'invoice_matching\'' )) queries = [] all_params = [] for rule in self: # N.B: 'communication_flag' is there to distinguish invoice matching through the number/reference # (higher priority) from invoice matching using the partner (lower priority). query = ''' SELECT %s AS sequence, %s AS model_id, st_line.id AS id, aml.id AS aml_id, aml.currency_id AS aml_currency_id, aml.date_maturity AS aml_date_maturity, aml.amount_residual AS aml_amount_residual, aml.amount_residual_currency AS aml_amount_residual_currency, aml.balance AS aml_balance, aml.amount_currency AS aml_amount_currency, account.internal_type AS account_internal_type, -- Determine a matching or not with the statement line communication using the move.name or move.ref. regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+') && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') OR ( move.ref IS NOT NULL AND regexp_split_to_array(TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')),'\s+') && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') ) AS communication_flag FROM account_bank_statement_line st_line LEFT JOIN account_journal journal ON journal.id = st_line.journal_id LEFT JOIN jnl_precision ON jnl_precision.journal_id = journal.id LEFT JOIN res_company company ON company.id = st_line.company_id LEFT JOIN partners_table line_partner ON line_partner.line_id = st_line.id , account_move_line aml LEFT JOIN account_move move ON move.id = aml.move_id LEFT JOIN account_account account ON account.id = aml.account_id WHERE st_line.id IN %s AND aml.company_id = st_line.company_id AND ( -- the field match_partner of the rule might enforce the second part of -- the OR condition, later in _apply_conditions() line_partner.partner_id = 0 OR aml.partner_id = line_partner.partner_id ) AND CASE WHEN st_line.amount > 0.0 THEN aml.balance > 0 ELSE aml.balance < 0 END -- if there is a partner, propose all aml of the partner, otherwise propose only the ones -- matching the statement line communication AND ( ( line_partner.partner_id != 0 AND aml.partner_id = line_partner.partner_id ) OR ( line_partner.partner_id = 0 AND TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')) != '' AND ( regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+') && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') OR ( move.ref IS NOT NULL AND regexp_split_to_array(TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')),'\s+') && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') ) ) ) ) AND ( ( -- blue lines appearance conditions aml.account_id IN (journal.default_credit_account_id, journal.default_debit_account_id) AND aml.statement_id IS NULL AND ( company.account_bank_reconciliation_start IS NULL OR aml.date > company.account_bank_reconciliation_start ) ) OR ( -- black lines appearance conditions account.reconcile IS TRUE AND aml.reconciled IS FALSE ) ) ''' # Filter on the same currency. if rule.match_same_currency: query += ''' AND COALESCE(st_line.currency_id, journal.currency_id, company.currency_id) = COALESCE(aml.currency_id, company.currency_id) ''' params = [rule.sequence, rule.id, tuple(st_lines.ids)] # Filter out excluded account.move.line. if excluded_ids: query += 'AND aml.id NOT IN %s' params += [tuple(excluded_ids)] query, params = rule._apply_conditions(query, params) queries.append(query) all_params += params full_query = self._get_with_tables(st_lines, partner_map=partner_map) full_query += ' UNION ALL '.join(queries) # Oldest due dates come first. full_query += ' ORDER BY aml_date_maturity, aml_id' return full_query, all_params @api.multi def _get_writeoff_suggestion_query(self, st_lines, excluded_ids=None, partner_map=None): ''' Get the query applying all reconciliation rules. :param st_lines: Account.bank.statement.lines recordset. :param excluded_ids: Account.move.lines to exclude. :param partner_map: Dict mapping each line with new partner eventually. :return: (query, params) ''' if any(m.rule_type != 'writeoff_suggestion' for m in self): raise UserError( _('Programmation Error: Can\'t call _get_wo_suggestion_query() for different rules than \'writeoff_suggestion\'' )) queries = [] all_params = [] for rule in self: query = ''' SELECT %s AS sequence, %s AS model_id, st_line.id AS id FROM account_bank_statement_line st_line LEFT JOIN account_journal journal ON journal.id = st_line.journal_id LEFT JOIN jnl_precision ON jnl_precision.journal_id = journal.id LEFT JOIN res_company company ON company.id = st_line.company_id LEFT JOIN partners_table line_partner ON line_partner.line_id = st_line.id WHERE st_line.id IN %s ''' params = [rule.sequence, rule.id, tuple(st_lines.ids)] query, params = rule._apply_conditions(query, params) queries.append(query) all_params += params full_query = self._get_with_tables(st_lines, partner_map=partner_map) full_query += ' UNION ALL '.join(queries) return full_query, all_params @api.multi def _check_rule_propositions(self, statement_line, candidates): ''' Check restrictions that can't be handled for each move.line separately. /!\ Only used by models having a type equals to 'invoice_matching'. :param statement_line: An account.bank.statement.line record. :param candidates: Fetched account.move.lines from query (dict). :return: True if the reconciliation propositions are accepted. False otherwise. ''' if not self.match_total_amount: return True # Match total residual amount. total_residual = 0.0 for aml in candidates: if aml['account_internal_type'] == 'liquidity': total_residual += aml['aml_currency_id'] and aml[ 'aml_amount_currency'] or aml['aml_balance'] else: total_residual += aml['aml_currency_id'] and aml[ 'aml_amount_residual_currency'] or aml[ 'aml_amount_residual'] line_residual = statement_line.currency_id and statement_line.amount_currency or statement_line.amount line_currency = statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.company_id.currency_id # Statement line amount is equal to the total residual. if float_is_zero(total_residual - line_residual, precision_rounding=line_currency.rounding): return True if line_residual > total_residual: amount_percentage = (total_residual / line_residual) * 100 else: amount_percentage = (line_residual / total_residual) * 100 return amount_percentage >= self.match_total_amount_param @api.multi def _apply_rules(self, st_lines, excluded_ids=None, partner_map=None): ''' Apply criteria to get candidates for all reconciliation models. :param st_lines: Account.bank.statement.lines recordset. :param excluded_ids: Account.move.lines to exclude. :param partner_map: Dict mapping each line with new partner eventually. :return: A dict mapping each statement line id with: * aml_ids: A list of account.move.line ids. * model: An account.reconcile.model record (optional). * status: 'reconciled' if the lines has been already reconciled, 'write_off' if the write-off must be applied on the statement line. ''' available_models = self.filtered( lambda m: m.rule_type != 'writeoff_button') results = dict((r.id, {'aml_ids': []}) for r in st_lines) if not available_models: return results ordered_models = available_models.sorted( key=lambda m: (m.sequence, m.id)) grouped_candidates = {} # Type == 'invoice_matching'. # Map each (st_line.id, model_id) with matching amls. invoices_models = ordered_models.filtered( lambda m: m.rule_type == 'invoice_matching') if invoices_models: query, params = invoices_models._get_invoice_matching_query( st_lines, excluded_ids=excluded_ids, partner_map=partner_map) self._cr.execute(query, params) query_res = self._cr.dictfetchall() for res in query_res: grouped_candidates.setdefault(res['id'], {}) grouped_candidates[res['id']].setdefault(res['model_id'], []) grouped_candidates[res['id']][res['model_id']].append(res) # Type == 'writeoff_suggestion'. # Map each (st_line.id, model_id) with a flag indicating the st_line matches the criteria. write_off_models = ordered_models.filtered( lambda m: m.rule_type == 'writeoff_suggestion') if write_off_models: query, params = write_off_models._get_writeoff_suggestion_query( st_lines, excluded_ids=excluded_ids, partner_map=partner_map) self._cr.execute(query, params) query_res = self._cr.dictfetchall() for res in query_res: grouped_candidates.setdefault(res['id'], {}) grouped_candidates[res['id']].setdefault(res['model_id'], True) # Keep track of already processed amls. amls_ids_to_exclude = set() # Keep track of already reconciled amls. reconciled_amls_ids = set() # Iterate all and create results. for line in st_lines: line_currency = line.currency_id or line.journal_id.currency_id or line.company_id.currency_id line_residual = line.currency_id and line.amount_currency or line.amount # Search for applicable rule. # /!\ BREAK are very important here to avoid applying multiple rules on the same line. for model in ordered_models: # No result found. if not grouped_candidates.get( line.id) or not grouped_candidates[line.id].get( model.id): continue excluded_lines_found = False if model.rule_type == 'invoice_matching': candidates = grouped_candidates[line.id][model.id] # If some invoices match on the communication, suggest them. # Otherwise, suggest all invoices having the same partner. # N.B: The only way to match a line without a partner is through the communication. first_batch_candidates = [] second_batch_candidates = [] for c in candidates: # Don't take into account already reconciled lines. if c['aml_id'] in reconciled_amls_ids: continue # Dispatch candidates between lines matching invoices with the communication or only the partner. if c['communication_flag']: first_batch_candidates.append(c) elif not first_batch_candidates: second_batch_candidates.append(c) available_candidates = first_batch_candidates or second_batch_candidates # Special case: the amount are the same, submit the line directly. for c in available_candidates: residual_amount = c['aml_currency_id'] and c[ 'aml_amount_residual_currency'] or c[ 'aml_amount_residual'] if float_is_zero( residual_amount - line_residual, precision_rounding=line_currency.rounding): available_candidates = [c] break # Needed to handle check on total residual amounts. if first_batch_candidates or model._check_rule_propositions( line, available_candidates): results[line.id]['model'] = model # Add candidates to the result. for candidate in available_candidates: # Special case: the propositions match the rule but some of them are already consumed by # another one. Then, suggest the remaining propositions to the user but don't make any # automatic reconciliation. if candidate['aml_id'] in amls_ids_to_exclude: excluded_lines_found = True continue results[line.id]['aml_ids'].append( candidate['aml_id']) amls_ids_to_exclude.add(candidate['aml_id']) if excluded_lines_found: break # Create write-off lines. move_lines = self.env['account.move.line'].browse( results[line.id]['aml_ids']) partner = partner_map and partner_map.get( line.id) and self.env['res.partner'].browse( partner_map[line.id]) reconciliation_results = model._prepare_reconciliation( line, move_lines, partner=partner) # A write-off must be applied. if reconciliation_results['new_aml_dicts']: results[line.id]['status'] = 'write_off' # Process auto-reconciliation. if model.auto_reconcile: # An open balance is needed but no partner has been found. if reconciliation_results[ 'open_balance_dict'] is False: break new_aml_dicts = reconciliation_results[ 'new_aml_dicts'] if reconciliation_results['open_balance_dict']: new_aml_dicts.append( reconciliation_results['open_balance_dict'] ) if not line.partner_id and partner: line.partner_id = partner counterpart_moves = line.process_reconciliation( counterpart_aml_dicts=reconciliation_results[ 'counterpart_aml_dicts'], payment_aml_rec=reconciliation_results[ 'payment_aml_rec'], new_aml_dicts=new_aml_dicts, ) results[line.id]['status'] = 'reconciled' results[line.id][ 'reconciled_lines'] = counterpart_moves.mapped( 'line_ids') # The reconciled move lines are no longer candidates for another rule. reconciled_amls_ids.update(move_lines.ids) # Break models loop. break elif model.rule_type == 'writeoff_suggestion' and grouped_candidates[ line.id][model.id]: results[line.id]['model'] = model results[line.id]['status'] = 'write_off' # Create write-off lines. partner = partner_map and partner_map.get( line.id) and self.env['res.partner'].browse( partner_map[line.id]) reconciliation_results = model._prepare_reconciliation( line, partner=partner) # An open balance is needed but no partner has been found. if reconciliation_results['open_balance_dict'] is False: break # Process auto-reconciliation. if model.auto_reconcile: new_aml_dicts = reconciliation_results['new_aml_dicts'] if reconciliation_results['open_balance_dict']: new_aml_dicts.append( reconciliation_results['open_balance_dict']) if not line.partner_id and partner: line.partner_id = partner counterpart_moves = line.process_reconciliation( counterpart_aml_dicts=reconciliation_results[ 'counterpart_aml_dicts'], payment_aml_rec=reconciliation_results[ 'payment_aml_rec'], new_aml_dicts=new_aml_dicts, ) results[line.id]['status'] = 'reconciled' results[line.id][ 'reconciled_lines'] = counterpart_moves.mapped( 'line_ids') # Break models loop. break return results
class PaperLessRegistration(models.Model): _inherit = 'registration' health_form_signed = fields.Boolean('Health form signed') postal_address = fields.Text('Postal Address') student_is_living_with = fields.Char('Student is living with') lang_spoken_at_home = fields.Many2many('res.lang', string='Language(s) spoken at home') english_is_spoken_at_home = fields.Selection([('yes','YES'),('no','NO')], string="English is spoken at home (Yes / No)") english_written = fields.Selection([('none','None'),('some','Some'), ('satisfactory','Satisfactory'), ('proficient','Proficient')],string="Written") english_spoken = fields.Selection([('none','None'),('some','Some'), ('satisfactory','Satisfactory'), ('proficient','Proficient')],string="Spoken") english_reading = fields.Selection([('none','None'),('some','Some'), ('satisfactory','Satisfactory'), ('proficient','Proficient')],string="Reading") father_nationality = fields.Many2one('res.country',string="Father Nationality") father_passport = fields.Char('Father Passport') father_emirates_id = fields.Char('Father Emirates Id') father_designation = fields.Char('Father Designation') mother_nationality = fields.Many2one('res.country',string="Mother Nationality") mother_passport = fields.Char('Mother Passport') mother_emirates_id = fields.Char('Mother Emirates Id') mother_designation = fields.Char('Mother Designation') medium_of_instruction = fields.Char('Medium of Instruction') identified_gifted_or_talented = fields.Selection([('yes','YES'),('no','NO')], string="Has your child ever been identified as gifted or talented ?") has_child_detained = fields.Selection([('yes','YES'),('no','NO')], string="Has your child ever been detained ?") has_child_detained_grade = fields.Many2one('course','Grade') child_received_academic_distinction = fields.Selection([('yes','YES'),('no','NO')], string="Has your child received any academic distinction ?") child_received_academic_distinction_details = fields.Char(string='If yes, please indicate details') has_suspended_expelled_by_school = fields.Selection([('yes','YES'),('no','NO')], string="Has your child ever been suspended/ expelled by any school in the past ?") has_suspended_expelled_by_school_details = fields.Char(string='If yes, please indicate details') child_associated_with_awareness = fields.Selection([('yes','YES'),('no','NO')], string="Has your child been associated with any social awareness programme?") child_associated_with_awareness_details = fields.Char(string='If yes, please indicate details') member_of_environment_protection = fields.Selection([('yes','YES'),('no','NO')], string="Has your child been a member of environment protection group ?") member_of_environment_protection_details = fields.Char(string='If yes, please indicate details') leadership_positions_in_school = fields.Selection([('yes','YES'),('no','NO')], string="Has your child held any leadership positions in School? (Prefectorial Board/ Student Council)") leadership_positions_in_school_details = fields.Char(string='If yes, please indicate details') special_education_programme = fields.Selection([('yes','YES'),('no','NO')], string="Has your child ever been in a speech therapy, remedial reading support, special education programme?") special_education_programme_details = fields.Char(string='If yes, please indicate details') special_learning_disability = fields.Selection([('yes','YES'),('no','NO')], string="Has your child ever been identified as having a special learning disability?") special_learning_disability_details = fields.Selection([('reading','Reading'),('language','Language'),('mathematics','Mathematics')], string="If yes, please indicate learning disability area") has_other_than_english_languages = fields.Selection([('yes','YES'),('no','NO')], string="Has your child ever studied any languages other than English ?") other_than_english_languages = fields.Many2one('res.lang', 'If yes, please mention the other languages') hobbies_interests = fields.Text("Please list your child's hobbies / interests") has_play_any_musical_instrument = fields.Selection([('yes','YES'),('no','NO')], string="Does your child play any musical instrument ?") musical_instrument_details = fields.Char(string='If yes, please specify') has_formal_training_in_music = fields.Selection([('yes','YES'),('no','NO')], string="Has your child had any formal training in music ?") training_in_music_details = fields.Char(string='If yes, please give details') sport_child_play = fields.Char(string='Which sport does your child play ?') has_training_or_interest_art = fields.Selection([('yes','YES'),('no','NO')], string="Has your child had any training / shown interest in fine arts ?") has_training_or_interest_art_details = fields.Char(string='If yes, please give details') inter_school_competitions = fields.Selection([('yes','YES'),('no','NO')], string="Has your child participated in inter/ intra school competitions?") inter_school_competitions_details = fields.Char(string='If yes, please give details') special_activity_interested = fields.Char(string='Any other special activity your child is interested in?') adjusts_new_situations_with_ease = fields.Boolean('Adjusts to new situations with ease') has_small_group_of_friends = fields.Boolean('Has a small group of friends') has_never_adjust_new_situation = fields.Boolean('Has never had to adjust to a new situation') has_many_friends = fields.Boolean('Has many friends') likes_be_active_in_school = fields.Boolean('Likes to be active in school') expressions_describe_your_child = fields.Selection([('very_active','Very Active'),('very_quiet','Very Quiet'), ('average','Average'),('above_average','Above Average'), ('shy','Shy'),('sociable','Sociable'), ('aggressive','Aggressive'),('other','Other')], string='Please Select the expression that describe your child.') social_emotional_behavioural_difficulties = fields.Selection([('yes','YES'),('no','NO')], string="Has your child ever experienced social, emotional or behavioural difficulties?") useful_information_for_educating = fields.Char(string='Is there any other information you feel would be useful for those educating your child?') person_to_call = fields.Char('Person to call') emergency_relationship = fields.Char(string='Relationship') # emergency_tel_no = fields.Char(string='Tel. Nos. to call') has_use_bus_facility = fields.Selection([('yes', 'YES'), ('no', 'NO')], string="Would your child be using bus facility?") normal_delivery = fields.Char('Normal delivery') caesarean = fields.Char('Caesarean') premature = fields.Char('Premature') developmental_milestones = fields.Char('Developmental Milestones') age_your_child_talk = fields.Char('At what age did your child talk? (14 months +)') hand_preference = fields.Selection([('left', 'Left'), ('right', 'Right'),('both', 'Both')], string="Hand Preference") can_button_his_shirt = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Can the child button his shirt?') can_zip_his_pant = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Can the child zip his pant ?') can_child_indicate_his_toilet_needs = fields.Selection([('yes', 'YES'), ('no', 'NO')], string = 'Can the child indicate his toilet needs?') child_indicate_his_toilet_needs_details = fields.Char('If yes, how?') child_know_his_phone_number = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Does your child know his phone number?') toys_likes_to_play_with = fields.Char('What are the toys he / she likes to play with?') special_interest = fields.Char('Any special interest that your child has?') child_like_to_play_with = fields.Char(string='Does your child like to play: alone / with friends / with family members') child_like_to_look_at_picture = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Does your child like to look at picture books?') child_like_to_watch_tv_programmes = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Does your child like to watch TV programmes?') channels_like_to_watch = fields.Char('What channels does he / she watch?') child_have_any_health_problem = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Does your child have any health problem?') health_problem_details = fields.Char('If yes, what?') health_card_no = fields.Char('Health Card No') diphtheria = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Diphtheria') accident = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Accident') @api.multi def reminder_for_additional_form(self): if self.fee_structure_confirm != True: raise except_orm(_("Warning !"), _('Please Confirm the fee Structure before send reminder For Additional form')) return super(PaperLessRegistration,self).reminder_for_additional_form()
class OpRoomDistribution(models.TransientModel): """ Exam Room Distribution """ _name = "op.room.distribution" _description = "Room Distribution" @api.depends('student_ids') def _compute_get_total_student(self): for record in self: total_student = 0 if record.student_ids: total_student = len(record.student_ids) record.total_student = total_student @api.depends('room_ids', 'room_ids.capacity') def _compute_get_room_capacity(self): for record in self: room_capacity = 0 if record.room_ids: for room in record.room_ids: room_capacity += (room.capacity or 0) record.room_capacity = room_capacity exam_id = fields.Many2one('op.exam', 'Exam(s)') subject_id = fields.Many2one('op.subject', 'Subject', related="exam_id.subject_id") name = fields.Char("Exam") start_time = fields.Datetime("Start Time") end_time = fields.Datetime("End Time") exam_session = fields.Many2one("op.exam.session", 'Exam Session') course_id = fields.Many2one("op.course", 'Course') batch_id = fields.Many2one("op.batch", 'Batch') total_student = fields.Integer( "Total Student", compute="_compute_get_total_student") room_capacity = fields.Integer( "Room Capacity", compute="_compute_get_room_capacity") room_ids = fields.Many2many("op.exam.room", string="Exam Rooms") student_ids = fields.Many2many("op.student", String='Student') @api.model def default_get(self, fields): res = super(OpRoomDistribution, self).default_get(fields) active_id = self.env.context.get('active_id', False) exam = self.env['op.exam'].browse(active_id) session = exam.session_id reg_ids = self.env['op.subject.registration'].search( [('course_id', '=', session.course_id.id)]) student_ids = [] for reg in reg_ids: if exam.subject_id.subject_type == 'compulsory': student_ids.append(reg.student_id.id) else: for sub in reg.elective_subject_ids: if sub.id == exam.subject_id.id: student_ids.append(reg.student_id.id) student_ids = list(set(student_ids)) total_student = len(student_ids) res.update({ 'exam_id': active_id, 'name': exam.name, 'start_time': exam.start_time, 'end_time': exam.end_time, 'exam_session': session.id, 'course_id': session.course_id.id, 'batch_id': session.batch_id.id, 'total_student': total_student, 'student_ids': [(6, 0, student_ids)], }) return res def schedule_exam(self): attendance = self.env['op.exam.attendees'] for exam in self: if exam.total_student > exam.room_capacity: raise exceptions.AccessError( _("Room capacity must be greater than total number \ of student")) student_ids = exam.student_ids.ids for room in exam.room_ids: for i in range(room.capacity): if not student_ids: continue attendance.create({ 'exam_id': exam.exam_id.id, 'student_id': student_ids[0], 'status': 'present', 'course_id': exam.course_id.id, 'batch_id': exam.batch_id.id, 'room_id': room.id }) student_ids.remove(student_ids[0]) exam.exam_id.state = 'schedule' return True
class PosPaymentMethod(models.Model): """ Used to classify pos.payment. Generic characteristics of a pos.payment is described in this model. E.g. A cash payment can be described by a pos.payment.method with fields: is_cash_count = True and a cash_journal_id set to an `account.journal` (type='cash') record. When a pos.payment.method is cash, cash_journal_id is required as it will be the journal where the account.bank.statement.line records will be created. """ _name = "pos.payment.method" _description = "Point of Sale Payment Methods" _order = "id asc" def _get_payment_terminal_selection(self): return [] name = fields.Char(string="Payment Method", required=True) receivable_account_id = fields.Many2one( 'account.account', string='Intermediary Account', required=True, domain=[('reconcile', '=', True), ('user_type_id.type', '=', 'receivable')], default=lambda self: self.env.company. account_default_pos_receivable_account_id, ondelete='restrict', help= 'Account used as counterpart of the income account in the accounting entry representing the pos sales.' ) is_cash_count = fields.Boolean(string='Cash') cash_journal_id = fields.Many2one( 'account.journal', string='Cash Journal', domain=[('type', '=', 'cash')], ondelete='restrict', help= 'The payment method is of type cash. A cash statement will be automatically generated.' ) split_transactions = fields.Boolean( string='Split Transactions', default=False, help= 'If ticked, each payment will generate a separated journal item. Ticking that option will slow the closing of the PoS.' ) open_session_ids = fields.Many2many( 'pos.session', string='Pos Sessions', compute='_compute_open_session_ids', help='Open PoS sessions that are using this payment method.') config_ids = fields.Many2many('pos.config', string='Point of Sale Configurations') company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) use_payment_terminal = fields.Selection( selection=lambda self: self._get_payment_terminal_selection(), string='Use a Payment Terminal', help='Record payments with a terminal on this journal.') hide_use_payment_terminal = fields.Boolean( compute='_compute_hide_use_payment_terminal', help='Technical field which is used to ' 'hide use_payment_terminal when no payment interfaces are installed.') @api.depends('is_cash_count') def _compute_hide_use_payment_terminal(self): no_terminals = not bool( self._fields['use_payment_terminal'].selection(self)) for payment_method in self: payment_method.hide_use_payment_terminal = no_terminals or payment_method.is_cash_count @api.onchange('use_payment_terminal') def _onchange_use_payment_terminal(self): """Used by inheriting model to unset the value of the field related to the unselected payment terminal.""" pass @api.depends('config_ids') def _compute_open_session_ids(self): for payment_method in self: payment_method.open_session_ids = self.env['pos.session'].search([ ('config_id', 'in', payment_method.config_ids.ids), ('state', '!=', 'closed') ]) @api.onchange('is_cash_count') def _onchange_is_cash_count(self): if not self.is_cash_count: self.cash_journal_id = False else: self.use_payment_terminal = False def _is_write_forbidden(self, fields): return bool(fields and self.open_session_ids) def write(self, vals): if self._is_write_forbidden(set(vals.keys())): raise UserError( 'Kindly close and validate the following open PoS Sessions before modifying this payment method.\n' 'Open sessions: %s' % (' '.join(self.open_session_ids.mapped('name')), )) return super(PosPaymentMethod, self).write(vals)
class educationExamResultWizard(models.TransientModel): _name = 'education.exam.result.wizard' _description = 'print academic transcript for selected exams' academic_year = fields.Many2one('education.academic.year', "Academic Year") level = fields.Many2one('education.class', "Level") exams = fields.Many2many('education.exam') specific_section = fields.Boolean('For a specific section', default="True") section = fields.Many2one('education.class.division', required="True") specific_student = fields.Boolean('For a specific Student') student = fields.Many2one('education.student', 'Student') report_type = fields.Selection([('1', 'Regular'), ('2', 'Converted')], string="Report Type", default='1', required='True') state = fields.Selection([('draft', 'Draft'), ('done', 'Done')], compute='calculate_state') show_paper = fields.Boolean("Show Papers") show_tut = fields.Boolean("Show Monthly") show_subjective = fields.Boolean("Show Subjective") show_merit_class = fields.Boolean("Show Merit Class Position", default="True") show_merit_group = fields.Boolean("Show Merit Group Position") show_merit_section = fields.Boolean("Show Merit Section Position") show_objective = fields.Boolean("show objective") show_prac = fields.Boolean("Show Practical") show_total = fields.Boolean("Show Total") show_average = fields.Boolean("Show average", default="True") show_average_only = fields.Boolean("Show average Only") record_per_page = fields.Integer(string="Student Per Page", default="6") @api.model def del_generated_results(self): for exam in self.exams: records = self.env['education.exam.results.new'].search([ ('exam_id', '=', exam.id) ]).unlink() records = self.env['exam.subject.pass.rules'].search([ ('exam_id', '=', exam.id) ]).unlink() records = self.env['exam.paper.pass.rules'].search([ ('subject_rule_id.exam_id', '=', exam.id) ]).unlink() result_lines = self.env['education.exam.result.exam.line'].search([ ('exam_count', '=', '1'), ('exam_ids', '=', exam.id) ]) result_lines.unlink() exam_lines = self.env['education.exam.result.exam.line'].search([ ('exam_ids', 'in', [exa.id for exa in exam]) ]) exam_lines.unlink() print('ok') @api.model def render_html(self, docids, data=None): return { 'type': 'ir.actions.report', 'report_name': 'education_exam.report_exam_marksheet', 'model': 'education.exam.result.wizard', 'report_type': "qweb-pdf", 'paperformat': "legal_landscape", } @api.model def calculate_state(self): results = self.env[('education.exam.results')].search([ ('academic_year', '=', self.academic_year.id), ('class_id', '=', 'level') ]) for exam in self.exams: rec = results.search([('exam_id', '=', exam.id)]) for line in rec: if line.state != 'done': self.state = 'draft' return True self.state = 'done' @api.model def get_merit_list(self): exam_lines = [] for exam in self.exams: result_exam_lines = self.env[ 'education.exam.result.exam.line'].search([ ('exam_count', '=', '1'), ('exam_ids', '=', exam.id) ]) exam_lines.append(result_exam_lines) if len(self.exams) > 1: exam_no = len(self.exams) result_exam_lines = self.env[ 'education.exam.result.exam.line'].search([ ('exam_count', '=', exam_no), ('exam_ids', 'in', [exa.id for exa in self.exams]) ]) exam_lines.append(result_exam_lines) self.env['education.exam.results.new'].calculate_merit_list( exam_lines, self.level) @api.model @api.onchange('level', 'section') def get_student_domain(self): for rec in self: domain = [] if rec.section: domain.append(('class_id', '=', rec.section.id)) else: domain.append(('class_id.class_id.id', '=', rec.level.id)) return {'domain': {'student': domain}} @api.model @api.onchange('specific_section') def onchange_specific_section(self): for rec in self: if rec.specific_section == False: rec.specific_student = False rec.section = False rec.student = False @api.model @api.onchange('specific_student') def onchange_specific_student(self): for rec in self: if rec.specific_student == False: rec.student = False @api.model def generate_results(self): for exam in self.exams: result_exam_lines = self.env[ 'education.exam.result.exam.line'].search([ ('exam_count', '=', '1'), ('exam_ids', '=', exam.id) ]) if len(result_exam_lines) == 0: data = { 'exam_ids': [(6, 0, [exam.id])], 'academic_year': exam.academic_year.id, 'total_working_days': exam.total_working_days, 'name': exam.name, # 'exam_result_line':result_exam_lines.id, 'exam_count': 1, } result_exam_lines = result_exam_lines.create(data) # todo this line is hashed for average line test fast for line in result_exam_lines: result_exam_lines.calculate_exam_results(line) if len(self.exams) > 1: t_working_days = 0 exam_list = [] for exam in self.exams: result_exam_lines = self.env[ 'education.exam.result.exam.line'].search([ ('exam_count', '=', '1'), ('exam_ids', '=', exam.id) ]) t_working_days = t_working_days + result_exam_lines.total_working_days exam_list.append(exam.id) exam_no = len(self.exams) search_str = [('exam_count', '=', exam_no)] result_exam_lines = self.env[ 'education.exam.result.exam.line'].search([ ('exam_count', '=', exam_no), ('exam_ids', 'in', [exa for exa in exam_list]) ]) if len(result_exam_lines) == 0: data = { 'exam_ids': [(6, 0, exam_list)], 'academic_year': self.academic_year.id, 'total_working_days': t_working_days, 'name': 'Average & Final', 'exam_count': exam_no, } result_exam_lines = result_exam_lines.create(data) else: result_exam_lines.total_working_days = t_working_days result_exam_lines.process_average_results(result_exam_lines) @api.model def calculate_subject_rules(self, subject_list, exam): for subjects in subject_list: subjectRules = self.env['exam.subject.pass.rules'].search([ ('exam_id', '=', exam.id), ('subject_id', '=', subjects.id) ]) for line in subjectRules: for paper_rule in line.paper_ids: paper_rule.name = paper_rule.paper_id.paper paper_rule.paper_marks = paper_rule.tut_mark + paper_rule.subj_mark + paper_rule.obj_mark + paper_rule.prac_mark line.academic_year = line.exam_id.academic_year.id line.name = line.subject_id.name + " for " + line.class_id.name + "-" + line.academic_year.name subject_full_marks = 0 subjective_mark = 0 objective_mark = 0 tutorial_mark = 0 practical_mark = 0 for paper in line.paper_ids: subject_full_marks = subject_full_marks + paper.paper_marks subjective_mark = subjective_mark + paper.subj_mark objective_mark = objective_mark + paper.obj_mark tutorial_mark = tutorial_mark + paper.tut_mark practical_mark = practical_mark + paper.prac_mark line.subject_marks = subject_full_marks line.prac_mark = practical_mark line.obj_mark = objective_mark line.subj_mark = subjective_mark line.tut_mark = tutorial_mark @api.model def calculate_subjects_results(self, exam): student_lines = self.env['education.exam.results.new'].search([ ('exam_id', '=', exam.id) ]) for student in student_lines: obtained_general = 0 obtained_general_converted = 0 count_general_subjects = 0 count_general_paper = 0 count_general_fail = 0 student.general_fail_count = 0 full_general_mark = 0 full_general_mark_converted = 0 gp_general = 0 obtained_optional = 0 obtained_optional_converted = 0 count_optional_subjects = 0 count_optional_paper = 0 count_optional_fail = 0 optional_full_mark = 0 optional_full_mark_converted = 0 gp_optional = 0 obtained_extra = 0 obtained_extra_converted = 0 count_extra_subjects = 0 count_extra_paper = 0 count_extra_fail = 0 extra_full_mark = 0 extra_full_mark_converted = 0 gp_extra = 0 res_type_count = 0 hide_tut = True hide_subj = True hide_obj = True hide_prac = True hide_paper = True for subject in student.subject_line: paper_count = 0 PassFail = True optional = False extra = False obt_tut = 0 obt_prac = 0 obt_subj = 0 obt_obj = 0 mark_tut = 0 mark_prac = 0 mark_subj = 0 mark_obj = 0 subject_obtained = 0 subject_obtained_converted = 0 subject_full = 0 subject_full_converted = 0 count_fail = 0 for paper in subject.paper_ids: paper_obtained = 0 paper_obtained_converted = 0 paper_full = 0 paper_full_converted = 0 paper_count = paper_count + 1 if paper.paper_id in student.student_history.optional_subjects: optional = True elif paper.paper_id.evaluation_type == 'extra': extra = True if paper.pass_rule_id.tut_mark > 0: hide_tut = False if paper.tut_pr == True: paper_obtained = paper_obtained + paper.tut_obt obt_tut = obt_tut + paper.tut_obt paper_full = paper_full + paper.pass_rule_id.tut_mark mark_tut = mark_tut + paper.pass_rule_id.tut_mark else: PassFail = False if paper.pass_rule_id.subj_mark > 0: hide_subj = False if paper.subj_pr == True: paper_obtained = paper_obtained + paper.subj_obt paper_full = paper_full + paper.pass_rule_id.subj_mark obt_subj = obt_subj + paper.subj_obt mark_subj = mark_subj + paper.pass_rule_id.subj_mark else: PassFail = False if paper.pass_rule_id.obj_mark > 0: hide_obj = False if paper.obj_pr == True: paper_obtained = paper_obtained + paper.obj_obt paper_full = paper_full + paper.pass_rule_id.obj_mark obt_obj = obt_obj + paper.obj_obt mark_obj = mark_obj + paper.pass_rule_id.obj_mark else: PassFail = False if paper.pass_rule_id.prac_mark > 0: hide_prac = False if paper.prac_pr == True: paper_obtained = paper_obtained + paper.prac_obt paper_full = paper_full + paper.pass_rule_id.prac_mark obt_prac = obt_prac + paper.prac_obt mark_prac = mark_prac + paper.pass_rule_id.prac_mark else: PassFail = False if paper.pass_rule_id.tut_pass > paper.tut_obt: PassFail = False elif paper.pass_rule_id.subj_pass > paper.subj_obt: PassFail = False elif paper.pass_rule_id.obj_pass > paper.obj_obt: PassFail = False elif paper.pass_rule_id.prac_pass > paper.prac_obt: PassFail = False paper.paper_obt = paper_obtained paper.passed = PassFail paper.paper_marks = paper_full if paper_full >= 100: paper.paper_marks_converted = 100 else: paper.paper_marks_converted = 50 paper.paper_obt_converted = self.env[ 'report.education_exam.report_dsblsc_marksheet'].half_round_up( (paper_obtained / paper_full) * paper.paper_marks_converted) subject_obtained = subject_obtained + paper.paper_obt subject_obtained_converted = subject_obtained_converted + paper.paper_obt_converted subject_full = subject_full + paper_full subject_full_converted = subject_full_converted + paper.paper_marks_converted subject.obj_obt = obt_obj subject.tut_obt = obt_tut subject.subj_obt = obt_subj subject.prac_obt = obt_prac subject.subject_obt = subject_obtained subject.subject_obt_converted = self.env[ 'report.education_exam.report_dsblsc_marksheet'].half_round_up( (subject_obtained / subject_full) * subject_full_converted) #subject_obtained_converted subject.subject_marks = subject_full subject.subject_marks_converted = subject_full_converted if subject.pass_rule_id.tut_pass > subject.tut_obt: PassFail = False elif subject.pass_rule_id.subj_pass > subject.subj_obt: PassFail = False elif subject.pass_rule_id.obj_pass > subject.obj_obt: PassFail = False elif subject.pass_rule_id.prac_pass > subject.prac_obt: PassFail = False subject.pass_or_fail = PassFail if PassFail == False: count_fail = 1 subject_grade_point = 0 subject_letter_grade = 'F' else: count_fail = 0 subject_grade_point = self.env[ 'education.result.grading'].get_grade_point( subject_full, subject_obtained) subject_letter_grade = self.env[ 'education.result.grading'].get_letter_grade( subject_full, subject_obtained) if subject_letter_grade == 'F': count_fail = 1 subject.grade_point = subject_grade_point subject.letter_grade = subject_letter_grade if extra == True: subject.extra_for = student.id obtained_extra = obtained_extra + subject.subject_obt obtained_extra_converted = obtained_extra_converted + subject.subject_obt_converted count_extra_subjects = count_extra_subjects + 1 count_extra_paper = count_extra_paper + paper_count extra_full_mark = extra_full_mark + subject_full extra_full_mark_converted = extra_full_mark_converted + subject_full_converted gp_extra = gp_extra + subject_grade_point count_extra_fail = count_extra_fail + count_fail elif optional == True: subject.optional_for = student.id obtained_optional = obtained_optional + subject.subject_obt obtained_optional_converted = obtained_optional_converted + subject.subject_obt_converted count_optional_subjects = count_optional_subjects + 1 count_optional_paper = count_optional_paper + paper_count optional_full_mark = optional_full_mark + subject.pass_rule_id.subject_marks optional_full_mark_converted = optional_full_mark_converted + subject_full_converted gp_optional = gp_optional + subject_grade_point count_optional_fail = count_optional_fail + count_fail else: full_general_mark = full_general_mark + subject_full full_general_mark_converted = full_general_mark_converted + subject_full_converted subject.general_for = student.id count_general_subjects = count_general_subjects + 1 obtained_general = obtained_general + subject.subject_obt obtained_general_converted = obtained_general_converted + subject.subject_obt_converted count_general_paper = count_general_paper + paper_count gp_general = gp_general + subject_grade_point count_general_fail = count_general_fail + count_fail subject.paper_count = paper_count if paper_count > 1: hide_paper = False if hide_tut == True: student.show_tut = False else: student.show_tut = True if hide_subj == True: student.show_subj = False else: student.show_subj = True if hide_obj == True: student.show_obj = False else: student.show_obj = True if hide_prac == True: student.show_prac = False else: student.show_prac = True if hide_paper == True: student.show_paper = False else: student.show_paper = True if student.show_tut == True: res_type_count = res_type_count + 1 if student.show_subj == True: res_type_count = res_type_count + 1 if student.show_obj == True: res_type_count = res_type_count + 1 if student.show_prac == True: res_type_count = res_type_count + 1 student.result_type_count = res_type_count student.extra_row_count = count_extra_paper student.extra_count = count_extra_subjects student.extra_obtained = obtained_extra student.extra_obtained_converted = obtained_extra_converted student.extra_fail_count = count_extra_fail student.extra_full_mark = extra_full_mark student.extra_full_mark_converted = extra_full_mark_converted student.general_row_count = count_general_paper student.general_count = count_general_subjects student.general_obtained = obtained_general student.general_obtained_converted = obtained_general_converted student.general_fail_count = count_general_fail student.general_gp = gp_general student.general_full_mark = full_general_mark student.general_full_mark_converted = full_general_mark_converted student.optional_row_count = count_optional_paper student.optional_count = count_optional_subjects student.optional_obtained = obtained_optional student.optional_obtained_converted = obtained_optional_converted student.optional_fail_count = count_optional_fail student.optional_gp = gp_optional student.optional_full_mark = optional_full_mark student.optional_full_converted = optional_full_mark_converted if student.general_count > 0: student.general_gpa = student.general_gp / student.general_count else: student.general_gpa = 0 if student.optional_count > 0: student.optional_gpa = student.optional_gp / student.optional_count if student.optional_gpa > 2: student.optional_gpa_above_2 = student.optional_gpa - 2 else: student.optional_gpa = 0 if student.optional_gpa > 0: optional_40_perc = student.optional_full_mark * 40 / 100 optional_40_perc_converted = student.optional_full_converted * 40 / 100 student.optional_obtained_above_40_perc = student.optional_obtained - optional_40_perc student.optional_obtained_above_40_perc_converted = student.optional_obtained_converted - optional_40_perc_converted student.net_obtained = student.general_obtained + student.optional_obtained_above_40_perc student.net_obtained_converted = student.general_obtained_converted + student.optional_obtained_above_40_perc_converted if student.general_count > 0: if student.optional_gpa_above_2 < 0: student.optional_gpa_above_2 = 0 netGPA = student.general_gpa + (student.optional_gpa_above_2 / student.general_count) if netGPA < 5: student.net_gpa = round(netGPA, 2) else: student.net_gpa = 5 student.net_lg = self.env['education.result.grading'].get_lg( student.net_gpa) if student.extra_count > 0: if student.extra_fail_count < 1: student.extra_gpa = student.extra_gp / student.extra_count # TODO Here to genrate Merit List # result_lines=self.env['education.exam.results.new'].sorted(key=lambda r: (r.name, r.country_id.name)) # # ############# TODO get subject Highest subject_rule_lines = self.env['exam.subject.pass.rules'].search([ ('exam_id', '=', exam.id) ]) for subject_rule_line in subject_rule_lines: subject_result_lines = self.env['results.subject.line.new'].search( [('pass_rule_id', '=', subject_rule_line.id)], limit=1, order='subject_obt DESC') subject_rule_line.subject_highest = subject_result_lines.subject_obt for paper_rule_line in subject_rule_line.paper_ids: paper_result_line = self.env['results.paper.line'].search( [('pass_rule_id', '=', paper_rule_line.id)], limit=1, order='paper_obt DESC') paper_rule_line.paper_highest = paper_result_line.paper_obt # subjectLines=self.env['results.subject.line.new'].search([('result_id.exam_id','=',exam.id)]) # ##### distinct values search # subject=subjectLines.mapped('subject_id') # for value in set(subject): # lines=subjectLines.search([('subject_id','=',value.id)], order='subject_mark DESC') # highest_set=False # for line in lines: # if highest_set==False: # highest=line.subject_mark # highest_set=True # line.subject_highest=highest @api.model def calculate_result_paper_lines(self, result_paper_lines): for rec in result_paper_lines: passFail = True if rec.pass_rule_id.tut_pass > rec.tut_obt: passFail = False elif rec.pass_rule_id.subj_pass > rec.subj_obt: passFail = False elif rec.pass_rule_id.obj_pass > rec.obj_obt: passFail = False elif rec.pass_rule_id.prac_pass > rec.prac_obt: passFail = False elif rec.pass_rule_id.tut_mark > 0: if rec.tut_pr == False: passFail = False elif rec.pass_rule_id.subj_mark > 0: if rec.subj_pr == False: passFail = False elif rec.pass_rule_id.obj_mark > 0: if rec.obj_pr == False: passFail = False elif rec.pass_rule_id.prac_mark > 0: if rec.prac_pr == False: passFail = False paper_obtained = 0 if rec.pass_rule_id.tut_mark > 0: paper_obtained = paper_obtained + rec.tut_obt if rec.pass_rule_id.subj_mark > 0: paper_obtained = paper_obtained + rec.subj_obt if rec.pass_rule_id.obj_mark > 0: paper_obtained = paper_obtained + rec.obj_obt if rec.pass_rule_id.prac_mark > 0: paper_obtained = paper_obtained + rec.prac_obt rec.paper_obt = paper_obtained rec.passed = passFail if passFail == True: rec.gp = self.env['education.result.grading'].get_grade_point( rec.pass_rule_id.paper_marks, rec.paper_obt) rec.lg = self.env['education.result.grading'].get_letter_grade( rec.pass_rule_id.paper_marks, rec.paper_obt) else: rec.gp = 0 rec.lg = 'F' @api.model def calculate_result_subject_lines(self, result_subject_lines): for rec in result_subject_lines: practical_obt = 0 subjective_obt = 0 objective_obt = 0 tutorial_obt = 0 practical_mark = 0 subjective_mark = 0 objective_mark = 0 tutorial_mark = 0 PassFail = True for line in rec.paper_ids: practical_obt = practical_obt + line.prac_obt subjective_obt = subjective_obt + line.subj_obt objective_obt = objective_obt + line.obj_obt tutorial_obt = tutorial_obt + line.tut_obt practical_mark = practical_mark + line.pass_rule_id.tut_mark subjective_mark = subjective_mark + line.pass_rule_id.subj_mark objective_mark = objective_mark + line.pass_rule_id.obj_mark tutorial_mark = tutorial_mark + line.pass_rule_id.tut_mark if line.passed == False: PassFail = False rec.tut_obt = tutorial_obt rec.prac_obt = practical_obt rec.subj_obt = subjective_obt rec.obj_obt = objective_obt rec.tut_mark = tutorial_mark rec.prac_mark = practical_mark rec.subj_mark = subjective_mark rec.obj_mark = objective_mark if PassFail == False: PassFail = False elif rec.pass_rule_id.tut_pass > rec.tut_obt: PassFail = False elif rec.pass_rule_id.subj_pass > rec.subj_obt: PassFail = False elif rec.pass_rule_id.obj_pass > rec.obj_obt: PassFail = False elif rec.pass_rule_id.prac_pass > rec.prac_obt: PassFail = False rec.mark_scored = 0 if rec.pass_rule_id.tut_mark > 0: rec.mark_scored = rec.mark_scored + rec.tut_obt if rec.pass_rule_id.subj_mark > 0: rec.mark_scored = rec.mark_scored + rec.subj_obt if rec.pass_rule_id.obj_mark > 0: rec.mark_scored = rec.mark_scored + rec.obj_obt if rec.pass_rule_id.prac_mark > 0: rec.mark_scored = rec.mark_scored + rec.prac_obt if PassFail == True: rec.grade_point = rec.env[ 'education.result.grading'].get_grade_point( rec.pass_rule_id.subject_marks, rec.mark_scored) rec.letter_grade = rec.env[ 'education.result.grading'].get_letter_grade( rec.pass_rule_id.subject_marks, rec.mark_scored) else: rec.grade_point = 0 rec.letter_grade = 'F' @api.model def get_result_type_count(self, exam): result_lines = self.env['education.exam.results.new'].search([ ('exam_id', '=', exam.id) ]) for rec in result_lines: res_type_count = 0 if rec.show_tut == True: res_type_count = res_type_count + 1 if rec.show_subj == True: res_type_count = res_type_count + 1 if rec.show_obj == True: res_type_count = res_type_count + 1 if rec.show_prac == True: res_type_count = res_type_count + 1 rec.result_type_count = res_type_count
class DeliveryCarrier(models.Model): _name = 'delivery.carrier' _description = "Delivery 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') 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.Integer(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") _sql_constraints = [ ('margin_not_under_100_percent', 'CHECK (margin >= -100)', 'Margin cannot be lower than -100%'), ] 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', 'ilike', 'delivery_']], 'type': 'ir.actions.act_window', 'help': _('''<p class="o_view_nocontent"> Buy Eagle 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('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 + (float(self.margin) / 100.0)) # 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'] = _('Info:\nThe shipping is free because the order amount exceeds %.2f.\n(The actual shipping cost is: %.2f)') % (self.amount, res['price']) 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_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 Partner(models.Model): _inherit = 'res.partner' website_tag_ids = fields.Many2many('res.partner.tag', 'res_partner_res_partner_tag_rel', 'partner_id', 'tag_id', string='Website tags')
class CRMLeadMiningRequest(models.Model): _name = 'crm.iap.lead.mining.request' _description = 'CRM Lead Mining Request' def _default_lead_type(self): if self.env.user.has_group('crm.group_use_lead'): return 'lead' else: return 'opportunity' name = fields.Char(string='Request Number', required=True, readonly=True, default=lambda self: _('New'), copy=False) state = fields.Selection([('draft', 'Draft'), ('done', 'Done'), ('error', 'Error')], string='Status', required=True, default='draft') # Request Data lead_number = fields.Integer(string='Number of Leads', required=True, default=10) search_type = fields.Selection([('companies', 'Companies'), ('people', 'Companies and their Contacts')], string='Target', required=True, default='companies') error = fields.Text(string='Error', readonly=True) # Lead / Opportunity Data lead_type = fields.Selection([('lead', 'Lead'), ('opportunity', 'Opportunity')], string='Type', required=True, default=_default_lead_type) team_id = fields.Many2one('crm.team', string='Sales Team', domain="[('use_opportunities', '=', True)]") user_id = fields.Many2one('res.users', string='Salesperson') tag_ids = fields.Many2many('crm.lead.tag', string='Tags') lead_ids = fields.One2many('crm.lead', 'lead_mining_request_id', string='Generated Lead / Opportunity') leads_count = fields.Integer(compute='_compute_leads_count', string='Number of Generated Leads') # Company Criteria Filter filter_on_size = fields.Boolean(string='Filter on Size', default=False) company_size_min = fields.Integer(string='Size', default=1) company_size_max = fields.Integer(default=1000) country_ids = fields.Many2many('res.country', string='Countries') state_ids = fields.Many2many('res.country.state', string='States') industry_ids = fields.Many2many('crm.iap.lead.industry', string='Industries') # Contact Generation Filter contact_number = fields.Integer(string='Number of Contacts', default=1) contact_filter_type = fields.Selection([('role', 'Role'), ('seniority', 'Seniority')], string='Filter on', default='role') preferred_role_id = fields.Many2one('crm.iap.lead.role', string='Preferred Role') role_ids = fields.Many2many('crm.iap.lead.role', string='Other Roles') seniority_id = fields.Many2one('crm.iap.lead.seniority', string='Seniority') # Fields for the blue tooltip lead_credits = fields.Char(compute='_compute_tooltip', readonly=True) lead_contacts_credits = fields.Char(compute='_compute_tooltip', readonly=True) lead_total_credits = fields.Char(compute='_compute_tooltip', readonly=True) @api.onchange('lead_number', 'contact_number') def _compute_tooltip(self): for record in self: company_credits = CREDIT_PER_COMPANY * record.lead_number contact_credits = CREDIT_PER_CONTACT * record.contact_number total_contact_credits = contact_credits * record.lead_number record.lead_contacts_credits = _("Up to %d additional credits will be consumed to identify %d contacts per company.") % (contact_credits*company_credits, record.contact_number) record.lead_credits = _('%d credits will be consumed to find %d companies.') % (company_credits, record.lead_number) record.lead_total_credits = _("This makes a total of %d credits for this request.") % (total_contact_credits + company_credits) @api.depends('lead_ids') def _compute_leads_count(self): for req in self: req.leads_count = len(req.lead_ids) @api.onchange('lead_number') def _onchange_lead_number(self): if self.lead_number <= 0: self.lead_number = 1 elif self.lead_number > MAX_LEAD: self.lead_number = MAX_LEAD @api.onchange('contact_number') def _onchange_contact_number(self): if self.contact_number <= 0: self.contact_number = 1 elif self.contact_number > MAX_CONTACT: self.contact_number = MAX_CONTACT @api.onchange('country_ids') def _onchange_country_ids(self): self.state_ids = [] @api.onchange('company_size_min') def _onchange_company_size_min(self): if self.company_size_min <= 0: self.company_size_min = 1 elif self.company_size_min > self.company_size_max: self.company_size_min = self.company_size_max @api.onchange('company_size_max') def _onchange_company_size_max(self): if self.company_size_max < self.company_size_min: self.company_size_max = self.company_size_min def _prepare_iap_payload(self): """ This will prepare the data to send to the server """ self.ensure_one() payload = {'lead_number': self.lead_number, 'search_type': self.search_type, 'countries': self.country_ids.mapped('code')} if self.state_ids: payload['states'] = self.state_ids.mapped('code') if self.filter_on_size: payload.update({'company_size_min': self.company_size_min, 'company_size_max': self.company_size_max}) if self.industry_ids: payload['industry_ids'] = self.industry_ids.mapped('reveal_id') if self.search_type == 'people': payload.update({'contact_number': self.contact_number, 'contact_filter_type': self.contact_filter_type}) if self.contact_filter_type == 'role': payload.update({'preferred_role': self.preferred_role_id.reveal_id, 'other_roles': self.role_ids.mapped('reveal_id')}) elif self.contact_filter_type == 'seniority': payload['seniority'] = self.seniority_id.reveal_id return payload def _perform_request(self): """ This will perform the request and create the corresponding leads. The user will be notified if he hasn't enough credits. """ server_payload = self._prepare_iap_payload() reveal_account = self.env['iap.account'].get('reveal') dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid') endpoint = self.env['ir.config_parameter'].sudo().get_param('reveal.endpoint', DEFAULT_ENDPOINT) + '/iap/clearbit/1/lead_mining_request' params = { 'account_token': reveal_account.account_token, 'dbuuid': dbuuid, 'data': server_payload } try: response = jsonrpc(endpoint, params=params, timeout=300) return response['data'] except InsufficientCreditError as e: self.error = 'Insufficient credits. Recharge your account and retry.' self.state = 'error' self._cr.commit() raise e def _create_leads_from_response(self, result): """ This method will get the response from the service and create the leads accordingly """ self.ensure_one() lead_vals = [] messages_to_post = {} for data in result: lead_vals.append(self._lead_vals_from_response(data)) messages_to_post[data['company_data']['clearbit_id']] = self.env['crm.iap.lead.helpers'].format_data_for_message_post(data['company_data'], data.get('people_data')) leads = self.env['crm.lead'].create(lead_vals) for lead in leads: if messages_to_post.get(lead.reveal_id): lead.message_post_with_view('crm_iap_lead.lead_message_template', values=messages_to_post[lead.reveal_id], subtype_id=self.env.ref('mail.mt_note').id) # Methods responsible for format response data into valid eagle lead data @api.model def _lead_vals_from_response(self, data): self.ensure_one() company_data = data.get('company_data') people_data = data.get('people_data') lead_vals = self.env['crm.iap.lead.helpers'].lead_vals_from_response(self.lead_type, self.team_id.id, self.tag_ids.ids, self.user_id.id, company_data, people_data) lead_vals['lead_mining_request_id'] = self.id return lead_vals @api.model def get_empty_list_help(self, help): help_title = _('Create a Lead Mining Request') sub_title = _('Generate new leads based on their country, industry, size, etc.') return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (help_title, sub_title) def action_draft(self): self.ensure_one() self.name = _('New') self.state = 'draft' def action_submit(self): self.ensure_one() if self.name == _('New'): self.name = self.env['ir.sequence'].next_by_code('crm.iap.lead.mining.request') or _('New') results = self._perform_request() if results: self._create_leads_from_response(results) self.state = 'done' if self.lead_type == 'lead': return self.action_get_lead_action() elif self.lead_type == 'opportunity': return self.action_get_opportunity_action() def action_get_lead_action(self): self.ensure_one() action = self.env.ref('crm.crm_lead_all_leads').read()[0] action['domain'] = [('id', 'in', self.lead_ids.ids), ('type', '=', 'lead')] action['help'] = _("""<p class="o_view_nocontent_empty_folder"> No leads found </p><p> No leads could be generated according to your search criteria </p>""") return action def action_get_opportunity_action(self): self.ensure_one() action = self.env.ref('crm.crm_lead_opportunities').read()[0] action['domain'] = [('id', 'in', self.lead_ids.ids), ('type', '=', 'opportunity')] action['help'] = _("""<p class="o_view_nocontent_empty_folder"> No opportunities found </p><p> No opportunities could be generated according to your search criteria </p>""") return action
class Event(models.Model): _inherit = "event.event" track_ids = fields.One2many('event.track', 'event_id', 'Tracks') track_count = fields.Integer('Track Count', compute='_compute_track_count') sponsor_ids = fields.One2many('event.sponsor', 'event_id', 'Sponsors') sponsor_count = fields.Integer('Sponsor Count', compute='_compute_sponsor_count') website_track = fields.Boolean('Tracks on Website') website_track_proposal = fields.Boolean('Proposals on Website') track_menu_ids = fields.One2many('website.event.menu', 'event_id', string='Event Tracks Menus', domain=[('menu_type', '=', 'track')]) track_proposal_menu_ids = fields.One2many('website.event.menu', 'event_id', string='Event Proposals Menus', domain=[('menu_type', '=', 'track_proposal')]) allowed_track_tag_ids = fields.Many2many( 'event.track.tag', relation='event_allowed_track_tags_rel', string='Available Track Tags') tracks_tag_ids = fields.Many2many('event.track.tag', relation='event_track_tags_rel', string='Track Tags', compute='_compute_tracks_tag_ids', store=True) @api.multi def _compute_track_count(self): data = self.env['event.track'].read_group( [('stage_id.is_cancel', '!=', True)], ['event_id'], ['event_id']) result = dict( (data['event_id'][0], data['event_id_count']) for data in data) for event in self: event.track_count = result.get(event.id, 0) @api.multi def _compute_sponsor_count(self): data = self.env['event.sponsor'].read_group([], ['event_id'], ['event_id']) result = dict( (data['event_id'][0], data['event_id_count']) for data in data) for event in self: event.sponsor_count = result.get(event.id, 0) def _toggle_create_website_menus(self, vals): super(Event, self)._toggle_create_website_menus(vals) for event in self: if 'website_track' in vals: if vals['website_track']: for sequence, (name, url, xml_id, menu_type) in enumerate( event._get_track_menu_entries()): menu = super(Event, event)._create_menu( sequence, name, url, xml_id) event.env['website.event.menu'].create({ 'menu_id': menu.id, 'event_id': event.id, 'menu_type': menu_type, }) else: event.track_menu_ids.mapped('menu_id').unlink() if 'website_track_proposal' in vals: if vals['website_track_proposal']: for sequence, (name, url, xml_id, menu_type) in enumerate( event._get_track_proposal_menu_entries()): menu = super(Event, event)._create_menu( sequence, name, url, xml_id) event.env['website.event.menu'].create({ 'menu_id': menu.id, 'event_id': event.id, 'menu_type': menu_type, }) else: event.track_proposal_menu_ids.mapped('menu_id').unlink() def _get_track_menu_entries(self): self.ensure_one() res = [(_('Talks'), '/event/%s/track' % slug(self), False, 'track'), (_('Agenda'), '/event/%s/agenda' % slug(self), False, 'track')] return res def _get_track_proposal_menu_entries(self): self.ensure_one() res = [(_('Talk Proposals'), '/event/%s/track_proposal' % slug(self), False, 'track_proposal')] return res @api.multi @api.depends('track_ids.tag_ids') def _compute_tracks_tag_ids(self): for event in self: event.tracks_tag_ids = event.track_ids.mapped('tag_ids').ids @api.onchange('event_type_id') def _onchange_type(self): super(Event, self)._onchange_type() if self.event_type_id and self.website_menu: self.website_track = self.event_type_id.website_track self.website_track_proposal = self.event_type_id.website_track_proposal @api.onchange('website_menu') def _onchange_website_menu(self): if not self.website_menu: self.website_track = False self.website_track_proposal = False @api.onchange('website_track') def _onchange_website_track(self): if not self.website_track: self.website_track_proposal = False @api.onchange('website_track_proposal') def _onchange_website_track_proposal(self): if self.website_track_proposal: self.website_track = True
class CrmTeam(models.Model): _name = "crm.team" _inherit = ['mail.thread'] _description = "Sales Team" _order = "sequence" _check_company_auto = True @api.model @api.returns('self', lambda value: value.id if value else False) def _get_default_team_id(self, user_id=None, domain=None): if not user_id: user_id = self.env.uid team_id = self.env['crm.team'].search([ '|', ('user_id', '=', user_id), ('member_ids', '=', user_id), '|', ('company_id', '=', False), ('company_id', '=', self.env.company.id) ], limit=1) if not team_id and 'default_team_id' in self.env.context: team_id = self.env['crm.team'].browse( self.env.context.get('default_team_id')) if not team_id: team_domain = domain or [] default_team_id = self.env['crm.team'].search(team_domain, limit=1) return default_team_id or self.env['crm.team'] return team_id def _get_default_favorite_user_ids(self): return [(6, 0, [self.env.uid])] name = fields.Char('Sales Team', required=True, translate=True) sequence = fields.Integer('Sequence', default=10) active = fields.Boolean( default=True, help= "If the active field is set to false, it will allow you to hide the Sales Team without removing it." ) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company, index=True) currency_id = fields.Many2one("res.currency", related='company_id.currency_id', string="Currency", readonly=True) user_id = fields.Many2one('res.users', string='Team Leader', check_company=True) member_ids = fields.One2many( 'res.users', 'sale_team_id', string='Channel Members', check_company=True, domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_user' ).id)], help= "Add members to automatically assign their documents to this sales team. You can only be member of one team." ) favorite_user_ids = fields.Many2many( 'res.users', 'team_favorite_user_rel', 'team_id', 'user_id', string='Favorite Members', default=_get_default_favorite_user_ids) is_favorite = fields.Boolean( string='Show on dashboard', compute='_compute_is_favorite', inverse='_inverse_is_favorite', help= "Favorite teams to display them in the dashboard and access them easily." ) color = fields.Integer(string='Color Index', help="The color of the channel") dashboard_button_name = fields.Char( string="Dashboard Button", compute='_compute_dashboard_button_name') dashboard_graph_data = fields.Text(compute='_compute_dashboard_graph') def _compute_dashboard_graph(self): for team in self: team.dashboard_graph_data = json.dumps(team._get_graph()) def _compute_is_favorite(self): for team in self: team.is_favorite = self.env.user in team.favorite_user_ids def _inverse_is_favorite(self): sudoed_self = self.sudo() to_fav = sudoed_self.filtered( lambda team: self.env.user not in team.favorite_user_ids) to_fav.write({'favorite_user_ids': [(4, self.env.uid)]}) (sudoed_self - to_fav).write( {'favorite_user_ids': [(3, self.env.uid)]}) return True def _graph_get_model(self): """ skeleton function defined here because it'll be called by crm and/or sale """ raise UserError( _('Undefined graph model for Sales Team: %s') % self.name) def _graph_get_dates(self, today): """ return a coherent start and end date for the dashboard graph covering a month period grouped by week. """ start_date = today - relativedelta(months=1) # we take the start of the following week if we group by week # (to avoid having twice the same week from different month) start_date += relativedelta(days=8 - start_date.isocalendar()[2]) return [start_date, today] def _graph_date_column(self): return 'create_date' def _graph_x_query(self): return 'EXTRACT(WEEK FROM %s)' % self._graph_date_column() def _graph_y_query(self): raise UserError( _('Undefined graph model for Sales Team: %s') % self.name) def _extra_sql_conditions(self): return '' def _graph_title_and_key(self): """ Returns an array containing the appropriate graph title and key respectively. The key is for lineCharts, to have the on-hover label. """ return ['', ''] def _graph_data(self, start_date, end_date): """ return format should be an iterable of dicts that contain {'x_value': ..., 'y_value': ...} x_values should be weeks. y_values are floats. """ query = """SELECT %(x_query)s as x_value, %(y_query)s as y_value FROM %(table)s WHERE team_id = %(team_id)s AND DATE(%(date_column)s) >= %(start_date)s AND DATE(%(date_column)s) <= %(end_date)s %(extra_conditions)s GROUP BY x_value;""" # apply rules dashboard_graph_model = self._graph_get_model() GraphModel = self.env[dashboard_graph_model] graph_table = GraphModel._table extra_conditions = self._extra_sql_conditions() where_query = GraphModel._where_calc([]) GraphModel._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql() if where_clause: extra_conditions += " AND " + where_clause query = query % { 'x_query': self._graph_x_query(), 'y_query': self._graph_y_query(), 'table': graph_table, 'team_id': "%s", 'date_column': self._graph_date_column(), 'start_date': "%s", 'end_date': "%s", 'extra_conditions': extra_conditions } self._cr.execute(query, [self.id, start_date, end_date] + where_clause_params) return self.env.cr.dictfetchall() def _get_graph(self): def get_week_name(start_date, locale): """ Generates a week name (string) from a datetime according to the locale: E.g.: locale start_date (datetime) return string "en_US" November 16th "16-22 Nov" "en_US" December 28th "28 Dec-3 Jan" """ if (start_date + relativedelta(days=6)).month == start_date.month: short_name_from = format_date(start_date, 'd', locale=locale) else: short_name_from = format_date(start_date, 'd MMM', locale=locale) short_name_to = format_date(start_date + relativedelta(days=6), 'd MMM', locale=locale) return short_name_from + '-' + short_name_to self.ensure_one() values = [] today = fields.Date.from_string(fields.Date.context_today(self)) start_date, end_date = self._graph_get_dates(today) graph_data = self._graph_data(start_date, end_date) x_field = 'label' y_field = 'value' # generate all required x_fields and update the y_values where we have data for them locale = self._context.get('lang') or 'en_US' weeks_in_start_year = int( date(start_date.year, 12, 28).isocalendar() [1]) # This date is always in the last week of ISO years for week in range( 0, (end_date.isocalendar()[1] - start_date.isocalendar()[1]) % weeks_in_start_year + 1): short_name = get_week_name( start_date + relativedelta(days=7 * week), locale) values.append({x_field: short_name, y_field: 0}) for data_item in graph_data: index = int( (data_item.get('x_value') - start_date.isocalendar()[1]) % weeks_in_start_year) values[index][y_field] = data_item.get('y_value') [graph_title, graph_key] = self._graph_title_and_key() color = '#875A7B' if '+e' in version else '#7c7bad' return [{ 'values': values, 'area': True, 'title': graph_title, 'key': graph_key, 'color': color }] def _compute_dashboard_button_name(self): """ Sets the adequate dashboard button name depending on the Sales Team's options """ for team in self: team.dashboard_button_name = _( "Big Pretty Button :)") # placeholder def action_primary_channel_button(self): """ skeleton function to be overloaded It will return the adequate action depending on the Sales Team's options """ return False @api.model def create(self, values): team = super( CrmTeam, self.with_context(mail_create_nosubscribe=True)).create(values) if values.get('member_ids'): team._add_members_to_favorites() return team def write(self, values): res = super(CrmTeam, self).write(values) if values.get('member_ids'): self._add_members_to_favorites() return res def unlink(self): default_teams = [ self.env.ref('sales_team.salesteam_website_sales'), self.env.ref('sales_team.pos_sales_team'), self.env.ref('sales_team.ebay_sales_team') ] for team in self: if team in default_teams: raise UserError( _('Cannot delete default team "%s"' % (team.name))) return super(CrmTeam, self).unlink() def _add_members_to_favorites(self): for team in self: team.favorite_user_ids = [(4, member.id) for member in team.member_ids]
class PurchaseRequisitionLine(models.Model): _name = "purchase.requisition.line" _description = "Purchase Requisition Line" _rec_name = 'product_id' product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], required=True) product_uom_id = fields.Many2one('uom.uom', string='Product Unit of Measure') product_qty = fields.Float( string='Quantity', digits=dp.get_precision('Product Unit of Measure')) price_unit = fields.Float(string='Unit Price', digits=dp.get_precision('Product Price')) qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities') requisition_id = fields.Many2one('purchase.requisition', required=True, string='Purchase Agreement', ondelete='cascade') company_id = fields.Many2one( 'res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default=lambda self: self.env['res.company']._company_default_get( 'purchase.requisition.line')) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') schedule_date = fields.Date(string='Scheduled Date') move_dest_id = fields.Many2one('stock.move', 'Downstream Move') supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id') @api.model def create(self, vals): res = super(PurchaseRequisitionLine, self).create(vals) if res.requisition_id.state not in [ 'draft', 'cancel', 'done' ] and res.requisition_id.is_quantity_copy == 'none': supplier_infos = self.env['product.supplierinfo'].search([ ('product_id', '=', vals.get('product_id')), ('name', '=', res.requisition_id.vendor_id.id), ]) if not any([s.purchase_requisition_id for s in supplier_infos]): res.create_supplier_info() if vals['price_unit'] <= 0.0: raise UserError( _('You cannot confirm the blanket order without price.')) return res @api.multi def write(self, vals): res = super(PurchaseRequisitionLine, self).write(vals) if 'price_unit' in vals: if vals['price_unit'] <= 0.0: raise UserError( _('You cannot confirm the blanket order without price.')) # If the price is updated, we have to update the related SupplierInfo self.supplier_info_ids.write({'price': vals['price_unit']}) return res def unlink(self): to_unlink = self.filtered(lambda r: r.requisition_id.state not in ['draft', 'cancel', 'done']) to_unlink.mapped('supplier_info_ids').unlink() return super(PurchaseRequisitionLine, self).unlink() def create_supplier_info(self): purchase_requisition = self.requisition_id if purchase_requisition.type_id.quantity_copy == 'none' and purchase_requisition.vendor_id: # create a supplier_info only in case of blanket order self.env['product.supplierinfo'].create({ 'name': purchase_requisition.vendor_id.id, 'product_id': self.product_id.id, 'product_tmpl_id': self.product_id.product_tmpl_id.id, 'price': self.price_unit, 'currency_id': self.requisition_id.currency_id.id, 'purchase_requisition_id': purchase_requisition.id, 'purchase_requisition_line_id': self.id, }) @api.multi @api.depends('requisition_id.purchase_ids.state') def _compute_ordered_qty(self): for line in self: total = 0.0 for po in line.requisition_id.purchase_ids.filtered( lambda purchase_order: purchase_order.state in ['purchase', 'done']): for po_line in po.order_line.filtered( lambda order_line: order_line.product_id == line. product_id): if po_line.product_uom != line.product_uom_id: total += po_line.product_uom._compute_quantity( po_line.product_qty, line.product_uom_id) else: total += po_line.product_qty line.qty_ordered = total @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_po_id self.product_qty = 1.0 if not self.schedule_date: self.schedule_date = self.requisition_id.schedule_date @api.multi def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, taxes_ids=False): self.ensure_one() requisition = self.requisition_id if requisition.schedule_date: date_planned = datetime.combine(requisition.schedule_date, time.min) else: date_planned = datetime.now() return { 'name': name, 'product_id': self.product_id.id, 'product_uom': self.product_id.uom_po_id.id, 'product_qty': product_qty, 'price_unit': price_unit, 'taxes_id': [(6, 0, taxes_ids)], 'date_planned': date_planned, 'account_analytic_id': self.account_analytic_id.id, 'analytic_tag_ids': self.analytic_tag_ids.ids, 'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or [] }
class ServerActions(models.Model): """ Add email option in server actions. """ _name = 'ir.actions.server' _description = 'Server Action' _inherit = ['ir.actions.server'] state = fields.Selection(selection_add=[ ('email', 'Send Email'), ('followers', 'Add Followers'), ('next_activity', 'Create Next Activity'), ]) # Followers partner_ids = fields.Many2many('res.partner', string='Add Followers') channel_ids = fields.Many2many('mail.channel', string='Add Channels') # Template template_id = fields.Many2one( 'mail.template', 'Email Template', ondelete='set null', domain="[('model_id', '=', model_id)]", ) # Next Activity activity_type_id = fields.Many2one( 'mail.activity.type', string='Activity', domain= "['|', ('res_model_id', '=', False), ('res_model_id', '=', model_id)]") activity_summary = fields.Char('Summary') activity_note = fields.Html('Note') activity_date_deadline_range = fields.Integer(string='Due Date In') activity_date_deadline_range_type = fields.Selection([ ('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months'), ], string='Due type', default='days') activity_user_type = fields.Selection( [('specific', 'Specific User'), ('generic', 'Generic User From Record')], default="specific", required=True, help= "Use 'Specific User' to always assign the same user on the next activity. Use 'Generic User From Record' to specify the field name of the user to choose on the record." ) activity_user_id = fields.Many2one('res.users', string='Responsible') activity_user_field_name = fields.Char( 'User field name', help="Technical name of the user on the record", default="user_id") @api.onchange('activity_date_deadline_range') def _onchange_activity_date_deadline_range(self): if self.activity_date_deadline_range < 0: raise UserError(_("The 'Due Date In' value can't be negative.")) @api.onchange('template_id') def on_change_template_id(self): """ Render the raw template in the server action fields. """ if self.template_id and not self.template_id.email_from: raise UserError(_('Your template should define email_from')) @api.constrains('state', 'model_id') def _check_mail_thread(self): for action in self: if action.state == 'followers' and not action.model_id.is_mail_thread: raise ValidationError( _("Add Followers can only be done on a mail thread model")) @api.constrains('state', 'model_id') def _check_activity_mixin(self): for action in self: if action.state == 'next_activity' and not action.model_id.is_mail_thread: raise ValidationError( _("A next activity can only be planned on models that use the chatter" )) @api.model def run_action_followers_multi(self, action, eval_context=None): Model = self.env[action.model_name] if self.partner_ids or self.channel_ids and hasattr( Model, 'message_subscribe'): records = Model.browse( self._context.get('active_ids', self._context.get('active_id'))) records.message_subscribe(self.partner_ids.ids, self.channel_ids.ids) return False @api.model def _is_recompute(self, action): """When an activity is set on update of a record, update might be triggered many times by recomputes. When need to know it to skip these steps. Except if the computed field is supposed to trigger the action """ records = self.env[action.model_name].browse( self._context.get('active_ids', self._context.get('active_id'))) old_values = action._context.get('old_values') if old_values: domain_post = action._context.get('domain_post') tracked_fields = [] if domain_post: for leaf in domain_post: if isinstance(leaf, (tuple, list)): tracked_fields.append(leaf[0]) fields_to_check = [ field for record, field_names in old_values.items() for field in field_names if field not in tracked_fields ] if fields_to_check: field = records._fields[fields_to_check[0]] # Pick an arbitrary field; if it is marked to be recomputed, # it means we are in an extraneous write triggered by the recompute. # In this case, we should not create a new activity. if records & self.env.records_to_compute(field): return True return False @api.model def run_action_email(self, action, eval_context=None): # TDE CLEANME: when going to new api with server action, remove action if not action.template_id or not self._context.get( 'active_id') or self._is_recompute(action): return False # Clean context from default_type to avoid making attachment # with wrong values in subsequent operations cleaned_ctx = dict(self.env.context) cleaned_ctx.pop('default_type', None) cleaned_ctx.pop('default_parent_id', None) action.template_id.with_context(cleaned_ctx).send_mail( self._context.get('active_id'), force_send=False, raise_exception=False) return False @api.model def run_action_next_activity(self, action, eval_context=None): if not action.activity_type_id or not self._context.get( 'active_id') or self._is_recompute(action): return False records = self.env[action.model_name].browse( self._context.get('active_ids', self._context.get('active_id'))) vals = { 'summary': action.activity_summary or '', 'note': action.activity_note or '', 'activity_type_id': action.activity_type_id.id, } if action.activity_date_deadline_range > 0: vals['date_deadline'] = fields.Date.context_today( action) + relativedelta( **{ action.activity_date_deadline_range_type: action.activity_date_deadline_range }) for record in records: if action.activity_user_type == 'specific': user = action.activity_user_id elif action.activity_user_type == 'generic' and action.activity_user_field_name in record: user = record[action.activity_user_field_name] if user: vals['user_id'] = user.id record.activity_schedule(**vals) return False @api.model def _get_eval_context(self, action=None): """ Override the method giving the evaluation context but also the context used in all subsequent calls. Add the mail_notify_force_send key set to False in the context. This way all notification emails linked to the currently executed action will be set in the queue instead of sent directly. This will avoid possible break in transactions. """ eval_context = super(ServerActions, self)._get_eval_context(action=action) ctx = dict(eval_context['env'].context) ctx['mail_notify_force_send'] = False eval_context['env'].context = ctx return eval_context
class M2MReadonly(models.Model): _name = 'test_testing_utilities.g' _description = 'Testing Utilities G' m2m = fields.Many2many('test_testing_utilities.sub3', readonly=True)