Ejemplo n.º 1
0
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()
Ejemplo n.º 2
0
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)],
        }
Ejemplo n.º 3
0
class Channel(models.Model):
    _inherit = 'slide.channel'

    nbr_certification = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True)
Ejemplo n.º 4
0
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())
Ejemplo n.º 5
0
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()
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
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)
Ejemplo n.º 8
0
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'))
        }
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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)
Ejemplo n.º 11
0
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
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
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
Ejemplo n.º 16
0
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)]
Ejemplo n.º 17
0
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'})
Ejemplo n.º 18
0
class Foo(models.Model):
    _name = 'test_new_api.foo'

    name = fields.Char()
    value1 = fields.Integer(change_default=True)
    value2 = fields.Integer()
Ejemplo n.º 19
0
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')
    ]
Ejemplo n.º 20
0
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
Ejemplo n.º 21
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')
Ejemplo n.º 22
0
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
Ejemplo n.º 23
0
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())
Ejemplo n.º 24
0
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)
Ejemplo n.º 25
0
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'})
Ejemplo n.º 26
0
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,
        }
Ejemplo n.º 27
0
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),
                            })
Ejemplo n.º 28
0
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,
        }
Ejemplo n.º 29
0
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)
Ejemplo n.º 30
0
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