class Team(models.Model): _name = 'crm.team' _inherit = ['mail.alias.mixin', 'crm.team'] _description = 'Sales Team' use_leads = fields.Boolean( 'Leads', help= "Check this box to filter and qualify incoming requests as leads before converting them into opportunities and assigning them to a salesperson." ) use_opportunities = fields.Boolean( 'Pipeline', default=True, help="Check this box to manage a presales process with opportunities.") alias_id = fields.Many2one( 'mail.alias', string='Alias', ondelete="restrict", required=True, help= "The email address associated with this channel. New emails received will automatically create new leads assigned to the channel." ) # statistics about leads / opportunities / both lead_unassigned_count = fields.Integer( string='# Unassigned Leads', compute='_compute_lead_unassigned_count') lead_all_assigned_month_count = fields.Integer( string='# Leads/Opps assigned this month', compute='_compute_lead_all_assigned_month_count', help="Number of leads and opportunities assigned this last month.") opportunities_count = fields.Integer(string='# Opportunities', compute='_compute_opportunities_data') opportunities_amount = fields.Monetary( string='Opportunities Revenues', compute='_compute_opportunities_data') opportunities_overdue_count = fields.Integer( string='# Overdue Opportunities', compute='_compute_opportunities_overdue_data') opportunities_overdue_amount = fields.Monetary( string='Overdue Opportunities Revenues', compute='_compute_opportunities_overdue_data', ) # alias: improve fields coming from _inherits, use inherited to avoid replacing them alias_user_id = fields.Many2one( 'res.users', related='alias_id.alias_user_id', inherited=True, domain=lambda self: [( 'groups_id', 'in', self.env.ref('sales_team.group_sale_salesman_all_leads').id)]) def _compute_lead_unassigned_count(self): leads_data = self.env['crm.lead'].read_group([ ('team_id', 'in', self.ids), ('type', '=', 'lead'), ('user_id', '=', False), ], ['team_id'], ['team_id']) counts = { datum['team_id'][0]: datum['team_id_count'] for datum in leads_data } for team in self: team.lead_unassigned_count = counts.get(team.id, 0) def _compute_lead_all_assigned_month_count(self): limit_date = datetime.datetime.now() - datetime.timedelta(days=30) leads_data = self.env['crm.lead'].read_group([ ('team_id', 'in', self.ids), ('date_open', '>=', fields.Datetime.to_string(limit_date)), ('user_id', '!=', False), ], ['team_id'], ['team_id']) counts = { datum['team_id'][0]: datum['team_id_count'] for datum in leads_data } for team in self: team.lead_all_assigned_month_count = counts.get(team.id, 0) def _compute_opportunities_data(self): opportunity_data = self.env['crm.lead'].read_group([ ('team_id', 'in', self.ids), ('probability', '<', 100), ('type', '=', 'opportunity'), ], ['expected_revenue:sum', 'team_id'], ['team_id']) counts = { datum['team_id'][0]: datum['team_id_count'] for datum in opportunity_data } amounts = { datum['team_id'][0]: datum['expected_revenue'] for datum in opportunity_data } for team in self: team.opportunities_count = counts.get(team.id, 0) team.opportunities_amount = amounts.get(team.id, 0) def _compute_opportunities_overdue_data(self): opportunity_data = self.env['crm.lead'].read_group( [('team_id', 'in', self.ids), ('probability', '<', 100), ('type', '=', 'opportunity'), ('date_deadline', '<', fields.Date.to_string( fields.Datetime.now()))], ['expected_revenue', 'team_id'], ['team_id']) counts = { datum['team_id'][0]: datum['team_id_count'] for datum in opportunity_data } amounts = { datum['team_id'][0]: (datum['expected_revenue']) for datum in opportunity_data } for team in self: team.opportunities_overdue_count = counts.get(team.id, 0) team.opportunities_overdue_amount = amounts.get(team.id, 0) @api.onchange('use_leads', 'use_opportunities') def _onchange_use_leads_opportunities(self): if not self.use_leads and not self.use_opportunities: self.alias_name = False # ------------------------------------------------------------ # ORM # ------------------------------------------------------------ def write(self, vals): result = super(Team, self).write(vals) if 'use_leads' in vals or 'use_opportunities' in vals: for team in self: alias_vals = team._alias_get_creation_values() team.write({ 'alias_name': alias_vals.get('alias_name', team.alias_name), 'alias_defaults': alias_vals.get('alias_defaults'), }) return result # ------------------------------------------------------------ # MESSAGING # ------------------------------------------------------------ def _alias_get_creation_values(self): values = super(Team, self)._alias_get_creation_values() values['alias_model_id'] = self.env['ir.model']._get('crm.lead').id if self.id: if not self.use_leads and not self.use_opportunities: values['alias_name'] = False values['alias_defaults'] = defaults = ast.literal_eval( self.alias_defaults or "{}") has_group_use_lead = self.env.user.has_group('crm.group_use_lead') defaults[ 'type'] = 'lead' if has_group_use_lead and self.use_leads else 'opportunity' defaults['team_id'] = self.id return values # ------------------------------------------------------------ # ACTIONS # ------------------------------------------------------------ #TODO JEM : refactor this stuff with xml action, proper customization, @api.model def action_your_pipeline(self): action = self.env["ir.actions.actions"]._for_xml_id( "crm.crm_lead_action_pipeline") user_team_id = self.env.user.sale_team_id.id if user_team_id: # To ensure that the team is readable in multi company user_team_id = self.search([('id', '=', user_team_id)], limit=1).id else: user_team_id = self.search([], limit=1).id action['help'] = _( """<p class='o_view_nocontent_smiling_face'>Add new opportunities</p><p> Looks like you are not a member of a Sales Team. You should add yourself as a member of one of the Sales Team. </p>""") if user_team_id: action['help'] += _( "<p>As you don't belong to any Sales Team, Flectra opens the first one by default.</p>" ) action_context = safe_eval(action['context'], {'uid': self.env.uid}) if user_team_id: action_context['default_team_id'] = user_team_id action['context'] = action_context return action def _compute_dashboard_button_name(self): super(Team, self)._compute_dashboard_button_name() team_with_pipelines = self.filtered(lambda el: el.use_opportunities) team_with_pipelines.update({'dashboard_button_name': _("Pipeline")}) def action_primary_channel_button(self): if self.use_opportunities: return self.env["ir.actions.actions"]._for_xml_id( "crm.crm_case_form_view_salesteams_opportunity") return super(Team, self).action_primary_channel_button() def _graph_get_model(self): if self.use_opportunities: return 'crm.lead' return super(Team, self)._graph_get_model() def _graph_date_column(self): if self.use_opportunities: return 'create_date' return super(Team, self)._graph_date_column() def _graph_y_query(self): if self.use_opportunities: return 'count(*)' return super(Team, self)._graph_y_query() def _extra_sql_conditions(self): if self.use_opportunities: return "AND type LIKE 'opportunity'" return super(Team, self)._extra_sql_conditions() def _graph_title_and_key(self): if self.use_opportunities: return ['', _('New Opportunities')] # no more title return super(Team, self)._graph_title_and_key()
class HelpdeskTicket(models.Model): _name = 'helpdesk.ticket' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'id desc' _description = 'Helpdesk Ticket' @api.model def _get_default_state(self): if self.team_id and self.team_id.stage_ids: return self.team_id.stage_ids[0] active = fields.Boolean('Active', default=True) color = fields.Integer(string='Color Index') name = fields.Char('Name', translate=True) ticket_seq = fields.Char('Sequence', default='New', copy=False, oldname='sequence') priority = fields.Selection([('1', 'Low'), ('2', 'Medium'), ('3', 'High')], default='1') user_id = fields.Many2one('res.users', string='Created By', track_visibility='onchange') partner_id = fields.Many2one('res.partner', store=True, related='user_id.partner_id', string='Related Partner', track_visibility='onchange') email = fields.Char(string='Email', default=lambda s: s.env.user.partner_id.email or False) issue_type_id = fields.Many2one('issue.type', string='Issue Type', track_visibility='onchange') team_id = fields.Many2one('helpdesk.team', 'Team', track_visibility='onchange') assigned_to_id = fields.Many2one('res.users', string='Assigned To', track_visibility='onchange') tag_ids = fields.Many2many('helpdesk.tag', string='Tag(s)') start_date = fields.Datetime(string='Start Date', default=fields.Datetime.now, track_visibility='onchange') end_date = fields.Datetime(string='End Date', default=fields.Datetime.now, track_visibility='onchange') description = fields.Text(string='Description', size=128, translate=True, track_visibility='onchange') attachment_ids = fields.One2many( 'ir.attachment', compute='_compute_attachments', string="Main Attachments", help="Attachment that don't come from message.") attachments_count = fields.Integer(compute='_compute_attachments', string='Add Attachments') is_accessible = fields.Boolean('Is Accessible', compute='_compute_is_accessible') is_assigned = fields.Boolean('Is Accessible', compute='_compute_is_accessible') stage_id = fields.Many2one('helpdesk.stage', string='Stage', default=_get_default_state, track_visibility='onchange') @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'assigned_to_id' in init_values and self.assigned_to_id: # assigned -> new return 'helpdesk_basic.mt_issue_new' elif 'stage_id' in init_values and self.stage_id and \ self.stage_id.sequence <= 1: # start stage -> new return 'helpdesk_basic.mt_issue_new' elif 'stage_id' in init_values: return 'helpdesk_basic.mt_issue_stage' return super(HelpdeskTicket, self)._track_subtype(init_values) def add_followers(self): followers = [] followers.append(self.assigned_to_id.id) followers.append(self.user_id.id) self.message_subscribe_users(user_ids=followers) @api.model def create(self, values): if not values.get('user_id'): values.update({'user_id': self.env.user.id}) if 'ticket_seq' not in values or values['ticket_seq'] == _('New'): values['ticket_seq'] = self.env['ir.sequence'].next_by_code( 'helpdesk.ticket') or _('New') if values.get('team_id'): team = self.team_id.browse(values.get('team_id')) values.update( {'stage_id': team.stage_ids and team.stage_ids[0].id or False}) res = super(HelpdeskTicket, self).create(values) if not res.stage_id and res.team_id and res.team_id.stage_ids: res.stage_id = res.team_id.stage_ids[0] res.add_followers() return res @api.multi def write(self, vals): if vals.get('partner_id', False) or vals.get('assigned_to_id', False): self.add_followers() return super(HelpdeskTicket, self).write(vals) @api.onchange('team_id') def onchange_team_id(self): self.assigned_to_id = False if self.team_id: self.stage_id = \ self.team_id.stage_ids and self.team_id.stage_ids.ids[0] return { 'domain': { 'assigned_to_id': [('id', 'in', self.team_id.member_ids and self.team_id.member_ids.ids or [])], 'stage_id': [('id', 'in', self.team_id.stage_ids and self.team_id.stage_ids.ids or [])] } } @api.onchange('issue_type_id') def onchange_issue_type_id(self): self.team_id = False if self.issue_type_id: self.description = self.issue_type_id.reporting_template or '' return { 'domain': { 'team_id': [('issue_type_ids', 'in', self.issue_type_id.id)] } } @api.multi def _compute_is_accessible(self): has_group = self.env.user.has_group('base.group_no_one') for ticket in self: if self.env.user.partner_id.id == ticket.partner_id.id or \ has_group: ticket.is_accessible = True if self.env.user.id == ticket.assigned_to_id.id or has_group: ticket.is_assigned = True @api.multi def _compute_attachments(self): for ticket in self: attachment_ids = self.env['ir.attachment'].search([ ('res_model', '=', ticket._name), ('res_id', '=', ticket.id) ]) ticket.attachments_count = len(attachment_ids.ids) ticket.attachment_ids = attachment_ids @api.multi def action_get_attachments(self): self.ensure_one() return { 'name': _('Attachments'), 'res_model': 'ir.attachment', 'type': 'ir.actions.act_window', 'view_mode': 'kanban,form', 'view_type': 'form', 'domain': [('res_model', '=', self._name), ('res_id', '=', self.id)], 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, self.id), } @api.multi def action_get_issue_type(self): form_id = self.env.ref('helpdesk_basic.view_issue_type_form') tree_id = self.env.ref('helpdesk_basic.view_issue_type_tree') return { 'name': 'Issue Types', 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'issue.type', 'views': [ (tree_id.id, 'tree'), (form_id.id, 'form'), ], 'domain': [('id', '=', self.issue_type_id.id)], } @api.multi def action_get_team(self): form_id = self.env.ref('helpdesk_basic.helpdesk_team_form_view') tree_id = self.env.ref('helpdesk_basic.helpdesk_team_tree_view') return { 'name': 'Helpdesk Teams', 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'helpdesk.team', 'views': [ (tree_id.id, 'tree'), (form_id.id, 'form'), ], 'domain': [('id', '=', self.team_id.id)], }
class Channel(models.Model): _inherit = 'slide.channel' nbr_certification = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True)
class PurchaseReport(models.Model): _name = "purchase.report" _inherit = ['ir.branch.company.mixin'] _description = "Purchases Orders" _auto = False _order = 'date_order desc, price_total desc' date_order = fields.Datetime( 'Order Date', readonly=True, help="Date on which this document has been created", oldname='date') state = fields.Selection([('draft', 'Draft RFQ'), ('sent', 'RFQ Sent'), ('to approve', 'To Approve'), ('purchase', 'Purchase Order'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Order Status', readonly=True) product_id = fields.Many2one('product.product', 'Product', readonly=True) picking_type_id = fields.Many2one('stock.warehouse', 'Warehouse', readonly=True) partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True) date_approve = fields.Date('Date Approved', readonly=True) product_uom = fields.Many2one('product.uom', 'Reference Unit of Measure', required=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) user_id = fields.Many2one('res.users', 'Responsible', readonly=True) delay = fields.Float('Days to Validate', digits=(16, 2), readonly=True) delay_pass = fields.Float('Days to Deliver', digits=(16, 2), readonly=True) unit_quantity = fields.Float('Product Quantity', readonly=True, oldname='quantity') price_total = fields.Float('Total Price', readonly=True) price_average = fields.Float('Average Price', readonly=True, group_operator="avg") negociation = fields.Float('Purchase-Standard Price', readonly=True, group_operator="avg") price_standard = fields.Float('Products Value', readonly=True, group_operator="sum") nbr_lines = fields.Integer('# of Lines', readonly=True, oldname='nbr') category_id = fields.Many2one('product.category', 'Product Category', readonly=True) product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True) country_id = fields.Many2one('res.country', 'Partner Country', readonly=True) fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position', readonly=True) account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True) commercial_partner_id = fields.Many2one('res.partner', 'Commercial Entity', readonly=True) weight = fields.Float('Gross Weight', readonly=True) volume = fields.Float('Volume', readonly=True) @api.model_cr def init(self): tools.drop_view_if_exists(self._cr, 'purchase_report') self._cr.execute(""" create view purchase_report as ( WITH currency_rate as (%s) select min(l.id) as id, s.date_order as date_order, s.state, s.date_approve, s.dest_address_id, spt.warehouse_id as picking_type_id, s.partner_id as partner_id, s.create_uid as user_id, s.company_id as company_id, s.branch_id as branch_id, s.fiscal_position_id as fiscal_position_id, l.product_id, p.product_tmpl_id, t.categ_id as category_id, s.currency_id, t.uom_id as product_uom, sum(l.product_qty/u.factor*u2.factor) as unit_quantity, extract(epoch from age(s.date_approve,s.date_order))/(24*60*60)::decimal(16,2) as delay, extract(epoch from age(l.date_planned,s.date_order))/(24*60*60)::decimal(16,2) as delay_pass, count(*) as nbr_lines, sum(l.price_unit / COALESCE(NULLIF(cr.rate, 0), 1.0) * l.product_qty)::decimal(16,2) as price_total, avg(100.0 * (l.price_unit / COALESCE(NULLIF(cr.rate, 0),1.0) * l.product_qty) / NULLIF(ip.value_float*l.product_qty/u.factor*u2.factor, 0.0))::decimal(16,2) as negociation, sum(ip.value_float*l.product_qty/u.factor*u2.factor)::decimal(16,2) as price_standard, (sum(l.product_qty * l.price_unit / COALESCE(NULLIF(cr.rate, 0), 1.0))/NULLIF(sum(l.product_qty/u.factor*u2.factor),0.0))::decimal(16,2) as price_average, partner.country_id as country_id, partner.commercial_partner_id as commercial_partner_id, analytic_account.id as account_analytic_id, sum(p.weight * l.product_qty/u.factor*u2.factor) as weight, sum(p.volume * l.product_qty/u.factor*u2.factor) as volume from purchase_order_line l join purchase_order s on (l.order_id=s.id) join res_partner partner on s.partner_id = partner.id left join product_product p on (l.product_id=p.id) left join product_template t on (p.product_tmpl_id=t.id) LEFT JOIN ir_property ip ON (ip.name='standard_price' AND ip.res_id=CONCAT('product.product,',p.id) AND ip.company_id=s.company_id) left join product_uom u on (u.id=l.product_uom) left join product_uom u2 on (u2.id=t.uom_id) left join stock_picking_type spt on (spt.id=s.picking_type_id) left join account_analytic_account analytic_account on (l.account_analytic_id = analytic_account.id) left join currency_rate cr on (cr.currency_id = s.currency_id and cr.company_id = s.company_id and cr.date_start <= coalesce(s.date_order, now()) and (cr.date_end is null or cr.date_end > coalesce(s.date_order, now()))) group by s.company_id, s.branch_id, s.create_uid, s.partner_id, u.factor, s.currency_id, l.price_unit, s.date_approve, l.date_planned, l.product_uom, s.dest_address_id, s.fiscal_position_id, l.product_id, p.product_tmpl_id, t.categ_id, s.date_order, s.state, spt.warehouse_id, u.uom_type, u.category_id, t.uom_id, u.id, u2.factor, partner.country_id, partner.commercial_partner_id, analytic_account.id ) """ % self.env['res.currency']._select_companies_rates())
class IrActionsServer(models.Model): """ Server actions model. Server action work on a base model and offer various type of actions that can be executed automatically, for example using base action rules, of manually, by adding the action in the 'More' contextual menu. Since Flectra 8.0 a button 'Create Menu Action' button is available on the action form view. It creates an entry in the More menu of the base model. This allows to create server actions and run them in mass mode easily through the interface. The available actions are : - 'Execute Python Code': a block of python code that will be executed - 'Run a Client Action': choose a client action to launch - 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database - 'Write on a Record': update the values of a record - 'Execute several actions': define an action that triggers several other server actions """ _name = 'ir.actions.server' _table = 'ir_act_server' _inherit = 'ir.actions.actions' _sequence = 'ir_actions_id_seq' _order = 'sequence,name' DEFAULT_PYTHON_CODE = """# Available variables: # - env: Flectra Environment on which the action is triggered # - model: Flectra Model of the record on which the action is triggered; is a void recordset # - record: record on which the action is triggered; may be be void # - records: recordset of all records on which the action is triggered in multi-mode; may be void # - time, datetime, dateutil, timezone: useful Python libraries # - log: log(message, level='info'): logging function to record debug information in ir.logging table # - Warning: Warning Exception to use with raise # To return an action, assign: action = {...}\n\n\n\n""" @api.model def _select_objects(self): records = self.env['ir.model'].search([]) return [(record.model, record.name) for record in records] + [('', '')] name = fields.Char(string='Action Name', translate=True) type = fields.Char(default='ir.actions.server') usage = fields.Selection([('ir_actions_server', 'Server Action'), ('ir_cron', 'Scheduled Action')], string='Usage', default='ir_actions_server', required=True) state = fields.Selection( [('code', 'Execute Python Code'), ('object_create', 'Create a new Record'), ('object_write', 'Update the Record'), ('multi', 'Execute several actions')], string='Action To Do', default='object_write', required=True, help="Type of server action. The following values are available:\n" "- 'Execute Python Code': a block of python code that will be executed\n" "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n" "- 'Write on a Record': update the values of a record\n" "- 'Execute several actions': define an action that triggers several other server actions\n" "- 'Add Followers': add followers to a record (available in Discuss)\n" "- 'Send Email': automatically send an email (available in email_template)" ) # Generic sequence = fields.Integer( default=5, help="When dealing with multiple actions, the execution order is " "based on the sequence. Low number means high priority.") model_id = fields.Many2one('ir.model', string='Model', required=True, ondelete='cascade', help="Model on which the server action runs.") model_name = fields.Char(related='model_id.model', readonly=True, store=True) # Python code code = fields.Text( string='Python Code', groups='base.group_system', default=DEFAULT_PYTHON_CODE, help= "Write Python code that the action will execute. Some variables are " "available for use; help about pyhon expression is given in the help tab." ) # Multi child_ids = fields.Many2many( 'ir.actions.server', 'rel_server_actions', 'server_id', 'action_id', string='Child Actions', help= 'Child server actions that will be executed. Note that the last return returned action value will be used as global return value.' ) # Create crud_model_id = fields.Many2one( 'ir.model', string='Create/Write Target Model', oldname='srcmodel_id', help= "Model for record creation / update. Set this field only to specify a different model than the base model." ) crud_model_name = fields.Char(related='crud_model_id.name', readonly=True) link_field_id = fields.Many2one( 'ir.model.fields', string='Link using field', help="Provide the field used to link the newly created record " "on the record on used by the server action.") fields_lines = fields.One2many('ir.server.object.lines', 'server_id', string='Value Mapping', copy=True) @api.constrains('code') def _check_python_code(self): for action in self.sudo().filtered('code'): msg = test_python_expr(expr=action.code.strip(), mode="exec") if msg: raise ValidationError(msg) @api.constrains('child_ids') def _check_recursion(self): if not self._check_m2m_recursion('child_ids'): raise ValidationError(_('Recursion found in child server actions')) @api.onchange('crud_model_id') def _onchange_crud_model_id(self): self.link_field_id = False self.crud_model_name = self.crud_model_id.model @api.onchange('model_id') def _onchange_model_id(self): self.model_name = self.model_id.model @api.multi def create_action(self): """ Create a contextual action for each server action. """ for action in self: action.write({ 'binding_model_id': action.model_id.id, 'binding_type': 'action' }) return True @api.multi def unlink_action(self): """ Remove the contextual actions created for the server actions. """ self.check_access_rights('write', raise_exception=True) self.filtered('binding_model_id').write({'binding_model_id': False}) return True @api.model def run_action_code_multi(self, action, eval_context=None): safe_eval(action.sudo().code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action' if 'action' in eval_context: return eval_context['action'] @api.model def run_action_multi(self, action, eval_context=None): res = False for act in action.child_ids: result = act.run() if result: res = result return res @api.model def run_action_object_write(self, action, eval_context=None): """ Write server action. - 1. evaluate the value mapping - 2. depending on the write configuration: - `current`: id = active_id - `other`: id = from reference object - `expression`: id = from expression evaluation """ res = {} for exp in action.fields_lines: res[exp.col1.name] = exp.eval_value( eval_context=eval_context)[exp.id] if self._context.get('onchange_self'): record_cached = self._context['onchange_self'] for field, new_value in res.items(): record_cached[field] = new_value else: self.env[action.model_id.model].browse( self._context.get('active_id')).write(res) @api.model def run_action_object_create(self, action, eval_context=None): """ Create and Copy server action. - 1. evaluate the value mapping - 2. depending on the write configuration: - `new`: new record in the base model - `copy_current`: copy the current record (id = active_id) + gives custom values - `new_other`: new record in target model - `copy_other`: copy the current record (id from reference object) + gives custom values """ res = {} for exp in action.fields_lines: res[exp.col1.name] = exp.eval_value( eval_context=eval_context)[exp.id] res = self.env[action.crud_model_id.model].create(res) if action.link_field_id: record = self.env[action.model_id.model].browse( self._context.get('active_id')) record.write({action.link_field_id.name: res.id}) @api.model def _get_eval_context(self, action=None): """ Prepare the context used when evaluating python code, like the python formulas or code server actions. :param action: the current server action :type action: browse record :returns: dict -- evaluation context given to (safe_)safe_eval """ def log(message, level="info"): with self.pool.cursor() as cr: cr.execute( """ INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func) VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s) """, (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name)) eval_context = super(IrActionsServer, self)._get_eval_context(action=action) model_name = action.model_id.sudo().model model = self.env[model_name] record = None records = None if self._context.get( 'active_model') == model_name and self._context.get( 'active_id'): record = model.browse(self._context['active_id']) if self._context.get( 'active_model') == model_name and self._context.get( 'active_ids'): records = model.browse(self._context['active_ids']) if self._context.get('onchange_self'): record = self._context['onchange_self'] eval_context.update({ # orm 'env': self.env, 'model': model, # Exceptions 'Warning': flectra.exceptions.Warning, # record 'record': record, 'records': records, # helpers 'log': log, }) return eval_context @api.multi def run(self): """ Runs the server action. For each server action, the run_action_<STATE> method is called. This allows easy overriding of the server actions. :param dict context: context should contain following keys - active_id: id of the current object (single mode) - active_model: current model that should equal the action's model The following keys are optional: - active_ids: ids of the current records (mass mode). If active_ids and active_id are present, active_ids is given precedence. :return: an action_id to be executed, or False is finished correctly without return action """ res = False for action in self: eval_context = self._get_eval_context(action) if hasattr(self, 'run_action_%s_multi' % action.state): # call the multi method run_self = self.with_context(eval_context['env'].context) func = getattr(run_self, 'run_action_%s_multi' % action.state) res = func(action, eval_context=eval_context) elif hasattr(self, 'run_action_%s' % action.state): active_id = self._context.get('active_id') if not active_id and self._context.get('onchange_self'): active_id = self._context['onchange_self']._origin.id active_ids = self._context.get( 'active_ids', [active_id] if active_id else []) for active_id in active_ids: # run context dedicated to a particular active_id run_self = self.with_context(active_ids=[active_id], active_id=active_id) eval_context["env"].context = run_self._context # call the single method related to the action: run_action_<STATE> func = getattr(run_self, 'run_action_%s' % action.state) res = func(action, eval_context=eval_context) return res @api.model def _run_actions(self, ids): """ Run server actions with given ids. Allow crons to run specific server actions """ return self.browse(ids).run()
class SaleOrder(models.Model): _inherit = "sale.order" current_revision_id = fields.Many2one('sale.order', 'Current revision', readonly=True, copy=True) old_revision_ids = fields.One2many('sale.order', 'current_revision_id', 'Old revisions', readonly=True, context={'active_test': False}) revision_number = fields.Integer('Revision', copy=False) unrevisioned_name = fields.Char('Order Reference', copy=False, readonly=True) active = fields.Boolean('Active', default=True, copy=True) @api.model def create(self, vals): if 'unrevisioned_name' not in vals: if vals.get('name', 'New') == 'New': seq = self.env['ir.sequence'] vals['name'] = seq.next_by_code('sale.order') or '/' vals['unrevisioned_name'] = vals['name'] return super(SaleOrder, self).create(vals) @api.multi def action_revision(self): self.ensure_one() view_ref = self.env['ir.model.data'].get_object_reference( 'sale', 'view_order_form') view_id = view_ref and view_ref[1] or False, self.with_context(sale_revision_history=True).copy() self.write({'state': 'draft'}) self.order_line.write({'state': 'draft'}) self.mapped('order_line').write({'sale_line_id': False}) return { 'type': 'ir.actions.act_window', 'name': _('Sales Order'), 'res_model': 'sale.order', 'res_id': self.id, 'view_type': 'form', 'view_mode': 'form', 'view_id': view_id, 'target': 'current', 'nodestroy': True, } @api.returns('self', lambda value: value.id) @api.multi def copy(self, defaults=None): if not defaults: defaults = {} if self.env.context.get('sale_revision_history'): prev_name = self.name revno = self.revision_number # if sale order exist and amendment name sent as false. replace current sale name if self.unrevisioned_name == False: self.unrevisioned_name = self.name self.write({ 'revision_number': revno + 1, 'name': '%s-%02d' % (self.unrevisioned_name, revno + 1) }) defaults.update({ 'name': prev_name, 'revision_number': revno, 'active': False, 'state': 'cancel', 'current_revision_id': self.id, 'unrevisioned_name': self.unrevisioned_name, }) return super(SaleOrder, self).copy(defaults)
class IrSequenceDateRange(models.Model): _name = 'ir.sequence.date_range' _rec_name = "sequence_id" def _get_number_next_actual(self): '''Return number from ir_sequence row when no_gap implementation, and number from postgres sequence when standard implementation.''' for seq in self: if seq.sequence_id.implementation != 'standard': seq.number_next_actual = seq.number_next else: # get number from postgres sequence. Cannot use currval, because that might give an error when # not having used nextval before. self._cr.execute( "SELECT last_value, increment_by, is_called FROM ir_sequence_%03d_%03d" % (seq.sequence_id.id, seq.id)) (last_value, increment_by, is_called) = self._cr.fetchone() if is_called: seq.number_next_actual = last_value + increment_by else: seq.number_next_actual = last_value def _set_number_next_actual(self): for seq in self: seq.write({'number_next': seq.number_next_actual or 0}) date_from = fields.Date(string='From', required=True) date_to = fields.Date(string='To', required=True) sequence_id = fields.Many2one("ir.sequence", string='Main Sequence', required=True, ondelete='cascade') number_next = fields.Integer(string='Next Number', required=True, default=1, help="Next number of this sequence") number_next_actual = fields.Integer( compute='_get_number_next_actual', inverse='_set_number_next_actual', string='Next Number', help="Next number that will be used. This number can be incremented " "frequently so the displayed value might already be obsolete") def _next(self): if self.sequence_id.implementation == 'standard': number_next = _select_nextval( self._cr, 'ir_sequence_%03d_%03d' % (self.sequence_id.id, self.id)) else: number_next = _update_nogap(self, self.sequence_id.number_increment) return self.sequence_id.get_next_char(number_next) @api.multi def _alter_sequence(self, number_increment=None, number_next=None): for seq in self: _alter_sequence(self._cr, "ir_sequence_%03d_%03d" % (seq.sequence_id.id, seq.id), number_increment=number_increment, number_next=number_next) @api.model def create(self, values): """ Create a sequence, in implementation == standard a fast gaps-allowed PostgreSQL sequence is used. """ seq = super(IrSequenceDateRange, self).create(values) main_seq = seq.sequence_id if main_seq.implementation == 'standard': _create_sequence(self._cr, "ir_sequence_%03d_%03d" % (main_seq.id, seq.id), main_seq.number_increment, values.get('number_next_actual', 1)) return seq @api.multi def unlink(self): _drop_sequences( self._cr, ["ir_sequence_%03d_%03d" % (x.sequence_id.id, x.id) for x in self]) return super(IrSequenceDateRange, self).unlink() @api.multi def write(self, values): if values.get('number_next'): seq_to_alter = self.filtered( lambda seq: seq.sequence_id.implementation == 'standard') seq_to_alter._alter_sequence(number_next=values.get('number_next')) return super(IrSequenceDateRange, self).write(values)
class SurveyQuestion(models.Model): """ Questions that will be asked in a survey. Each question can have one of more suggested answers (eg. in case of multi-answer checkboxes, radio buttons...). Technical note: survey.question is also the model used for the survey's pages (with the "is_page" field set to True). A page corresponds to a "section" in the interface, and the fact that it separates the survey in actual pages in the interface depends on the "questions_layout" parameter on the survey.survey model. Pages are also used when randomizing questions. The randomization can happen within a "page". Using the same model for questions and pages allows to put all the pages and questions together in a o2m field (see survey.survey.question_and_page_ids) on the view side and easily reorganize your survey by dragging the items around. It also removes on level of encoding by directly having 'Add a page' and 'Add a question' links on the tree view of questions, enabling a faster encoding. However, this has the downside of making the code reading a little bit more complicated. Efforts were made at the model level to create computed fields so that the use of these models still seems somewhat logical. That means: - A survey still has "page_ids" (question_and_page_ids filtered on is_page = True) - These "page_ids" still have question_ids (questions located between this page and the next) - These "question_ids" still have a "page_id" That makes the use and display of these information at view and controller levels easier to understand. """ _name = 'survey.question' _description = 'Survey Question' _rec_name = 'title' _order = 'sequence,id' @api.model def default_get(self, fields): defaults = super(SurveyQuestion, self).default_get(fields) if (not fields or 'question_type' in fields): defaults['question_type'] = False if defaults.get( 'is_page') == True else 'text_box' return defaults # question generic data title = fields.Char('Title', required=True, translate=True) description = fields.Html( 'Description', translate=True, sanitize= False, # TDE TODO: sanitize but find a way to keep youtube iframe media stuff help= "Use this field to add additional explanations about your question or to illustrate it with pictures or a video" ) survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade') scoring_type = fields.Selection(related='survey_id.scoring_type', string='Scoring Type', readonly=True) sequence = fields.Integer('Sequence', default=10) # page specific is_page = fields.Boolean('Is a page?') question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_question_ids") questions_selection = fields.Selection( related='survey_id.questions_selection', readonly=True, help= "If randomized is selected, add the number of random questions next to the section." ) random_questions_count = fields.Integer( 'Random questions count', default=1, help= "Used on randomized sections to take X random questions from all the questions of that section." ) # question specific page_id = fields.Many2one('survey.question', string='Page', compute="_compute_page_id", store=True) question_type = fields.Selection( [('text_box', 'Multiple Lines Text Box'), ('char_box', 'Single Line Text Box'), ('numerical_box', 'Numerical Value'), ('date', 'Date'), ('datetime', 'Datetime'), ('simple_choice', 'Multiple choice: only one answer'), ('multiple_choice', 'Multiple choice: multiple answers allowed'), ('matrix', 'Matrix')], string='Question Type', compute='_compute_question_type', readonly=False, store=True) is_scored_question = fields.Boolean( 'Scored', compute='_compute_is_scored_question', readonly=False, store=True, copy=True, help= "Include this question as part of quiz scoring. Requires an answer and answer score to be taken into account." ) # -- scoreable/answerable simple answer_types: numerical_box / date / datetime answer_numerical_box = fields.Float( 'Correct numerical answer', help="Correct number answer for this question.") answer_date = fields.Date('Correct date answer', help="Correct date answer for this question.") answer_datetime = fields.Datetime( 'Correct datetime answer', help="Correct date and time answer for this question.") answer_score = fields.Float( 'Score', help="Score value for a correct answer to this question.") # -- char_box save_as_email = fields.Boolean( "Save as user email", compute='_compute_save_as_email', readonly=False, store=True, copy=True, help= "If checked, this option will save the user's answer as its email address." ) save_as_nickname = fields.Boolean( "Save as user nickname", compute='_compute_save_as_nickname', readonly=False, store=True, copy=True, help= "If checked, this option will save the user's answer as its nickname.") # -- simple choice / multiple choice / matrix suggested_answer_ids = fields.One2many( 'survey.question.answer', 'question_id', string='Types of answers', copy=True, help= 'Labels used for proposed choices: simple choice, multiple choice and columns of matrix' ) allow_value_image = fields.Boolean( 'Images on answers', help= 'Display images in addition to answer label. Valid only for simple / multiple choice questions.' ) # -- matrix matrix_subtype = fields.Selection( [('simple', 'One choice per row'), ('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple') matrix_row_ids = fields.One2many( 'survey.question.answer', 'matrix_question_id', string='Matrix Rows', copy=True, help='Labels used for proposed choices: rows of matrix') # -- display & timing options column_nb = fields.Selection( [('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')], string='Number of columns', default='12', help= 'These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap for dropdown-based simple and multiple choice questions.' ) is_time_limited = fields.Boolean( "The question is limited in time", help="Currently only supported for live sessions.") time_limit = fields.Integer("Time limit (seconds)") # -- comments (simple choice, multiple choice, matrix (without count as an answer)) comments_allowed = fields.Boolean('Show Comments Field') comments_message = fields.Char( 'Comment Message', translate=True, default=lambda self: _("If other, please specify:")) comment_count_as_answer = fields.Boolean( 'Comment Field is an Answer Choice') # question validation validation_required = fields.Boolean('Validate entry') validation_email = fields.Boolean('Input must be an email') validation_length_min = fields.Integer('Minimum Text Length', default=0) validation_length_max = fields.Integer('Maximum Text Length', default=0) validation_min_float_value = fields.Float('Minimum value', default=0.0) validation_max_float_value = fields.Float('Maximum value', default=0.0) validation_min_date = fields.Date('Minimum Date') validation_max_date = fields.Date('Maximum Date') validation_min_datetime = fields.Datetime('Minimum Datetime') validation_max_datetime = fields.Datetime('Maximum Datetime') validation_error_msg = fields.Char( 'Validation Error message', translate=True, default=lambda self: _("The answer you entered is not valid.")) constr_mandatory = fields.Boolean('Mandatory Answer') constr_error_msg = fields.Char( 'Error message', translate=True, default=lambda self: _("This question requires an answer.")) # answers user_input_line_ids = fields.One2many('survey.user_input.line', 'question_id', string='Answers', domain=[('skipped', '=', False)], groups='survey.group_survey_user') # Conditional display is_conditional = fields.Boolean( string='Conditional Display', copy=False, help="""If checked, this question will be displayed only if the specified conditional answer have been selected in a previous question""" ) triggering_question_id = fields.Many2one( 'survey.question', string="Triggering Question", copy=False, compute="_compute_triggering_question_id", store=True, readonly=False, help= "Question containing the triggering answer to display the current question.", domain="""[('survey_id', '=', survey_id), '&', ('question_type', 'in', ['simple_choice', 'multiple_choice']), '|', ('sequence', '<', sequence), '&', ('sequence', '=', sequence), ('id', '<', id)]""") triggering_answer_id = fields.Many2one( 'survey.question.answer', string="Triggering Answer", copy=False, compute="_compute_triggering_answer_id", store=True, readonly=False, help="Answer that will trigger the display of the current question.", domain="[('question_id', '=', triggering_question_id)]") _sql_constraints = [ ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'), ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'), ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'), ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'), ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!'), ('validation_datetime', 'CHECK (validation_min_datetime <= validation_max_datetime)', 'Max datetime cannot be smaller than min datetime!'), ('positive_answer_score', 'CHECK (answer_score >= 0)', 'An answer score for a non-multiple choice question cannot be negative!' ), ('scored_datetime_have_answers', "CHECK (is_scored_question != True OR question_type != 'datetime' OR answer_datetime is not null)", 'All "Is a scored question = True" and "Question Type: Datetime" questions need an answer' ), ('scored_date_have_answers', "CHECK (is_scored_question != True OR question_type != 'date' OR answer_date is not null)", 'All "Is a scored question = True" and "Question Type: Date" questions need an answer' ) ] @api.depends('is_page') def _compute_question_type(self): for question in self: if not question.question_type or question.is_page: question.question_type = False @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') def _compute_question_ids(self): """Will take all questions of the survey for which the index is higher than the index of this page and lower than the index of the next page.""" for question in self: if question.is_page: next_page_index = False for page in question.survey_id.page_ids: if page._index() > question._index(): next_page_index = page._index() break question.question_ids = question.survey_id.question_ids.filtered( lambda q: q._index() > question._index() and (not next_page_index or q._index() < next_page_index)) else: question.question_ids = self.env['survey.question'] @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') def _compute_page_id(self): """Will find the page to which this question belongs to by looking inside the corresponding survey""" for question in self: if question.is_page: question.page_id = None else: page = None for q in question.survey_id.question_and_page_ids.sorted(): if q == question: break if q.is_page: page = q question.page_id = page @api.depends('question_type', 'validation_email') def _compute_save_as_email(self): for question in self: if question.question_type != 'char_box' or not question.validation_email: question.save_as_email = False @api.depends('question_type') def _compute_save_as_nickname(self): for question in self: if question.question_type != 'char_box': question.save_as_nickname = False @api.depends('is_conditional') def _compute_triggering_question_id(self): """ Used as an 'onchange' : Reset the triggering question if user uncheck 'Conditional Display' Avoid CacheMiss : set the value to False if the value is not set yet.""" for question in self: if not question.is_conditional or question.triggering_question_id is None: question.triggering_question_id = False @api.depends('triggering_question_id') def _compute_triggering_answer_id(self): """ Used as an 'onchange' : Reset the triggering answer if user unset or change the triggering question or uncheck 'Conditional Display'. Avoid CacheMiss : set the value to False if the value is not set yet.""" for question in self: if not question.triggering_question_id \ or question.triggering_question_id != question.triggering_answer_id.question_id\ or question.triggering_answer_id is None: question.triggering_answer_id = False @api.depends('question_type', 'scoring_type', 'answer_date', 'answer_datetime', 'answer_numerical_box') def _compute_is_scored_question(self): """ Computes whether a question "is scored" or not. Handles following cases: - inconsistent Boolean=None edge case that breaks tests => False - survey is not scored => False - 'date'/'datetime'/'numerical_box' question types w/correct answer => True (implied without user having to activate, except for numerical whose correct value is 0.0) - 'simple_choice / multiple_choice': set to True even if logic is a bit different (coming from answers) - question_type isn't scoreable (note: choice questions scoring logic handled separately) => False """ for question in self: if question.is_scored_question is None or question.scoring_type == 'no_scoring': question.is_scored_question = False elif question.question_type == 'date': question.is_scored_question = bool(question.answer_date) elif question.question_type == 'datetime': question.is_scored_question = bool(question.answer_datetime) elif question.question_type == 'numerical_box' and question.answer_numerical_box: question.is_scored_question = True elif question.question_type in [ 'simple_choice', 'multiple_choice' ]: question.is_scored_question = True else: question.is_scored_question = False # ------------------------------------------------------------ # VALIDATION # ------------------------------------------------------------ def validate_question(self, answer, comment=None): """ Validate question, depending on question type and parameters for simple choice, text, date and number, answer is simply the answer of the question. For other multiple choices questions, answer is a list of answers (the selected choices or a list of selected answers per question -for matrix type-): - Simple answer : answer = 'example' or 2 or question_answer_id or 2019/10/10 - Multiple choice : answer = [question_answer_id1, question_answer_id2, question_answer_id3] - Matrix: answer = { 'rowId1' : [colId1, colId2,...], 'rowId2' : [colId1, colId3, ...] } return dict {question.id (int): error (str)} -> empty dict if no validation error. """ self.ensure_one() if isinstance(answer, str): answer = answer.strip() # Empty answer to mandatory question if self.constr_mandatory and not answer and self.question_type not in [ 'simple_choice', 'multiple_choice' ]: return {self.id: self.constr_error_msg} # because in choices question types, comment can count as answer if answer or self.question_type in [ 'simple_choice', 'multiple_choice' ]: if self.question_type == 'char_box': return self._validate_char_box(answer) elif self.question_type == 'numerical_box': return self._validate_numerical_box(answer) elif self.question_type in ['date', 'datetime']: return self._validate_date(answer) elif self.question_type in ['simple_choice', 'multiple_choice']: return self._validate_choice(answer, comment) elif self.question_type == 'matrix': return self._validate_matrix(answer) return {} def _validate_char_box(self, answer): # Email format validation # all the strings of the form "<something>@<anything>.<extension>" will be accepted if self.validation_email: if not tools.email_normalize(answer): return {self.id: _('This answer must be an email address')} # Answer validation (if properly defined) # Length of the answer must be in a range if self.validation_required: if not (self.validation_length_min <= len(answer) <= self.validation_length_max): return {self.id: self.validation_error_msg} return {} def _validate_numerical_box(self, answer): try: floatanswer = float(answer) except ValueError: return {self.id: _('This is not a number')} if self.validation_required: # Answer is not in the right range with tools.ignore(Exception): if not (self.validation_min_float_value <= floatanswer <= self.validation_max_float_value): return {self.id: self.validation_error_msg} return {} def _validate_date(self, answer): isDatetime = self.question_type == 'datetime' # Checks if user input is a date try: dateanswer = fields.Datetime.from_string( answer) if isDatetime else fields.Date.from_string(answer) except ValueError: return {self.id: _('This is not a date')} if self.validation_required: # Check if answer is in the right range if isDatetime: min_date = fields.Datetime.from_string( self.validation_min_datetime) max_date = fields.Datetime.from_string( self.validation_max_datetime) dateanswer = fields.Datetime.from_string(answer) else: min_date = fields.Date.from_string(self.validation_min_date) max_date = fields.Date.from_string(self.validation_max_date) dateanswer = fields.Date.from_string(answer) if (min_date and max_date and not (min_date <= dateanswer <= max_date))\ or (min_date and not min_date <= dateanswer)\ or (max_date and not dateanswer <= max_date): return {self.id: self.validation_error_msg} return {} def _validate_choice(self, answer, comment): # Empty comment if self.constr_mandatory \ and not answer \ and not (self.comments_allowed and self.comment_count_as_answer and comment): return {self.id: self.constr_error_msg} return {} def _validate_matrix(self, answers): # Validate that each line has been answered if self.constr_mandatory and len(self.matrix_row_ids) != len(answers): return {self.id: self.constr_error_msg} return {} def _index(self): """We would normally just use the 'sequence' field of questions BUT, if the pages and questions are created without ever moving records around, the sequence field can be set to 0 for all the questions. However, the order of the recordset is always correct so we can rely on the index method.""" self.ensure_one() return list(self.survey_id.question_and_page_ids).index(self) # ------------------------------------------------------------ # STATISTICS / REPORTING # ------------------------------------------------------------ def _prepare_statistics(self, user_input_lines): """ Compute statistical data for questions by counting number of vote per choice on basis of filter """ all_questions_data = [] for question in self: question_data = {'question': question, 'is_page': question.is_page} if question.is_page: all_questions_data.append(question_data) continue # fetch answer lines, separate comments from real answers all_lines = user_input_lines.filtered( lambda line: line.question_id == question) if question.question_type in [ 'simple_choice', 'multiple_choice', 'matrix' ]: answer_lines = all_lines.filtered( lambda line: line.answer_type == 'suggestion' or (line.answer_type == 'char_box' and question. comment_count_as_answer)) comment_line_ids = all_lines.filtered( lambda line: line.answer_type == 'char_box') else: answer_lines = all_lines comment_line_ids = self.env['survey.user_input.line'] skipped_lines = answer_lines.filtered(lambda line: line.skipped) done_lines = answer_lines - skipped_lines question_data.update( answer_line_ids=answer_lines, answer_line_done_ids=done_lines, answer_input_done_ids=done_lines.mapped('user_input_id'), answer_input_skipped_ids=skipped_lines.mapped('user_input_id'), comment_line_ids=comment_line_ids) question_data.update( question._get_stats_summary_data(answer_lines)) # prepare table and graph data table_data, graph_data = question._get_stats_data(answer_lines) question_data['table_data'] = table_data question_data['graph_data'] = json.dumps(graph_data) all_questions_data.append(question_data) return all_questions_data def _get_stats_data(self, user_input_lines): if self.question_type == 'simple_choice': return self._get_stats_data_answers(user_input_lines) elif self.question_type == 'multiple_choice': table_data, graph_data = self._get_stats_data_answers( user_input_lines) return table_data, [{'key': self.title, 'values': graph_data}] elif self.question_type == 'matrix': return self._get_stats_graph_data_matrix(user_input_lines) return [line for line in user_input_lines], [] def _get_stats_data_answers(self, user_input_lines): """ Statistics for question.answer based questions (simple choice, multiple choice.). A corner case with a void record survey.question.answer is added to count comments that should be considered as valid answers. This small hack allow to have everything available in the same standard structure. """ suggested_answers = [ answer for answer in self.mapped('suggested_answer_ids') ] if self.comment_count_as_answer: suggested_answers += [self.env['survey.question.answer']] count_data = dict.fromkeys(suggested_answers, 0) for line in user_input_lines: if line.suggested_answer_id or (line.value_char_box and self.comment_count_as_answer): count_data[line.suggested_answer_id] += 1 table_data = [{ 'value': _('Other (see comments)') if not sug_answer else sug_answer.value, 'suggested_answer': sug_answer, 'count': count_data[sug_answer] } for sug_answer in suggested_answers] graph_data = [{ 'text': _('Other (see comments)') if not sug_answer else sug_answer.value, 'count': count_data[sug_answer] } for sug_answer in suggested_answers] return table_data, graph_data def _get_stats_graph_data_matrix(self, user_input_lines): suggested_answers = self.mapped('suggested_answer_ids') matrix_rows = self.mapped('matrix_row_ids') count_data = dict.fromkeys( itertools.product(matrix_rows, suggested_answers), 0) for line in user_input_lines: if line.matrix_row_id and line.suggested_answer_id: count_data[(line.matrix_row_id, line.suggested_answer_id)] += 1 table_data = [{ 'row': row, 'columns': [{ 'suggested_answer': sug_answer, 'count': count_data[(row, sug_answer)] } for sug_answer in suggested_answers], } for row in matrix_rows] graph_data = [{ 'key': sug_answer.value, 'values': [{ 'text': row.value, 'count': count_data[(row, sug_answer)] } for row in matrix_rows] } for sug_answer in suggested_answers] return table_data, graph_data def _get_stats_summary_data(self, user_input_lines): stats = {} if self.question_type in ['simple_choice', 'multiple_choice']: stats.update(self._get_stats_summary_data_choice(user_input_lines)) elif self.question_type == 'numerical_box': stats.update( self._get_stats_summary_data_numerical(user_input_lines)) if self.question_type in ['numerical_box', 'date', 'datetime']: stats.update(self._get_stats_summary_data_scored(user_input_lines)) return stats def _get_stats_summary_data_choice(self, user_input_lines): right_inputs, partial_inputs = self.env['survey.user_input'], self.env[ 'survey.user_input'] right_answers = self.suggested_answer_ids.filtered( lambda label: label.is_correct) if self.question_type == 'multiple_choice': for user_input, lines in tools.groupby( user_input_lines, operator.itemgetter('user_input_id')): user_input_answers = self.env['survey.user_input.line'].concat( *lines).filtered(lambda l: l.answer_is_correct).mapped( 'suggested_answer_id') if user_input_answers and user_input_answers < right_answers: partial_inputs += user_input elif user_input_answers: right_inputs += user_input else: right_inputs = user_input_lines.filtered( lambda line: line.answer_is_correct).mapped('user_input_id') return { 'right_answers': right_answers, 'right_inputs_count': len(right_inputs), 'partial_inputs_count': len(partial_inputs), } def _get_stats_summary_data_numerical(self, user_input_lines): all_values = user_input_lines.filtered( lambda line: not line.skipped).mapped('value_numerical_box') lines_sum = sum(all_values) return { 'numerical_max': max(all_values, default=0), 'numerical_min': min(all_values, default=0), 'numerical_average': round(lines_sum / (len(all_values) or 1), 2), } def _get_stats_summary_data_scored(self, user_input_lines): return { 'common_lines': collections.Counter( user_input_lines.filtered(lambda line: not line.skipped). mapped('value_%s' % self.question_type)).most_common(5) if self.question_type != 'datetime' else [], 'right_inputs_count': len( user_input_lines.filtered(lambda line: line.answer_is_correct). mapped('user_input_id')) }
class ProductProduct(models.Model): _name = "product.product" _description = "Product" _inherits = {'product.template': 'product_tmpl_id'} _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'default_code, name, id' # price: total price, context dependent (partner, pricelist, quantity) price = fields.Float( 'Price', compute='_compute_product_price', digits='Product Price', inverse='_set_product_price') # price_extra: catalog extra value only, sum of variant extra attributes price_extra = fields.Float( 'Variant Price Extra', compute='_compute_product_price_extra', digits='Product Price', help="This is the sum of the extra price of all attributes") # lst_price: catalog value + extra, context dependent (uom) lst_price = fields.Float( 'Public Price', compute='_compute_product_lst_price', digits='Product Price', inverse='_set_product_lst_price', help="The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices.") default_code = fields.Char('Internal Reference', index=True) code = fields.Char('Reference', compute='_compute_product_code') partner_ref = fields.Char('Customer Ref', compute='_compute_partner_ref') active = fields.Boolean( 'Active', default=True, help="If unchecked, it will allow you to hide the product without removing it.") product_tmpl_id = fields.Many2one( 'product.template', 'Product Template', auto_join=True, index=True, ondelete="cascade", required=True) barcode = fields.Char( 'Barcode', copy=False, help="International Article Number used for product identification.") product_template_attribute_value_ids = fields.Many2many('product.template.attribute.value', relation='product_variant_combination', string="Attribute Values", ondelete='restrict') combination_indices = fields.Char(compute='_compute_combination_indices', store=True, index=True) is_product_variant = fields.Boolean(compute='_compute_is_product_variant') standard_price = fields.Float( 'Cost', company_dependent=True, digits='Product Price', groups="base.group_user", help="""In Standard Price & AVCO: value of the product (automatically computed in AVCO). In FIFO: value of the last unit that left the stock (automatically computed). Used to value the product when the purchase cost is not known (e.g. inventory adjustment). Used to compute margins on sale orders.""") volume = fields.Float('Volume', digits='Volume') weight = fields.Float('Weight', digits='Stock Weight') pricelist_item_count = fields.Integer("Number of price rules", compute="_compute_variant_item_count") packaging_ids = fields.One2many( 'product.packaging', 'product_id', 'Product Packages', help="Gives the different ways to package the same product.") # all image fields are base64 encoded and PIL-supported # all image_variant fields are technical and should not be displayed to the user image_variant_1920 = fields.Image("Variant Image", max_width=1920, max_height=1920) # resized fields stored (as attachment) for performance image_variant_1024 = fields.Image("Variant Image 1024", related="image_variant_1920", max_width=1024, max_height=1024, store=True) image_variant_512 = fields.Image("Variant Image 512", related="image_variant_1920", max_width=512, max_height=512, store=True) image_variant_256 = fields.Image("Variant Image 256", related="image_variant_1920", max_width=256, max_height=256, store=True) image_variant_128 = fields.Image("Variant Image 128", related="image_variant_1920", max_width=128, max_height=128, store=True) can_image_variant_1024_be_zoomed = fields.Boolean("Can Variant Image 1024 be zoomed", compute='_compute_can_image_variant_1024_be_zoomed', store=True) # Computed fields that are used to create a fallback to the template if # necessary, it's recommended to display those fields to the user. image_1920 = fields.Image("Image", compute='_compute_image_1920', inverse='_set_image_1920') image_1024 = fields.Image("Image 1024", compute='_compute_image_1024') image_512 = fields.Image("Image 512", compute='_compute_image_512') image_256 = fields.Image("Image 256", compute='_compute_image_256') image_128 = fields.Image("Image 128", compute='_compute_image_128') can_image_1024_be_zoomed = fields.Boolean("Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed') @api.depends('image_variant_1920', 'image_variant_1024') def _compute_can_image_variant_1024_be_zoomed(self): for record in self: record.can_image_variant_1024_be_zoomed = record.image_variant_1920 and tools.is_image_size_above(record.image_variant_1920, record.image_variant_1024) def _compute_image_1920(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_1920 = record.image_variant_1920 or record.product_tmpl_id.image_1920 def _set_image_1920(self): for record in self: if ( # We are trying to remove an image even though it is already # not set, remove it from the template instead. not record.image_1920 and not record.image_variant_1920 or # We are trying to add an image, but the template image is # not set, write on the template instead. record.image_1920 and not record.product_tmpl_id.image_1920 or # There is only one variant, always write on the template. self.search_count([ ('product_tmpl_id', '=', record.product_tmpl_id.id), ('active', '=', True), ]) <= 1 ): record.image_variant_1920 = False record.product_tmpl_id.image_1920 = record.image_1920 else: record.image_variant_1920 = record.image_1920 def _compute_image_1024(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_1024 = record.image_variant_1024 or record.product_tmpl_id.image_1024 def _compute_image_512(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_512 = record.image_variant_512 or record.product_tmpl_id.image_512 def _compute_image_256(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_256 = record.image_variant_256 or record.product_tmpl_id.image_256 def _compute_image_128(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_128 = record.image_variant_128 or record.product_tmpl_id.image_128 def _compute_can_image_1024_be_zoomed(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.can_image_1024_be_zoomed = record.can_image_variant_1024_be_zoomed if record.image_variant_1920 else record.product_tmpl_id.can_image_1024_be_zoomed def init(self): """Ensure there is at most one active variant for each combination. There could be no variant for a combination if using dynamic attributes. """ self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS product_product_combination_unique ON %s (product_tmpl_id, combination_indices) WHERE active is true" % self._table) _sql_constraints = [ ('barcode_uniq', 'unique(barcode)', "A barcode can only be assigned to one product !"), ] def _get_invoice_policy(self): return False @api.depends('product_template_attribute_value_ids') def _compute_combination_indices(self): for product in self: product.combination_indices = product.product_template_attribute_value_ids._ids2str() def _compute_is_product_variant(self): self.is_product_variant = True @api.depends_context('pricelist', 'partner', 'quantity', 'uom', 'date', 'no_variant_attributes_price_extra') def _compute_product_price(self): prices = {} pricelist_id_or_name = self._context.get('pricelist') if pricelist_id_or_name: pricelist = None partner = self.env.context.get('partner', False) quantity = self.env.context.get('quantity', 1.0) # Support context pricelists specified as list, display_name or ID for compatibility if isinstance(pricelist_id_or_name, list): pricelist_id_or_name = pricelist_id_or_name[0] if isinstance(pricelist_id_or_name, str): pricelist_name_search = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) if pricelist_name_search: pricelist = self.env['product.pricelist'].browse([pricelist_name_search[0][0]]) elif isinstance(pricelist_id_or_name, int): pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name) if pricelist: quantities = [quantity] * len(self) partners = [partner] * len(self) prices = pricelist.get_products_price(self, quantities, partners) for product in self: product.price = prices.get(product.id, 0.0) def _set_product_price(self): for product in self: if self._context.get('uom'): value = self.env['uom.uom'].browse(self._context['uom'])._compute_price(product.price, product.uom_id) else: value = product.price value -= product.price_extra product.write({'list_price': value}) def _set_product_lst_price(self): for product in self: if self._context.get('uom'): value = self.env['uom.uom'].browse(self._context['uom'])._compute_price(product.lst_price, product.uom_id) else: value = product.lst_price value -= product.price_extra product.write({'list_price': value}) def _compute_product_price_extra(self): for product in self: product.price_extra = sum(product.product_template_attribute_value_ids.mapped('price_extra')) @api.depends('list_price', 'price_extra') @api.depends_context('uom') def _compute_product_lst_price(self): to_uom = None if 'uom' in self._context: to_uom = self.env['uom.uom'].browse(self._context['uom']) for product in self: if to_uom: list_price = product.uom_id._compute_price(product.list_price, to_uom) else: list_price = product.list_price product.lst_price = list_price + product.price_extra @api.depends_context('partner_id') def _compute_product_code(self): for product in self: for supplier_info in product.seller_ids: if supplier_info.name.id == product._context.get('partner_id'): product.code = supplier_info.product_code or product.default_code break else: product.code = product.default_code @api.depends_context('partner_id') def _compute_partner_ref(self): for product in self: for supplier_info in product.seller_ids: if supplier_info.name.id == product._context.get('partner_id'): product_name = supplier_info.product_name or product.default_code or product.name product.partner_ref = '%s%s' % (product.code and '[%s] ' % product.code or '', product_name) break else: product.partner_ref = product.display_name def _compute_variant_item_count(self): for product in self: domain = ['|', '&', ('product_tmpl_id', '=', product.product_tmpl_id.id), ('applied_on', '=', '1_product'), '&', ('product_id', '=', product.id), ('applied_on', '=', '0_product_variant')] product.pricelist_item_count = self.env['product.pricelist.item'].search_count(domain) @api.onchange('uom_id') def _onchange_uom_id(self): if self.uom_id: self.uom_po_id = self.uom_id.id @api.onchange('uom_po_id') def _onchange_uom(self): if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id: self.uom_po_id = self.uom_id @api.model_create_multi def create(self, vals_list): products = super(ProductProduct, self.with_context(create_product_product=True)).create(vals_list) # `_get_variant_id_for_combination` depends on existing variants self.clear_caches() return products def write(self, values): res = super(ProductProduct, self).write(values) if 'product_template_attribute_value_ids' in values: # `_get_variant_id_for_combination` depends on `product_template_attribute_value_ids` self.clear_caches() if 'active' in values: # prefetched o2m have to be reloaded (because of active_test) # (eg. product.template: product_variant_ids) self.flush() self.invalidate_cache() # `_get_first_possible_variant_id` depends on variants active state self.clear_caches() return res def unlink(self): unlink_products = self.env['product.product'] unlink_templates = self.env['product.template'] for product in self: # If there is an image set on the variant and no image set on the # template, move the image to the template. if product.image_variant_1920 and not product.product_tmpl_id.image_1920: product.product_tmpl_id.image_1920 = product.image_variant_1920 # Check if product still exists, in case it has been unlinked by unlinking its template if not product.exists(): continue # Check if the product is last product of this template... other_products = self.search([('product_tmpl_id', '=', product.product_tmpl_id.id), ('id', '!=', product.id)]) # ... and do not delete product template if it's configured to be created "on demand" if not other_products and not product.product_tmpl_id.has_dynamic_attributes(): unlink_templates |= product.product_tmpl_id unlink_products |= product res = super(ProductProduct, unlink_products).unlink() # delete templates after calling super, as deleting template could lead to deleting # products due to ondelete='cascade' unlink_templates.unlink() # `_get_variant_id_for_combination` depends on existing variants self.clear_caches() return res def _filter_to_unlink(self, check_access=True): return self def _unlink_or_archive(self, check_access=True): """Unlink or archive products. Try in batch as much as possible because it is much faster. Use dichotomy when an exception occurs. """ # Avoid access errors in case the products is shared amongst companies # but the underlying objects are not. If unlink fails because of an # AccessError (e.g. while recomputing fields), the 'write' call will # fail as well for the same reason since the field has been set to # recompute. if check_access: self.check_access_rights('unlink') self.check_access_rule('unlink') self.check_access_rights('write') self.check_access_rule('write') self = self.sudo() to_unlink = self._filter_to_unlink() to_archive = self - to_unlink to_archive.write({'active': False}) self = to_unlink try: with self.env.cr.savepoint(), tools.mute_logger('flectra.sql_db'): self.unlink() except Exception: # We catch all kind of exceptions to be sure that the operation # doesn't fail. if len(self) > 1: self[:len(self) // 2]._unlink_or_archive(check_access=False) self[len(self) // 2:]._unlink_or_archive(check_access=False) else: if self.active: # Note: this can still fail if something is preventing # from archiving. # This is the case from existing stock reordering rules. self.write({'active': False}) @api.returns('self', lambda value: value.id) def copy(self, default=None): """Variants are generated depending on the configuration of attributes and values on the template, so copying them does not make sense. For convenience the template is copied instead and its first variant is returned. """ return self.product_tmpl_id.copy(default=default).product_variant_id @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): # TDE FIXME: strange if self._context.get('search_default_categ_id'): args.append((('categ_id', 'child_of', self._context['search_default_categ_id']))) return super(ProductProduct, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) def name_get(self): # TDE: this could be cleaned a bit I think def _name_get(d): name = d.get('name', '') code = self._context.get('display_default_code', True) and d.get('default_code', False) or False if code: name = '[%s] %s' % (code,name) return (d['id'], name) partner_id = self._context.get('partner_id') if partner_id: partner_ids = [partner_id, self.env['res.partner'].browse(partner_id).commercial_partner_id.id] else: partner_ids = [] company_id = self.env.context.get('company_id') # all user don't have access to seller and partner # check access and use superuser self.check_access_rights("read") self.check_access_rule("read") result = [] # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields # Use `load=False` to not call `name_get` for the `product_tmpl_id` self.sudo().read(['name', 'default_code', 'product_tmpl_id'], load=False) product_template_ids = self.sudo().mapped('product_tmpl_id').ids if partner_ids: supplier_info = self.env['product.supplierinfo'].sudo().search([ ('product_tmpl_id', 'in', product_template_ids), ('name', 'in', partner_ids), ]) # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields # Use `load=False` to not call `name_get` for the `product_tmpl_id` and `product_id` supplier_info.sudo().read(['product_tmpl_id', 'product_id', 'product_name', 'product_code'], load=False) supplier_info_by_template = {} for r in supplier_info: supplier_info_by_template.setdefault(r.product_tmpl_id, []).append(r) for product in self.sudo(): variant = product.product_template_attribute_value_ids._get_combination_name() name = variant and "%s (%s)" % (product.name, variant) or product.name sellers = [] if partner_ids: product_supplier_info = supplier_info_by_template.get(product.product_tmpl_id, []) sellers = [x for x in product_supplier_info if x.product_id and x.product_id == product] if not sellers: sellers = [x for x in product_supplier_info if not x.product_id] # Filter out sellers based on the company. This is done afterwards for a better # code readability. At this point, only a few sellers should remain, so it should # not be a performance issue. if company_id: sellers = [x for x in sellers if x.company_id.id in [company_id, False]] if sellers: for s in sellers: seller_variant = s.product_name and ( variant and "%s (%s)" % (s.product_name, variant) or s.product_name ) or False mydict = { 'id': product.id, 'name': seller_variant or name, 'default_code': s.product_code or product.default_code, } temp = _name_get(mydict) if temp not in result: result.append(temp) else: mydict = { 'id': product.id, 'name': name, 'default_code': product.default_code, } result.append(_name_get(mydict)) return result @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): if not args: args = [] if name: positive_operators = ['=', 'ilike', '=ilike', 'like', '=like'] product_ids = [] if operator in positive_operators: product_ids = list(self._search([('default_code', '=', name)] + args, limit=limit, access_rights_uid=name_get_uid)) if not product_ids: product_ids = list(self._search([('barcode', '=', name)] + args, limit=limit, access_rights_uid=name_get_uid)) if not product_ids and operator not in expression.NEGATIVE_TERM_OPERATORS: # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal # on a database with thousands of matching products, due to the huge merge+unique needed for the # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table # Performing a quick memory merge of ids in Python will give much better performance product_ids = list(self._search(args + [('default_code', operator, name)], limit=limit)) if not limit or len(product_ids) < limit: # we may underrun the limit because of dupes in the results, that's fine limit2 = (limit - len(product_ids)) if limit else False product2_ids = self._search(args + [('name', operator, name), ('id', 'not in', product_ids)], limit=limit2, access_rights_uid=name_get_uid) product_ids.extend(product2_ids) elif not product_ids and operator in expression.NEGATIVE_TERM_OPERATORS: domain = expression.OR([ ['&', ('default_code', operator, name), ('name', operator, name)], ['&', ('default_code', '=', False), ('name', operator, name)], ]) domain = expression.AND([args, domain]) product_ids = list(self._search(domain, limit=limit, access_rights_uid=name_get_uid)) if not product_ids and operator in positive_operators: ptrn = re.compile('(\[(.*?)\])') res = ptrn.search(name) if res: product_ids = list(self._search([('default_code', '=', res.group(2))] + args, limit=limit, access_rights_uid=name_get_uid)) # still no results, partner in context: search on supplier info as last hope to find something if not product_ids and self._context.get('partner_id'): suppliers_ids = self.env['product.supplierinfo']._search([ ('name', '=', self._context.get('partner_id')), '|', ('product_code', operator, name), ('product_name', operator, name)], access_rights_uid=name_get_uid) if suppliers_ids: product_ids = self._search([('product_tmpl_id.seller_ids', 'in', suppliers_ids)], limit=limit, access_rights_uid=name_get_uid) else: product_ids = self._search(args, limit=limit, access_rights_uid=name_get_uid) return product_ids @api.model def view_header_get(self, view_id, view_type): if self._context.get('categ_id'): return _( 'Products: %(category)s', category=self.env['product.category'].browse(self.env.context['categ_id']).name, ) return super().view_header_get(view_id, view_type) def open_pricelist_rules(self): self.ensure_one() domain = ['|', '&', ('product_tmpl_id', '=', self.product_tmpl_id.id), ('applied_on', '=', '1_product'), '&', ('product_id', '=', self.id), ('applied_on', '=', '0_product_variant')] return { 'name': _('Price Rules'), 'view_mode': 'tree,form', 'views': [(self.env.ref('product.product_pricelist_item_tree_view_from_product').id, 'tree'), (False, 'form')], 'res_model': 'product.pricelist.item', 'type': 'ir.actions.act_window', 'target': 'current', 'domain': domain, 'context': { 'default_product_id': self.id, 'default_applied_on': '0_product_variant', } } def open_product_template(self): """ Utility method used to add an "Open Template" button in product views """ self.ensure_one() return {'type': 'ir.actions.act_window', 'res_model': 'product.template', 'view_mode': 'form', 'res_id': self.product_tmpl_id.id, 'target': 'new'} def _prepare_sellers(self, params=False): # This search is made to avoid retrieving seller_ids from the cache. return self.env['product.supplierinfo'].search([('product_tmpl_id', '=', self.product_tmpl_id.id), ('name.active', '=', True)]).sorted(lambda s: (s.sequence, -s.min_qty, s.price, s.id)) def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False): self.ensure_one() if date is None: date = fields.Date.context_today(self) precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') res = self.env['product.supplierinfo'] sellers = self._prepare_sellers(params) sellers = sellers.filtered(lambda s: not s.company_id or s.company_id.id == self.env.company.id) for seller in sellers: # Set quantity in UoM of seller quantity_uom_seller = quantity if quantity_uom_seller and uom_id and uom_id != seller.product_uom: quantity_uom_seller = uom_id._compute_quantity(quantity_uom_seller, seller.product_uom) if seller.date_start and seller.date_start > date: continue if seller.date_end and seller.date_end < date: continue if partner_id and seller.name not in [partner_id, partner_id.parent_id]: continue if float_compare(quantity_uom_seller, seller.min_qty, precision_digits=precision) == -1: continue if seller.product_id and seller.product_id != self: continue if not res or res.name == seller.name: res |= seller return res.sorted('price')[:1] def price_compute(self, price_type, uom=False, currency=False, company=None): # TDE FIXME: delegate to template or not ? fields are reencoded here ... # compatibility about context keys used a bit everywhere in the code if not uom and self._context.get('uom'): uom = self.env['uom.uom'].browse(self._context['uom']) if not currency and self._context.get('currency'): currency = self.env['res.currency'].browse(self._context['currency']) products = self if price_type == 'standard_price': # standard_price field can only be seen by users in base.group_user # Thus, in order to compute the sale price from the cost for users not in this group # We fetch the standard price as the superuser products = self.with_company(company or self.env.company).sudo() prices = dict.fromkeys(self.ids, 0.0) for product in products: prices[product.id] = product[price_type] or 0.0 if price_type == 'list_price': prices[product.id] += product.price_extra # we need to add the price from the attributes that do not generate variants # (see field product.attribute create_variant) if self._context.get('no_variant_attributes_price_extra'): # we have a list of price_extra that comes from the attribute values, we need to sum all that prices[product.id] += sum(self._context.get('no_variant_attributes_price_extra')) if uom: prices[product.id] = product.uom_id._compute_price(prices[product.id], uom) # Convert from current user company currency to asked one # This is right cause a field cannot be in more than one currency if currency: prices[product.id] = product.currency_id._convert( prices[product.id], currency, product.company_id, fields.Date.today()) return prices @api.model def get_empty_list_help(self, help): self = self.with_context( empty_list_help_document_name=_("product"), ) return super(ProductProduct, self).get_empty_list_help(help) def get_product_multiline_description_sale(self): """ Compute a multiline description of this product, in the context of sales (do not use for purchases or other display reasons that don't intend to use "description_sale"). It will often be used as the default description of a sale order line referencing this product. """ name = self.display_name if self.description_sale: name += '\n' + self.description_sale return name def _is_variant_possible(self, parent_combination=None): """Return whether the variant is possible based on its own combination, and optionally a parent combination. See `_is_combination_possible` for more information. :param parent_combination: combination from which `self` is an optional or accessory product. :type parent_combination: recordset `product.template.attribute.value` :return: ẁhether the variant is possible based on its own combination :rtype: bool """ self.ensure_one() return self.product_tmpl_id._is_combination_possible(self.product_template_attribute_value_ids, parent_combination=parent_combination, ignore_no_variant=True) def toggle_active(self): """ Archiving related product.template if there is not any more active product.product (and vice versa, unarchiving the related product template if there is now an active product.product) """ result = super().toggle_active() # We deactivate product templates which are active with no active variants. tmpl_to_deactivate = self.filtered(lambda product: (product.product_tmpl_id.active and not product.product_tmpl_id.product_variant_ids)).mapped('product_tmpl_id') # We activate product templates which are inactive with active variants. tmpl_to_activate = self.filtered(lambda product: (not product.product_tmpl_id.active and product.product_tmpl_id.product_variant_ids)).mapped('product_tmpl_id') (tmpl_to_deactivate + tmpl_to_activate).toggle_active() return result
class MaintenanceRequest(models.Model): _name = 'maintenance.request' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Maintenance Requests' _order = "id desc" @api.returns('self') def _default_stage(self): return self.env['maintenance.stage'].search([], limit=1) @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'stage_id' in init_values and self.stage_id.sequence <= 1: return 'maintenance.mt_req_created' elif 'stage_id' in init_values and self.stage_id.sequence > 1: return 'maintenance.mt_req_status' return super(MaintenanceRequest, self)._track_subtype(init_values) def _get_default_team_id(self): return self.env.ref('maintenance.equipment_team_maintenance', raise_if_not_found=False) name = fields.Char('Subjects', required=True) description = fields.Text('Description') request_date = fields.Date( 'Request Date', track_visibility='onchange', default=fields.Date.context_today, help="Date requested for the maintenance to happen") owner_user_id = fields.Many2one('res.users', string='Created by', default=lambda s: s.env.uid) category_id = fields.Many2one('maintenance.equipment.category', related='equipment_id.category_id', string='Category', store=True, readonly=True) equipment_id = fields.Many2one('maintenance.equipment', string='Equipment', index=True) technician_user_id = fields.Many2one('res.users', string='Owner', track_visibility='onchange', oldname='user_id') stage_id = fields.Many2one('maintenance.stage', string='Stage', track_visibility='onchange', group_expand='_read_group_stage_ids', default=_default_stage) priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority') color = fields.Integer('Color Index') close_date = fields.Date('Close Date', help="Date the maintenance was finished. ") kanban_state = fields.Selection([('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], string='Kanban State', required=True, default='normal', track_visibility='onchange') # active = fields.Boolean(default=True, help="Set active to false to hide the maintenance request without deleting it.") archive = fields.Boolean( default=False, help= "Set archive to true to hide the maintenance request without deleting it." ) maintenance_type = fields.Selection([('corrective', 'Corrective'), ('preventive', 'Preventive')], string='Maintenance Type', default="corrective") schedule_date = fields.Datetime( 'Scheduled Date', help= "Date the maintenance team plans the maintenance. It should not differ much from the Request Date. " ) maintenance_team_id = fields.Many2one('maintenance.team', string='Team', required=True, default=_get_default_team_id) duration = fields.Float(help="Duration in minutes and seconds.") @api.multi def archive_equipment_request(self): self.write({'archive': True}) @api.multi def reset_equipment_request(self): """ Reinsert the maintenance request into the maintenance pipe in the first stage""" first_stage_obj = self.env['maintenance.stage'].search( [], order="sequence asc", limit=1) # self.write({'active': True, 'stage_id': first_stage_obj.id}) self.write({'archive': False, 'stage_id': first_stage_obj.id}) @api.onchange('equipment_id') def onchange_equipment_id(self): if self.equipment_id: self.technician_user_id = self.equipment_id.technician_user_id if self.equipment_id.technician_user_id else self.equipment_id.category_id.technician_user_id self.category_id = self.equipment_id.category_id if self.equipment_id.maintenance_team_id: self.maintenance_team_id = self.equipment_id.maintenance_team_id.id @api.onchange('category_id') def onchange_category_id(self): if not self.technician_user_id or not self.equipment_id or ( self.technician_user_id and not self.equipment_id.technician_user_id): self.technician_user_id = self.category_id.technician_user_id @api.model def create(self, vals): # context: no_log, because subtype already handle this self = self.with_context(mail_create_nolog=True) request = super(MaintenanceRequest, self).create(vals) if request.owner_user_id or request.technician_user_id: request._add_followers() if request.equipment_id and not request.maintenance_team_id: request.maintenance_team_id = request.equipment_id.maintenance_team_id return request @api.multi def write(self, vals): # Overridden to reset the kanban_state to normal whenever # the stage (stage_id) of the Maintenance Request changes. if vals and 'kanban_state' not in vals and 'stage_id' in vals: vals['kanban_state'] = 'normal' res = super(MaintenanceRequest, self).write(vals) if vals.get('owner_user_id') or vals.get('technician_user_id'): self._add_followers() if self.stage_id.done and 'stage_id' in vals: self.write({'close_date': fields.Date.today()}) return res def _add_followers(self): for request in self: user_ids = (request.owner_user_id + request.technician_user_id).ids request.message_subscribe_users(user_ids=user_ids) @api.model def _read_group_stage_ids(self, stages, domain, order): """ Read group customization in order to display all the stages in the kanban view, even if they are empty """ stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID) return stages.browse(stage_ids)
class SaleOrder(models.Model): _inherit = "sale.order" @api.model def _default_warehouse_id(self): company = self.env.user.company_id.id warehouse_ids = self.env['stock.warehouse'].search([('company_id', '=', company)], limit=1) return warehouse_ids incoterm = fields.Many2one( 'stock.incoterms', 'Incoterms', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.") picking_policy = fields.Selection([ ('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')], string='Shipping Policy', required=True, readonly=True, default='direct', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}) warehouse_id = fields.Many2one( 'stock.warehouse', string='Warehouse', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, default=_default_warehouse_id) picking_ids = fields.One2many('stock.picking', 'sale_id', string='Pickings') delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) @api.multi def _action_confirm(self): super(SaleOrder, self)._action_confirm() for order in self: order.order_line._action_launch_procurement_rule() @api.depends('picking_ids') def _compute_picking_ids(self): for order in self: order.delivery_count = len(order.picking_ids) @api.onchange('warehouse_id') def _onchange_warehouse_id(self): if self.warehouse_id.company_id: self.company_id = self.warehouse_id.company_id.id @api.multi def action_view_delivery(self): ''' This function returns an action that display existing delivery orders of given sales order ids. It can either be a in a list or in a form view, if there is only one delivery order to show. ''' action = self.env.ref('stock.action_picking_tree_all').read()[0] pickings = self.mapped('picking_ids') if len(pickings) > 1: action['domain'] = [('id', 'in', pickings.ids)] elif pickings: action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] action['res_id'] = pickings.id return action @api.multi def action_cancel(self): self.mapped('picking_ids').action_cancel() return super(SaleOrder, self).action_cancel() @api.multi def _prepare_invoice(self): invoice_vals = super(SaleOrder, self)._prepare_invoice() invoice_vals['incoterms_id'] = self.incoterm.id or False return invoice_vals @api.model def _get_customer_lead(self, product_tmpl_id): super(SaleOrder, self)._get_customer_lead(product_tmpl_id) return product_tmpl_id.sale_delay
class MaintenanceEquipmentCategory(models.Model): _name = 'maintenance.equipment.category' _inherit = ['mail.alias.mixin', 'mail.thread'] _description = 'Asset Category' @api.one @api.depends('equipment_ids') def _compute_fold(self): self.fold = False if self.equipment_count else True name = fields.Char('Category Name', required=True, translate=True) technician_user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.uid, oldname='user_id') color = fields.Integer('Color Index') note = fields.Text('Comments', translate=True) equipment_ids = fields.One2many('maintenance.equipment', 'category_id', string='Equipments', copy=False) equipment_count = fields.Integer(string="Equipment", compute='_compute_equipment_count') maintenance_ids = fields.One2many('maintenance.request', 'category_id', copy=False) maintenance_count = fields.Integer(string="Maintenance", compute='_compute_maintenance_count') alias_id = fields.Many2one( 'mail.alias', 'Alias', ondelete='restrict', required=True, help= "Email alias for this equipment category. New emails will automatically " "create new maintenance request for this equipment category.") fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True) @api.multi def _compute_equipment_count(self): equipment_data = self.env['maintenance.equipment'].read_group( [('category_id', 'in', self.ids)], ['category_id'], ['category_id']) mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data]) for category in self: category.equipment_count = mapped_data.get(category.id, 0) @api.multi def _compute_maintenance_count(self): maintenance_data = self.env['maintenance.request'].read_group( [('category_id', 'in', self.ids)], ['category_id'], ['category_id']) mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in maintenance_data]) for category in self: category.maintenance_count = mapped_data.get(category.id, 0) @api.model def create(self, vals): self = self.with_context(alias_model_name='maintenance.request', alias_parent_model_name=self._name) if not vals.get('alias_name'): vals['alias_name'] = vals.get('name') category_id = super(MaintenanceEquipmentCategory, self).create(vals) category_id.alias_id.write({ 'alias_parent_thread_id': category_id.id, 'alias_defaults': { 'category_id': category_id.id } }) return category_id @api.multi def unlink(self): MailAlias = self.env['mail.alias'] for category in self: if category.equipment_ids or category.maintenance_ids: raise UserError( _("You cannot delete an equipment category containing equipments or maintenance requests." )) MailAlias += category.alias_id res = super(MaintenanceEquipmentCategory, self).unlink() MailAlias.unlink() return res def get_alias_model_name(self, vals): return vals.get('alias_model', 'maintenance.equipment') def get_alias_values(self): values = super(MaintenanceEquipmentCategory, self).get_alias_values() values['alias_defaults'] = {'category_id': self.id} return values
class MaintenanceEquipment(models.Model): _name = 'maintenance.equipment' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Equipment' @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'owner_user_id' in init_values and self.owner_user_id: return 'maintenance.mt_mat_assign' return super(MaintenanceEquipment, self)._track_subtype(init_values) @api.multi def name_get(self): result = [] for record in self: if record.name and record.serial_no: result.append( (record.id, record.name + '/' + record.serial_no)) if record.name and not record.serial_no: result.append((record.id, record.name)) return result @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] recs = self.browse() if name: recs = self.search([('name', '=', name)] + args, limit=limit) if not recs: recs = self.search([('name', operator, name)] + args, limit=limit) return recs.name_get() name = fields.Char('Equipment Name', required=True, translate=True) active = fields.Boolean(default=True) technician_user_id = fields.Many2one('res.users', string='Technician', track_visibility='onchange', oldname='user_id') owner_user_id = fields.Many2one('res.users', string='Owner', track_visibility='onchange') category_id = fields.Many2one('maintenance.equipment.category', string='Equipment Category', track_visibility='onchange', group_expand='_read_group_category_ids') partner_id = fields.Many2one('res.partner', string='Vendor', domain="[('supplier', '=', 1)]") partner_ref = fields.Char('Vendor Reference') location = fields.Char('Location') model = fields.Char('Model') serial_no = fields.Char('Serial Number', copy=False) assign_date = fields.Date('Assigned Date', track_visibility='onchange') cost = fields.Float('Cost') note = fields.Text('Note') warranty = fields.Date('Warranty') color = fields.Integer('Color Index') scrap_date = fields.Date('Scrap Date') maintenance_ids = fields.One2many('maintenance.request', 'equipment_id') maintenance_count = fields.Integer(compute='_compute_maintenance_count', string="Maintenance", store=True) maintenance_open_count = fields.Integer( compute='_compute_maintenance_count', string="Current Maintenance", store=True) period = fields.Integer('Days between each preventive maintenance') next_action_date = fields.Date( compute='_compute_next_maintenance', string='Date of the next preventive maintenance', store=True) maintenance_team_id = fields.Many2one('maintenance.team', string='Maintenance Team') maintenance_duration = fields.Float(help="Maintenance Duration in hours.") @api.depends('period', 'maintenance_ids.request_date', 'maintenance_ids.close_date') def _compute_next_maintenance(self): date_now = fields.Date.context_today(self) for equipment in self.filtered(lambda x: x.period > 0): next_maintenance_todo = self.env['maintenance.request'].search( [('equipment_id', '=', equipment.id), ('maintenance_type', '=', 'preventive'), ('stage_id.done', '!=', True), ('close_date', '=', False)], order="request_date asc", limit=1) last_maintenance_done = self.env['maintenance.request'].search( [('equipment_id', '=', equipment.id), ('maintenance_type', '=', 'preventive'), ('stage_id.done', '=', True), ('close_date', '!=', False)], order="close_date desc", limit=1) if next_maintenance_todo and last_maintenance_done: next_date = next_maintenance_todo.request_date date_gap = fields.Date.from_string( next_maintenance_todo.request_date ) - fields.Date.from_string(last_maintenance_done.close_date) # If the gap between the last_maintenance_done and the next_maintenance_todo one is bigger than 2 times the period and next request is in the future # We use 2 times the period to avoid creation too closed request from a manually one created if date_gap > timedelta(0) and date_gap > timedelta( days=equipment.period) * 2 and fields.Date.from_string( next_maintenance_todo.request_date ) > fields.Date.from_string(date_now): # If the new date still in the past, we set it for today if fields.Date.from_string( last_maintenance_done.close_date) + timedelta( days=equipment.period ) < fields.Date.from_string(date_now): next_date = date_now else: next_date = fields.Date.to_string( fields.Date.from_string( last_maintenance_done.close_date) + timedelta(days=equipment.period)) elif next_maintenance_todo: next_date = next_maintenance_todo.request_date date_gap = fields.Date.from_string( next_maintenance_todo.request_date ) - fields.Date.from_string(date_now) # If next maintenance to do is in the future, and in more than 2 times the period, we insert an new request # We use 2 times the period to avoid creation too closed request from a manually one created if date_gap > timedelta(0) and date_gap > timedelta( days=equipment.period) * 2: next_date = fields.Date.to_string( fields.Date.from_string(date_now) + timedelta(days=equipment.period)) elif last_maintenance_done: next_date = fields.Date.from_string( last_maintenance_done.close_date) + timedelta( days=equipment.period) # If when we add the period to the last maintenance done and we still in past, we plan it for today if next_date < fields.Date.from_string(date_now): next_date = date_now else: next_date = fields.Date.to_string( fields.Date.from_string(date_now) + timedelta(days=equipment.period)) equipment.next_action_date = next_date @api.one @api.depends('maintenance_ids.stage_id.done') def _compute_maintenance_count(self): self.maintenance_count = len(self.maintenance_ids) self.maintenance_open_count = len( self.maintenance_ids.filtered(lambda x: not x.stage_id.done)) @api.onchange('category_id') def _onchange_category_id(self): self.technician_user_id = self.category_id.technician_user_id _sql_constraints = [ ('serial_no', 'unique(serial_no)', "Another asset already exists with this serial number!"), ] @api.model def create(self, vals): equipment = super(MaintenanceEquipment, self).create(vals) if equipment.owner_user_id: equipment.message_subscribe_users( user_ids=[equipment.owner_user_id.id]) return equipment @api.multi def write(self, vals): if vals.get('owner_user_id'): self.message_subscribe_users(user_ids=[vals['owner_user_id']]) return super(MaintenanceEquipment, self).write(vals) @api.model def _read_group_category_ids(self, categories, domain, order): """ Read group customization in order to display all the categories in the kanban view, even if they are empty. """ category_ids = categories._search([], order=order, access_rights_uid=SUPERUSER_ID) return categories.browse(category_ids) def _create_new_request(self, date): self.ensure_one() self.env['maintenance.request'].create({ 'name': _('Preventive Maintenance - %s') % self.name, 'request_date': date, 'schedule_date': date, 'category_id': self.category_id.id, 'equipment_id': self.id, 'maintenance_type': 'preventive', 'owner_user_id': self.owner_user_id.id, 'technician_user_id': self.technician_user_id.id, 'maintenance_team_id': self.maintenance_team_id.id, 'duration': self.maintenance_duration, }) @api.model def _cron_generate_requests(self): """ Generates maintenance request on the next_action_date or today if none exists """ for equipment in self.search([('period', '>', 0)]): next_requests = self.env['maintenance.request'].search([ ('stage_id.done', '=', False), ('equipment_id', '=', equipment.id), ('maintenance_type', '=', 'preventive'), ('request_date', '=', equipment.next_action_date) ]) if not next_requests: equipment._create_new_request(equipment.next_action_date)
class IrMailServer(models.Model): """Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities.""" _name = "ir.mail_server" NO_VALID_RECIPIENT = ("At least one valid recipient address should be " "specified for outgoing emails (To/Cc/Bcc)") name = fields.Char(string='Description', required=True, index=True) smtp_host = fields.Char(string='SMTP Server', required=True, help="Hostname or IP of SMTP server") smtp_port = fields.Integer( string='SMTP Port', size=5, required=True, default=25, help="SMTP Port. Usually 465 for SSL, and 25 or 587 for other cases.") smtp_user = fields.Char(string='Username', help="Optional username for SMTP authentication") smtp_pass = fields.Char(string='Password', help="Optional password for SMTP authentication") smtp_encryption = fields.Selection( [('none', 'None'), ('starttls', 'TLS (STARTTLS)'), ('ssl', 'SSL/TLS')], string='Connection Security', required=True, default='none', help="Choose the connection encryption scheme:\n" "- None: SMTP sessions are done in cleartext.\n" "- TLS (STARTTLS): TLS encryption is requested at start of SMTP session (Recommended)\n" "- SSL/TLS: SMTP sessions are encrypted with SSL/TLS through a dedicated port (default: 465)" ) smtp_debug = fields.Boolean( string='Debugging', help="If enabled, the full output of SMTP sessions will " "be written to the server log at DEBUG level " "(this is very verbose and may include confidential info!)") sequence = fields.Integer( string='Priority', default=10, help= "When no specific mail server is requested for a mail, the highest priority one " "is used. Default priority is 10 (smaller number = higher priority)") active = fields.Boolean(default=True) @api.multi def name_get(self): return [(server.id, "(%s)" % server.name) for server in self] @api.multi def test_smtp_connection(self): for server in self: smtp = False try: smtp = self.connect(mail_server_id=server.id) except Exception as e: raise UserError( _("Connection Test Failed! Here is what we got instead:\n %s" ) % ustr(e)) finally: try: if smtp: smtp.quit() except Exception: # ignored, just a consequence of the previous exception pass raise UserError( _("Connection Test Succeeded! Everything seems properly set up!")) def connect(self, host=None, port=None, user=None, password=None, encryption=None, smtp_debug=False, mail_server_id=None): """Returns a new SMTP connection to the given SMTP server. When running in test mode, this method does nothing and returns `None`. :param host: host or IP of SMTP server to connect to, if mail_server_id not passed :param int port: SMTP port to connect to :param user: optional username to authenticate with :param password: optional password to authenticate with :param string encryption: optional, ``'ssl'`` | ``'starttls'`` :param bool smtp_debug: toggle debugging of SMTP sessions (all i/o will be output in logs) :param mail_server_id: ID of specific mail server to use (overrides other parameters) """ # Do not actually connect while running in test mode if getattr(threading.currentThread(), 'testing', False): return None mail_server = smtp_encryption = None if mail_server_id: mail_server = self.sudo().browse(mail_server_id) elif not host: mail_server = self.sudo().search([], order='sequence', limit=1) if mail_server: smtp_server = mail_server.smtp_host smtp_port = mail_server.smtp_port smtp_user = mail_server.smtp_user smtp_password = mail_server.smtp_pass smtp_encryption = mail_server.smtp_encryption smtp_debug = smtp_debug or mail_server.smtp_debug else: # we were passed individual smtp parameters or nothing and there is no default server smtp_server = host or tools.config.get('smtp_server') smtp_port = tools.config.get('smtp_port', 25) if port is None else port smtp_user = user or tools.config.get('smtp_user') smtp_password = password or tools.config.get('smtp_password') smtp_encryption = encryption if smtp_encryption is None and tools.config.get('smtp_ssl'): smtp_encryption = 'starttls' # smtp_ssl => STARTTLS as of v7 if not smtp_server: raise UserError((_("Missing SMTP Server") + "\n" + _("Please define at least one SMTP server, " "or provide the SMTP parameters explicitly."))) if smtp_encryption == 'ssl': if 'SMTP_SSL' not in smtplib.__all__: raise UserError( _("Your Flectra Server does not support SMTP-over-SSL. " "You could use STARTTLS instead. " "If SSL is needed, an upgrade to Python 2.6 on the server-side " "should do the trick.")) connection = smtplib.SMTP_SSL(smtp_server, smtp_port) else: connection = smtplib.SMTP(smtp_server, smtp_port) connection.set_debuglevel(smtp_debug) if smtp_encryption == 'starttls': # starttls() will perform ehlo() if needed first # and will discard the previous list of services # after successfully performing STARTTLS command, # (as per RFC 3207) so for example any AUTH # capability that appears only on encrypted channels # will be correctly detected for next step connection.starttls() if smtp_user: # Attempt authentication - will raise if AUTH service not supported # The user/password must be converted to bytestrings in order to be usable for # certain hashing schemes, like HMAC. # See also bug #597143 and python issue #5285 smtp_user = pycompat.to_native(ustr(smtp_user)) smtp_password = pycompat.to_native(ustr(smtp_password)) connection.login(smtp_user, smtp_password) return connection def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None, body_alternative=None, subtype_alternative='plain'): """Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it. :param string email_from: sender email address :param list email_to: list of recipient addresses (to be joined with commas) :param string subject: email subject (no pre-encoding/quoting necessary) :param string body: email body, of the type ``subtype`` (by default, plaintext). If html subtype is used, the message will be automatically converted to plaintext and wrapped in multipart/alternative, unless an explicit ``body_alternative`` version is passed. :param string body_alternative: optional alternative body, of the type specified in ``subtype_alternative`` :param string reply_to: optional value of Reply-To header :param string object_id: optional tracking identifier, to be included in the message-id for recognizing replies. Suggested format for object-id is "res_id-model", e.g. "12345-crm.lead". :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'), must match the format of the ``body`` parameter. Default is 'plain', making the content part of the mail "text/plain". :param string subtype_alternative: optional mime subtype of ``body_alternative`` (usually 'plain' or 'html'). Default is 'plain'. :param list attachments: list of (filename, filecontents) pairs, where filecontents is a string containing the bytes of the attachment :param list email_cc: optional list of string values for CC header (to be joined with commas) :param list email_bcc: optional list of string values for BCC header (to be joined with commas) :param dict headers: optional map of headers to set on the outgoing mail (may override the other headers, including Subject, Reply-To, Message-Id, etc.) :rtype: email.message.Message (usually MIMEMultipart) :return: the new RFC2822 email message """ email_from = email_from or tools.config.get('email_from') assert email_from, "You must either provide a sender address explicitly or configure "\ "a global sender address in the server configuration or with the "\ "--email-from startup parameter." # Note: we must force all strings to to 8-bit utf-8 when crafting message, # or use encode_header() for headers, which does it automatically. headers = headers or {} # need valid dict later email_cc = email_cc or [] email_bcc = email_bcc or [] body = body or u'' email_body = ustr(body) email_text_part = MIMEText(email_body, _subtype=subtype, _charset='utf-8') msg = MIMEMultipart() if not message_id: if object_id: message_id = tools.generate_tracking_message_id(object_id) else: message_id = make_msgid() msg['Message-Id'] = encode_header(message_id) if references: msg['references'] = encode_header(references) msg['Subject'] = encode_header(subject) msg['From'] = encode_rfc2822_address_header(email_from) del msg['Reply-To'] if reply_to: msg['Reply-To'] = encode_rfc2822_address_header(reply_to) else: msg['Reply-To'] = msg['From'] msg['To'] = encode_rfc2822_address_header(COMMASPACE.join(email_to)) if email_cc: msg['Cc'] = encode_rfc2822_address_header( COMMASPACE.join(email_cc)) if email_bcc: msg['Bcc'] = encode_rfc2822_address_header( COMMASPACE.join(email_bcc)) msg['Date'] = formatdate() # Custom headers may override normal headers or provide additional ones for key, value in headers.items(): msg[pycompat.to_native(ustr(key))] = encode_header(value) if subtype == 'html' and not body_alternative: # Always provide alternative text body ourselves if possible. text = html2text.html2text(email_body) alternative_part = MIMEMultipart(_subtype="alternative") alternative_part.attach( MIMEText(text, _charset='utf-8', _subtype='plain')) alternative_part.attach(email_text_part) msg.attach(alternative_part) elif body_alternative: # Include both alternatives, as specified, within a multipart/alternative part alternative_part = MIMEMultipart(_subtype="alternative") body_alternative_ = ustr(body_alternative) alternative_body_part = MIMEText(body_alternative_, _subtype=subtype_alternative, _charset='utf-8') alternative_part.attach(alternative_body_part) alternative_part.attach(email_text_part) msg.attach(alternative_part) else: msg.attach(email_text_part) if attachments: for (fname, fcontent, mime) in attachments: filename_rfc2047 = encode_header_param(fname) if mime and '/' in mime: maintype, subtype = mime.split('/', 1) part = MIMEBase(maintype, subtype) else: part = MIMEBase('application', "octet-stream") # The default RFC2231 encoding of Message.add_header() works in Thunderbird but not GMail # so we fix it by using RFC2047 encoding for the filename instead. part.set_param('name', filename_rfc2047) part.add_header('Content-Disposition', 'attachment', filename=filename_rfc2047) part.set_payload(fcontent) encoders.encode_base64(part) msg.attach(part) return msg @api.model def _get_default_bounce_address(self): '''Compute the default bounce address. The default bounce address is used to set the envelop address if no envelop address is provided in the message. It is formed by properly joining the parameters "mail.bounce.alias" and "mail.catchall.domain". If "mail.bounce.alias" is not set it defaults to "postmaster-flectra". If "mail.catchall.domain" is not set, return None. ''' get_param = self.env['ir.config_parameter'].sudo().get_param postmaster = get_param('mail.bounce.alias', default='postmaster-flectra') domain = get_param('mail.catchall.domain') if postmaster and domain: return '%s@%s' % (postmaster, domain) @api.model def send_email(self, message, mail_server_id=None, smtp_server=None, smtp_port=None, smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False, smtp_session=None): """Sends an email directly (no queuing). No retries are done, the caller should handle MailDeliveryException in order to ensure that the mail is never lost. If the mail_server_id is provided, sends using this mail server, ignoring other smtp_* arguments. If mail_server_id is None and smtp_server is None, use the default mail server (highest priority). If mail_server_id is None and smtp_server is not None, use the provided smtp_* arguments. If both mail_server_id and smtp_server are None, look for an 'smtp_server' value in server config, and fails if not found. :param message: the email.message.Message to send. The envelope sender will be extracted from the ``Return-Path`` (if present), or will be set to the default bounce address. The envelope recipients will be extracted from the combined list of ``To``, ``CC`` and ``BCC`` headers. :param smtp_session: optional pre-established SMTP session. When provided, overrides `mail_server_id` and all the `smtp_*` parameters. Passing the matching `mail_server_id` may yield better debugging/log messages. The caller is in charge of disconnecting the session. :param mail_server_id: optional id of ir.mail_server to use for sending. overrides other smtp_* arguments. :param smtp_server: optional hostname of SMTP server to use :param smtp_encryption: optional TLS mode, one of 'none', 'starttls' or 'ssl' (see ir.mail_server fields for explanation) :param smtp_port: optional SMTP port, if mail_server_id is not passed :param smtp_user: optional SMTP user, if mail_server_id is not passed :param smtp_password: optional SMTP password to use, if mail_server_id is not passed :param smtp_debug: optional SMTP debug flag, if mail_server_id is not passed :return: the Message-ID of the message that was just sent, if successfully sent, otherwise raises MailDeliveryException and logs root cause. """ # Use the default bounce address **only if** no Return-Path was # provided by caller. Caller may be using Variable Envelope Return # Path (VERP) to detect no-longer valid email addresses. smtp_from = message['Return-Path'] or self._get_default_bounce_address( ) or message['From'] assert smtp_from, "The Return-Path or From header is required for any outbound email" # The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters. from_rfc2822 = extract_rfc2822_addresses(smtp_from) assert from_rfc2822, ( "Malformed 'Return-Path' or 'From' address: %r - " "It should contain one valid plain ASCII email") % smtp_from # use last extracted email, to support rarities like 'Support@MyComp <*****@*****.**>' smtp_from = from_rfc2822[-1] email_to = message['To'] email_cc = message['Cc'] email_bcc = message['Bcc'] del message['Bcc'] smtp_to_list = [ address for base in [email_to, email_cc, email_bcc] for address in extract_rfc2822_addresses(base) if address ] assert smtp_to_list, self.NO_VALID_RECIPIENT x_forge_to = message['X-Forge-To'] if x_forge_to: # `To:` header forged, e.g. for posting on mail.channels, to avoid confusion del message['X-Forge-To'] del message['To'] # avoid multiple To: headers! message['To'] = x_forge_to # Do not actually send emails in testing mode! if getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode(): _test_logger.info("skip sending email in test mode") return message['Message-Id'] try: message_id = message['Message-Id'] smtp = smtp_session try: smtp = smtp or self.connect(smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption, smtp_debug, mail_server_id=mail_server_id) smtp.sendmail(smtp_from, smtp_to_list, message.as_string()) finally: # do not quit() a pre-established smtp_session if smtp is not None and not smtp_session: smtp.quit() except Exception as e: params = (ustr(smtp_server), e.__class__.__name__, ustr(e)) msg = _( "Mail delivery failed via SMTP server '%s'.\n%s: %s") % params _logger.info(msg) raise MailDeliveryException(_("Mail Delivery Failed"), msg) return message_id @api.onchange('smtp_encryption') def _onchange_encryption(self): result = {} if self.smtp_encryption == 'ssl': self.smtp_port = 465 if not 'SMTP_SSL' in smtplib.__all__: result['warning'] = { 'title': _('Warning'), 'message': _('Your server does not seem to support SSL, you may want to try STARTTLS instead' ), } else: self.smtp_port = 25 return result
class MailingTrace(models.Model): """ Improve statistics model to add SMS support. Main attributes of statistics model are used, only some specific data is required. """ _inherit = 'mailing.trace' CODE_SIZE = 3 trace_type = fields.Selection(selection_add=[('sms', 'SMS')], ondelete={'sms': 'set default'}) sms_sms_id = fields.Many2one('sms.sms', string='SMS', index=True, ondelete='set null') sms_sms_id_int = fields.Integer( string='SMS ID (tech)', help='ID of the related sms.sms. This field is an integer field because ' 'the related sms.sms can be deleted separately from its statistics. ' 'However the ID is needed for several action and controllers.', index=True, ) sms_number = fields.Char('Number') sms_code = fields.Char('Code') failure_type = fields.Selection(selection_add=[ ('sms_number_missing', 'Missing Number'), ('sms_number_format', 'Wrong Number Format'), ('sms_credit', 'Insufficient Credit'), ('sms_server', 'Server Error'), ('sms_acc', 'Unregistered Account'), # mass mode specific codes ('sms_blacklist', 'Blacklisted'), ('sms_duplicate', 'Duplicate'), ]) @api.model_create_multi def create(self, values_list): for values in values_list: if 'sms_sms_id' in values: values['sms_sms_id_int'] = values['sms_sms_id'] if values.get( 'trace_type') == 'sms' and not values.get('sms_code'): values['sms_code'] = self._get_random_code() return super(MailingTrace, self).create(values_list) def _get_random_code(self): """ Generate a random code for trace. Uniqueness is not really necessary as it serves as obfuscation when unsubscribing. A valid trio code / mailing_id / number will be requested. """ return ''.join( random.choice(string.ascii_letters + string.digits) for dummy in range(self.CODE_SIZE)) def _get_records_from_sms(self, sms_sms_ids=None, additional_domain=None): if not self.ids and sms_sms_ids: domain = [('sms_sms_id_int', 'in', sms_sms_ids)] else: domain = [('id', 'in', self.ids)] if additional_domain: domain = expression.AND([domain, additional_domain]) return self.search(domain) def set_failed(self, failure_type): for trace in self: trace.write({ 'exception': fields.Datetime.now(), 'failure_type': failure_type }) def set_sms_sent(self, sms_sms_ids=None): statistics = self._get_records_from_sms(sms_sms_ids, [('sent', '=', False)]) statistics.write({'sent': fields.Datetime.now()}) return statistics def set_sms_clicked(self, sms_sms_ids=None): statistics = self._get_records_from_sms(sms_sms_ids, [('clicked', '=', False)]) statistics.write({'clicked': fields.Datetime.now()}) return statistics def set_sms_ignored(self, sms_sms_ids=None): statistics = self._get_records_from_sms(sms_sms_ids, [('ignored', '=', False)]) statistics.write({'ignored': fields.Datetime.now()}) return statistics def set_sms_exception(self, sms_sms_ids=None): statistics = self._get_records_from_sms(sms_sms_ids, [('exception', '=', False)]) statistics.write({'exception': fields.Datetime.now()}) return statistics
class Message(models.Model): _name = 'test_new_api.message' discussion = fields.Many2one('test_new_api.discussion', ondelete='cascade') body = fields.Text() author = fields.Many2one('res.users', default=lambda self: self.env.user) name = fields.Char(string='Title', compute='_compute_name', store=True) display_name = fields.Char(string='Abstract', compute='_compute_display_name') size = fields.Integer(compute='_compute_size', search='_search_size') double_size = fields.Integer(compute='_compute_double_size') discussion_name = fields.Char(related='discussion.name', string="Discussion Name") author_partner = fields.Many2one('res.partner', compute='_compute_author_partner', search='_search_author_partner') important = fields.Boolean() @api.one @api.constrains('author', 'discussion') def _check_author(self): if self.discussion and self.author not in self.discussion.participants: raise ValidationError( _("Author must be among the discussion participants.")) @api.one @api.depends('author.name', 'discussion.name') def _compute_name(self): self.name = "[%s] %s" % (self.discussion.name or '', self.author.name or '') @api.one @api.depends('author.name', 'discussion.name', 'body') def _compute_display_name(self): stuff = "[%s] %s: %s" % (self.author.name, self.discussion.name or '', self.body or '') self.display_name = stuff[:80] @api.one @api.depends('body') def _compute_size(self): self.size = len(self.body or '') def _search_size(self, operator, value): if operator not in ('=', '!=', '<', '<=', '>', '>=', 'in', 'not in'): return [] # retrieve all the messages that match with a specific SQL query query = """SELECT id FROM "%s" WHERE char_length("body") %s %%s""" % \ (self._table, operator) self.env.cr.execute(query, (value, )) ids = [t[0] for t in self.env.cr.fetchall()] return [('id', 'in', ids)] @api.one @api.depends('size') def _compute_double_size(self): # This illustrates a subtle situation: self.double_size depends on # self.size. When size is computed, self.size is assigned, which should # normally invalidate self.double_size. However, this may not happen # while self.double_size is being computed: the last statement below # would fail, because self.double_size would be undefined. self.double_size = 0 size = self.size self.double_size = self.double_size + size @api.one @api.depends('author', 'author.partner_id') def _compute_author_partner(self): self.author_partner = self.author.partner_id @api.model def _search_author_partner(self, operator, value): return [('author.partner_id', operator, value)]
class PurchaseOrder(models.Model): _inherit = 'purchase.order' _description = "Purchase Order" revision = fields.Integer(string='Amendment Revision') state = fields.Selection(selection_add=[('amendment', 'Amendment')]) amendment_name = fields.Char('Order Reference', copy=True, readonly=True) current_amendment_id = fields.Many2one('purchase.order', 'Current Amendment', readonly=True, copy=True) old_amendment_ids = fields.One2many('purchase.order', 'current_amendment_id', 'Old Amendment', readonly=True, context={'active_test': False}) @api.model def create(self, vals): if 'amendment_name' not in vals: if vals.get('name', 'New') == 'New': # sequence number with amendment number seq = self.env['ir.sequence'] vals['name'] = seq.next_by_code('purchase.order') or '/' vals['amendment_name'] = vals['name'] return super(PurchaseOrder, self).create(vals) @api.multi def button_draft(self): orders = self.filtered( lambda s: s.state in ['cancel', 'sent', 'amendment']) orders.write({ 'state': 'draft', }) orders.mapped('order_line').write({'purchase_line_id': False}) @api.multi def create_amendment(self): self.ensure_one() # Assign Form view before amendment view_ref = self.env['ir.model.data'].get_object_reference( 'purchase', 'purchase_order_form') view_id = view_ref and view_ref[1] or False, self.with_context(new_purchase_amendment=True).copy() self.write({'state': 'draft'}) self.order_line.write({'state': 'draft'}) self.mapped('order_line').write({'purchase_line_id': True}) return { 'type': 'ir.actions.act_window', 'name': ('Purchase Order'), 'res_model': 'sale.order', 'res_id': self.id, 'view_type': 'form', 'view_mode': 'form', 'view_id': view_id, 'target': 'current', 'nodestroy': True, } @api.returns('self', lambda value: value.id) @api.multi def copy(self, defaults=None): if not defaults: defaults = {} if self.env.context.get('new_purchase_amendment'): prev_name = self.name revno = self.revision # if sale order exist and amendment name sent as false. replace current sale name if self.amendment_name == False: self.amendment_name = self.name # Assign default values for views self.write({ 'revision': revno + 1, 'name': '%s-%02d' % (self.amendment_name, revno + 1) }) defaults.update({ 'name': prev_name, 'revision': revno, 'state': 'cancel', 'invoice_count': 0, 'current_amendment_id': self.id, 'amendment_name': self.amendment_name, }) return super(PurchaseOrder, self).copy(defaults) def button_amend(self): for purchase in self: for picking_loop in purchase.picking_ids: if picking_loop.state == 'done': raise UserError( 'Unable to amend this purchase order, You must first cancel all receptions related to this purchase order.' ) else: picking_loop.filtered( lambda r: r.state != 'cancel').action_cancel() for invoice_loop in purchase.invoice_ids: if invoice_loop.state != 'draft': raise UserError( 'Unable to amend this purchase order, You must first cancel all Supplier Invoices related to this purchase order.' ) else: invoice_loop.filtered( lambda r: r.state != 'cancel').action_invoice_cancel() # amendment_values = { # 'purchase_link_id': purchase.id, # 'name': purchase.name, # 'quotation_date': purchase.date_order, # 'amendment': purchase.revision, # 'amount_untaxed': purchase.amount_untaxed, # 'amount_tax': purchase.amount_tax, # 'amount_total': purchase.amount_total, # 'currency_id': purchase.currency_id.id, # 'amendment_date': datetime.today(), # 'purchase_amendment_line': [], # } # amendment_lines_values = [] # for i in purchase.order_line: # amendment_lines_values.append((0, 0, { # 'product_id': i.product_id.id, # 'purchase_amendment_id': purchase.revision, # 'product_uom_qty': i.product_qty, # 'product_uom': i.product_uom.id, # 'unit_price': i.price_unit, # 'subtotal': i.price_subtotal, # })) # amendment_values['purchase_amendment_line'] = amendment_lines_values # amendment_obj.create(amendment_values) # revision = self.revision + 1 # purchase.write({'state': 'amendment','revision': revision}) self.button_draft() self.create_amendment() self.write({'state': 'amendment'})
class Foo(models.Model): _name = 'test_new_api.foo' name = fields.Char() value1 = fields.Integer(change_default=True) value2 = fields.Integer()
class link_tracker(models.Model): """link_tracker allow users to wrap any URL into a short and trackable URL. link_tracker counts clicks on each tracked link. This module is also used by mass_mailing, where each link in mail_mail html_body are converted into a trackable link to get the click-through rate of each mass_mailing.""" _name = "link.tracker" _rec_name = "short_url" _inherit = ['utm.mixin'] url = fields.Char(string='Target URL', required=True) count = fields.Integer(string='Number of Clicks', compute='_compute_count', store=True) short_url = fields.Char(string='Tracked URL', compute='_compute_short_url') link_click_ids = fields.One2many('link.tracker.click', 'link_id', string='Clicks') title = fields.Char(string='Page Title', store=True) favicon = fields.Char(string='Favicon', compute='_compute_favicon', store=True) link_code_ids = fields.One2many('link.tracker.code', 'link_id', string='Codes') code = fields.Char(string='Short URL code', compute='_compute_code') redirected_url = fields.Char(string='Redirected URL', compute='_compute_redirected_url') short_url_host = fields.Char(string='Host of the short URL', compute='_compute_short_url_host') icon_src = fields.Char(string='Favicon Source', compute='_compute_icon_src') @api.model def convert_links(self, html, vals, blacklist=None): for match in re.findall(URL_REGEX, html): short_schema = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') + '/r/' href = match[0] long_url = match[1] vals['url'] = utils.unescape(long_url) if not blacklist or not [ s for s in blacklist if s in long_url ] and not long_url.startswith(short_schema): link = self.create(vals) shorten_url = self.browse(link.id)[0].short_url if shorten_url: new_href = href.replace(long_url, shorten_url) html = html.replace(href, new_href) return html @api.one @api.depends('link_click_ids.link_id') def _compute_count(self): self.count = len(self.link_click_ids) @api.one @api.depends('code') def _compute_short_url(self): base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') self.short_url = urls.url_join(base_url, '/r/%(code)s' % {'code': self.code}) @api.one def _compute_short_url_host(self): self.short_url_host = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') + '/r/' @api.one def _compute_code(self): record = self.env['link.tracker.code'].search( [('link_id', '=', self.id)], limit=1, order='id DESC') self.code = record.code @api.one @api.depends('favicon') def _compute_icon_src(self): self.icon_src = b'data:image/png;base64,' + self.favicon @api.one @api.depends('url') def _compute_redirected_url(self): parsed = urls.url_parse(self.url) utms = {} for key, field, cook in self.env['utm.mixin'].tracking_fields(): attr = getattr(self, field).name if attr: utms[key] = attr utms.update(parsed.decode_query()) self.redirected_url = parsed.replace( query=urls.url_encode(utms)).to_url() @api.model @api.depends('url') def _get_title_from_url(self, url): try: page = requests.get(url, timeout=5) p = html.fromstring(page.text.encode('utf-8'), parser=html.HTMLParser(encoding='utf-8')) title = p.find('.//title').text except: title = url return title @api.one @api.depends('url') def _compute_favicon(self): try: icon = requests.get('http://www.google.com/s2/favicons', params={ 'domain': self.url }, timeout=5).content icon_base64 = base64.b64encode(icon).replace(b"\n", b"").decode('ascii') except: icon_base64 = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAACiElEQVQ4EaVTzU8TURCf2tJuS7tQtlRb6UKBIkQwkRRSEzkQgyEc6lkOKgcOph78Y+CgjXjDs2i44FXY9AMTlQRUELZapVlouy3d7kKtb0Zr0MSLTvL2zb75eL838xtTvV6H/xELBptMJojeXLCXyobnyog4YhzXYvmCFi6qVSfaeRdXdrfaU1areV5KykmX06rcvzumjY/1ggkR3Jh+bNf1mr8v1D5bLuvR3qDgFbvbBJYIrE1mCIoCrKxsHuzK+Rzvsi29+6DEbTZz9unijEYI8ObBgXOzlcrx9OAlXyDYKUCzwwrDQx1wVDGg089Dt+gR3mxmhcUnaWeoxwMbm/vzDFzmDEKMMNhquRqduT1KwXiGt0vre6iSeAUHNDE0d26NBtAXY9BACQyjFusKuL2Ry+IPb/Y9ZglwuVscdHaknUChqLF/O4jn3V5dP4mhgRJgwSYm+gV0Oi3XrvYB30yvhGa7BS70eGFHPoTJyQHhMK+F0ZesRVVznvXw5Ixv7/C10moEo6OZXbWvlFAF9FVZDOqEABUMRIkMd8GnLwVWg9/RkJF9sA4oDfYQAuzzjqzwvnaRUFxn/X2ZlmGLXAE7AL52B4xHgqAUqrC1nSNuoJkQtLkdqReszz/9aRvq90NOKdOS1nch8TpL555WDp49f3uAMXhACRjD5j4ykuCtf5PP7Fm1b0DIsl/VHGezzP1KwOiZQobFF9YyjSRYQETRENSlVzI8iK9mWlzckpSSCQHVALmN9Az1euDho9Xo8vKGd2rqooA8yBcrwHgCqYR0kMkWci08t/R+W4ljDCanWTg9TJGwGNaNk3vYZ7VUdeKsYJGFNkfSzjXNrSX20s4/h6kB81/271ghG17l+rPTAAAAAElFTkSuQmCC' self.favicon = icon_base64 @api.multi def action_view_statistics(self): action = self.env['ir.actions.act_window'].for_xml_id( 'link_tracker', 'action_view_click_statistics') action['domain'] = [('link_id', '=', self.id)] return action @api.multi def action_visit_page(self): return { 'name': _("Visit Webpage"), 'type': 'ir.actions.act_url', 'url': self.url, 'target': 'new', } @api.model def recent_links(self, filter, limit): if filter == 'newest': return self.search_read([], order='create_date DESC', limit=limit) elif filter == 'most-clicked': return self.search_read([('count', '!=', 0)], order='count DESC', limit=limit) elif filter == 'recently-used': return self.search_read([('count', '!=', 0)], order='write_date DESC', limit=limit) else: return {'Error': "This filter doesn't exist."} @api.model def create(self, vals): create_vals = vals.copy() if 'url' not in create_vals: raise ValueError('URL field required') else: create_vals['url'] = VALIDATE_URL(vals['url']) search_domain = [] for fname, value in create_vals.items(): search_domain.append((fname, '=', value)) result = self.search(search_domain, limit=1) if result: return result if not create_vals.get('title'): create_vals['title'] = self._get_title_from_url(create_vals['url']) # Prevent the UTMs to be set by the values of UTM cookies for (key, fname, cook) in self.env['utm.mixin'].tracking_fields(): if fname not in create_vals: create_vals[fname] = False link = super(link_tracker, self).create(create_vals) code = self.env['link.tracker.code'].get_random_code_string() self.env['link.tracker.code'].create({ 'code': code, 'link_id': link.id }) return link @api.model def get_url_from_code(self, code, context=None): code_rec = self.env['link.tracker.code'].sudo().search([('code', '=', code)]) if not code_rec: return None return code_rec.link_id.redirected_url sql_constraints = [ ('url_utms_uniq', 'unique (url, campaign_id, medium_id, source_id)', 'The URL and the UTM combination must be unique') ]
class HrPayslip(models.Model): _name = 'hr.payslip' _description = 'Pay Slip' struct_id = fields.Many2one( 'hr.payroll.structure', string='Structure', readonly=True, states={'draft': [('readonly', False)]}, help= 'Defines the rules that have to be applied to this payslip, accordingly ' 'to the contract chosen. If you let empty the field contract, this field isn\'t ' 'mandatory anymore and thus the rules applied will be all the rules set on the ' 'structure of all contracts of the employee valid for the chosen period' ) name = fields.Char(string='Payslip Name', readonly=True, states={'draft': [('readonly', False)]}) number = fields.Char(string='Reference', readonly=True, copy=False, states={'draft': [('readonly', False)]}) employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}) date_from = fields.Date(string='Date From', readonly=True, required=True, default=time.strftime('%Y-%m-01'), states={'draft': [('readonly', False)]}) date_to = fields.Date( string='Date To', readonly=True, required=True, default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1)) [:10], states={'draft': [('readonly', False)]}) # this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed) state = fields.Selection( [ ('draft', 'Draft'), ('verify', 'Waiting'), ('done', 'Done'), ('cancel', 'Rejected'), ], string='Status', index=True, readonly=True, copy=False, default='draft', help="""* When the payslip is created the status is \'Draft\' \n* If the payslip is under verification, the status is \'Waiting\'. \n* If the payslip is confirmed then status is set to \'Done\'. \n* When user cancel payslip the status is \'Rejected\'.""") line_ids = fields.One2many('hr.payslip.line', 'slip_id', string='Payslip Lines', readonly=True, states={'draft': [('readonly', False)]}) company_id = fields.Many2one( 'res.company', string='Company', readonly=True, copy=False, default=lambda self: self.env['res.company']._company_default_get(), states={'draft': [('readonly', False)]}) worked_days_line_ids = fields.One2many( 'hr.payslip.worked_days', 'payslip_id', string='Payslip Worked Days', copy=True, readonly=True, states={'draft': [('readonly', False)]}) input_line_ids = fields.One2many('hr.payslip.input', 'payslip_id', string='Payslip Inputs', readonly=True, states={'draft': [('readonly', False)]}) paid = fields.Boolean(string='Made Payment Order ? ', readonly=True, copy=False, states={'draft': [('readonly', False)]}) note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)]}) contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True, states={'draft': [('readonly', False)]}) details_by_salary_rule_category = fields.One2many( 'hr.payslip.line', compute='_compute_details_by_salary_rule_category', string='Details by Salary Rule Category') credit_note = fields.Boolean( string='Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="Indicates this payslip has a refund of another") payslip_run_id = fields.Many2one('hr.payslip.run', string='Payslip Batches', readonly=True, copy=False, states={'draft': [('readonly', False)]}) payslip_count = fields.Integer(compute='_compute_payslip_count', string="Payslip Computation Details") @api.multi def _compute_details_by_salary_rule_category(self): for payslip in self: payslip.details_by_salary_rule_category = payslip.mapped( 'line_ids').filtered(lambda line: line.category_id) @api.multi def _compute_payslip_count(self): for payslip in self: payslip.payslip_count = len(payslip.line_ids) @api.constrains('date_from', 'date_to') def _check_dates(self): if any( self.filtered( lambda payslip: payslip.date_from > payslip.date_to)): raise ValidationError( _("Payslip 'Date From' must be before 'Date To'.")) @api.multi def action_payslip_draft(self): return self.write({'state': 'draft'}) @api.multi def action_payslip_done(self): self.compute_sheet() return self.write({'state': 'done'}) @api.multi def action_payslip_cancel(self): if self.filtered(lambda slip: slip.state == 'done'): raise UserError(_("Cannot cancel a payslip that is done.")) return self.write({'state': 'cancel'}) @api.multi def refund_sheet(self): for payslip in self: copied_payslip = payslip.copy({ 'credit_note': True, 'name': _('Refund: ') + payslip.name }) copied_payslip.compute_sheet() copied_payslip.action_payslip_done() formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False) treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False) return { 'name': ("Refund Payslip"), 'view_mode': 'tree, form', 'view_id': False, 'view_type': 'form', 'res_model': 'hr.payslip', 'type': 'ir.actions.act_window', 'target': 'current', 'domain': "[('id', 'in', %s)]" % copied_payslip.ids, 'views': [(treeview_ref and treeview_ref.id or False, 'tree'), (formview_ref and formview_ref.id or False, 'form')], 'context': {} } @api.multi def check_done(self): return True @api.multi def unlink(self): if any( self.filtered(lambda payslip: payslip.state not in ('draft', 'cancel'))): raise UserError( _('You cannot delete a payslip which is not draft or cancelled!' )) return super(HrPayslip, self).unlink() # TODO move this function into hr_contract module, on hr.employee object @api.model def get_contract(self, employee, date_from, date_to): """ @param employee: recordset of employee @param date_from: date field @param date_to: date field @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates """ # a contract is valid if it ends between the given dates clause_1 = [ '&', ('date_end', '<=', date_to), ('date_end', '>=', date_from) ] # OR if it starts between the given dates clause_2 = [ '&', ('date_start', '<=', date_to), ('date_start', '>=', date_from) ] # OR if it starts before the date_from and finish after the date_end (or never finish) clause_3 = [ '&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to) ] clause_final = [('employee_id', '=', employee.id), ('state', '=', 'open'), '|', '|' ] + clause_1 + clause_2 + clause_3 return self.env['hr.contract'].search(clause_final).ids @api.multi def compute_sheet(self): for payslip in self: number = payslip.number or self.env['ir.sequence'].next_by_code( 'salary.slip') # delete old payslip lines payslip.line_ids.unlink() # set the list of contract for which the rules have to be applied # if we don't give the contract, then the rules to apply should be for all current contracts of the employee contract_ids = payslip.contract_id.ids or \ self.get_contract(payslip.employee_id, payslip.date_from, payslip.date_to) lines = [ (0, 0, line) for line in self._get_payslip_lines(contract_ids, payslip.id) ] payslip.write({'line_ids': lines, 'number': number}) return True @api.model def get_worked_day_lines(self, contracts, date_from, date_to): """ @param contract: Browse record of contracts @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to """ res = [] # fill only if the contract as a working schedule linked for contract in contracts.filtered( lambda contract: contract.resource_calendar_id): day_from = datetime.combine(fields.Date.from_string(date_from), datetime_time.min) day_to = datetime.combine(fields.Date.from_string(date_to), datetime_time.max) # compute leave days leaves = {} day_leave_intervals = contract.employee_id.iter_leaves( day_from, day_to, calendar=contract.resource_calendar_id) for day_intervals in day_leave_intervals: for interval in day_intervals: holiday = interval[2]['leaves'].holiday_id current_leave_struct = leaves.setdefault( holiday.holiday_status_id, { 'name': holiday.holiday_status_id.name or _('Global Leaves'), 'sequence': 5, 'code': holiday.holiday_status_id.name or 'GLOBAL', 'number_of_days': 0.0, 'number_of_hours': 0.0, 'contract_id': contract.id, }) leave_time = (interval[1] - interval[0]).seconds / 3600 current_leave_struct['number_of_hours'] += leave_time work_hours = contract.employee_id.get_day_work_hours_count( interval[0].date(), calendar=contract.resource_calendar_id) if work_hours: current_leave_struct[ 'number_of_days'] += leave_time / work_hours # compute worked days work_data = contract.employee_id.with_context( no_tz_convert=True).get_work_days_data( day_from, day_to, calendar=contract.resource_calendar_id) attendances = { 'name': _("Normal Working Days paid at 100%"), 'sequence': 1, 'code': 'WORK100', 'number_of_days': work_data['days'], 'number_of_hours': work_data['hours'], 'contract_id': contract.id, } res.append(attendances) res.extend(leaves.values()) return res @api.model def get_inputs(self, contracts, date_from, date_to): res = [] structure_ids = contracts.get_all_structures() rule_ids = self.env['hr.payroll.structure'].browse( structure_ids).get_all_rules() sorted_rule_ids = [ id for id, sequence in sorted(rule_ids, key=lambda x: x[1]) ] inputs = self.env['hr.salary.rule'].browse(sorted_rule_ids).mapped( 'input_ids') for contract in contracts: for input in inputs: input_data = { 'name': input.name, 'code': input.code, 'contract_id': contract.id, } res += [input_data] return res @api.model def _get_payslip_lines(self, contract_ids, payslip_id): def _sum_salary_rule_category(localdict, category, amount): if category.parent_id: localdict = _sum_salary_rule_category(localdict, category.parent_id, amount) localdict['categories'].dict[ category.code] = category.code in localdict[ 'categories'].dict and localdict['categories'].dict[ category.code] + amount or amount return localdict class BrowsableObject(object): def __init__(self, employee_id, dict, env): self.employee_id = employee_id self.dict = dict self.env = env def __getattr__(self, attr): return attr in self.dict and self.dict.__getitem__(attr) or 0.0 class InputLine(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """ SELECT sum(amount) as sum FROM hr_payslip as hp, hr_payslip_input as pi WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", (self.employee_id, from_date, to_date, code)) return self.env.cr.fetchone()[0] or 0.0 class WorkedDays(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def _sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """ SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours FROM hr_payslip as hp, hr_payslip_worked_days as pi WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", (self.employee_id, from_date, to_date, code)) return self.env.cr.fetchone() def sum(self, code, from_date, to_date=None): res = self._sum(code, from_date, to_date) return res and res[0] or 0.0 def sum_hours(self, code, from_date, to_date=None): res = self._sum(code, from_date, to_date) return res and res[1] or 0.0 class Payslips(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end) FROM hr_payslip as hp, hr_payslip_line as pl WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""", (self.employee_id, from_date, to_date, code)) res = self.env.cr.fetchone() return res and res[0] or 0.0 #we keep a dict with the result because a value can be overwritten by another rule with the same code result_dict = {} rules_dict = {} worked_days_dict = {} inputs_dict = {} blacklist = [] payslip = self.env['hr.payslip'].browse(payslip_id) for worked_days_line in payslip.worked_days_line_ids: worked_days_dict[worked_days_line.code] = worked_days_line for input_line in payslip.input_line_ids: inputs_dict[input_line.code] = input_line categories = BrowsableObject(payslip.employee_id.id, {}, self.env) inputs = InputLine(payslip.employee_id.id, inputs_dict, self.env) worked_days = WorkedDays(payslip.employee_id.id, worked_days_dict, self.env) payslips = Payslips(payslip.employee_id.id, payslip, self.env) rules = BrowsableObject(payslip.employee_id.id, rules_dict, self.env) baselocaldict = { 'categories': categories, 'rules': rules, 'payslip': payslips, 'worked_days': worked_days, 'inputs': inputs } #get the ids of the structures on the contracts and their parent id as well contracts = self.env['hr.contract'].browse(contract_ids) if len(contracts) == 1 and payslip.struct_id: structure_ids = list( set(payslip.struct_id._get_parent_structure().ids)) else: structure_ids = contracts.get_all_structures() #get the rules of the structure and thier children rule_ids = self.env['hr.payroll.structure'].browse( structure_ids).get_all_rules() #run the rules by sequence sorted_rule_ids = [ id for id, sequence in sorted(rule_ids, key=lambda x: x[1]) ] sorted_rules = self.env['hr.salary.rule'].browse(sorted_rule_ids) for contract in contracts: employee = contract.employee_id localdict = dict(baselocaldict, employee=employee, contract=contract) for rule in sorted_rules: key = rule.code + '-' + str(contract.id) localdict['result'] = None localdict['result_qty'] = 1.0 localdict['result_rate'] = 100 #check if the rule can be applied if rule._satisfy_condition( localdict) and rule.id not in blacklist: #compute the amount of the rule amount, qty, rate = rule._compute_rule(localdict) #check if there is already a rule computed with that code previous_amount = rule.code in localdict and localdict[ rule.code] or 0.0 #set/overwrite the amount computed for this rule in the localdict tot_rule = amount * qty * rate / 100.0 localdict[rule.code] = tot_rule rules_dict[rule.code] = rule #sum the amount for its salary category localdict = _sum_salary_rule_category( localdict, rule.category_id, tot_rule - previous_amount) #create/overwrite the rule in the temporary results result_dict[key] = { 'salary_rule_id': rule.id, 'contract_id': contract.id, 'name': rule.name, 'code': rule.code, 'category_id': rule.category_id.id, 'sequence': rule.sequence, 'appears_on_payslip': rule.appears_on_payslip, 'condition_select': rule.condition_select, 'condition_python': rule.condition_python, 'condition_range': rule.condition_range, 'condition_range_min': rule.condition_range_min, 'condition_range_max': rule.condition_range_max, 'amount_select': rule.amount_select, 'amount_fix': rule.amount_fix, 'amount_python_compute': rule.amount_python_compute, 'amount_percentage': rule.amount_percentage, 'amount_percentage_base': rule.amount_percentage_base, 'register_id': rule.register_id.id, 'amount': amount, 'employee_id': contract.employee_id.id, 'quantity': qty, 'rate': rate, } else: #blacklist this rule and its children blacklist += [ id for id, seq in rule._recursive_search_of_rules() ] return list(result_dict.values()) # YTI TODO To rename. This method is not really an onchange, as it is not in any view # employee_id and contract_id could be browse records @api.multi def onchange_employee_id(self, date_from, date_to, employee_id=False, contract_id=False): #defaults res = { 'value': { 'line_ids': [], #delete old input lines 'input_line_ids': [( 2, x, ) for x in self.input_line_ids.ids], #delete old worked days lines 'worked_days_line_ids': [( 2, x, ) for x in self.worked_days_line_ids.ids], #'details_by_salary_head':[], TODO put me back 'name': '', 'contract_id': False, 'struct_id': False, } } if (not employee_id) or (not date_from) or (not date_to): return res ttyme = datetime.fromtimestamp( time.mktime(time.strptime(date_from, "%Y-%m-%d"))) employee = self.env['hr.employee'].browse(employee_id) locale = self.env.context.get('lang') or 'en_US' res['value'].update({ 'name': _('Salary Slip of %s for %s') % (employee.name, tools.ustr( babel.dates.format_date( date=ttyme, format='MMMM-y', locale=locale))), 'company_id': employee.company_id.id, }) if not self.env.context.get('contract'): #fill with the first contract of the employee contract_ids = self.get_contract(employee, date_from, date_to) else: if contract_id: #set the list of contract for which the input have to be filled contract_ids = [contract_id] else: #if we don't give the contract, then the input to fill should be for all current contracts of the employee contract_ids = self.get_contract(employee, date_from, date_to) if not contract_ids: return res contract = self.env['hr.contract'].browse(contract_ids[0]) res['value'].update({'contract_id': contract.id}) struct = contract.struct_id if not struct: return res res['value'].update({ 'struct_id': struct.id, }) #computation of the salary input contracts = self.env['hr.contract'].browse(contract_ids) worked_days_line_ids = self.get_worked_day_lines( contracts, date_from, date_to) input_line_ids = self.get_inputs(contracts, date_from, date_to) res['value'].update({ 'worked_days_line_ids': worked_days_line_ids, 'input_line_ids': input_line_ids, }) return res @api.onchange('employee_id', 'date_from', 'date_to') def onchange_employee(self): if (not self.employee_id) or (not self.date_from) or ( not self.date_to): return employee = self.employee_id date_from = self.date_from date_to = self.date_to contract_ids = [] ttyme = datetime.fromtimestamp( time.mktime(time.strptime(date_from, "%Y-%m-%d"))) locale = self.env.context.get('lang') or 'en_US' self.name = _('Salary Slip of %s for %s') % ( employee.name, tools.ustr( babel.dates.format_date( date=ttyme, format='MMMM-y', locale=locale))) self.company_id = employee.company_id if not self.env.context.get('contract') or not self.contract_id: contract_ids = self.get_contract(employee, date_from, date_to) if not contract_ids: return self.contract_id = self.env['hr.contract'].browse(contract_ids[0]) if not self.contract_id.struct_id: return self.struct_id = self.contract_id.struct_id #computation of the salary input contracts = self.env['hr.contract'].browse(contract_ids) worked_days_line_ids = self.get_worked_day_lines( contracts, date_from, date_to) worked_days_lines = self.worked_days_line_ids.browse([]) for r in worked_days_line_ids: worked_days_lines += worked_days_lines.new(r) self.worked_days_line_ids = worked_days_lines input_line_ids = self.get_inputs(contracts, date_from, date_to) input_lines = self.input_line_ids.browse([]) for r in input_line_ids: input_lines += input_lines.new(r) self.input_line_ids = input_lines return @api.onchange('contract_id') def onchange_contract(self): if not self.contract_id: self.struct_id = False self.with_context(contract=True).onchange_employee() return def get_salary_line_total(self, code): self.ensure_one() line = self.line_ids.filtered(lambda line: line.code == code) if line: return line[0].total else: return 0.0
class IrSequence(models.Model): """ Sequence model. The sequence model allows to define and use so-called sequence objects. Such objects are used to generate unique identifiers in a transaction-safe way. """ _name = 'ir.sequence' _order = 'name' def _get_number_next_actual(self): '''Return number from ir_sequence row when no_gap implementation, and number from postgres sequence when standard implementation.''' for seq in self: if seq.implementation != 'standard': seq.number_next_actual = seq.number_next else: # get number from postgres sequence. Cannot use currval, because that might give an error when # not having used nextval before. query = "SELECT last_value, increment_by, is_called FROM ir_sequence_%03d" % seq.id self._cr.execute(query) (last_value, increment_by, is_called) = self._cr.fetchone() if is_called: seq.number_next_actual = last_value + increment_by else: seq.number_next_actual = last_value def _set_number_next_actual(self): for seq in self: seq.write({'number_next': seq.number_next_actual or 0}) @api.model def _get_current_sequence(self): '''Returns the object on which we can find the number_next to consider for the sequence. It could be an ir.sequence or an ir.sequence.date_range depending if use_date_range is checked or not. This function will also create the ir.sequence.date_range if none exists yet for today ''' if not self.use_date_range: return self now = fields.Date.today() seq_date = self.env['ir.sequence.date_range'].search( [('sequence_id', '=', self.id), ('date_from', '<=', now), ('date_to', '>=', now)], limit=1) if seq_date: return seq_date[0] #no date_range sequence was found, we create a new one return self._create_date_range_seq(now) name = fields.Char(required=True) code = fields.Char(string='Sequence Code') implementation = fields.Selection( [('standard', 'Standard'), ('no_gap', 'No gap')], string='Implementation', required=True, default='standard', help="Two sequence object implementations are offered: Standard " "and 'No gap'. The later is slower than the former but forbids any" "gap in the sequence (while they are possible in the former).") active = fields.Boolean(default=True) prefix = fields.Char(help="Prefix value of the record for the sequence") suffix = fields.Char(help="Suffix value of the record for the sequence") number_next = fields.Integer(string='Next Number', required=True, default=1, help="Next number of this sequence") number_next_actual = fields.Integer( compute='_get_number_next_actual', inverse='_set_number_next_actual', string='Next Number', help="Next number that will be used. This number can be incremented " "frequently so the displayed value might already be obsolete") number_increment = fields.Integer( string='Step', required=True, default=1, help= "The next number of the sequence will be incremented by this number") padding = fields.Integer( string='Sequence Size', required=True, default=0, help="Flectra will automatically adds some '0' on the left of the " "'Next Number' to get the required padding size.") company_id = fields.Many2one('res.company', string='Company', default=lambda s: s.env['res.company']. _company_default_get('ir.sequence')) use_date_range = fields.Boolean(string='Use subsequences per date_range') date_range_ids = fields.One2many('ir.sequence.date_range', 'sequence_id', string='Subsequences') @api.model def create(self, values): """ Create a sequence, in implementation == standard a fast gaps-allowed PostgreSQL sequence is used. """ seq = super(IrSequence, self).create(values) if values.get('implementation', 'standard') == 'standard': _create_sequence(self._cr, "ir_sequence_%03d" % seq.id, values.get('number_increment', 1), values.get('number_next', 1)) return seq @api.multi def unlink(self): _drop_sequences(self._cr, ["ir_sequence_%03d" % x.id for x in self]) return super(IrSequence, self).unlink() @api.multi def write(self, values): new_implementation = values.get('implementation') for seq in self: # 4 cases: we test the previous impl. against the new one. i = values.get('number_increment', seq.number_increment) n = values.get('number_next', seq.number_next) if seq.implementation == 'standard': if new_implementation in ('standard', None): # Implementation has NOT changed. # Only change sequence if really requested. if values.get('number_next'): _alter_sequence(self._cr, "ir_sequence_%03d" % seq.id, number_next=n) if seq.number_increment != i: _alter_sequence(self._cr, "ir_sequence_%03d" % seq.id, number_increment=i) seq.date_range_ids._alter_sequence(number_increment=i) else: _drop_sequences(self._cr, ["ir_sequence_%03d" % seq.id]) for sub_seq in seq.date_range_ids: _drop_sequences( self._cr, ["ir_sequence_%03d_%03d" % (seq.id, sub_seq.id)]) else: if new_implementation in ('no_gap', None): pass else: _create_sequence(self._cr, "ir_sequence_%03d" % seq.id, i, n) for sub_seq in seq.date_range_ids: _create_sequence( self._cr, "ir_sequence_%03d_%03d" % (seq.id, sub_seq.id), i, n) return super(IrSequence, self).write(values) def _next_do(self): if self.implementation == 'standard': number_next = _select_nextval(self._cr, 'ir_sequence_%03d' % self.id) else: number_next = _update_nogap(self, self.number_increment) return self.get_next_char(number_next) def _get_prefix_suffix(self): def _interpolate(s, d): return (s % d) if s else '' def _interpolation_dict(): now = range_date = effective_date = datetime.now( pytz.timezone(self._context.get('tz') or 'UTC')) if self._context.get('ir_sequence_date'): effective_date = datetime.strptime( self._context.get('ir_sequence_date'), '%Y-%m-%d') if self._context.get('ir_sequence_date_range'): range_date = datetime.strptime( self._context.get('ir_sequence_date_range'), '%Y-%m-%d') sequences = { 'year': '%Y', 'month': '%m', 'day': '%d', 'y': '%y', 'doy': '%j', 'woy': '%W', 'weekday': '%w', 'h24': '%H', 'h12': '%I', 'min': '%M', 'sec': '%S' } res = {} for key, format in sequences.items(): res[key] = effective_date.strftime(format) res['range_' + key] = range_date.strftime(format) res['current_' + key] = now.strftime(format) return res d = _interpolation_dict() try: interpolated_prefix = _interpolate(self.prefix, d) interpolated_suffix = _interpolate(self.suffix, d) except ValueError: raise UserError( _('Invalid prefix or suffix for sequence \'%s\'') % (self.get('name'))) return interpolated_prefix, interpolated_suffix def get_next_char(self, number_next): interpolated_prefix, interpolated_suffix = self._get_prefix_suffix() return interpolated_prefix + '%%0%sd' % self.padding % number_next + interpolated_suffix def _create_date_range_seq(self, date): year = fields.Date.from_string(date).strftime('%Y') date_from = '{}-01-01'.format(year) date_to = '{}-12-31'.format(year) date_range = self.env['ir.sequence.date_range'].search( [('sequence_id', '=', self.id), ('date_from', '>=', date), ('date_from', '<=', date_to)], order='date_from desc', limit=1) if date_range: date_to = datetime.strptime(date_range.date_from, '%Y-%m-%d') + timedelta(days=-1) date_to = date_to.strftime('%Y-%m-%d') date_range = self.env['ir.sequence.date_range'].search( [('sequence_id', '=', self.id), ('date_to', '>=', date_from), ('date_to', '<=', date)], order='date_to desc', limit=1) if date_range: date_from = datetime.strptime(date_range.date_to, '%Y-%m-%d') + timedelta(days=1) date_from = date_from.strftime('%Y-%m-%d') seq_date_range = self.env['ir.sequence.date_range'].sudo().create({ 'date_from': date_from, 'date_to': date_to, 'sequence_id': self.id, }) return seq_date_range def _next(self): """ Returns the next number in the preferred sequence in all the ones given in self.""" if not self.use_date_range: return self._next_do() # date mode dt = fields.Date.today() if self._context.get('ir_sequence_date'): dt = self._context.get('ir_sequence_date') seq_date = self.env['ir.sequence.date_range'].search( [('sequence_id', '=', self.id), ('date_from', '<=', dt), ('date_to', '>=', dt)], limit=1) if not seq_date: seq_date = self._create_date_range_seq(dt) return seq_date.with_context( ir_sequence_date_range=seq_date.date_from)._next() @api.multi def next_by_id(self): """ Draw an interpolated string using the specified sequence.""" self.check_access_rights('read') return self._next() @api.model def next_by_code(self, sequence_code): """ Draw an interpolated string using a sequence with the requested code. If several sequences with the correct code are available to the user (multi-company cases), the one from the user's current company will be used. :param dict context: context dictionary may contain a ``force_company`` key with the ID of the company to use instead of the user's current company for the sequence selection. A matching sequence for that specific company will get higher priority. """ self.check_access_rights('read') force_company = self._context.get('force_company') if not force_company: force_company = self.env.user.company_id.id seq_ids = self.search([('code', '=', sequence_code), ('company_id', 'in', [force_company, False])], order='company_id') if not seq_ids: _logger.debug( "No ir.sequence has been found for code '%s'. Please make sure a sequence is set for current company." % sequence_code) return False seq_id = seq_ids[0] return seq_id._next() @api.model def get_id(self, sequence_code_or_id, code_or_id='id'): """ Draw an interpolated string using the specified sequence. The sequence to use is specified by the ``sequence_code_or_id`` argument, which can be a code or an id (as controlled by the ``code_or_id`` argument. This method is deprecated. """ _logger.warning( "ir_sequence.get() and ir_sequence.get_id() are deprecated. " "Please use ir_sequence.next_by_code() or ir_sequence.next_by_id()." ) if code_or_id == 'id': return self.browse(sequence_code_or_id).next_by_id() else: return self.next_by_code(sequence_code_or_id) @api.model def get(self, code): """ Draw an interpolated string using the specified sequence. The sequence to use is specified by its code. This method is deprecated. """ return self.get_id(code, 'code')
class account_journal(models.Model): _inherit = "account.journal" @api.one def _kanban_dashboard(self): self.kanban_dashboard = json.dumps(self.get_journal_dashboard_datas()) @api.one def _kanban_dashboard_graph(self): if (self.type in ['sale', 'purchase']): self.kanban_dashboard_graph = json.dumps( self.get_bar_graph_datas()) elif (self.type in ['cash', 'bank']): self.kanban_dashboard_graph = json.dumps( self.get_line_graph_datas()) kanban_dashboard = fields.Text(compute='_kanban_dashboard') kanban_dashboard_graph = fields.Text(compute='_kanban_dashboard_graph') show_on_dashboard = fields.Boolean( string='Show journal on dashboard', help="Whether this journal should be displayed on the dashboard or not", default=True) color = fields.Integer("Color Index", default=0) account_setup_bank_data_done = fields.Boolean( string='Bank setup marked as done', related='company_id.account_setup_bank_data_done', help="Technical field used in the special view for the setup bar step." ) def _graph_title_and_key(self): if self.type == 'sale': return ['', _('Sales: Untaxed Total')] elif self.type == 'purchase': return ['', _('Purchase: Untaxed Total')] elif self.type == 'cash': return ['', _('Cash: Balance')] elif self.type == 'bank': return ['', _('Bank: Balance')] @api.multi def get_line_graph_datas(self): data = [] today = datetime.today() last_month = today + timedelta(days=-30) bank_stmt = [] # Query to optimize loading of data for bank statement graphs # Return a list containing the latest bank statement balance per day for the # last 30 days for current journal query = """SELECT a.date, a.balance_end FROM account_bank_statement AS a, (SELECT c.date, max(c.id) AS stmt_id FROM account_bank_statement AS c WHERE c.journal_id = %s AND c.date > %s AND c.date <= %s GROUP BY date) AS b WHERE a.id = b.stmt_id ORDER BY date;""" self.env.cr.execute(query, (self.id, last_month, today)) bank_stmt = self.env.cr.dictfetchall() last_bank_stmt = self.env['account.bank.statement'].search( [('journal_id', 'in', self.ids), ('date', '<=', last_month.strftime(DF))], order="date desc, id desc", limit=1) start_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0 locale = self._context.get('lang') or 'en_US' show_date = last_month #get date in locale format name = format_date(show_date, 'd LLLL Y', locale=locale) short_name = format_date(show_date, 'd MMM', locale=locale) data.append({'x': short_name, 'y': start_balance, 'name': name}) for stmt in bank_stmt: #fill the gap between last data and the new one number_day_to_add = (datetime.strptime(stmt.get('date'), DF) - show_date).days last_balance = data[len(data) - 1]['y'] for day in range(0, number_day_to_add + 1): show_date = show_date + timedelta(days=1) #get date in locale format name = format_date(show_date, 'd LLLL Y', locale=locale) short_name = format_date(show_date, 'd MMM', locale=locale) data.append({'x': short_name, 'y': last_balance, 'name': name}) #add new stmt value data[len(data) - 1]['y'] = stmt.get('balance_end') #continue the graph if the last statement isn't today if show_date != today: number_day_to_add = (today - show_date).days last_balance = data[len(data) - 1]['y'] for day in range(0, number_day_to_add): show_date = show_date + timedelta(days=1) #get date in locale format name = format_date(show_date, 'd LLLL Y', locale=locale) short_name = format_date(show_date, 'd MMM', locale=locale) data.append({'x': short_name, 'y': last_balance, 'name': name}) [graph_title, graph_key] = self._graph_title_and_key() color = '#009efb' if '+e' in version else '#7c7bad' return [{ 'values': data, 'title': graph_title, 'key': graph_key, 'area': True, 'color': color }] @api.multi def get_bar_graph_datas(self): data = [] today = datetime.strptime(fields.Date.context_today(self), DF) data.append({'label': _('Past'), 'value': 0.0, 'type': 'past'}) day_of_week = int( format_datetime(today, 'e', locale=self._context.get('lang') or 'en_US')) first_day_of_week = today + timedelta(days=-day_of_week + 1) for i in range(-1, 4): if i == 0: label = _('This Week') elif i == 3: label = _('Future') else: start_week = first_day_of_week + timedelta(days=i * 7) end_week = start_week + timedelta(days=6) if start_week.month == end_week.month: label = str(start_week.day) + '-' + str( end_week.day) + ' ' + format_date( end_week, 'MMM', locale=self._context.get('lang') or 'en_US') else: label = format_date(start_week, 'd MMM', locale=self._context.get('lang') or 'en_US') + '-' + format_date( end_week, 'd MMM', locale=self._context.get('lang') or 'en_US') data.append({ 'label': label, 'value': 0.0, 'type': 'past' if i < 0 else 'future' }) # Build SQL query to find amount aggregated by week (select_sql_clause, query_args) = self._get_bar_graph_select_query() query = '' start_date = (first_day_of_week + timedelta(days=-7)) for i in range(0, 6): if i == 0: query += "(" + select_sql_clause + " and date < '" + start_date.strftime( DF) + "')" elif i == 5: query += " UNION ALL (" + select_sql_clause + " and date >= '" + start_date.strftime( DF) + "')" else: next_date = start_date + timedelta(days=7) query += " UNION ALL (" + select_sql_clause + " and date >= '" + start_date.strftime( DF) + "' and date < '" + next_date.strftime(DF) + "')" start_date = next_date self.env.cr.execute(query, query_args) query_results = self.env.cr.dictfetchall() for index in range(0, len(query_results)): if query_results[index].get('aggr_date') != None: data[index]['value'] = query_results[index].get('total') [graph_title, graph_key] = self._graph_title_and_key() return [{'values': data, 'title': graph_title, 'key': graph_key}] def _get_bar_graph_select_query(self): """ Returns a tuple containing the base SELECT SQL query used to gather the bar graph's data as its first element, and the arguments dictionary for it as its second. """ return ( """SELECT sum(residual_company_signed) as total, min(date) as aggr_date FROM account_invoice WHERE journal_id = %(journal_id)s and state = 'open'""", { 'journal_id': self.id }) @api.multi def get_journal_dashboard_datas(self): currency = self.currency_id or self.company_id.currency_id number_to_reconcile = last_balance = account_sum = 0 title = '' number_draft = number_waiting = number_late = 0 sum_draft = sum_waiting = sum_late = 0.0 if self.type in ['bank', 'cash']: last_bank_stmt = self.env['account.bank.statement'].search( [('journal_id', 'in', self.ids)], order="date desc, id desc", limit=1) last_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0 #Get the number of items to reconcile for that bank journal self.env.cr.execute( """SELECT COUNT(DISTINCT(line.id)) FROM account_bank_statement_line AS line LEFT JOIN account_bank_statement AS st ON line.statement_id = st.id WHERE st.journal_id IN %s AND st.state = 'open' AND line.amount != 0.0 AND not exists (select 1 from account_move_line aml where aml.statement_line_id = line.id) """, (tuple(self.ids), )) number_to_reconcile = self.env.cr.fetchone()[0] # optimization to read sum of balance from account_move_line account_ids = tuple(ac for ac in [ self.default_debit_account_id.id, self.default_credit_account_id.id ] if ac) if account_ids: amount_field = 'balance' if ( not self.currency_id or self.currency_id == self.company_id.currency_id) else 'amount_currency' query = """SELECT sum(%s) FROM account_move_line WHERE account_id in %%s AND date <= %%s;""" % ( amount_field, ) self.env.cr.execute(query, ( account_ids, fields.Date.today(), )) query_results = self.env.cr.dictfetchall() if query_results and query_results[0].get('sum') != None: account_sum = query_results[0].get('sum') #TODO need to check if all invoices are in the same currency than the journal!!!! elif self.type in ['sale', 'purchase']: title = _('Bills to pay') if self.type == 'purchase' else _( 'Invoices owed to you') (query, query_args) = self._get_open_bills_to_pay_query() self.env.cr.execute(query, query_args) query_results_to_pay = self.env.cr.dictfetchall() (query, query_args) = self._get_draft_bills_query() self.env.cr.execute(query, query_args) query_results_drafts = self.env.cr.dictfetchall() today = datetime.today() query = """SELECT amount_total, currency_id AS currency, type FROM account_invoice WHERE journal_id = %s AND date < %s AND state = 'open';""" self.env.cr.execute(query, (self.id, today)) late_query_results = self.env.cr.dictfetchall() (number_waiting, sum_waiting) = self._count_results_and_sum_amounts( query_results_to_pay, currency) (number_draft, sum_draft) = self._count_results_and_sum_amounts( query_results_drafts, currency) (number_late, sum_late) = self._count_results_and_sum_amounts( late_query_results, currency) difference = currency.round(last_balance - account_sum) + 0.0 return { 'number_to_reconcile': number_to_reconcile, 'account_balance': formatLang(self.env, currency.round(account_sum) + 0.0, currency_obj=currency), 'last_balance': formatLang(self.env, currency.round(last_balance) + 0.0, currency_obj=currency), 'difference': formatLang(self.env, difference, currency_obj=currency) if difference else False, 'number_draft': number_draft, 'number_waiting': number_waiting, 'number_late': number_late, 'sum_draft': formatLang(self.env, currency.round(sum_draft) + 0.0, currency_obj=currency), 'sum_waiting': formatLang(self.env, currency.round(sum_waiting) + 0.0, currency_obj=currency), 'sum_late': formatLang(self.env, currency.round(sum_late) + 0.0, currency_obj=currency), 'currency_id': currency.id, 'bank_statements_source': self.bank_statements_source, 'title': title, } def _get_open_bills_to_pay_query(self): """ Returns a tuple contaning the SQL query used to gather the open bills data as its first element, and the arguments dictionary to use to run it as its second. """ return ("""SELECT state, amount_total, currency_id AS currency, type FROM account_invoice WHERE journal_id = %(journal_id)s AND state = 'open';""", { 'journal_id': self.id }) def _get_draft_bills_query(self): """ Returns a tuple containing as its first element the SQL query used to gather the bills in draft state data, and the arguments dictionary to use to run it as its second. """ return ("""SELECT state, amount_total, currency_id AS currency, type FROM account_invoice WHERE journal_id = %(journal_id)s AND state = 'draft';""", { 'journal_id': self.id }) def _count_results_and_sum_amounts(self, results_dict, target_currency): """ Loops on a query result to count the total number of invoices and sum their amount_total field (expressed in the given target currency). """ rslt_count = 0 rslt_sum = 0.0 for result in results_dict: cur = self.env['res.currency'].browse(result.get('currency')) rslt_count += 1 type_factor = result.get('type') in ('in_refund', 'out_refund') and -1 or 1 rslt_sum += type_factor * cur.compute(result.get('amount_total'), target_currency) return (rslt_count, rslt_sum) @api.multi def action_create_new(self): ctx = self._context.copy() model = 'account.invoice' if self.type == 'sale': ctx.update({ 'journal_type': self.type, 'default_type': 'out_invoice', 'type': 'out_invoice', 'default_journal_id': self.id }) if ctx.get('refund'): ctx.update({ 'default_type': 'out_refund', 'type': 'out_refund' }) view_id = self.env.ref('account.invoice_form').id elif self.type == 'purchase': ctx.update({ 'journal_type': self.type, 'default_type': 'in_invoice', 'type': 'in_invoice', 'default_journal_id': self.id }) if ctx.get('refund'): ctx.update({'default_type': 'in_refund', 'type': 'in_refund'}) view_id = self.env.ref('account.invoice_supplier_form').id else: ctx.update({ 'default_journal_id': self.id, 'view_no_maturity': True }) view_id = self.env.ref('account.view_move_form').id model = 'account.move' return { 'name': _('Create invoice/bill'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': model, 'view_id': view_id, 'context': ctx, } @api.multi def create_cash_statement(self): ctx = self._context.copy() ctx.update({ 'journal_id': self.id, 'default_journal_id': self.id, 'default_journal_type': 'cash' }) return { 'name': _('Create cash statement'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.bank.statement', 'context': ctx, } @api.multi def action_open_reconcile(self): if self.type in ['bank', 'cash']: # Open reconciliation view for bank statements belonging to this journal bank_stmt = self.env['account.bank.statement'].search([ ('journal_id', 'in', self.ids) ]) return { 'type': 'ir.actions.client', 'tag': 'bank_statement_reconciliation_view', 'context': { 'statement_ids': bank_stmt.ids, 'company_ids': self.mapped('company_id').ids }, } else: # Open reconciliation view for customers/suppliers action_context = { 'show_mode_selector': False, 'company_ids': self.mapped('company_id').ids } if self.type == 'sale': action_context.update({'mode': 'customers'}) elif self.type == 'purchase': action_context.update({'mode': 'suppliers'}) return { 'type': 'ir.actions.client', 'tag': 'manual_reconciliation_view', 'context': action_context, } @api.multi def open_action(self): """return action based on type for related journals""" action_name = self._context.get('action_name', False) if not action_name: if self.type == 'bank': action_name = 'action_bank_statement_tree' elif self.type == 'cash': action_name = 'action_view_bank_statement_tree' elif self.type == 'sale': action_name = 'action_invoice_tree1' elif self.type == 'purchase': action_name = 'action_invoice_tree2' else: action_name = 'action_move_journal_line' _journal_invoice_type_map = { ('sale', None): 'out_invoice', ('purchase', None): 'in_invoice', ('sale', 'refund'): 'out_refund', ('purchase', 'refund'): 'in_refund', ('bank', None): 'bank', ('cash', None): 'cash', ('general', None): 'general', } invoice_type = _journal_invoice_type_map[( self.type, self._context.get('invoice_type'))] ctx = self._context.copy() ctx.pop('group_by', None) ctx.update({ 'journal_type': self.type, 'default_journal_id': self.id, 'default_type': invoice_type, 'type': invoice_type }) [action] = self.env.ref('account.%s' % action_name).read() if not self.env.context.get('use_domain'): ctx['search_default_journal_id'] = self.id action['context'] = ctx action['domain'] = self._context.get('use_domain', []) account_invoice_filter = self.env.ref( 'account.view_account_invoice_filter', False) if action_name in ['action_invoice_tree1', 'action_invoice_tree2']: action[ 'search_view_id'] = account_invoice_filter and account_invoice_filter.id or False if action_name in [ 'action_bank_statement_tree', 'action_view_bank_statement_tree' ]: action['views'] = False action['view_id'] = False return action @api.multi def open_spend_money(self): return self.open_payments_action('outbound') @api.multi def open_collect_money(self): return self.open_payments_action('inbound') @api.multi def open_transfer_money(self): return self.open_payments_action('transfer') @api.multi def open_payments_action(self, payment_type): ctx = self._context.copy() ctx.update({ 'default_payment_type': payment_type, 'default_journal_id': self.id }) ctx.pop('group_by', None) action_rec = self.env['ir.model.data'].xmlid_to_object( 'account.action_account_payments') if action_rec: action = action_rec.read([])[0] action['context'] = ctx action['domain'] = [('journal_id', '=', self.id), ('payment_type', '=', payment_type)] return action @api.multi def open_action_with_context(self): action_name = self.env.context.get('action_name', False) if not action_name: return False ctx = dict(self.env.context, default_journal_id=self.id) if ctx.get('search_default_journal', False): ctx.update(search_default_journal_id=self.id) ctx.pop('group_by', None) ir_model_obj = self.env['ir.model.data'] model, action_id = ir_model_obj.get_object_reference( 'account', action_name) [action] = self.env[model].browse(action_id).read() action['context'] = ctx if ctx.get('use_domain', False): action['domain'] = [ '|', ('journal_id', '=', self.id), ('journal_id', '=', False) ] action['name'] += ' for journal ' + self.name return action @api.multi def create_bank_statement(self): """return action to create a bank statements. This button should be called only on journals with type =='bank'""" self.bank_statements_source = 'manual' action = self.env.ref('account.action_bank_statement_tree').read()[0] action.update({ 'views': [[False, 'form']], 'context': "{'default_journal_id': " + str(self.id) + "}", }) return action ##################### # Setup Steps Stuff # ##################### @api.model def retrieve_account_dashboard_setup_bar(self): """ Returns the data used by the setup bar on the Accounting app dashboard.""" company = self.env.user.company_id return { 'show_setup_bar': not company.account_setup_bar_closed, 'company': company.account_setup_company_data_done, 'bank': company.account_setup_bank_data_done, 'fiscal_year': company.account_setup_fy_data_done, 'chart_of_accounts': company.account_setup_coa_done, 'initial_balance': company.opening_move_posted(), } def mark_bank_setup_as_done_action(self): """ Marks the 'bank setup' step as done in the setup bar and in the company.""" self.company_id.account_setup_bank_data_done = True def unmark_bank_setup_as_done_action(self): """ Marks the 'bank setup' step as not done in the setup bar and in the company.""" self.company_id.account_setup_bank_data_done = False
class IrActionsActWindow(models.Model): _name = 'ir.actions.act_window' _table = 'ir_act_window' _inherit = 'ir.actions.actions' _sequence = 'ir_actions_id_seq' _order = 'name' @api.constrains('res_model', 'src_model') def _check_model(self): for action in self: if action.res_model not in self.env: raise ValidationError( _('Invalid model name %r in action definition.') % action.res_model) if action.src_model and action.src_model not in self.env: raise ValidationError( _('Invalid model name %r in action definition.') % action.src_model) @api.depends('view_ids.view_mode', 'view_mode', 'view_id.type') def _compute_views(self): """ Compute an ordered list of the specific view modes that should be enabled when displaying the result of this action, along with the ID of the specific view to use for each mode, if any were required. This function hides the logic of determining the precedence between the view_modes string, the view_ids o2m, and the view_id m2o that can be set on the action. """ for act in self: act.views = [(view.view_id.id, view.view_mode) for view in act.view_ids] got_modes = [view.view_mode for view in act.view_ids] all_modes = act.view_mode.split(',') missing_modes = [ mode for mode in all_modes if mode not in got_modes ] if missing_modes: if act.view_id.type in missing_modes: # reorder missing modes to put view_id first if present missing_modes.remove(act.view_id.type) act.views.append((act.view_id.id, act.view_id.type)) act.views.extend([(False, mode) for mode in missing_modes]) @api.depends('res_model', 'search_view_id') def _compute_search_view(self): for act in self: fvg = self.env[act.res_model].fields_view_get( act.search_view_id.id, 'search') act.search_view = str(fvg) name = fields.Char(string='Action Name', translate=True) type = fields.Char(default="ir.actions.act_window") view_id = fields.Many2one('ir.ui.view', string='View Ref.', ondelete='set null') domain = fields.Char( string='Domain Value', help= "Optional domain filtering of the destination data, as a Python expression" ) context = fields.Char( string='Context Value', default={}, required=True, help= "Context dictionary as Python expression, empty by default (Default: {})" ) res_id = fields.Integer( string='Record ID', help= "Database ID of record to open in form view, when ``view_mode`` is set to 'form' only" ) res_model = fields.Char( string='Destination Model', required=True, help="Model name of the object to open in the view window") src_model = fields.Char( string='Source Model', help= "Optional model name of the objects on which this action should be visible" ) target = fields.Selection([('current', 'Current Window'), ('new', 'New Window'), ('inline', 'Inline Edit'), ('fullscreen', 'Full Screen'), ('main', 'Main action of Current Window')], default="current", string='Target Window') view_mode = fields.Char( required=True, default='tree,form', help= "Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)" ) view_type = fields.Selection( [('tree', 'Tree'), ('form', 'Form')], default="form", string='View Type', required=True, help= "View type: Tree type to use for the tree view, set to 'tree' for a hierarchical tree view, or 'form' for a regular list view" ) usage = fields.Char( string='Action Usage', help="Used to filter menu and home actions from the user form.") view_ids = fields.One2many('ir.actions.act_window.view', 'act_window_id', string='Views') views = fields.Binary(compute='_compute_views', help="This function field computes the ordered list of views that should be enabled " \ "when displaying the result of an action, federating view mode, views and " \ "reference view. The result is returned as an ordered list of pairs (view_id,view_mode).") limit = fields.Integer(default=80, help='Default limit for the list view') groups_id = fields.Many2many('res.groups', 'ir_act_window_group_rel', 'act_id', 'gid', string='Groups') search_view_id = fields.Many2one('ir.ui.view', string='Search View Ref.') filter = fields.Boolean() auto_search = fields.Boolean(default=True) search_view = fields.Text(compute='_compute_search_view') multi = fields.Boolean( string='Restrict to lists', help= "If checked and the action is bound to a model, it will only appear in the More menu on list views" ) @api.multi def read(self, fields=None, load='_classic_read'): """ call the method get_empty_list_help of the model and set the window action help message """ result = super(IrActionsActWindow, self).read(fields, load=load) if not fields or 'help' in fields: for values in result: model = values.get('res_model') if model in self.env: values['help'] = self.env[model].get_empty_list_help( values.get('help', "")) return result @api.model def for_xml_id(self, module, xml_id): """ Returns the act_window object created for the provided xml_id :param module: the module the act_window originates in :param xml_id: the namespace-less id of the action (the @id attribute from the XML file) :return: A read() view of the ir.actions.act_window """ record = self.env.ref("%s.%s" % (module, xml_id)) return record.read()[0] @api.model def create(self, vals): self.clear_caches() return super(IrActionsActWindow, self).create(vals) @api.multi def unlink(self): self.clear_caches() return super(IrActionsActWindow, self).unlink() @api.multi def exists(self): ids = self._existing() existing = self.filtered(lambda rec: rec.id in ids) if len(existing) < len(self): # mark missing records in cache with a failed value exc = MissingError(_("Record does not exist or has been deleted.")) for record in (self - existing): record._cache.set_failed(self._fields, exc) return existing @api.model @tools.ormcache() def _existing(self): self._cr.execute("SELECT id FROM %s" % self._table) return set(row[0] for row in self._cr.fetchall())
class AccountMove(models.Model): _inherit = "account.move" # TO DO in master : refactor hashing algo to go into a mixin l10n_fr_secure_sequence_number = fields.Integer( string="Inalteralbility No Gap Sequence #", readonly=True, copy=False) l10n_fr_hash = fields.Char(string="Inalterability Hash", readonly=True, copy=False) l10n_fr_string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True, store=False) def _get_new_hash(self, secure_seq_number): """ Returns the hash to write on journal entries when they get posted""" self.ensure_one() #get the only one exact previous move in the securisation sequence prev_move = self.search([('state', '=', 'posted'), ('company_id', '=', self.company_id.id), ('l10n_fr_secure_sequence_number', '!=', 0), ('l10n_fr_secure_sequence_number', '=', int(secure_seq_number) - 1)]) if prev_move and len(prev_move) != 1: raise UserError( _('An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry.' )) #build and return the hash return self._compute_hash(prev_move.l10n_fr_hash if prev_move else u'') def _compute_hash(self, previous_hash): """ Computes the hash of the browse_record given as self, based on the hash of the previous record in the company's securisation sequence given as parameter""" self.ensure_one() hash_string = sha256( (previous_hash + self.l10n_fr_string_to_hash).encode('utf-8')) return hash_string.hexdigest() def _compute_string_to_hash(self): def _getattrstring(obj, field_str): field_value = obj[field_str] if obj._fields[field_str].type == 'many2one': field_value = field_value.id return str(field_value) for move in self: values = {} for field in MOVE_FIELDS: values[field] = _getattrstring(move, field) for line in move.line_ids: for field in LINE_FIELDS: k = 'line_%d_%s' % (line.id, field) values[k] = _getattrstring(line, field) #make the json serialization canonical # (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00) move.l10n_fr_string_to_hash = dumps(values, sort_keys=True, ensure_ascii=True, indent=None, separators=(',', ':')) @api.multi def write(self, vals): has_been_posted = False for move in self: if move.company_id._is_accounting_unalterable(): # write the hash and the secure_sequence_number when posting an account.move if vals.get('state') == 'posted': has_been_posted = True # restrict the operation in case we are trying to write a forbidden field if (move.state == "posted" and set(vals).intersection(MOVE_FIELDS)): raise UserError( _("According to the French law, you cannot modify a journal entry in order for its posted data to be updated or deleted. Unauthorized field: %s." ) % ', '.join(MOVE_FIELDS)) # restrict the operation in case we are trying to overwrite existing hash if (move.l10n_fr_hash and 'l10n_fr_hash' in vals) or ( move.l10n_fr_secure_sequence_number and 'l10n_fr_secure_sequence_number' in vals): raise UserError( _('You cannot overwrite the values ensuring the inalterability of the accounting.' )) res = super(AccountMove, self).write(vals) # write the hash and the secure_sequence_number when posting an account.move if has_been_posted: for move in self.filtered( lambda m: m.company_id._is_accounting_unalterable() and not (m.l10n_fr_secure_sequence_number or m.l10n_fr_hash)): new_number = move.company_id.l10n_fr_secure_sequence_id.next_by_id( ) vals_hashing = { 'l10n_fr_secure_sequence_number': new_number, 'l10n_fr_hash': move._get_new_hash(new_number) } res |= super(AccountMove, move).write(vals_hashing) return res @api.multi def button_cancel(self): #by-pass the normal behavior/message that tells people can cancel a posted journal entry #if the journal allows it. if self.company_id._is_accounting_unalterable(): raise UserError( _('You cannot modify a posted journal entry. This ensures its inalterability.' )) super(AccountMove, self).button_cancel() @api.model def _check_hash_integrity(self, company_id): """Checks that all posted moves have still the same data as when they were posted and raises an error with the result. """ def build_move_info(move): entry_reference = _('(ref.: %s)') move_reference_string = move.ref and entry_reference % move.ref or '' return [move.name, move_reference_string] moves = self.search([('state', '=', 'posted'), ('company_id', '=', company_id), ('l10n_fr_secure_sequence_number', '!=', 0)], order="l10n_fr_secure_sequence_number ASC") if not moves: raise UserError( _('There isn\'t any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis.' ) % self.env.user.company_id.name) previous_hash = u'' start_move_info = [] for move in moves: if move.l10n_fr_hash != move._compute_hash( previous_hash=previous_hash): raise UserError( _('Corrupted data on journal entry with id %s.') % move.id) if not previous_hash: #save the date and sequence number of the first move hashed start_move_info = build_move_info(move) previous_hash = move.l10n_fr_hash end_move_info = build_move_info(move) report_dict = { 'start_move_name': start_move_info[0], 'start_move_ref': start_move_info[1], 'end_move_name': end_move_info[0], 'end_move_ref': end_move_info[1] } # Raise on success raise UserError( _('''Successful test ! The journal entries are guaranteed to be in their original and inalterable state From: %(start_move_name)s %(start_move_ref)s To: %(end_move_name)s %(end_move_ref)s For this report to be legally meaningful, please download your certification from your customer account on Flectra.com (Only for Flectra Enterprise users).''' ) % report_dict)
class IrActionsTodo(models.Model): """ Configuration Wizards """ _name = 'ir.actions.todo' _description = "Configuration Wizards" _order = "sequence, id" action_id = fields.Many2one('ir.actions.actions', string='Action', required=True, index=True) sequence = fields.Integer(default=10) state = fields.Selection([('open', 'To Do'), ('done', 'Done')], string='Status', default='open', required=True) name = fields.Char() @api.model def create(self, vals): todo = super(IrActionsTodo, self).create(vals) if todo.state == "open": self.ensure_one_open_todo() return todo @api.multi def write(self, vals): res = super(IrActionsTodo, self).write(vals) if vals.get('state', '') == 'open': self.ensure_one_open_todo() return res @api.model def ensure_one_open_todo(self): open_todo = self.search([('state', '=', 'open')], order='sequence asc, id desc', offset=1) if open_todo: open_todo.write({'state': 'done'}) @api.multi def name_get(self): return [(record.id, record.action_id.name) for record in self] @api.multi def unlink(self): if self: try: todo_open_menu = self.env.ref('base.open_menu') # don't remove base.open_menu todo but set its original action if todo_open_menu in self: todo_open_menu.action_id = self.env.ref( 'base.action_client_base_menu').id self -= todo_open_menu except ValueError: pass return super(IrActionsTodo, self).unlink() @api.model def name_search(self, name, args=None, operator='ilike', limit=100): if args is None: args = [] if name: actions = self.search([('action_id', operator, name)] + args, limit=limit) return actions.name_get() return super(IrActionsTodo, self).name_search(name, args=args, operator=operator, limit=limit) @api.multi def action_launch(self, context=None): """ Launch Action of Wizard""" self.ensure_one() self.write({'state': 'done'}) # Load action action = self.env[self.action_id.type].browse(self.action_id.id) result = action.read()[0] if action._name != 'ir.actions.act_window': return result result.setdefault('context', '{}') # Open a specific record when res_id is provided in the context ctx = safe_eval(result['context'], {'user': self.env.user}) if ctx.get('res_id'): result['res_id'] = ctx.pop('res_id') # disable log for automatic wizards ctx['disable_log'] = True result['context'] = ctx return result @api.multi def action_open(self): """ Sets configuration wizard in TODO state""" return self.write({'state': 'open'})
class SaleAdvancePaymentInv(models.TransientModel): _name = "sale.advance.payment.inv" _description = "Sales Advance Payment Invoice" @api.model def _count(self): return len(self._context.get('active_ids', [])) @api.model def _default_product_id(self): product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id') return self.env['product.product'].browse(int(product_id)).exists() @api.model def _default_deposit_account_id(self): return self._default_product_id()._get_product_accounts()['income'] @api.model def _default_deposit_taxes_id(self): return self._default_product_id().taxes_id @api.model def _default_has_down_payment(self): if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False): sale_order = self.env['sale.order'].browse(self._context.get('active_id')) return sale_order.order_line.filtered( lambda sale_order_line: sale_order_line.is_downpayment ) return False @api.model def _default_currency_id(self): if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False): sale_order = self.env['sale.order'].browse(self._context.get('active_id')) return sale_order.currency_id advance_payment_method = fields.Selection([ ('delivered', 'Regular invoice'), ('percentage', 'Down payment (percentage)'), ('fixed', 'Down payment (fixed amount)') ], string='Create Invoice', default='delivered', required=True, help="A standard invoice is issued with all the order lines ready for invoicing, \ according to their invoicing policy (based on ordered or delivered quantity).") deduct_down_payments = fields.Boolean('Deduct down payments', default=True) has_down_payments = fields.Boolean('Has down payments', default=_default_has_down_payment, readonly=True) product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')], default=_default_product_id) count = fields.Integer(default=_count, string='Order Count') amount = fields.Float('Down Payment Amount', digits='Account', help="The percentage of amount to be invoiced in advance, taxes excluded.") currency_id = fields.Many2one('res.currency', string='Currency', default=_default_currency_id) fixed_amount = fields.Monetary('Down Payment Amount (Fixed)', help="The fixed amount to be invoiced in advance, taxes excluded.") deposit_account_id = fields.Many2one("account.account", string="Income Account", domain=[('deprecated', '=', False)], help="Account used for deposits", default=_default_deposit_account_id) deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes", help="Taxes used for deposits", default=_default_deposit_taxes_id) @api.onchange('advance_payment_method') def onchange_advance_payment_method(self): if self.advance_payment_method == 'percentage': amount = self.default_get(['amount']).get('amount') return {'value': {'amount': amount}} return {} def _prepare_invoice_values(self, order, name, amount, so_line): invoice_vals = { 'ref': order.client_order_ref, 'move_type': 'out_invoice', 'invoice_origin': order.name, 'invoice_user_id': order.user_id.id, 'narration': order.note, 'partner_id': order.partner_invoice_id.id, 'fiscal_position_id': (order.fiscal_position_id or order.fiscal_position_id.get_fiscal_position(order.partner_id.id)).id, 'partner_shipping_id': order.partner_shipping_id.id, 'currency_id': order.pricelist_id.currency_id.id, 'payment_reference': order.reference, 'invoice_payment_term_id': order.payment_term_id.id, 'partner_bank_id': order.company_id.partner_id.bank_ids[:1].id, 'team_id': order.team_id.id, 'campaign_id': order.campaign_id.id, 'medium_id': order.medium_id.id, 'source_id': order.source_id.id, 'invoice_line_ids': [(0, 0, { 'name': name, 'price_unit': amount, 'quantity': 1.0, 'product_id': self.product_id.id, 'product_uom_id': so_line.product_uom.id, 'tax_ids': [(6, 0, so_line.tax_id.ids)], 'sale_line_ids': [(6, 0, [so_line.id])], 'analytic_tag_ids': [(6, 0, so_line.analytic_tag_ids.ids)], 'analytic_account_id': order.analytic_account_id.id or False, })], } return invoice_vals def _get_advance_details(self, order): context = {'lang': order.partner_id.lang} if self.advance_payment_method == 'percentage': if all(self.product_id.taxes_id.mapped('price_include')): amount = order.amount_total * self.amount / 100 else: amount = order.amount_untaxed * self.amount / 100 name = _("Down payment of %s%%") % (self.amount) else: amount = self.fixed_amount name = _('Down Payment') del context return amount, name def _create_invoice(self, order, so_line, amount): if (self.advance_payment_method == 'percentage' and self.amount <= 0.00) or (self.advance_payment_method == 'fixed' and self.fixed_amount <= 0.00): raise UserError(_('The value of the down payment amount must be positive.')) amount, name = self._get_advance_details(order) invoice_vals = self._prepare_invoice_values(order, name, amount, so_line) if order.fiscal_position_id: invoice_vals['fiscal_position_id'] = order.fiscal_position_id.id invoice = self.env['account.move'].sudo().create(invoice_vals).with_user(self.env.uid) invoice.message_post_with_view('mail.message_origin_link', values={'self': invoice, 'origin': order}, subtype_id=self.env.ref('mail.mt_note').id) return invoice def _prepare_so_line(self, order, analytic_tag_ids, tax_ids, amount): context = {'lang': order.partner_id.lang} so_values = { 'name': _('Down Payment: %s') % (time.strftime('%m %Y'),), 'price_unit': amount, 'product_uom_qty': 0.0, 'order_id': order.id, 'discount': 0.0, 'product_uom': self.product_id.uom_id.id, 'product_id': self.product_id.id, 'analytic_tag_ids': analytic_tag_ids, 'tax_id': [(6, 0, tax_ids)], 'is_downpayment': True, 'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10, } del context return so_values def create_invoices(self): sale_orders = self.env['sale.order'].browse(self._context.get('active_ids', [])) if self.advance_payment_method == 'delivered': sale_orders._create_invoices(final=self.deduct_down_payments) else: # Create deposit product if necessary if not self.product_id: vals = self._prepare_deposit_product() self.product_id = self.env['product.product'].create(vals) self.env['ir.config_parameter'].sudo().set_param('sale.default_deposit_product_id', self.product_id.id) sale_line_obj = self.env['sale.order.line'] for order in sale_orders: amount, name = self._get_advance_details(order) if self.product_id.invoice_policy != 'order': raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.')) if self.product_id.type != 'service': raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product.")) taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id) tax_ids = order.fiscal_position_id.map_tax(taxes, self.product_id, order.partner_shipping_id).ids analytic_tag_ids = [] for line in order.order_line: analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in line.analytic_tag_ids] so_line_values = self._prepare_so_line(order, analytic_tag_ids, tax_ids, amount) so_line = sale_line_obj.create(so_line_values) self._create_invoice(order, so_line, amount) if self._context.get('open_invoices', False): return sale_orders.action_view_invoice() return {'type': 'ir.actions.act_window_close'} def _prepare_deposit_product(self): return { 'name': 'Down payment', 'type': 'service', 'invoice_policy': 'order', 'property_account_income_id': self.deposit_account_id.id, 'taxes_id': [(6, 0, self.deposit_taxes_id.ids)], 'company_id': False, }
class AccountInvoice(models.Model): _inherit = "account.invoice" timesheet_ids = fields.One2many('account.analytic.line', 'timesheet_invoice_id', string='Timesheets', readonly=True, copy=False) timesheet_count = fields.Integer("Number of timesheets", compute='_compute_timesheet_count') @api.multi @api.depends('timesheet_ids') def _compute_timesheet_count(self): timesheet_data = self.env['account.analytic.line'].read_group([('timesheet_invoice_id', 'in', self.ids)], ['timesheet_invoice_id'], ['timesheet_invoice_id']) mapped_data = dict([(t['timesheet_invoice_id'][0], t['timesheet_invoice_id_count']) for t in timesheet_data]) for invoice in self: invoice.timesheet_count = mapped_data.get(invoice.id, 0) def action_view_timesheet(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': _('Timesheets'), 'domain': [('project_id', '!=', False)], 'res_model': 'account.analytic.line', 'view_id': False, 'view_mode': 'tree,form', 'view_type': 'form', 'help': _(""" <p class="oe_view_nocontent_create"> Click to record timesheets. </p><p> You can register and track your workings hours by project every day. Every time spent on a project will become a cost and can be re-invoiced to customers if required. </p> """), 'limit': 80, 'context': { 'default_project_id': self.id, 'search_default_project_id': [self.id] } } @api.multi def invoice_validate(self): result = super(AccountInvoice, self).invoice_validate() self._compute_timesheet_revenue() return result def _compute_timesheet_revenue(self): for invoice in self: for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service').sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)): uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search([ ('so_line', 'in', invoice_line.sale_line_ids.ids), ('project_id', '!=', False), ('timesheet_invoice_id', '=', False), ('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed']) ]) # NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs) if uninvoiced_timesheet_lines: # delivered : update revenue with the prorata of number of hours on the timesheet line if invoice_line.product_id.invoice_policy == 'delivery': invoiced_price_per_hour = invoice_line.currency_id.round(invoice_line.price_subtotal / float(sum(uninvoiced_timesheet_lines.mapped('unit_amount')))) # invoicing analytic lines of different currency total_revenue_per_currency = dict.fromkeys(uninvoiced_timesheet_lines.mapped('company_currency_id').ids, 0.0) for index, timesheet_line in enumerate(uninvoiced_timesheet_lines.sorted(key=lambda ts: (ts.date, ts.id))): if index+1 != len(uninvoiced_timesheet_lines): line_revenue = invoice_line.currency_id.compute(invoiced_price_per_hour, timesheet_line.company_currency_id) * timesheet_line.unit_amount total_revenue_per_currency[timesheet_line.company_currency_id.id] += line_revenue else: # last line: add the difference to avoid rounding problem total_revenue = sum([self.env['res.currency'].browse(currency_id).compute(amount, timesheet_line.company_currency_id) for currency_id, amount in total_revenue_per_currency.items()]) line_revenue = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.company_currency_id) - total_revenue timesheet_line.write({ 'timesheet_invoice_id': invoice.id, 'timesheet_revenue': timesheet_line.company_currency_id.round(line_revenue), }) # ordered : update revenue with the prorata of theorical revenue elif invoice_line.product_id.invoice_policy == 'order': zero_timesheet_revenue = uninvoiced_timesheet_lines.filtered(lambda line: line.timesheet_revenue == 0.0) no_zero_timesheet_revenue = uninvoiced_timesheet_lines.filtered(lambda line: line.timesheet_revenue != 0.0) # timesheet with zero theorical revenue keep the same revenue, but become invoiced (invoice_id set) zero_timesheet_revenue.write({'timesheet_invoice_id': invoice.id}) # invoicing analytic lines of different currency total_revenue_per_currency = dict.fromkeys(no_zero_timesheet_revenue.mapped('company_currency_id').ids, 0.0) for index, timesheet_line in enumerate(no_zero_timesheet_revenue.sorted(key=lambda ts: (ts.date, ts.id))): if index+1 != len(no_zero_timesheet_revenue): price_subtotal_inv = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.company_currency_id) price_subtotal_sol = timesheet_line.so_line.currency_id.compute(timesheet_line.so_line.price_subtotal, timesheet_line.company_currency_id) line_revenue = timesheet_line.timesheet_revenue * price_subtotal_inv / price_subtotal_sol total_revenue_per_currency[timesheet_line.company_currency_id.id] += line_revenue else: # last line: add the difference to avoid rounding problem last_price_subtotal_inv = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.company_currency_id) total_revenue = sum([self.env['res.currency'].browse(currency_id).compute(amount, timesheet_line.company_currency_id) for currency_id, amount in total_revenue_per_currency.items()]) line_revenue = last_price_subtotal_inv - total_revenue timesheet_line.write({ 'timesheet_invoice_id': invoice.id, 'timesheet_revenue': timesheet_line.company_currency_id.round(line_revenue), })
class SaleAdvancePaymentInv(models.TransientModel): _name = "sale.advance.payment.inv" _description = "Sales Advance Payment Invoice" @api.model def _count(self): return len(self._context.get('active_ids', [])) @api.model def _get_advance_payment_method(self): if self._count() == 1: sale_obj = self.env['sale.order'] order = sale_obj.browse(self._context.get('active_ids'))[0] if all([ line.product_id.invoice_policy == 'order' for line in order.order_line ]) or order.invoice_count: return 'all' return 'delivered' @api.model def _default_product_id(self): product_id = self.env['ir.config_parameter'].sudo().get_param( 'sale.default_deposit_product_id') return self.env['product.product'].browse(int(product_id)) @api.model def _default_deposit_account_id(self): return self._default_product_id().property_account_income_id @api.model def _default_deposit_taxes_id(self): return self._default_product_id().taxes_id advance_payment_method = fields.Selection( [('delivered', 'Invoiceable lines'), ('all', 'Invoiceable lines (deduct down payments)'), ('percentage', 'Down payment (percentage)'), ('fixed', 'Down payment (fixed amount)')], string='What do you want to invoice?', default=_get_advance_payment_method, required=True) product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')], default=_default_product_id) count = fields.Integer(default=_count, string='# of Orders') amount = fields.Float( 'Down Payment Amount', digits=dp.get_precision('Account'), help="The amount to be invoiced in advance, taxes excluded.") deposit_account_id = fields.Many2one("account.account", string="Income Account", domain=[('deprecated', '=', False)], help="Account used for deposits", default=_default_deposit_account_id) deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes", help="Taxes used for deposits", default=_default_deposit_taxes_id) @api.onchange('advance_payment_method') def onchange_advance_payment_method(self): if self.advance_payment_method == 'percentage': return {'value': {'amount': 0}} return {} @api.multi def _create_invoice(self, order, so_line, amount): inv_obj = self.env['account.invoice'] ir_property_obj = self.env['ir.property'] account_id = False if self.product_id.id: account_id = self.product_id.property_account_income_id.id or self.product_id.categ_id.property_account_income_categ_id.id if not account_id: inc_acc = ir_property_obj.get('property_account_income_categ_id', 'product.category') account_id = order.fiscal_position_id.map_account( inc_acc).id if inc_acc else False if not account_id: raise UserError( _('There is no income account defined for this product: "%s". You may have to install a chart of account from Accounting app, settings menu.' ) % (self.product_id.name, )) if self.amount <= 0.00: raise UserError( _('The value of the down payment amount must be positive.')) context = {'lang': order.partner_id.lang} if self.advance_payment_method == 'percentage': amount = order.amount_untaxed * self.amount / 100 name = _("Down payment of %s%%") % (self.amount, ) else: amount = self.amount name = _('Down Payment') del context taxes = self.product_id.taxes_id.filtered( lambda r: not order.company_id or r.company_id == order.company_id) if order.fiscal_position_id and taxes: tax_ids = order.fiscal_position_id.map_tax(taxes).ids else: tax_ids = taxes.ids invoice = inv_obj.create({ 'name': order.client_order_ref or order.name, 'origin': order.name, 'type': 'out_invoice', 'reference': False, 'account_id': order.partner_id.property_account_receivable_id.id, 'partner_id': order.partner_invoice_id.id, 'partner_shipping_id': order.partner_shipping_id.id, 'invoice_line_ids': [(0, 0, { 'name': name, 'origin': order.name, 'account_id': account_id, 'price_unit': amount, 'quantity': 1.0, 'discount': 0.0, 'uom_id': self.product_id.uom_id.id, 'product_id': self.product_id.id, 'sale_line_ids': [(6, 0, [so_line.id])], 'invoice_line_tax_ids': [(6, 0, tax_ids)], 'account_analytic_id': order.analytic_account_id.id or False, })], 'currency_id': order.pricelist_id.currency_id.id, 'payment_term_id': order.payment_term_id.id, 'fiscal_position_id': order.fiscal_position_id.id or order.partner_id.property_account_position_id.id, 'team_id': order.team_id.id, 'user_id': order.user_id.id, 'comment': order.note, }) invoice.compute_taxes() invoice.message_post_with_view( 'mail.message_origin_link', values={ 'self': invoice, 'origin': order }, subtype_id=self.env.ref('mail.mt_note').id) return invoice @api.multi def create_invoices(self): sale_orders = self.env['sale.order'].browse( self._context.get('active_ids', [])) if self.advance_payment_method == 'delivered': sale_orders.action_invoice_create() elif self.advance_payment_method == 'all': sale_orders.action_invoice_create(final=True) else: # Create deposit product if necessary if not self.product_id: vals = self._prepare_deposit_product() self.product_id = self.env['product.product'].create(vals) self.env['ir.config_parameter'].sudo().set_param( 'sale.default_deposit_product_id', self.product_id.id) sale_line_obj = self.env['sale.order.line'] for order in sale_orders: if self.advance_payment_method == 'percentage': amount = order.amount_untaxed * self.amount / 100 else: amount = self.amount if self.product_id.invoice_policy != 'order': raise UserError( _('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.' )) if self.product_id.type != 'service': raise UserError( _("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product." )) taxes = self.product_id.taxes_id.filtered( lambda r: not order.company_id or r.company_id == order. company_id) if order.fiscal_position_id and taxes: tax_ids = order.fiscal_position_id.map_tax(taxes).ids else: tax_ids = taxes.ids context = {'lang': order.partner_id.lang} so_line = sale_line_obj.create({ 'name': _('Advance: %s') % (time.strftime('%m %Y'), ), 'price_unit': amount, 'product_uom_qty': 0.0, 'order_id': order.id, 'discount': 0.0, 'product_uom': self.product_id.uom_id.id, 'product_id': self.product_id.id, 'tax_id': [(6, 0, tax_ids)], 'is_downpayment': True, }) del context self._create_invoice(order, so_line, amount) if self._context.get('open_invoices', False): return sale_orders.action_view_invoice() return {'type': 'ir.actions.act_window_close'} def _prepare_deposit_product(self): return { 'name': 'Down payment', 'type': 'service', 'invoice_policy': 'order', 'property_account_income_id': self.deposit_account_id.id, 'taxes_id': [(6, 0, self.deposit_taxes_id.ids)], 'company_id': False, }
class Module(models.Model): _name = "ir.module.module" _rec_name = "shortdesc" _description = "Module" _order = 'application desc,sequence,name' @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): res = super(Module, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=False) if view_type == 'form' and res.get('toolbar',False): install_id = self.env.ref('base.action_server_module_immediate_install').id action = [rec for rec in res['toolbar']['action'] if rec.get('id', False) != install_id] res['toolbar'] = {'action': action} return res @classmethod def get_module_info(cls, name): try: return modules.load_information_from_description_file(name) except Exception: _logger.debug('Error when trying to fetch information for module %s', name, exc_info=True) return {} @api.depends('name', 'description') def _get_desc(self): for module in self: if not module.name: module.description_html = False continue path = modules.get_module_resource(module.name, 'static/description/index.html') if path: with tools.file_open(path, 'rb') as desc_file: doc = desc_file.read() html = lxml.html.document_fromstring(doc) for element, attribute, link, pos in html.iterlinks(): if element.get('src') and not '//' in element.get('src') and not 'static/' in element.get('src'): element.set('src', "/%s/static/description/%s" % (module.name, element.get('src'))) module.description_html = tools.html_sanitize(lxml.html.tostring(html)) else: overrides = { 'embed_stylesheet': False, 'doctitle_xform': False, 'output_encoding': 'unicode', 'xml_declaration': False, 'file_insertion_enabled': False, } output = publish_string(source=module.description if not module.application and module.description else '', settings_overrides=overrides, writer=MyWriter()) module.description_html = tools.html_sanitize(output) @api.depends('name') def _get_latest_version(self): default_version = modules.adapt_version('1.0') for module in self: module.installed_version = self.get_module_info(module.name).get('version', default_version) @api.depends('name', 'state') def _get_views(self): IrModelData = self.env['ir.model.data'].with_context(active_test=True) dmodels = ['ir.ui.view', 'ir.actions.report', 'ir.ui.menu'] for module in self: # Skip uninstalled modules below, no data to find anyway. if module.state not in ('installed', 'to upgrade', 'to remove'): module.views_by_module = "" module.reports_by_module = "" module.menus_by_module = "" continue # then, search and group ir.model.data records imd_models = defaultdict(list) imd_domain = [('module', '=', module.name), ('model', 'in', tuple(dmodels))] for data in IrModelData.sudo().search(imd_domain): imd_models[data.model].append(data.res_id) def browse(model): # as this method is called before the module update, some xmlid # may be invalid at this stage; explictly filter records before # reading them return self.env[model].browse(imd_models[model]).exists() def format_view(v): return '%s%s (%s)' % (v.inherit_id and '* INHERIT ' or '', v.name, v.type) module.views_by_module = "\n".join(sorted(format_view(v) for v in browse('ir.ui.view'))) module.reports_by_module = "\n".join(sorted(r.name for r in browse('ir.actions.report'))) module.menus_by_module = "\n".join(sorted(m.complete_name for m in browse('ir.ui.menu'))) @api.depends('icon') def _get_icon_image(self): for module in self: module.icon_image = '' if module.icon: path_parts = module.icon.split('/') path = modules.get_module_resource(path_parts[1], *path_parts[2:]) elif module.id: path = modules.module.get_module_icon(module.name) else: path = '' if path: with tools.file_open(path, 'rb') as image_file: module.icon_image = base64.b64encode(image_file.read()) name = fields.Char('Technical Name', readonly=True, required=True, index=True) category_id = fields.Many2one('ir.module.category', string='Category', readonly=True, index=True) shortdesc = fields.Char('Module Name', readonly=True, translate=True) summary = fields.Char('Summary', readonly=True, translate=True) description = fields.Text('Description', readonly=True, translate=True) description_html = fields.Html('Description HTML', compute='_get_desc') author = fields.Char("Author", readonly=True) maintainer = fields.Char('Maintainer', readonly=True) contributors = fields.Text('Contributors', readonly=True) website = fields.Char("Website", readonly=True) # attention: Incorrect field names !! # installed_version refers the latest version (the one on disk) # latest_version refers the installed version (the one in database) # published_version refers the version available on the repository installed_version = fields.Char('Latest Version', compute='_get_latest_version') latest_version = fields.Char('Installed Version', readonly=True) published_version = fields.Char('Published Version', readonly=True) url = fields.Char('URL', readonly=True) sequence = fields.Integer('Sequence', default=100) dependencies_id = fields.One2many('ir.module.module.dependency', 'module_id', string='Dependencies', readonly=True) exclusion_ids = fields.One2many('ir.module.module.exclusion', 'module_id', string='Exclusions', readonly=True) auto_install = fields.Boolean('Automatic Installation', help='An auto-installable module is automatically installed by the ' 'system when all its dependencies are satisfied. ' 'If the module has no dependency, it is always installed.') state = fields.Selection(STATES, string='Status', default='uninstallable', readonly=True, index=True) demo = fields.Boolean('Demo Data', default=False, readonly=True) license = fields.Selection([ ('GPL-2', 'GPL Version 2'), ('GPL-2 or any later version', 'GPL-2 or later version'), ('GPL-3', 'GPL Version 3'), ('GPL-3 or any later version', 'GPL-3 or later version'), ('AGPL-3', 'Affero GPL-3'), ('LGPL-3', 'LGPL Version 3'), ('Other OSI approved licence', 'Other OSI Approved License'), ('OEEL-1', 'Flectra Enterprise Edition License v1.0'), ('OPL-1', 'Flectra Proprietary License v1.0'), ('Other proprietary', 'Other Proprietary') ], string='License', default='LGPL-3', readonly=True) menus_by_module = fields.Text(string='Menus', compute='_get_views', store=True) reports_by_module = fields.Text(string='Reports', compute='_get_views', store=True) views_by_module = fields.Text(string='Views', compute='_get_views', store=True) application = fields.Boolean('Application', readonly=True) icon = fields.Char('Icon URL') icon_image = fields.Binary(string='Icon', compute='_get_icon_image') to_buy = fields.Boolean('Flectra Enterprise Module', default=False) has_iap = fields.Boolean(compute='_compute_has_iap') _sql_constraints = [ ('name_uniq', 'UNIQUE (name)', 'The name of the module must be unique!'), ] def _compute_has_iap(self): for module in self: module.has_iap = bool(module.id) and 'iap' in module.upstream_dependencies(exclude_states=('',)).mapped('name') def unlink(self): if not self: return True for module in self: if module.state in ('installed', 'to upgrade', 'to remove', 'to install'): raise UserError(_('You are trying to remove a module that is installed or will be installed.')) self.clear_caches() return super(Module, self).unlink() @staticmethod def _check_python_external_dependency(pydep): try: pkg_resources.get_distribution(pydep) except pkg_resources.DistributionNotFound as e: try: importlib.import_module(pydep) _logger.info("python external dependency on '%s' does not appear to be a valid PyPI package. Using a PyPI package name is recommended.", pydep) except ImportError: # backward compatibility attempt failed _logger.warning("DistributionNotFound: %s", e) raise Exception('Python library not installed: %s' % (pydep,)) except pkg_resources.VersionConflict as e: _logger.warning("VersionConflict: %s", e) raise Exception('Python library version conflict: %s' % (pydep,)) except Exception as e: _logger.warning("get_distribution(%s) failed: %s", pydep, e) raise Exception('Error finding python library %s' % (pydep,)) @staticmethod def _check_external_dependencies(terp): depends = terp.get('external_dependencies') if not depends: return for pydep in depends.get('python', []): Module._check_python_external_dependency(pydep) for binary in depends.get('bin', []): try: tools.find_in_path(binary) except IOError: raise Exception('Unable to find %r in path' % (binary,)) @classmethod def check_external_dependencies(cls, module_name, newstate='to install'): terp = cls.get_module_info(module_name) try: cls._check_external_dependencies(terp) except Exception as e: if newstate == 'to install': msg = _('Unable to install module "%s" because an external dependency is not met: %s') elif newstate == 'to upgrade': msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s') else: msg = _('Unable to process module "%s" because an external dependency is not met: %s') raise UserError(msg % (module_name, e.args[0])) def _state_update(self, newstate, states_to_update, level=100): if level < 1: raise UserError(_('Recursion error in modules dependencies !')) # whether some modules are installed with demo data demo = False for module in self: # determine dependency modules to update/others update_mods, ready_mods = self.browse(), self.browse() for dep in module.dependencies_id: if dep.state == 'unknown': raise UserError(_("You try to install module '%s' that depends on module '%s'.\nBut the latter module is not available in your system.") % (module.name, dep.name,)) if dep.depend_id.state == newstate: ready_mods += dep.depend_id else: update_mods += dep.depend_id # update dependency modules that require it, and determine demo for module update_demo = update_mods._state_update(newstate, states_to_update, level=level-1) module_demo = module.demo or update_demo or any(mod.demo for mod in ready_mods) demo = demo or module_demo if module.state in states_to_update: # check dependencies and update module itself self.check_external_dependencies(module.name, newstate) module.write({'state': newstate, 'demo': module_demo}) return demo @assert_log_admin_access def button_install(self): # domain to select auto-installable (but not yet installed) modules auto_domain = [('state', '=', 'uninstalled'), ('auto_install', '=', True)] # determine whether an auto-install module must be installed: # - all its dependencies are installed or to be installed, # - at least one dependency is 'to install' install_states = frozenset(('installed', 'to install', 'to upgrade')) def must_install(module): states = {dep.state for dep in module.dependencies_id if dep.auto_install_required} return states <= install_states and 'to install' in states modules = self while modules: # Mark the given modules and their dependencies to be installed. modules._state_update('to install', ['uninstalled']) # Determine which auto-installable modules must be installed. modules = self.search(auto_domain).filtered(must_install) # the modules that are installed/to install/to upgrade install_mods = self.search([('state', 'in', list(install_states))]) # check individual exclusions install_names = {module.name for module in install_mods} for module in install_mods: for exclusion in module.exclusion_ids: if exclusion.name in install_names: msg = _('Modules "%s" and "%s" are incompatible.') raise UserError(msg % (module.shortdesc, exclusion.exclusion_id.shortdesc)) # check category exclusions def closure(module): todo = result = module while todo: result |= todo todo = todo.dependencies_id.depend_id return result exclusives = self.env['ir.module.category'].search([('exclusive', '=', True)]) for category in exclusives: # retrieve installed modules in category and sub-categories categories = category.search([('id', 'child_of', category.ids)]) modules = install_mods.filtered(lambda mod: mod.category_id in categories) # the installation is valid if all installed modules in categories # belong to the transitive dependencies of one of them if modules and not any(modules <= closure(module) for module in modules): msg = _('You are trying to install incompatible modules in category "%s":') labels = dict(self.fields_get(['state'])['state']['selection']) raise UserError("\n".join([msg % category.name] + [ "- %s (%s)" % (module.shortdesc, labels[module.state]) for module in modules ])) return dict(ACTION_DICT, name=_('Install')) @assert_log_admin_access def button_immediate_install(self): """ Installs the selected module(s) immediately and fully, returns the next res.config action to execute :returns: next res.config item to execute :rtype: dict[str, object] """ _logger.info('User #%d triggered module installation', self.env.uid) # We use here the request object (which is thread-local) as a kind of # "global" env because the env is not usable in the following use case. # When installing a Chart of Account, I would like to send the # allowed companies to configure it on the correct company. # Otherwise, the SUPERUSER won't be aware of that and will try to # configure the CoA on his own company, which makes no sense. if request: request.allowed_company_ids = self.env.companies.ids return self._button_immediate_function(type(self).button_install) @assert_log_admin_access def button_install_cancel(self): self.write({'state': 'uninstalled', 'demo': False}) return True @assert_log_admin_access def module_uninstall(self): """ Perform the various steps required to uninstall a module completely including the deletion of all database structures created by the module: tables, columns, constraints, etc. """ modules_to_remove = self.mapped('name') self.env['ir.model.data']._module_data_uninstall(modules_to_remove) # we deactivate prefetching to not try to read a column that has been deleted self.with_context(prefetch_fields=False).write({'state': 'uninstalled', 'latest_version': False}) return True def _remove_copied_views(self): """ Remove the copies of the views installed by the modules in `self`. Those copies do not have an external id so they will not be cleaned by `_module_data_uninstall`. This is why we rely on `key` instead. It is important to remove these copies because using them will crash if they rely on data that don't exist anymore if the module is removed. """ domain = expression.OR([[('key', '=like', m.name + '.%')] for m in self]) orphans = self.env['ir.ui.view'].with_context(**{'active_test': False, MODULE_UNINSTALL_FLAG: True}).search(domain) orphans.unlink() @api.returns('self') def downstream_dependencies(self, known_deps=None, exclude_states=('uninstalled', 'uninstallable', 'to remove')): """ Return the modules that directly or indirectly depend on the modules in `self`, and that satisfy the `exclude_states` filter. """ if not self: return self known_deps = known_deps or self.browse() query = """ SELECT DISTINCT m.id FROM ir_module_module_dependency d JOIN ir_module_module m ON (d.module_id=m.id) WHERE d.name IN (SELECT name from ir_module_module where id in %s) AND m.state NOT IN %s AND m.id NOT IN %s """ self._cr.execute(query, (tuple(self.ids), tuple(exclude_states), tuple(known_deps.ids or self.ids))) new_deps = self.browse([row[0] for row in self._cr.fetchall()]) missing_mods = new_deps - known_deps known_deps |= new_deps if missing_mods: known_deps |= missing_mods.downstream_dependencies(known_deps, exclude_states) return known_deps @api.returns('self') def upstream_dependencies(self, known_deps=None, exclude_states=('installed', 'uninstallable', 'to remove')): """ Return the dependency tree of modules of the modules in `self`, and that satisfy the `exclude_states` filter. """ if not self: return self known_deps = known_deps or self.browse() query = """ SELECT DISTINCT m.id FROM ir_module_module_dependency d JOIN ir_module_module m ON (d.module_id=m.id) WHERE m.name IN (SELECT name from ir_module_module_dependency where module_id in %s) AND m.state NOT IN %s AND m.id NOT IN %s """ self._cr.execute(query, (tuple(self.ids), tuple(exclude_states), tuple(known_deps.ids or self.ids))) new_deps = self.browse([row[0] for row in self._cr.fetchall()]) missing_mods = new_deps - known_deps known_deps |= new_deps if missing_mods: known_deps |= missing_mods.upstream_dependencies(known_deps, exclude_states) return known_deps def next(self): """ Return the action linked to an ir.actions.todo is there exists one that should be executed. Otherwise, redirect to /web """ Todos = self.env['ir.actions.todo'] _logger.info('getting next %s', Todos) active_todo = Todos.search([('state', '=', 'open')], limit=1) if active_todo: _logger.info('next action is "%s"', active_todo.name) return active_todo.action_launch() return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/web', } def _button_immediate_function(self, function): if getattr(threading.currentThread(), 'testing', False): raise RuntimeError( "Module operations inside tests are not transactional and thus forbidden.\n" "If you really need to perform module operations to test a specific behavior, it " "is best to write it as a standalone script, and ask the runbot/metastorm team " "for help." ) try: # This is done because the installation/uninstallation/upgrade can modify a currently # running cron job and prevent it from finishing, and since the ir_cron table is locked # during execution, the lock won't be released until timeout. self._cr.execute("SELECT * FROM ir_cron FOR UPDATE NOWAIT") except psycopg2.OperationalError: raise UserError(_("Flectra is currently processing a scheduled action.\n" "Module operations are not possible at this time, " "please try again later or contact your system administrator.")) function(self) self._cr.commit() api.Environment.reset() modules.registry.Registry.new(self._cr.dbname, update_module=True) self._cr.commit() env = api.Environment(self._cr, self._uid, self._context) # pylint: disable=next-method-called config = env['ir.module.module'].next() or {} if config.get('type') not in ('ir.actions.act_window_close',): return config # reload the client; open the first available root menu menu = env['ir.ui.menu'].search([('parent_id', '=', False)])[:1] return { 'type': 'ir.actions.client', 'tag': 'reload', 'params': {'menu_id': menu.id}, } @assert_log_admin_access def button_immediate_uninstall(self): """ Uninstall the selected module(s) immediately and fully, returns the next res.config action to execute """ _logger.info('User #%d triggered module uninstallation', self.env.uid) return self._button_immediate_function(type(self).button_uninstall) @assert_log_admin_access def button_uninstall(self): if 'base' in self.mapped('name'): raise UserError(_("The `base` module cannot be uninstalled")) if any(state not in ('installed', 'to upgrade') for state in self.mapped('state')): raise UserError(_( "One or more of the selected modules have already been uninstalled, if you " "believe this to be an error, you may try again later or contact support." )) deps = self.downstream_dependencies() (self + deps).write({'state': 'to remove'}) return dict(ACTION_DICT, name=_('Uninstall')) @assert_log_admin_access def button_uninstall_wizard(self): """ Launch the wizard to uninstall the given module. """ return { 'type': 'ir.actions.act_window', 'target': 'new', 'name': _('Uninstall module'), 'view_mode': 'form', 'res_model': 'base.module.uninstall', 'context': {'default_module_id': self.id}, } def button_uninstall_cancel(self): self.write({'state': 'installed'}) return True @assert_log_admin_access def button_immediate_upgrade(self): """ Upgrade the selected module(s) immediately and fully, return the next res.config action to execute """ return self._button_immediate_function(type(self).button_upgrade) @assert_log_admin_access def button_upgrade(self): Dependency = self.env['ir.module.module.dependency'] self.update_list() todo = list(self) i = 0 while i < len(todo): module = todo[i] i += 1 if module.state not in ('installed', 'to upgrade'): raise UserError(_("Can not upgrade module '%s'. It is not installed.") % (module.name,)) self.check_external_dependencies(module.name, 'to upgrade') for dep in Dependency.search([('name', '=', module.name)]): if ( dep.module_id.state == 'installed' and dep.module_id not in todo and dep.module_id.name != 'studio_customization' ): todo.append(dep.module_id) self.browse(module.id for module in todo).write({'state': 'to upgrade'}) to_install = [] for module in todo: for dep in module.dependencies_id: if dep.state == 'unknown': raise UserError(_('You try to upgrade the module %s that depends on the module: %s.\nBut this module is not available in your system.') % (module.name, dep.name,)) if dep.state == 'uninstalled': to_install += self.search([('name', '=', dep.name)]).ids self.browse(to_install).button_install() return dict(ACTION_DICT, name=_('Apply Schedule Upgrade')) @assert_log_admin_access def button_upgrade_cancel(self): self.write({'state': 'installed'}) return True @staticmethod def get_values_from_terp(terp): return { 'description': terp.get('description', ''), 'shortdesc': terp.get('name', ''), 'author': terp.get('author', 'Unknown'), 'maintainer': terp.get('maintainer', False), 'contributors': ', '.join(terp.get('contributors', [])) or False, 'website': terp.get('website', ''), 'license': terp.get('license', 'LGPL-3'), 'sequence': terp.get('sequence', 100), 'application': terp.get('application', False), 'auto_install': terp.get('auto_install', False) is not False, 'icon': terp.get('icon', False), 'summary': terp.get('summary', ''), 'url': terp.get('url') or terp.get('live_test_url', ''), 'to_buy': False } @api.model def create(self, vals): new = super(Module, self).create(vals) module_metadata = { 'name': 'module_%s' % vals['name'], 'model': 'ir.module.module', 'module': 'base', 'res_id': new.id, 'noupdate': True, } self.env['ir.model.data'].create(module_metadata) return new # update the list of available packages @assert_log_admin_access @api.model def update_list(self): res = [0, 0] # [update, add] default_version = modules.adapt_version('1.0') known_mods = self.with_context(lang=None).search([]) known_mods_names = {mod.name: mod for mod in known_mods} # iterate through detected modules and update/create them in db for mod_name in modules.get_modules(): mod = known_mods_names.get(mod_name) terp = self.get_module_info(mod_name) values = self.get_values_from_terp(terp) if mod: updated_values = {} for key in values: old = getattr(mod, key) if (old or values[key]) and values[key] != old: updated_values[key] = values[key] if terp.get('installable', True) and mod.state == 'uninstallable': updated_values['state'] = 'uninstalled' if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version): res[0] += 1 if updated_values: mod.write(updated_values) else: mod_path = modules.get_module_path(mod_name) if not mod_path or not terp: continue state = "uninstalled" if terp.get('installable', True) else "uninstallable" mod = self.create(dict(name=mod_name, state=state, **values)) res[1] += 1 mod._update_dependencies(terp.get('depends', []), terp.get('auto_install')) mod._update_exclusions(terp.get('excludes', [])) mod._update_category(terp.get('category', 'Uncategorized')) return res @assert_log_admin_access def download(self, download=True): return [] @assert_log_admin_access @api.model def install_from_urls(self, urls): if not self.env.user.has_group('base.group_system'): raise AccessDenied() # One-click install is opt-in - cfr Issue #15225 ad_dir = tools.config.addons_data_dir if not os.access(ad_dir, os.W_OK): msg = (_("Automatic install of downloaded Apps is currently disabled.") + "\n\n" + _("To enable it, make sure this directory exists and is writable on the server:") + "\n%s" % ad_dir) _logger.warning(msg) raise UserError(msg) apps_server = urls.url_parse(self.get_apps_server()) OPENERP = flectra.release.product_name.lower() tmp = tempfile.mkdtemp() _logger.debug('Install from url: %r', urls) try: # 1. Download & unzip missing modules for module_name, url in urls.items(): if not url: continue # nothing to download, local version is already the last one up = urls.url_parse(url) if up.scheme != apps_server.scheme or up.netloc != apps_server.netloc: raise AccessDenied() try: _logger.info('Downloading module `%s` from OpenERP Apps', module_name) response = requests.get(url) response.raise_for_status() content = response.content except Exception: _logger.exception('Failed to fetch module %s', module_name) raise UserError(_('The `%s` module appears to be unavailable at the moment, please try again later.', module_name)) else: zipfile.ZipFile(io.BytesIO(content)).extractall(tmp) assert os.path.isdir(os.path.join(tmp, module_name)) # 2a. Copy/Replace module source in addons path for module_name, url in urls.items(): if module_name == OPENERP or not url: continue # OPENERP is special case, handled below, and no URL means local module module_path = modules.get_module_path(module_name, downloaded=True, display_warning=False) bck = backup(module_path, False) _logger.info('Copy downloaded module `%s` to `%s`', module_name, module_path) shutil.move(os.path.join(tmp, module_name), module_path) if bck: shutil.rmtree(bck) # 2b. Copy/Replace server+base module source if downloaded if urls.get(OPENERP): # special case. it contains the server and the base module. # extract path is not the same base_path = os.path.dirname(modules.get_module_path('base')) # copy all modules in the SERVER/flectra/addons directory to the new "flectra" module (except base itself) for d in os.listdir(base_path): if d != 'base' and os.path.isdir(os.path.join(base_path, d)): destdir = os.path.join(tmp, OPENERP, 'addons', d) # XXX 'flectra' subdirectory ? shutil.copytree(os.path.join(base_path, d), destdir) # then replace the server by the new "base" module server_dir = tools.config['root_path'] # XXX or dirname() bck = backup(server_dir) _logger.info('Copy downloaded module `flectra` to `%s`', server_dir) shutil.move(os.path.join(tmp, OPENERP), server_dir) #if bck: # shutil.rmtree(bck) self.update_list() with_urls = [module_name for module_name, url in urls.items() if url] downloaded = self.search([('name', 'in', with_urls)]) installed = self.search([('id', 'in', downloaded.ids), ('state', '=', 'installed')]) to_install = self.search([('name', 'in', list(urls)), ('state', '=', 'uninstalled')]) post_install_action = to_install.button_immediate_install() if installed or to_install: # in this case, force server restart to reload python code... self._cr.commit() flectra.service.server.restart() return { 'type': 'ir.actions.client', 'tag': 'home', 'params': {'wait': True}, } return post_install_action finally: shutil.rmtree(tmp) @api.model def get_apps_server(self): return tools.config.get('apps_server', 'https://store.flectrahq.com/apps') def _update_dependencies(self, depends=None, auto_install_requirements=()): existing = set(dep.name for dep in self.dependencies_id) needed = set(depends or []) for dep in (needed - existing): self._cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (self.id, dep)) for dep in (existing - needed): self._cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (self.id, dep)) self._cr.execute('UPDATE ir_module_module_dependency SET auto_install_required = (name = any(%s)) WHERE module_id = %s', (list(auto_install_requirements or ()), self.id)) self.invalidate_cache(['dependencies_id'], self.ids) def _update_exclusions(self, excludes=None): existing = set(excl.name for excl in self.exclusion_ids) needed = set(excludes or []) for name in (needed - existing): self._cr.execute('INSERT INTO ir_module_module_exclusion (module_id, name) VALUES (%s, %s)', (self.id, name)) for name in (existing - needed): self._cr.execute('DELETE FROM ir_module_module_exclusion WHERE module_id=%s AND name=%s', (self.id, name)) self.invalidate_cache(['exclusion_ids'], self.ids) def _update_category(self, category='Uncategorized'): current_category = self.category_id current_category_path = [] while current_category: current_category_path.insert(0, current_category.name) current_category = current_category.parent_id categs = category.split('/') if categs != current_category_path: cat_id = modules.db.create_categories(self._cr, categs) self.write({'category_id': cat_id}) def _update_translations(self, filter_lang=None, overwrite=False): if not filter_lang: langs = self.env['res.lang'].get_installed() filter_lang = [code for code, _ in langs] elif not isinstance(filter_lang, (list, tuple)): filter_lang = [filter_lang] update_mods = self.filtered(lambda r: r.state in ('installed', 'to install', 'to upgrade')) mod_dict = { mod.name: mod.dependencies_id.mapped('name') for mod in update_mods } mod_names = topological_sort(mod_dict) self.env['ir.translation']._load_module_terms(mod_names, filter_lang, overwrite) def _check(self): for module in self: if not module.description_html: _logger.warning('module %s: description is empty !', module.name) @api.model @tools.ormcache() def _installed(self): """ Return the set of installed modules as a dictionary {name: id} """ return { module.name: module.id for module in self.sudo().search([('state', '=', 'installed')]) } @api.model def search_panel_select_range(self, field_name, **kwargs): if field_name == 'category_id': enable_counters = kwargs.get('enable_counters', False) domain = [('parent_id', '=', False), ('child_ids.module_ids', '!=', False)] excluded_xmlids = [ 'base.module_category_website_theme', 'base.module_category_theme', ] if not self.user_has_groups('base.group_no_one'): excluded_xmlids.append('base.module_category_hidden') excluded_category_ids = [] for excluded_xmlid in excluded_xmlids: categ = self.env.ref(excluded_xmlid, False) if not categ: continue excluded_category_ids.append(categ.id) if excluded_category_ids: domain = expression.AND([ domain, [('id', 'not in', excluded_category_ids)], ]) Module = self.env['ir.module.module'] records = self.env['ir.module.category'].search_read(domain, ['display_name'], order="sequence") values_range = OrderedDict() for record in records: record_id = record['id'] if enable_counters: model_domain = expression.AND([ kwargs.get('search_domain', []), kwargs.get('category_domain', []), kwargs.get('filter_domain', []), [('category_id', 'child_of', record_id), ('category_id', 'not in', excluded_category_ids)] ]) record['__count'] = Module.search_count(model_domain) values_range[record_id] = record return { 'parent_field': 'parent_id', 'values': list(values_range.values()), } return super(Module, self).search_panel_select_range(field_name, **kwargs)
class rombel(models.Model): _name = 'siswa_ocb11.rombel' name = fields.Char(string="Nama", required=True) # jenjang = fields.Selection([(1, 'PG'), (2, 'TK A'), (3, 'TK B')], string='Jenjang', required=True, default=1) jenjang_id = fields.Many2one('siswa_ocb11.jenjang',string='Jenjang', required=True) siswas = fields.One2many('siswa_ocb11.rombel_siswa', inverse_name='rombel_id' , string='Siswa') kapasitas = fields.Integer('Kapasitas', required=True, default=0) color = fields.Integer(string='Color Index') is_show_on_dashboard = fields.Boolean('Show on Dashboard', default=False) @api.one def get_jumlah_siswa(self): tahunajaran = self.env['siswa_ocb11.tahunajaran'].search([('active','=',True)]) print('tahunajaran : ' + str(tahunajaran.name)) data_siswa = self.siswas.filtered(lambda r: r.tahunajaran_id.id == tahunajaran.id) return len(data_siswa) # @api.onchange('is_show_on_dashboard') # def show_on_dashboard_change(self): # active_id = self._origin.id # if self.is_show_on_dashboard: # # insert to rombel_dashboard # tahunajaran = self.env['siswa_ocb11.tahunajaran'].search([('name','ilike','%'),('active','ilike','%')]) # for ta in tahunajaran: # print('create rombel_dasboard for tahunajaran : ' + ta.name + ' and rombel : ' + self.name) # self.env['siswa_ocb11.rombel_dashboard'].create({ # 'rombel_id' : active_id, # 'tahunajaran_id' : ta.id, # }) # else: # # self.env['siswa_ocb11.rombel_dashboard'].search([('rombel_id', '=', active_id)]).unlink() # query = "delete from siswa_ocb11_rombel_dashboard where rombel_id =" + str(active_id) # self.env.cr.execute(query) # print('Rombel Dashboard Deleted') @api.multi def write(self, vals): if 'is_show_on_dashboard' in vals: active_id = self.id if vals['is_show_on_dashboard']: # insert to rombel_dashboard tahunajaran = self.env['siswa_ocb11.tahunajaran'].search([('name','ilike','%'),('active','ilike','%')]) for ta in tahunajaran: print('create rombel_dasboard for tahunajaran : ' + ta.name + ' and rombel : ' + self.name) rb_dash = self.env['siswa_ocb11.rombel_dashboard'].create({ 'rombel_id' : active_id, 'tahunajaran_id' : ta.id, }) rb_dash.lets_compute_jumlah_siswa() else: # self.env['siswa_ocb11.rombel_dashboard'].search([('rombel_id', '=', active_id)]).unlink() query = "delete from siswa_ocb11_rombel_dashboard where rombel_id =" + str(active_id) self.env.cr.execute(query) print('Rombel Dashboard Deleted') result = super(rombel, self).write(vals) return result