Example #1
0
class EventLeadRule(models.Model):
    """ Rule model for creating / updating leads from event registrations.

    SPECIFICATIONS: CREATION TYPE

    There are two types of lead creation:

      * per attendee: create a lead for each registration;
      * per order: create a lead for a group of registrations;

    The last one is only available through interface if it is possible to register
    a group of attendees in one action (when event_sale or website_event are
    installed). Behavior itself is implemented directly in event_crm.

    Basically a group is either a list of registrations belonging to the same
    event and created in batch (website_event flow). With event_sale this
    definition will be improved to be based on sale_order.

    SPECIFICATIONS: CREATION TRIGGERS

    There are three options to trigger lead creation. We consider basically that
    lead quality increases if attendees confirmed or went to the event. Triggers
    allow therefore to run rules:

      * at attendee creation;
      * at attendee confirmation;
      * at attendee venue;

    This trigger defines when the rule will run.

    SPECIFICATIONS: FILTERING REGISTRATIONS

    When a batch of registrations matches the rule trigger we filter them based
    on conditions and rules defines on event_lead_rule model. Heuristic is the
    following:

      * the rule is active;
      * if a filter is set: filter registrations based on this filter. This is
        done like a search, and filter is a domain;
      * if a company is set on the rule, it must match event's company. Note
        that multi-company rules apply on event_lead_rule;
      * if an event category it set, it must match;
      * if an event is set, it must match;
      * if both event and category are set, one of them must match (OR). If none
        of those are set, it is considered as OK;

    If conditions are met, leads are created with pre-filled informations defined
    on the rule (type, user_id, team_id). Contact information coming from the
    registrations are computed (customer, name, email, phone, mobile, contact_name).

    SPECIFICATIONS: OTHER POINTS

    Note that all rules matching their conditions are applied. This means more
    than one lead can be created depending on the configuration. This is
    intended in order to give more freedom to the user using the automatic
    lead generation.
    """
    _name = "event.lead.rule"
    _description = "Event Lead Rules"

    # Definition
    name = fields.Char('Rule Name', required=True, translate=True)
    active = fields.Boolean('Active', default=True)
    lead_ids = fields.One2many('crm.lead',
                               'event_lead_rule_id',
                               string='Created Leads',
                               groups='sales_team.group_sale_salesman')
    # Triggers
    lead_creation_basis = fields.Selection(
        [('attendee', 'Per Attendee'), ('order', 'Per Order')],
        string='Create',
        default='attendee',
        required=True,
        help='Per Attendee : A Lead is created for each Attendee (B2C).\n'
        'Per Order : A single Lead is created per Ticket Batch/Sale Order (B2B)'
    )
    lead_creation_trigger = fields.Selection(
        [('create', 'Attendees are created'),
         ('confirm', 'Attendees are confirmed'),
         ('done', 'Attendees attended')],
        string='When',
        default='create',
        required=True,
        help='Creation: at attendee creation;\n'
        'Confirmation: when attendee is confirmed, manually or automatically;\n'
        'Attended: when attendance is confirmed and registration set to done;')
    # Filters
    event_type_ids = fields.Many2many(
        'event.type',
        string='Event Categories',
        help=
        'Filter the attendees to include those of this specific event category. If not set, no event category restriction will be applied.'
    )
    event_id = fields.Many2one(
        'event.event',
        string='Event',
        domain=
        "[('company_id', 'in', [company_id or current_company_id, False])]",
        help=
        'Filter the attendees to include those of this specific event. If not set, no event restriction will be applied.'
    )
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        help=
        "Restrict the trigger of this rule to events belonging to a specific company.\nIf not set, no company restriction will be applied."
    )
    event_registration_filter = fields.Text(
        string="Registrations Domain",
        help="Filter the attendees that will or not generate leads.")
    # Lead default_value fields
    lead_type = fields.Selection(
        [('lead', 'Lead'), ('opportunity', 'Opportunity')],
        string="Lead Type",
        required=True,
        default=lambda self: 'lead' if self.env['res.users'].has_group(
            'crm.group_use_lead') else 'opportunity',
        help="Default lead type when this rule is applied.")
    lead_sales_team_id = fields.Many2one(
        'crm.team',
        string='Sales Team',
        help="Automatically assign the created leads to this Sales Team.")
    lead_user_id = fields.Many2one(
        'res.users',
        string='Salesperson',
        help="Automatically assign the created leads to this Salesperson.")
    lead_tag_ids = fields.Many2many(
        'crm.tag',
        string='Tags',
        help="Automatically add these tags to the created leads.")

    def _run_on_registrations(self, registrations):
        """ Create or update leads based on rule configuration. Two main lead
        management type exists

          * per attendee: each registration creates a lead;
          * per order: registrations are grouped per group and one lead is created
            or updated with the batch (used mainly with sale order configuration
            in event_sale);

        Heuristic

          * first, check existing lead linked to registrations to ensure no
            duplication. Indeed for example attendee status change may trigger
            the same rule several times;
          * then for each rule, get the subset of registrations matching its
            filters;
          * then for each order-based rule, get the grouping information. This
            give a list of registrations by group (event, sale_order), with maybe
            an already-existing lead to update instead of creating a new one;
          * finally apply rules. Attendee-based rules create a lead for each
            attendee, group-based rules use the grouping information to create
            or update leads;

        :param registrations: event.registration recordset on which rules given by
          self have to run. Triggers should already be checked, only filters are
          applied here.

        :return leads: newly-created leads. Updated leads are not returned.
        """
        # order by ID, ensure first created wins
        registrations = registrations.sorted('id')

        # first: ensure no duplicate by searching existing registrations / rule
        existing_leads = self.env['crm.lead'].search([
            ('registration_ids', 'in', registrations.ids),
            ('event_lead_rule_id', 'in', self.ids)
        ])
        rule_to_existing_regs = defaultdict(
            lambda: self.env['event.registration'])
        for lead in existing_leads:
            rule_to_existing_regs[
                lead.event_lead_rule_id] += lead.registration_ids

        # second: check registrations matching rules (in batch)
        new_registrations = self.env['event.registration']
        rule_to_new_regs = dict()
        for rule in self:
            new_for_rule = registrations.filtered(
                lambda reg: reg not in rule_to_existing_regs[rule])
            rule_registrations = rule._filter_registrations(new_for_rule)
            new_registrations |= rule_registrations
            rule_to_new_regs[rule] = rule_registrations
        new_registrations.sorted('id')  # as an OR was used, re-ensure order

        # third: check grouping
        order_based_rules = self.filtered(
            lambda rule: rule.lead_creation_basis == 'order')
        rule_group_info = new_registrations._get_lead_grouping(
            order_based_rules, rule_to_new_regs)

        lead_vals_list = []
        for rule in self:
            if rule.lead_creation_basis == 'attendee':
                matching_registrations = rule_to_new_regs[rule].sorted('id')
                for registration in matching_registrations:
                    lead_vals_list.append(registration._get_lead_values(rule))
            else:
                # check if registrations are part of a group, for example a sale order, to know if we update or create leads
                for (toupdate_leads, group_key,
                     group_registrations) in rule_group_info[rule]:
                    if toupdate_leads:
                        additionnal_description = group_registrations._get_lead_description(
                            _("New registrations"), line_counter=True)
                        for lead in toupdate_leads:
                            lead.write({
                                'description':
                                "%s\n%s" %
                                (lead.description, additionnal_description),
                                'registration_ids':
                                [(4, reg.id) for reg in group_registrations],
                            })
                    elif group_registrations:
                        lead_vals_list.append(
                            group_registrations._get_lead_values(rule))

        return self.env['crm.lead'].create(lead_vals_list)

    def _filter_registrations(self, registrations):
        """ Keep registrations matching rule conditions. Those are

          * if a filter is set: filter registrations based on this filter. This is
            done like a search, and filter is a domain;
          * if a company is set on the rule, it must match event's company. Note
            that multi-company rules apply on event_lead_rule;
          * if an event category it set, it must match;
          * if an event is set, it must match;
          * if both event and category are set, one of them must match (OR). If none
            of those are set, it is considered as OK;

        :param registrations: event.registration recordset on which rule filters
          will be evaluated;
        :return: subset of registrations matching rules
        """
        self.ensure_one()
        if self.event_registration_filter and self.event_registration_filter != '[]':
            registrations = registrations.search(
                expression.AND([[('id', 'in', registrations.ids)],
                                literal_eval(self.event_registration_filter)]))

        # check from direct m2o to linked m2o / o2m to filter first without inner search
        company_ok = lambda registration: registration.company_id == self.company_id if self.company_id else True
        event_or_event_type_ok = \
            lambda registration: \
                registration.event_id == self.event_id or registration.event_id.event_type_id in self.event_type_ids \
                if (self.event_id or self.event_type_ids) else True

        return registrations.filtered(
            lambda r: company_ok(r) and event_or_event_type_ok(r))
Example #2
0
class SurveyUserInputLine(models.Model):
    _name = 'survey.user_input.line'
    _description = 'Survey User Input Line'
    _rec_name = 'user_input_id'
    _order = 'question_sequence, id'

    # survey data
    user_input_id = fields.Many2one('survey.user_input',
                                    string='User Input',
                                    ondelete='cascade',
                                    required=True)
    survey_id = fields.Many2one(related='user_input_id.survey_id',
                                string='Survey',
                                store=True,
                                readonly=False)
    question_id = fields.Many2one('survey.question',
                                  string='Question',
                                  ondelete='cascade',
                                  required=True)
    page_id = fields.Many2one(related='question_id.page_id',
                              string="Section",
                              readonly=False)
    question_sequence = fields.Integer('Sequence',
                                       related='question_id.sequence',
                                       store=True)
    # answer
    skipped = fields.Boolean('Skipped')
    answer_type = fields.Selection([('text_box', 'Free Text'),
                                    ('char_box', 'Text'),
                                    ('numerical_box', 'Number'),
                                    ('date', 'Date'), ('datetime', 'Datetime'),
                                    ('suggestion', 'Suggestion')],
                                   string='Answer Type')
    value_char_box = fields.Char('Text answer')
    value_numerical_box = fields.Float('Numerical answer')
    value_date = fields.Date('Date answer')
    value_datetime = fields.Datetime('Datetime answer')
    value_text_box = fields.Text('Free Text answer')
    suggested_answer_id = fields.Many2one('survey.question.answer',
                                          string="Suggested answer")
    matrix_row_id = fields.Many2one('survey.question.answer',
                                    string="Row answer")
    # scoring
    answer_score = fields.Float('Score')
    answer_is_correct = fields.Boolean('Correct')

    @api.constrains('skipped', 'answer_type')
    def _check_answer_type_skipped(self):
        for line in self:
            if (line.skipped == bool(line.answer_type)):
                raise ValidationError(
                    _('A question can either be skipped or answered, not both.'
                      ))

            # allow 0 for numerical box
            if line.answer_type == 'numerical_box' and float_is_zero(
                    line['value_numerical_box'], precision_digits=6):
                continue
            if line.answer_type == 'suggestion':
                field_name = 'suggested_answer_id'
            elif line.answer_type:
                field_name = 'value_%s' % line.answer_type
            else:  # skipped
                field_name = False

            if field_name and not line[field_name]:
                raise ValidationError(
                    _('The answer must be in the right type'))

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            score_vals = self._get_answer_score_values(vals)
            if not vals.get('answer_score'):
                vals.update(score_vals)
        return super(SurveyUserInputLine, self).create(vals_list)

    def write(self, vals):
        score_vals = self._get_answer_score_values(vals,
                                                   compute_speed_score=False)
        if not vals.get('answer_score'):
            vals.update(score_vals)
        return super(SurveyUserInputLine, self).write(vals)

    @api.model
    def _get_answer_score_values(self, vals, compute_speed_score=True):
        """ Get values for: answer_is_correct and associated answer_score.

        Requires vals to contain 'answer_type', 'question_id', and 'user_input_id'.
        Depending on 'answer_type' additional value of 'suggested_answer_id' may also be
        required.

        Calculates whether an answer_is_correct and its score based on 'answer_type' and
        corresponding question. Handles choice (answer_type == 'suggestion') questions
        separately from other question types. Each selected choice answer is handled as an
        individual answer.

        If score depends on the speed of the answer, it is adjusted as follows:
         - If the user answers in less than 2 seconds, they receive 100% of the possible points.
         - If user answers after that, they receive 50% of the possible points + the remaining
            50% scaled by the time limit and time taken to answer [i.e. a minimum of 50% of the
            possible points is given to all correct answers]

        Example of returned values:
            * {'answer_is_correct': False, 'answer_score': 0} (default)
            * {'answer_is_correct': True, 'answer_score': 2.0}
        """
        user_input_id = vals.get('user_input_id')
        answer_type = vals.get('answer_type')
        question_id = vals.get('question_id')
        if not question_id:
            raise ValueError(
                _('Computing score requires a question in arguments.'))
        question = self.env['survey.question'].browse(int(question_id))

        # default and non-scored questions
        answer_is_correct = False
        answer_score = 0

        # record selected suggested choice answer_score (can be: pos, neg, or 0)
        if question.question_type in ['simple_choice', 'multiple_choice']:
            if answer_type == 'suggestion':
                suggested_answer_id = vals.get('suggested_answer_id')
                if suggested_answer_id:
                    question_answer = self.env[
                        'survey.question.answer'].browse(
                            int(suggested_answer_id))
                    answer_score = question_answer.answer_score
                    answer_is_correct = question_answer.is_correct
        # for all other scored question cases, record question answer_score (can be: pos or 0)
        elif question.is_scored_question:
            answer = vals.get('value_%s' % answer_type)
            if answer_type == 'numerical_box':
                answer = float(answer)
            elif answer_type == 'date':
                answer = fields.Date.from_string(answer)
            elif answer_type == 'datetime':
                answer = fields.Datetime.from_string(answer)
            if answer and answer == question['answer_%s' % answer_type]:
                answer_is_correct = True
                answer_score = question.answer_score

        if compute_speed_score and answer_score > 0:
            user_input = self.env['survey.user_input'].browse(user_input_id)
            session_speed_rating = user_input.exists(
            ) and user_input.is_session_answer and user_input.survey_id.session_speed_rating
            if session_speed_rating:
                max_score_delay = 2
                time_limit = question.time_limit
                now = fields.Datetime.now()
                seconds_to_answer = (
                    now - user_input.survey_id.session_question_start_time
                ).total_seconds()
                question_remaining_time = time_limit - seconds_to_answer
                # if answered within the max_score_delay => leave score as is
                if question_remaining_time < 0:  # if no time left
                    answer_score /= 2
                elif seconds_to_answer > max_score_delay:
                    time_limit -= max_score_delay  # we remove the max_score_delay to have all possible values
                    score_proportion = (time_limit -
                                        seconds_to_answer) / time_limit
                    answer_score = (answer_score / 2) * (1 + score_proportion)

        return {
            'answer_is_correct': answer_is_correct,
            'answer_score': answer_score
        }
Example #3
0
class FleetVehicleLogServices(models.Model):
    _name = 'fleet.vehicle.log.services'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _rec_name = 'service_type_id'
    _description = 'Services for vehicles'

    active = fields.Boolean(default=True)
    vehicle_id = fields.Many2one('fleet.vehicle',
                                 'Vehicle',
                                 required=True,
                                 help='Vehicle concerned by this log')
    amount = fields.Monetary('Cost')
    description = fields.Char('Description')
    odometer_id = fields.Many2one(
        'fleet.vehicle.odometer',
        'Odometer',
        help='Odometer measure of the vehicle at the moment of this log')
    odometer = fields.Float(
        compute="_get_odometer",
        inverse='_set_odometer',
        string='Odometer Value',
        help='Odometer measure of the vehicle at the moment of this log')
    odometer_unit = fields.Selection(related='vehicle_id.odometer_unit',
                                     string="Unit",
                                     readonly=True)
    date = fields.Date(help='Date when the cost has been executed',
                       default=fields.Date.context_today)
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env.company)
    currency_id = fields.Many2one('res.currency',
                                  related='company_id.currency_id')
    purchaser_id = fields.Many2one('res.partner',
                                   string="Driver",
                                   compute='_compute_purchaser_id',
                                   readonly=False,
                                   store=True)
    inv_ref = fields.Char('Vendor Reference')
    vendor_id = fields.Many2one('res.partner', 'Vendor')
    notes = fields.Text()
    service_type_id = fields.Many2one(
        'fleet.service.type',
        'Service Type',
        required=True,
        default=lambda self: self.env.ref('fleet.type_service_service_8',
                                          raise_if_not_found=False),
    )
    state = fields.Selection([
        ('todo', 'To Do'),
        ('running', 'Running'),
        ('done', 'Done'),
        ('cancelled', 'Cancelled'),
    ],
                             default='todo',
                             string='Stage')

    def _get_odometer(self):
        self.odometer = 0
        for record in self:
            if record.odometer_id:
                record.odometer = record.odometer_id.value

    def _set_odometer(self):
        for record in self:
            if not record.odometer:
                raise UserError(
                    _('Emptying the odometer value of a vehicle is not allowed.'
                      ))
            odometer = self.env['fleet.vehicle.odometer'].create({
                'value':
                record.odometer,
                'date':
                record.date or fields.Date.context_today(record),
                'vehicle_id':
                record.vehicle_id.id
            })
            self.odometer_id = odometer

    @api.model_create_multi
    def create(self, vals_list):
        for data in vals_list:
            if 'odometer' in data and not data['odometer']:
                # if received value for odometer is 0, then remove it from the
                # data as it would result to the creation of a
                # odometer log with 0, which is to be avoided
                del data['odometer']
        return super(FleetVehicleLogServices, self).create(vals_list)

    @api.depends('vehicle_id')
    def _compute_purchaser_id(self):
        for service in self:
            service.purchaser_id = service.vehicle_id.driver_id
Example #4
0
class CrmTeam(models.Model):
    _name = "crm.team"
    _inherit = ['mail.thread', 'ir.branch.company.mixin']
    _description = "Sales Channel"
    _order = "name"

    @api.model
    @api.returns('self', lambda value: value.id if value else False)
    def _get_default_team_id(self, user_id=None):
        if not user_id:
            user_id = self.env.uid
        company_id = self.sudo(user_id).env.user.company_id.id
        team_id = self.env['crm.team'].sudo().search([
            '|', ('user_id', '=', user_id), ('member_ids', '=', user_id), '|',
            ('company_id', '=', False),
            ('company_id', 'child_of', [company_id])
        ],
                                                     limit=1)
        if not team_id and 'default_team_id' in self.env.context:
            team_id = self.env['crm.team'].browse(
                self.env.context.get('default_team_id'))
        if not team_id:
            default_team_id = self.env.ref('sales_team.team_sales_department',
                                           raise_if_not_found=False)
            if default_team_id:
                try:
                    default_team_id.check_access_rule('read')
                except AccessError:
                    return self.env['crm.team']
                if self.env.context.get(
                        'default_type'
                ) != 'lead' or default_team_id.use_leads and default_team_id.active:
                    team_id = default_team_id
        return team_id

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

    name = fields.Char('Sales Channel', required=True, translate=True)
    active = fields.Boolean(
        default=True,
        help=
        "If the active field is set to false, it will allow you to hide the sales channel without removing it."
    )
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('crm.team'))
    currency_id = fields.Many2one("res.currency",
                                  related='company_id.currency_id',
                                  string="Currency",
                                  readonly=True)
    user_id = fields.Many2one('res.users', string='Channel Leader')
    member_ids = fields.One2many('res.users',
                                 'sale_team_id',
                                 string='Channel Members')
    favorite_user_ids = fields.Many2many(
        'res.users',
        'team_favorite_user_rel',
        'team_id',
        'user_id',
        string='Favorite Members',
        default=_get_default_favorite_user_ids)
    is_favorite = fields.Boolean(
        string='Show on dashboard',
        compute='_compute_is_favorite',
        inverse='_inverse_is_favorite',
        help=
        "Favorite teams to display them in the dashboard and access them easily."
    )
    reply_to = fields.Char(
        string='Reply-To',
        help=
        "The email address put in the 'Reply-To' of all emails sent by Flectra about cases in this sales channel"
    )
    color = fields.Integer(string='Color Index',
                           help="The color of the channel")
    team_type = fields.Selection(
        [('sales', 'Sales'), ('website', 'Website')],
        string='Channel Type',
        default='sales',
        required=True,
        help=
        "The type of this channel, it will define the resources this channel uses."
    )
    dashboard_button_name = fields.Char(
        string="Dashboard Button", compute='_compute_dashboard_button_name')
    dashboard_graph_data = fields.Text(compute='_compute_dashboard_graph')
    dashboard_graph_type = fields.Selection(
        [
            ('line', 'Line'),
            ('bar', 'Bar'),
        ],
        string='Type',
        compute='_compute_dashboard_graph',
        help='The type of graph this channel will display in the dashboard.')
    dashboard_graph_model = fields.Selection(
        [],
        string="Content",
        help='The graph this channel will display in the Dashboard.\n')
    dashboard_graph_group = fields.Selection(
        [
            ('day', 'Day'),
            ('week', 'Week'),
            ('month', 'Month'),
            ('user', 'Salesperson'),
        ],
        string='Group by',
        default='day',
        help="How this channel's dashboard graph will group the results.")
    dashboard_graph_period = fields.Selection(
        [
            ('week', 'Last Week'),
            ('month', 'Last Month'),
            ('year', 'Last Year'),
        ],
        string='Scale',
        default='month',
        help="The time period this channel's dashboard graph will consider.")

    @api.constrains('company_id', 'branch_id')
    def _check_company_branch(self):
        for record in self:
            if record.branch_id and record.company_id and record.company_id != record.branch_id.company_id:
                raise ValidationError(
                    _('Configuration Error of Company:\n'
                      'The Company (%s) in the Team and '
                      'the Company (%s) of Branch must '
                      'be the same company!') %
                    (record.company_id.name, record.branch_id.company_id.name))

    @api.depends('dashboard_graph_group', 'dashboard_graph_model',
                 'dashboard_graph_period')
    def _compute_dashboard_graph(self):
        for team in self.filtered('dashboard_graph_model'):
            if team.dashboard_graph_group in (False, 'user') or team.dashboard_graph_period == 'week' and team.dashboard_graph_group != 'day' \
                    or team.dashboard_graph_period == 'month' and team.dashboard_graph_group != 'day':
                team.dashboard_graph_type = 'bar'
            else:
                team.dashboard_graph_type = 'line'
            team.dashboard_graph_data = json.dumps(team._get_graph())

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

    def _inverse_is_favorite(self):
        sudoed_self = self.sudo()
        to_fav = sudoed_self.filtered(
            lambda team: self.env.user not in team.favorite_user_ids)
        to_fav.write({'favorite_user_ids': [(4, self.env.uid)]})
        (sudoed_self - to_fav).write(
            {'favorite_user_ids': [(3, self.env.uid)]})
        return True

    def _graph_get_dates(self, today):
        """ return a coherent start and end date for the dashboard graph according to the graph settings.
        """
        if self.dashboard_graph_period == 'week':
            start_date = today - relativedelta(weeks=1)
        elif self.dashboard_graph_period == 'year':
            start_date = today - relativedelta(years=1)
        else:
            start_date = today - relativedelta(months=1)

        # we take the start of the following month/week/day if we group by month/week/day
        # (to avoid having twice the same month/week/day from different years/month/week)
        if self.dashboard_graph_group == 'month':
            start_date = date(start_date.year + start_date.month // 12,
                              start_date.month % 12 + 1, 1)
            # handle period=week, grouping=month for silly managers
            if self.dashboard_graph_period == 'week':
                start_date = today.replace(day=1)
        elif self.dashboard_graph_group == 'week':
            start_date += relativedelta(days=8 - start_date.isocalendar()[2])
            # add a week to make sure no overlapping is possible in case of year period (will display max 52 weeks, avoid case of 53 weeks in a year)
            if self.dashboard_graph_period == 'year':
                start_date += relativedelta(weeks=1)
        else:
            start_date += relativedelta(days=1)

        return [start_date, today]

    def _graph_date_column(self):
        return 'create_date'

    def _graph_x_query(self):
        if self.dashboard_graph_group == 'user':
            return 'user_id'
        elif self.dashboard_graph_group == 'week':
            return 'EXTRACT(WEEK FROM %s)' % self._graph_date_column()
        elif self.dashboard_graph_group == 'month':
            return 'EXTRACT(MONTH FROM %s)' % self._graph_date_column()
        else:
            return 'DATE(%s)' % self._graph_date_column()

    def _graph_y_query(self):
        raise UserError(
            _('Undefined graph model for Sales Channel: %s') % self.name)

    def _extra_sql_conditions(self):
        return ''

    def _graph_title_and_key(self):
        """ Returns an array containing the appropriate graph title and key respectively.

            The key is for lineCharts, to have the on-hover label.
        """
        return ['', '']

    def _graph_data(self, start_date, end_date):
        """ return format should be an iterable of dicts that contain {'x_value': ..., 'y_value': ...}
            x_values should either be dates, weeks, months or user_ids depending on the self.dashboard_graph_group value.
            y_values are floats.
        """
        query = """SELECT %(x_query)s as x_value, %(y_query)s as y_value
                     FROM %(table)s
                    WHERE team_id = %(team_id)s
                      AND DATE(%(date_column)s) >= %(start_date)s
                      AND DATE(%(date_column)s) <= %(end_date)s
                      %(extra_conditions)s
                    GROUP BY x_value;"""

        # apply rules
        if not self.dashboard_graph_model:
            raise UserError(
                _('Undefined graph model for Sales Channel: %s') % self.name)
        GraphModel = self.env[self.dashboard_graph_model]
        graph_table = GraphModel._table
        extra_conditions = self._extra_sql_conditions()
        where_query = GraphModel._where_calc([])
        GraphModel._apply_ir_rules(where_query, 'read')
        from_clause, where_clause, where_clause_params = where_query.get_sql()
        if where_clause:
            extra_conditions += " AND " + where_clause

        query = query % {
            'x_query': self._graph_x_query(),
            'y_query': self._graph_y_query(),
            'table': graph_table,
            'team_id': "%s",
            'date_column': self._graph_date_column(),
            'start_date': "%s",
            'end_date': "%s",
            'extra_conditions': extra_conditions
        }
        self._cr.execute(query,
                         [self.id, start_date, end_date] + where_clause_params)
        return self.env.cr.dictfetchall()

    def _get_graph(self):
        def get_week_name(start_date, locale):
            """ Generates a week name (string) from a datetime according to the locale:
                E.g.: locale    start_date (datetime)      return string
                      "en_US"      November 16th           "16-22 Nov"
                      "en_US"      December 28th           "28 Dec-3 Jan"
            """
            if (start_date + relativedelta(days=6)).month == start_date.month:
                short_name_from = format_date(start_date, 'd', locale=locale)
            else:
                short_name_from = format_date(start_date,
                                              'd MMM',
                                              locale=locale)
            short_name_to = format_date(start_date + relativedelta(days=6),
                                        'd MMM',
                                        locale=locale)
            return short_name_from + '-' + short_name_to

        self.ensure_one()
        values = []
        today = fields.Date.from_string(fields.Date.context_today(self))
        start_date, end_date = self._graph_get_dates(today)
        graph_data = self._graph_data(start_date, end_date)

        # line graphs and bar graphs require different labels
        if self.dashboard_graph_type == 'line':
            x_field = 'x'
            y_field = 'y'
        else:
            x_field = 'label'
            y_field = 'value'

        # generate all required x_fields and update the y_values where we have data for them
        locale = self._context.get('lang') or 'en_US'
        if self.dashboard_graph_group == 'day':
            for day in range(0, (end_date - start_date).days + 1):
                short_name = format_date(start_date + relativedelta(days=day),
                                         'd MMM',
                                         locale=locale)
                values.append({x_field: short_name, y_field: 0})
            for data_item in graph_data:
                index = (
                    datetime.strptime(data_item.get('x_value'), DF).date() -
                    start_date).days
                values[index][y_field] = data_item.get('y_value')

        elif self.dashboard_graph_group == 'week':
            weeks_in_start_year = int(
                date(start_date.year, 12, 28).isocalendar()
                [1])  # This date is always in the last week of ISO years
            for week in range(
                    0,
                (end_date.isocalendar()[1] - start_date.isocalendar()[1]) %
                    weeks_in_start_year + 1):
                short_name = get_week_name(
                    start_date + relativedelta(days=7 * week), locale)
                values.append({x_field: short_name, y_field: 0})

            for data_item in graph_data:
                index = int(
                    (data_item.get('x_value') - start_date.isocalendar()[1]) %
                    weeks_in_start_year)
                values[index][y_field] = data_item.get('y_value')

        elif self.dashboard_graph_group == 'month':
            for month in range(0,
                               (end_date.month - start_date.month) % 12 + 1):
                short_name = format_date(start_date +
                                         relativedelta(months=month),
                                         'MMM',
                                         locale=locale)
                values.append({x_field: short_name, y_field: 0})

            for data_item in graph_data:
                index = int((data_item.get('x_value') - start_date.month) % 12)
                values[index][y_field] = data_item.get('y_value')

        elif self.dashboard_graph_group == 'user':
            for data_item in graph_data:
                values.append({
                    x_field:
                    self.env['res.users'].browse(data_item.get('x_value')).name
                    or _('Not Defined'),
                    y_field:
                    data_item.get('y_value')
                })

        else:
            for data_item in graph_data:
                values.append({
                    x_field: data_item.get('x_value'),
                    y_field: data_item.get('y_value')
                })

        [graph_title, graph_key] = self._graph_title_and_key()
        color = '#009efb' if '+e' in version else '#7c7bad'
        return [{
            'values': values,
            'area': True,
            'title': graph_title,
            'key': graph_key,
            'color': color
        }]

    def _compute_dashboard_button_name(self):
        """ Sets the adequate dashboard button name depending on the sales channel's options
        """
        for team in self:
            team.dashboard_button_name = _(
                "Big Pretty Button :)")  # placeholder

    def action_primary_channel_button(self):
        """ skeleton function to be overloaded
            It will return the adequate action depending on the sales channel's options
        """
        return False

    def _onchange_team_type(self):
        """ skeleton function defined here because it'll be called by crm and/or sale
        """
        self.ensure_one()

    @api.model
    def create(self, values):
        team = super(
            CrmTeam,
            self.with_context(mail_create_nosubscribe=True)).create(values)
        if values.get('member_ids'):
            team._add_members_to_favorites()
        return team

    @api.multi
    def write(self, values):
        res = super(CrmTeam, self).write(values)
        if values.get('member_ids'):
            self._add_members_to_favorites()
        return res

    def _add_members_to_favorites(self):
        for team in self:
            team.favorite_user_ids = [(4, member.id)
                                      for member in team.member_ids]
Example #5
0
class FleetVehicleLogFuel(models.Model):
    _name = 'fleet.vehicle.log.fuel'
    _description = 'Fuel log for vehicles'
    _inherits = {'fleet.vehicle.cost': 'cost_id'}

    @api.model
    def default_get(self, default_fields):
        res = super(FleetVehicleLogFuel, self).default_get(default_fields)
        service = self.env.ref('fleet.type_service_refueling',
                               raise_if_not_found=False)
        res.update({
            'date': fields.Date.context_today(self),
            'cost_subtype_id': service and service.id or False,
            'cost_type': 'fuel'
        })
        return res

    liter = fields.Float()
    price_per_liter = fields.Float()
    purchaser_id = fields.Many2one(
        'res.partner',
        'Purchaser',
        domain="['|',('customer','=',True),('employee','=',True)]")
    inv_ref = fields.Char('Invoice Reference', size=64)
    vendor_id = fields.Many2one('res.partner',
                                'Vendor',
                                domain="[('supplier','=',True)]")
    notes = fields.Text()
    cost_id = fields.Many2one('fleet.vehicle.cost',
                              'Cost',
                              required=True,
                              ondelete='cascade')
    # we need to keep this field as a related with store=True because the graph view doesn't support
    # (1) to address fields from inherited table
    # (2) fields that aren't stored in database
    cost_amount = fields.Float(related='cost_id.amount',
                               string='Amount',
                               store=True)

    @api.onchange('vehicle_id')
    def _onchange_vehicle(self):
        if self.vehicle_id:
            self.odometer_unit = self.vehicle_id.odometer_unit
            self.purchaser_id = self.vehicle_id.driver_id.id

    @api.onchange('liter', 'price_per_liter', 'amount')
    def _onchange_liter_price_amount(self):
        # need to cast in float because the value receveid from web client maybe an integer (Javascript and JSON do not
        # make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
        # liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
        # of 3.0/2=1.5)
        # If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
        # onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
        # computation to 2 decimal
        liter = float(self.liter)
        price_per_liter = float(self.price_per_liter)
        amount = float(self.amount)
        if liter > 0 and price_per_liter > 0 and round(liter * price_per_liter,
                                                       2) != amount:
            self.amount = round(liter * price_per_liter, 2)
        elif amount > 0 and liter > 0 and round(amount / liter,
                                                2) != price_per_liter:
            self.price_per_liter = round(amount / liter, 2)
        elif amount > 0 and price_per_liter > 0 and round(
                amount / price_per_liter, 2) != liter:
            self.liter = round(amount / price_per_liter, 2)
Example #6
0
class FleetVehicle(models.Model):
    _inherit = 'mail.thread'
    _name = 'fleet.vehicle'
    _description = 'Information on a vehicle'
    _order = 'license_plate asc'

    def _get_default_state(self):
        state = self.env.ref('fleet.vehicle_state_active',
                             raise_if_not_found=False)
        return state and state.id or False

    name = fields.Char(compute="_compute_vehicle_name", store=True)
    active = fields.Boolean('Active',
                            default=True,
                            track_visibility="onchange")
    company_id = fields.Many2one('res.company', 'Company')
    license_plate = fields.Char(
        required=True,
        help='License plate number of the vehicle (i = plate number for a car)'
    )
    vin_sn = fields.Char(
        'Chassis Number',
        help='Unique number written on the vehicle motor (VIN/SN number)',
        copy=False)
    driver_id = fields.Many2one('res.partner',
                                'Driver',
                                help='Driver of the vehicle')
    model_id = fields.Many2one('fleet.vehicle.model',
                               'Model',
                               required=True,
                               help='Model of the vehicle')
    log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id',
                               'Fuel Logs')
    log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id',
                                   'Services Logs')
    log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id',
                                    'Contracts')
    cost_count = fields.Integer(compute="_compute_count_all", string="Costs")
    contract_count = fields.Integer(compute="_compute_count_all",
                                    string='Contracts')
    service_count = fields.Integer(compute="_compute_count_all",
                                   string='Services')
    fuel_logs_count = fields.Integer(compute="_compute_count_all",
                                     string='Fuel Logs')
    odometer_count = fields.Integer(compute="_compute_count_all",
                                    string='Odometer')
    acquisition_date = fields.Date(
        'Immatriculation Date',
        required=False,
        help='Date when the vehicle has been immatriculated')
    color = fields.Char(help='Color of the vehicle')
    state_id = fields.Many2one('fleet.vehicle.state',
                               'State',
                               default=_get_default_state,
                               help='Current state of the vehicle',
                               ondelete="set null")
    location = fields.Char(help='Location of the vehicle (garage, ...)')
    seats = fields.Integer('Seats Number',
                           help='Number of seats of the vehicle')
    model_year = fields.Char('Model Year', help='Year of the model')
    doors = fields.Integer('Doors Number',
                           help='Number of doors of the vehicle',
                           default=5)
    tag_ids = fields.Many2many('fleet.vehicle.tag',
                               'fleet_vehicle_vehicle_tag_rel',
                               'vehicle_tag_id',
                               'tag_id',
                               'Tags',
                               copy=False)
    odometer = fields.Float(
        compute='_get_odometer',
        inverse='_set_odometer',
        string='Last Odometer',
        help='Odometer measure of the vehicle at the moment of this log')
    odometer_unit = fields.Selection([('kilometers', 'Kilometers'),
                                      ('miles', 'Miles')],
                                     'Odometer Unit',
                                     default='kilometers',
                                     help='Unit of the odometer ',
                                     required=True)
    transmission = fields.Selection([('manual', 'Manual'),
                                     ('automatic', 'Automatic')],
                                    'Transmission',
                                    help='Transmission Used by the vehicle')
    fuel_type = fields.Selection([('gasoline', 'Gasoline'),
                                  ('diesel', 'Diesel'),
                                  ('electric', 'Electric'),
                                  ('hybrid', 'Hybrid')],
                                 'Fuel Type',
                                 help='Fuel Used by the vehicle')
    horsepower = fields.Integer()
    horsepower_tax = fields.Float('Horsepower Taxation')
    power = fields.Integer('Power', help='Power in kW of the vehicle')
    co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle')
    image = fields.Binary(related='model_id.image', string="Logo")
    image_medium = fields.Binary(related='model_id.image_medium',
                                 string="Logo (medium)")
    image_small = fields.Binary(related='model_id.image_small',
                                string="Logo (small)")
    contract_renewal_due_soon = fields.Boolean(
        compute='_compute_contract_reminder',
        search='_search_contract_renewal_due_soon',
        string='Has Contracts to renew',
        multi='contract_info')
    contract_renewal_overdue = fields.Boolean(
        compute='_compute_contract_reminder',
        search='_search_get_overdue_contract_reminder',
        string='Has Contracts Overdue',
        multi='contract_info')
    contract_renewal_name = fields.Text(
        compute='_compute_contract_reminder',
        string='Name of contract to renew soon',
        multi='contract_info')
    contract_renewal_total = fields.Text(
        compute='_compute_contract_reminder',
        string='Total of contracts due or overdue minus one',
        multi='contract_info')
    car_value = fields.Float(string="Catalog Value (VAT Incl.)",
                             help='Value of the bought vehicle')
    residual_value = fields.Float()

    _sql_constraints = [('driver_id_unique', 'UNIQUE(driver_id)',
                         'Only one car can be assigned to the same employee!')]

    @api.depends('model_id', 'license_plate')
    def _compute_vehicle_name(self):
        for record in self:
            record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + record.license_plate

    def _get_odometer(self):
        FleetVehicalOdometer = self.env['fleet.vehicle.odometer']
        for record in self:
            vehicle_odometer = FleetVehicalOdometer.search(
                [('vehicle_id', '=', record.id)], limit=1, order='value desc')
            if vehicle_odometer:
                record.odometer = vehicle_odometer.value
            else:
                record.odometer = 0

    def _set_odometer(self):
        for record in self:
            if record.odometer:
                date = fields.Date.context_today(record)
                data = {
                    'value': record.odometer,
                    'date': date,
                    'vehicle_id': record.id
                }
                self.env['fleet.vehicle.odometer'].create(data)

    def _compute_count_all(self):
        Odometer = self.env['fleet.vehicle.odometer']
        LogFuel = self.env['fleet.vehicle.log.fuel']
        LogService = self.env['fleet.vehicle.log.services']
        LogContract = self.env['fleet.vehicle.log.contract']
        Cost = self.env['fleet.vehicle.cost']
        for record in self:
            record.odometer_count = Odometer.search_count([('vehicle_id', '=',
                                                            record.id)])
            record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=',
                                                            record.id)])
            record.service_count = LogService.search_count([('vehicle_id', '=',
                                                             record.id)])
            record.contract_count = LogContract.search_count([
                ('vehicle_id', '=', record.id), ('state', '!=', 'closed')
            ])
            record.cost_count = Cost.search_count([('vehicle_id', '=',
                                                    record.id),
                                                   ('parent_id', '=', False)])

    @api.depends('log_contracts')
    def _compute_contract_reminder(self):
        for record in self:
            overdue = False
            due_soon = False
            total = 0
            name = ''
            for element in record.log_contracts:
                if element.state in ('open',
                                     'expired') and element.expiration_date:
                    current_date_str = fields.Date.context_today(record)
                    due_time_str = element.expiration_date
                    current_date = fields.Date.from_string(current_date_str)
                    due_time = fields.Date.from_string(due_time_str)
                    diff_time = (due_time - current_date).days
                    if diff_time < 0:
                        overdue = True
                        total += 1
                    if diff_time < 15 and diff_time >= 0:
                        due_soon = True
                        total += 1
                    if overdue or due_soon:
                        log_contract = self.env[
                            'fleet.vehicle.log.contract'].search(
                                [('vehicle_id', '=', record.id),
                                 ('state', 'in', ('open', 'expired'))],
                                limit=1,
                                order='expiration_date asc')
                        if log_contract:
                            # we display only the name of the oldest overdue/due soon contract
                            name = log_contract.cost_subtype_id.name

            record.contract_renewal_overdue = overdue
            record.contract_renewal_due_soon = due_soon
            record.contract_renewal_total = total - 1  # we remove 1 from the real total for display purposes
            record.contract_renewal_name = name

    def _search_contract_renewal_due_soon(self, operator, value):
        res = []
        assert operator in ('=', '!=', '<>') and value in (
            True, False), 'Operation not supported'
        if (operator == '=' and value is True) or (operator in ('<>', '!=')
                                                   and value is False):
            search_operator = 'in'
        else:
            search_operator = 'not in'
        today = fields.Date.context_today(self)
        datetime_today = fields.Datetime.from_string(today)
        limit_date = fields.Datetime.to_string(datetime_today +
                                               relativedelta(days=+15))
        self.env.cr.execute(
            """SELECT cost.vehicle_id,
                        count(contract.id) AS contract_number
                        FROM fleet_vehicle_cost cost
                        LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id
                        WHERE contract.expiration_date IS NOT NULL
                          AND contract.expiration_date > %s
                          AND contract.expiration_date < %s
                          AND contract.state IN ('open', 'expired')
                        GROUP BY cost.vehicle_id""", (today, limit_date))
        res_ids = [x[0] for x in self.env.cr.fetchall()]
        res.append(('id', search_operator, res_ids))
        return res

    def _search_get_overdue_contract_reminder(self, operator, value):
        res = []
        assert operator in ('=', '!=', '<>') and value in (
            True, False), 'Operation not supported'
        if (operator == '=' and value is True) or (operator in ('<>', '!=')
                                                   and value is False):
            search_operator = 'in'
        else:
            search_operator = 'not in'
        today = fields.Date.context_today(self)
        self.env.cr.execute(
            '''SELECT cost.vehicle_id,
                        count(contract.id) AS contract_number
                        FROM fleet_vehicle_cost cost
                        LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id
                        WHERE contract.expiration_date IS NOT NULL
                          AND contract.expiration_date < %s
                          AND contract.state IN ('open', 'expired')
                        GROUP BY cost.vehicle_id ''', (today, ))
        res_ids = [x[0] for x in self.env.cr.fetchall()]
        res.append(('id', search_operator, res_ids))
        return res

    @api.onchange('model_id')
    def _onchange_model(self):
        if self.model_id:
            self.image_medium = self.model_id.image
        else:
            self.image_medium = False

    @api.model
    def create(self, data):
        vehicle = super(FleetVehicle,
                        self.with_context(mail_create_nolog=True)).create(data)
        vehicle.message_post(body=_('%s %s has been added to the fleet!') %
                             (vehicle.model_id.name, vehicle.license_plate))
        return vehicle

    @api.multi
    def write(self, vals):
        """
        This function write an entry in the openchatter whenever we change important information
        on the vehicle like the model, the drive, the state of the vehicle or its license plate
        """
        for vehicle in self:
            changes = []
            if 'model_id' in vals and vehicle.model_id.id != vals['model_id']:
                value = self.env['fleet.vehicle.model'].browse(
                    vals['model_id']).name
                oldmodel = vehicle.model_id.name or _('None')
                changes.append(
                    _("Model: from '%s' to '%s'") % (oldmodel, value))
            if 'driver_id' in vals and vehicle.driver_id.id != vals[
                    'driver_id']:
                value = self.env['res.partner'].browse(vals['driver_id']).name
                olddriver = (vehicle.driver_id.name) or _('None')
                changes.append(
                    _("Driver: from '%s' to '%s'") % (olddriver, value))
            if 'state_id' in vals and vehicle.state_id.id != vals['state_id']:
                value = self.env['fleet.vehicle.state'].browse(
                    vals['state_id']).name
                oldstate = vehicle.state_id.name or _('None')
                changes.append(
                    _("State: from '%s' to '%s'") % (oldstate, value))
            if 'license_plate' in vals and vehicle.license_plate != vals[
                    'license_plate']:
                old_license_plate = vehicle.license_plate or _('None')
                changes.append(
                    _("License Plate: from '%s' to '%s'") %
                    (old_license_plate, vals['license_plate']))

            if len(changes) > 0:
                self.message_post(body=", ".join(changes))

            return super(FleetVehicle, self).write(vals)

    @api.multi
    def return_action_to_open(self):
        """ This opens the xml view specified in xml_id for the current vehicle """
        self.ensure_one()
        xml_id = self.env.context.get('xml_id')
        if xml_id:
            res = self.env['ir.actions.act_window'].for_xml_id('fleet', xml_id)
            res.update(context=dict(self.env.context,
                                    default_vehicle_id=self.id,
                                    group_by=False),
                       domain=[('vehicle_id', '=', self.id)])
            return res
        return False

    @api.multi
    def act_show_log_cost(self):
        """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
            @return: the costs log view
        """
        self.ensure_one()
        res = self.env['ir.actions.act_window'].for_xml_id(
            'fleet', 'fleet_vehicle_costs_action')
        res.update(context=dict(self.env.context,
                                default_vehicle_id=self.id,
                                search_default_parent_false=True),
                   domain=[('vehicle_id', '=', self.id)])
        return res
Example #7
0
class MailMail(models.Model):
    """ Model holding RFC2822 email messages to send. This model also provides
        facilities to queue and send new email messages.  """
    _name = 'mail.mail'
    _description = 'Outgoing Mails'
    _inherits = {'mail.message': 'mail_message_id'}
    _order = 'id desc'
    _rec_name = 'subject'

    # content
    mail_message_id = fields.Many2one('mail.message',
                                      'Message',
                                      required=True,
                                      ondelete='cascade',
                                      index=True,
                                      auto_join=True)
    body_html = fields.Text('Rich-text Contents',
                            help="Rich-text/HTML message")
    references = fields.Text(
        'References',
        help='Message references, such as identifiers of previous messages',
        readonly=1)
    headers = fields.Text('Headers', copy=False)
    # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
    # and during unlink() we will not cascade delete the parent and its attachments
    notification = fields.Boolean(
        'Is Notification',
        help=
        'Mail has been created to notify people of an existing mail.message')
    # recipients
    email_to = fields.Text('To', help='Message recipients (emails)')
    email_cc = fields.Char('Cc', help='Carbon copy message recipients')
    recipient_ids = fields.Many2many('res.partner', string='To (Partners)')
    # process
    state = fields.Selection([
        ('outgoing', 'Outgoing'),
        ('sent', 'Sent'),
        ('received', 'Received'),
        ('exception', 'Delivery Failed'),
        ('cancel', 'Cancelled'),
    ],
                             'Status',
                             readonly=True,
                             copy=False,
                             default='outgoing')
    auto_delete = fields.Boolean(
        'Auto Delete',
        help="Permanently delete this email after sending it, to save space")
    keep_days = fields.Integer(
        'Keep days',
        default=-1,
        help="This value defines the no. of days "
        "the emails should be recorded "
        "in the system: \n -1 = Email will be deleted "
        "immediately once it is send \n greater than 0 = Email "
        "will be deleted after "
        "the no. of days are met.")
    delete_date = fields.Date(compute='_compute_delete_on_date',
                              string='Delete on.',
                              store=True)
    failure_reason = fields.Text(
        'Failure Reason',
        readonly=1,
        help=
        "Failure reason. This is usually the exception thrown by the email server, stored to ease the debugging of mailing issues."
    )
    scheduled_date = fields.Char(
        'Scheduled Send Date',
        help=
        "If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible."
    )

    @api.multi
    @api.depends('keep_days')
    def _compute_delete_on_date(self):
        for obj in self:
            mail_date = fields.Datetime.from_string(obj.date)
            if obj.keep_days > 0:
                delete_on = mail_date + datetime.timedelta(days=obj.keep_days)
                obj.delete_date = delete_on
            else:
                obj.delete_date = mail_date.date()

    @api.model
    def create(self, values):
        # notification field: if not set, set if mail comes from an existing mail.message
        if 'notification' not in values and values.get('mail_message_id'):
            values['notification'] = True
        if not values.get('mail_message_id'):
            self = self.with_context(message_create_from_mail_mail=True)
        new_mail = super(MailMail, self).create(values)
        if values.get('attachment_ids'):
            new_mail.attachment_ids.check(mode='read')
        return new_mail

    @api.multi
    def write(self, vals):
        res = super(MailMail, self).write(vals)
        if vals.get('attachment_ids'):
            for mail in self:
                mail.attachment_ids.check(mode='read')
        return res

    @api.multi
    def unlink(self):
        # cascade-delete the parent message for all mails that are not created for a notification
        to_cascade = self.search([('notification', '=', False),
                                  ('id', 'in', self.ids)
                                  ]).mapped('mail_message_id')
        res = super(MailMail, self).unlink()
        to_cascade.unlink()
        return res

    @api.model
    def default_get(self, fields):
        # protection for `default_type` values leaking from menu action context (e.g. for invoices)
        # To remove when automatic context propagation is removed in web client
        if self._context.get('default_type') not in type(
                self).message_type.base_field.selection:
            self = self.with_context(dict(self._context, default_type=None))
        return super(MailMail, self).default_get(fields)

    @api.multi
    def mark_outgoing(self):
        return self.write({'state': 'outgoing'})

    @api.multi
    def cancel(self):
        return self.write({'state': 'cancel'})

    @api.model
    def process_email_unlink(self):
        mail_ids = self.sudo().search([('delete_date', '=',
                                        datetime.datetime.now().date())])
        mail_ids.filtered('auto_delete').unlink()

    @api.model
    def process_email_queue(self, ids=None):
        """Send immediately queued messages, committing after each
           message is sent - this is not transactional and should
           not be called during another transaction!

           :param list ids: optional list of emails ids to send. If passed
                            no search is performed, and these ids are used
                            instead.
           :param dict context: if a 'filters' key is present in context,
                                this value will be used as an additional
                                filter to further restrict the outgoing
                                messages to send (by default all 'outgoing'
                                messages are sent).
        """
        filters = [
            '&', ('state', '=', 'outgoing'), '|',
            ('scheduled_date', '<', datetime.datetime.now()),
            ('scheduled_date', '=', False)
        ]
        if 'filters' in self._context:
            filters.extend(self._context['filters'])
        # TODO: make limit configurable
        filtered_ids = self.search(filters, limit=10000).ids
        if not ids:
            ids = filtered_ids
        else:
            ids = list(set(filtered_ids) & set(ids))
        ids.sort()

        res = None
        try:
            # auto-commit except in testing mode
            auto_commit = not getattr(threading.currentThread(), 'testing',
                                      False)
            res = self.browse(ids).send(auto_commit=auto_commit)
        except Exception:
            _logger.exception("Failed processing mail queue")
        return res

    @api.multi
    def _postprocess_sent_message(self, mail_sent=True):
        """Perform any post-processing necessary after sending ``mail``
        successfully, including deleting it completely along with its
        attachment if the ``auto_delete`` flag of the mail was set.
        Overridden by subclasses for extra post-processing behaviors.

        :return: True
        """
        notif_emails = self.filtered(lambda email: email.notification)
        if notif_emails:
            notifications = self.env['mail.notification'].search([
                ('mail_message_id', 'in',
                 notif_emails.mapped('mail_message_id').ids),
                ('is_email', '=', True)
            ])
            if mail_sent:
                notifications.write({
                    'email_status': 'sent',
                })
            else:
                notifications.write({
                    'email_status': 'exception',
                })
        if mail_sent:
            if self.keep_days > 0:
                return True
            self.sudo().filtered(lambda self: self.auto_delete).unlink()
        return True

    # ------------------------------------------------------
    # mail_mail formatting, tools and send mechanism
    # ------------------------------------------------------

    @api.multi
    def send_get_mail_body(self, partner=None):
        """Return a specific ir_email body. The main purpose of this method
        is to be inherited to add custom content depending on some module."""
        self.ensure_one()
        body = self.body_html or ''
        return body

    @api.multi
    def send_get_mail_to(self, partner=None):
        """Forge the email_to with the following heuristic:
          - if 'partner', recipient specific (Partner Name <email>)
          - else fallback on mail.email_to splitting """
        self.ensure_one()
        if partner:
            email_to = [
                formataddr((partner.name or 'False', partner.email or 'False'))
            ]
        else:
            email_to = tools.email_split_and_format(self.email_to)
        return email_to

    @api.multi
    def send_get_email_dict(self, partner=None):
        """Return a dictionary for specific email values, depending on a
        partner, or generic to the whole recipients given by mail.email_to.

            :param Model partner: specific recipient partner
        """
        self.ensure_one()
        body = self.send_get_mail_body(partner=partner)
        body_alternative = tools.html2plaintext(body)
        res = {
            'body': body,
            'body_alternative': body_alternative,
            'email_to': self.send_get_mail_to(partner=partner),
        }
        return res

    @api.multi
    def _split_by_server(self):
        """Returns an iterator of pairs `(mail_server_id, record_ids)` for current recordset.

        The same `mail_server_id` may repeat in order to limit batch size according to
        the `mail.session.batch.size` system parameter.
        """
        groups = defaultdict(list)
        # Turn prefetch OFF to avoid MemoryError on very large mail queues, we only care
        # about the mail server ids in this case.
        for mail in self.with_context(prefetch_fields=False):
            groups[mail.mail_server_id.id].append(mail.id)
        sys_params = self.env['ir.config_parameter'].sudo()
        batch_size = int(sys_params.get_param('mail.session.batch.size', 1000))
        for server_id, record_ids in groups.items():
            for mail_batch in tools.split_every(batch_size, record_ids):
                yield server_id, mail_batch

    @api.multi
    def send(self, auto_commit=False, raise_exception=False):
        """ Sends the selected emails immediately, ignoring their current
            state (mails that have already been sent should not be passed
            unless they should actually be re-sent).
            Emails successfully delivered are marked as 'sent', and those
            that fail to be deliver are marked as 'exception', and the
            corresponding error mail is output in the server logs.

            :param bool auto_commit: whether to force a commit of the mail status
                after sending each mail (meant only for scheduler processing);
                should never be True during normal transactions (default: False)
            :param bool raise_exception: whether to raise an exception if the
                email sending process has failed
            :return: True
        """
        for server_id, batch_ids in self._split_by_server():
            smtp_session = None
            try:
                smtp_session = self.env['ir.mail_server'].connect(
                    mail_server_id=server_id)
            except Exception as exc:
                if raise_exception:
                    # To be consistent and backward compatible with mail_mail.send() raised
                    # exceptions, it is encapsulated into an Flectra MailDeliveryException
                    raise MailDeliveryException(
                        _('Unable to connect to SMTP Server'), exc)
                else:
                    self.browse(batch_ids).write({
                        'state': 'exception',
                        'failure_reason': exc
                    })
            else:
                self.browse(batch_ids)._send(auto_commit=auto_commit,
                                             raise_exception=raise_exception,
                                             smtp_session=smtp_session)
                _logger.info('Sent batch %s emails via mail server ID #%s',
                             len(batch_ids), server_id)
            finally:
                if smtp_session:
                    smtp_session.quit()

    @api.multi
    def _send(self,
              auto_commit=False,
              raise_exception=False,
              smtp_session=None):
        IrMailServer = self.env['ir.mail_server']
        for mail_id in self.ids:
            try:
                mail = self.browse(mail_id)
                if mail.state != 'outgoing':
                    if mail.state != 'exception' and mail.auto_delete and \
                                    mail.keep_days < 0:
                        mail.sudo().unlink()
                    continue
                # TDE note: remove me when model_id field is present on mail.message - done here to avoid doing it multiple times in the sub method
                if mail.model:
                    model = self.env['ir.model']._get(mail.model)[0]
                else:
                    model = None
                if model:
                    mail = mail.with_context(model_name=model.name)

                # load attachment binary data with a separate read(), as prefetching all
                # `datas` (binary field) could bloat the browse cache, triggerring
                # soft/hard mem limits with temporary data.
                attachments = [(a['datas_fname'], base64.b64decode(a['datas']),
                                a['mimetype'])
                               for a in mail.attachment_ids.sudo().read(
                                   ['datas_fname', 'datas', 'mimetype'])]

                # specific behavior to customize the send email for notified partners
                email_list = []
                if mail.email_to:
                    email_list.append(mail.send_get_email_dict())
                for partner in mail.recipient_ids:
                    email_list.append(
                        mail.send_get_email_dict(partner=partner))

                # headers
                headers = {}
                ICP = self.env['ir.config_parameter'].sudo()
                bounce_alias = ICP.get_param("mail.bounce.alias")
                catchall_domain = ICP.get_param("mail.catchall.domain")
                if bounce_alias and catchall_domain:
                    if mail.model and mail.res_id:
                        headers['Return-Path'] = '%s+%d-%s-%d@%s' % (
                            bounce_alias, mail.id, mail.model, mail.res_id,
                            catchall_domain)
                    else:
                        headers['Return-Path'] = '%s+%d@%s' % (
                            bounce_alias, mail.id, catchall_domain)
                if mail.headers:
                    try:
                        headers.update(safe_eval(mail.headers))
                    except Exception:
                        pass

                # Writing on the mail object may fail (e.g. lock on user) which
                # would trigger a rollback *after* actually sending the email.
                # To avoid sending twice the same email, provoke the failure earlier
                mail.write({
                    'state':
                    'exception',
                    'failure_reason':
                    _('Error without exception. Probably due do sending an email without computed recipients.'
                      ),
                })
                mail_sent = False

                # build an RFC2822 email.message.Message object and send it without queuing
                res = None
                for email in email_list:
                    msg = IrMailServer.build_email(
                        email_from=mail.email_from,
                        email_to=email.get('email_to'),
                        subject=mail.subject,
                        body=email.get('body'),
                        body_alternative=email.get('body_alternative'),
                        email_cc=tools.email_split(mail.email_cc),
                        reply_to=mail.reply_to,
                        attachments=attachments,
                        message_id=mail.message_id,
                        references=mail.references,
                        object_id=mail.res_id
                        and ('%s-%s' % (mail.res_id, mail.model)),
                        subtype='html',
                        subtype_alternative='plain',
                        headers=headers)
                    try:
                        res = IrMailServer.send_email(
                            msg,
                            mail_server_id=mail.mail_server_id.id,
                            smtp_session=smtp_session)
                    except AssertionError as error:
                        if str(error) == IrMailServer.NO_VALID_RECIPIENT:
                            # No valid recipient found for this particular
                            # mail item -> ignore error to avoid blocking
                            # delivery to next recipients, if any. If this is
                            # the only recipient, the mail will show as failed.
                            _logger.info(
                                "Ignoring invalid recipients for mail.mail %s: %s",
                                mail.message_id, email.get('email_to'))
                        else:
                            raise
                if res:
                    mail.write({
                        'state': 'sent',
                        'message_id': res,
                        'failure_reason': False
                    })
                    mail_sent = True

                # /!\ can't use mail.state here, as mail.refresh() will cause an error
                # see revid:[email protected] in 6.1
                if mail_sent:
                    _logger.info(
                        'Mail with ID %r and Message-Id %r successfully sent',
                        mail.id, mail.message_id)
                mail._postprocess_sent_message(mail_sent=mail_sent)
            except MemoryError:
                # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
                # instead of marking the mail as failed
                _logger.exception(
                    'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
                    mail.id, mail.message_id)
                raise
            except psycopg2.Error:
                # If an error with the database occurs, chances are that the cursor is unusable.
                # This will lead to an `psycopg2.InternalError` being raised when trying to write
                # `state`, shadowing the original exception and forbid a retry on concurrent
                # update. Let's bubble it.
                raise
            except Exception as e:
                failure_reason = tools.ustr(e)
                _logger.exception('failed sending mail (id: %s) due to %s',
                                  mail.id, failure_reason)
                mail.write({
                    'state': 'exception',
                    'failure_reason': failure_reason
                })
                mail._postprocess_sent_message(mail_sent=False)
                if raise_exception:
                    if isinstance(e, AssertionError):
                        # get the args of the original error, wrap into a value and throw a MailDeliveryException
                        # that is an except_orm, with name and value as arguments
                        value = '. '.join(e.args)
                        raise MailDeliveryException(_("Mail Delivery Failed"),
                                                    value)
                    raise

            if auto_commit is True:
                self._cr.commit()
        return True
Example #8
0
class MailTracking(models.Model):
    _name = 'mail.tracking.value'
    _description = 'Mail Tracking Value'

    # TDE CLEANME: why not a m2o to ir model field ?
    field = fields.Char('Changed Field', required=True, readonly=1)
    field_desc = fields.Char('Field Description', required=True, readonly=1)
    field_type = fields.Char('Field Type')

    old_value_integer = fields.Integer('Old Value Integer', readonly=1)
    old_value_float = fields.Float('Old Value Float', readonly=1)
    old_value_monetary = fields.Float('Old Value Monetary', readonly=1)
    old_value_char = fields.Char('Old Value Char', readonly=1)
    old_value_text = fields.Text('Old Value Text', readonly=1)
    old_value_datetime = fields.Datetime('Old Value DateTime', readonly=1)

    new_value_integer = fields.Integer('New Value Integer', readonly=1)
    new_value_float = fields.Float('New Value Float', readonly=1)
    new_value_monetary = fields.Float('New Value Monetary', readonly=1)
    new_value_char = fields.Char('New Value Char', readonly=1)
    new_value_text = fields.Text('New Value Text', readonly=1)
    new_value_datetime = fields.Datetime('New Value Datetime', readonly=1)

    mail_message_id = fields.Many2one('mail.message',
                                      'Message ID',
                                      required=True,
                                      index=True,
                                      ondelete='cascade')

    @api.model
    def create_tracking_values(self, initial_value, new_value, col_name,
                               col_info):
        tracked = True
        values = {
            'field': col_name,
            'field_desc': col_info['string'],
            'field_type': col_info['type']
        }

        if col_info['type'] in [
                'integer', 'float', 'char', 'text', 'datetime', 'monetary'
        ]:
            values.update({
                'old_value_%s' % col_info['type']: initial_value,
                'new_value_%s' % col_info['type']: new_value
            })
        elif col_info['type'] == 'date':
            values.update({
                'old_value_datetime':
                initial_value and datetime.strftime(
                    datetime.combine(
                        datetime.strptime(initial_value,
                                          tools.DEFAULT_SERVER_DATE_FORMAT),
                        datetime.min.time()),
                    tools.DEFAULT_SERVER_DATETIME_FORMAT) or False,
                'new_value_datetime':
                new_value and datetime.strftime(
                    datetime.combine(
                        datetime.strptime(new_value,
                                          tools.DEFAULT_SERVER_DATE_FORMAT),
                        datetime.min.time()),
                    tools.DEFAULT_SERVER_DATETIME_FORMAT) or False,
            })
        elif col_info['type'] == 'boolean':
            values.update({
                'old_value_integer': initial_value,
                'new_value_integer': new_value
            })
        elif col_info['type'] == 'selection':
            values.update({
                'old_value_char':
                initial_value and dict(col_info['selection'])[initial_value]
                or '',
                'new_value_char':
                new_value and dict(col_info['selection'])[new_value] or ''
            })
        elif col_info['type'] == 'many2one':
            values.update({
                'old_value_integer':
                initial_value and initial_value.id or 0,
                'new_value_integer':
                new_value and new_value.id or 0,
                'old_value_char':
                initial_value and initial_value.name_get()[0][1] or '',
                'new_value_char':
                new_value and new_value.name_get()[0][1] or ''
            })
        else:
            tracked = False

        if tracked:
            return values
        return {}

    @api.multi
    def get_display_value(self, type):
        assert type in ('new', 'old')
        result = []
        for record in self:
            if record.field_type in [
                    'integer', 'float', 'char', 'text', 'monetary'
            ]:
                result.append(
                    getattr(record, '%s_value_%s' % (type, record.field_type)))
            elif record.field_type == 'datetime':
                if record['%s_value_datetime' % type]:
                    new_datetime = getattr(record, '%s_value_datetime' % type)
                    result.append('%sZ' % new_datetime)
                else:
                    result.append(record['%s_value_datetime' % type])
            elif record.field_type == 'date':
                if record['%s_value_datetime' % type]:
                    new_date = datetime.strptime(
                        record['%s_value_datetime' % type],
                        tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
                    result.append(
                        new_date.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
                else:
                    result.append(record['%s_value_datetime' % type])
            elif record.field_type == 'boolean':
                result.append(bool(record['%s_value_integer' % type]))
            else:
                result.append(record['%s_value_char' % type])
        return result

    @api.multi
    def get_old_display_value(self):
        # grep : # old_value_integer | old_value_datetime | old_value_char
        return self.get_display_value('old')

    @api.multi
    def get_new_display_value(self):
        # grep : # new_value_integer | new_value_datetime | new_value_char
        return self.get_display_value('new')
class BaseDocumentLayout(models.TransientModel):
    """
    Customise the company document layout and display a live preview
    """

    _name = 'base.document.layout'
    _description = 'Company Document Layout'

    company_id = fields.Many2one('res.company',
                                 default=lambda self: self.env.company,
                                 required=True)

    logo = fields.Binary(related='company_id.logo', readonly=False)
    preview_logo = fields.Binary(related='logo', string="Preview logo")
    report_header = fields.Text(related='company_id.report_header',
                                readonly=False)
    report_footer = fields.Text(related='company_id.report_footer',
                                readonly=False)

    # The paper format changes won't be reflected in the preview.
    paperformat_id = fields.Many2one(related='company_id.paperformat_id',
                                     readonly=False)

    external_report_layout_id = fields.Many2one(
        related='company_id.external_report_layout_id', readonly=False)

    font = fields.Selection(related='company_id.font', readonly=False)
    primary_color = fields.Char(related='company_id.primary_color',
                                readonly=False)
    secondary_color = fields.Char(related='company_id.secondary_color',
                                  readonly=False)

    custom_colors = fields.Boolean(compute="_compute_custom_colors",
                                   readonly=False)
    logo_primary_color = fields.Char(compute="_compute_logo_colors")
    logo_secondary_color = fields.Char(compute="_compute_logo_colors")

    report_layout_id = fields.Many2one('report.layout')

    # All the sanitization get disabled as we want true raw html to be passed to an iframe.
    preview = fields.Html(compute='_compute_preview',
                          sanitize=False,
                          sanitize_tags=False,
                          sanitize_attributes=False,
                          sanitize_style=False,
                          sanitize_form=False,
                          strip_style=False,
                          strip_classes=False)

    # Those following fields are required as a company to create invoice report
    partner_id = fields.Many2one(related='company_id.partner_id',
                                 readonly=True)
    phone = fields.Char(related='company_id.phone', readonly=True)
    email = fields.Char(related='company_id.email', readonly=True)
    website = fields.Char(related='company_id.website', readonly=True)
    vat = fields.Char(related='company_id.vat', readonly=True)
    name = fields.Char(related='company_id.name', readonly=True)
    country_id = fields.Many2one(related="company_id.country_id",
                                 readonly=True)

    @api.depends(
        'logo_primary_color',
        'logo_secondary_color',
        'primary_color',
        'secondary_color',
    )
    def _compute_custom_colors(self):
        for wizard in self:
            logo_primary = wizard.logo_primary_color or ''
            logo_secondary = wizard.logo_secondary_color or ''
            # Force lower case on color to ensure that FF01AA == ff01aa
            wizard.custom_colors = (
                wizard.logo and wizard.primary_color and wizard.secondary_color
                and
                not (wizard.primary_color.lower() == logo_primary.lower() and
                     wizard.secondary_color.lower() == logo_secondary.lower()))

    @api.depends('logo')
    def _compute_logo_colors(self):
        for wizard in self:
            if wizard._context.get('bin_size'):
                wizard_for_image = wizard.with_context(bin_size=False)
            else:
                wizard_for_image = wizard
            wizard.logo_primary_color, wizard.logo_secondary_color = wizard_for_image._parse_logo_colors(
            )

    @api.depends('report_layout_id', 'logo', 'font', 'primary_color',
                 'secondary_color', 'report_header', 'report_footer')
    def _compute_preview(self):
        """ compute a qweb based preview to display on the wizard """

        styles = self._get_asset_style()

        for wizard in self:
            if wizard.report_layout_id:
                preview_css = self._get_css_for_preview(styles, wizard.id)
                ir_ui_view = wizard.env['ir.ui.view']
                wizard.preview = ir_ui_view._render_template(
                    'web.report_invoice_wizard_preview', {
                        'company': wizard,
                        'preview_css': preview_css
                    })
            else:
                wizard.preview = False

    @api.onchange('company_id')
    def _onchange_company_id(self):
        for wizard in self:
            wizard.logo = wizard.company_id.logo
            wizard.report_header = wizard.company_id.report_header
            wizard.report_footer = wizard.company_id.report_footer
            wizard.paperformat_id = wizard.company_id.paperformat_id
            wizard.external_report_layout_id = wizard.company_id.external_report_layout_id
            wizard.font = wizard.company_id.font
            wizard.primary_color = wizard.company_id.primary_color
            wizard.secondary_color = wizard.company_id.secondary_color
            wizard_layout = wizard.env["report.layout"].search([
                ('view_id.key', '=',
                 wizard.company_id.external_report_layout_id.key)
            ])
            wizard.report_layout_id = wizard_layout or wizard_layout.search(
                [], limit=1)

            if not wizard.primary_color:
                wizard.primary_color = wizard.logo_primary_color or DEFAULT_PRIMARY
            if not wizard.secondary_color:
                wizard.secondary_color = wizard.logo_secondary_color or DEFAULT_SECONDARY

    @api.onchange('custom_colors')
    def _onchange_custom_colors(self):
        for wizard in self:
            if wizard.logo and not wizard.custom_colors:
                wizard.primary_color = wizard.logo_primary_color or DEFAULT_PRIMARY
                wizard.secondary_color = wizard.logo_secondary_color or DEFAULT_SECONDARY

    @api.onchange('report_layout_id')
    def _onchange_report_layout_id(self):
        for wizard in self:
            wizard.external_report_layout_id = wizard.report_layout_id.view_id

    @api.onchange('logo')
    def _onchange_logo(self):
        for wizard in self:
            # It is admitted that if the user puts the original image back, it won't change colors
            company = wizard.company_id
            # at that point wizard.logo has been assigned the value present in DB
            if wizard.logo == company.logo and company.primary_color and company.secondary_color:
                continue

            if wizard.logo_primary_color:
                wizard.primary_color = wizard.logo_primary_color
            if wizard.logo_secondary_color:
                wizard.secondary_color = wizard.logo_secondary_color

    def _parse_logo_colors(self, logo=None, white_threshold=225):
        """
        Identifies dominant colors

        First resizes the original image to improve performance, then discards
        transparent colors and white-ish colors, then calls the averaging
        method twice to evaluate both primary and secondary colors.

        :param logo: alternate logo to process
        :param white_threshold: arbitrary value defining the maximum value a color can reach

        :return colors: hex values of primary and secondary colors
        """
        self.ensure_one()
        logo = logo or self.logo
        if not logo:
            return False, False

        # The "===" gives different base64 encoding a correct padding
        logo += b'===' if type(logo) == bytes else '==='
        try:
            # Catches exceptions caused by logo not being an image
            image = tools.image_fix_orientation(tools.base64_to_image(logo))
        except Exception:
            return False, False

        base_w, base_h = image.size
        w = int(50 * base_w / base_h)
        h = 50

        # Converts to RGBA (if already RGBA, this is a noop)
        image_converted = image.convert('RGBA')
        image_resized = image_converted.resize((w, h), resample=Image.NEAREST)

        colors = []
        for color in image_resized.getcolors(w * h):
            if not (color[1][0] > white_threshold
                    and color[1][1] > white_threshold
                    and color[1][2] > white_threshold) and color[1][3] > 0:
                colors.append(color)

        if not colors:  # May happen when the whole image is white
            return False, False
        primary, remaining = tools.average_dominant_color(colors)
        secondary = tools.average_dominant_color(
            remaining)[0] if len(remaining) > 0 else primary

        # Lightness and saturation are calculated here.
        # - If both colors have a similar lightness, the most colorful becomes primary
        # - When the difference in lightness is too great, the brightest color becomes primary
        l_primary = tools.get_lightness(primary)
        l_secondary = tools.get_lightness(secondary)
        if (l_primary < 0.2 and l_secondary < 0.2) or (l_primary >= 0.2
                                                       and l_secondary >= 0.2):
            s_primary = tools.get_saturation(primary)
            s_secondary = tools.get_saturation(secondary)
            if s_primary < s_secondary:
                primary, secondary = secondary, primary
        elif l_secondary > l_primary:
            primary, secondary = secondary, primary

        return tools.rgb_to_hex(primary), tools.rgb_to_hex(secondary)

    @api.model
    def action_open_base_document_layout(self, action_ref=None):
        if not action_ref:
            action_ref = 'web.action_base_document_layout_configurator'
        res = self.env["ir.actions.actions"]._for_xml_id(action_ref)
        self.env[res["res_model"]].check_access_rights('write')
        return res

    def document_layout_save(self):
        # meant to be overridden
        return self.env.context.get('report_action') or {
            'type': 'ir.actions.act_window_close'
        }

    def _get_asset_style(self):
        """
        Compile the style template. It is a qweb template expecting company ids to generate all the code in one batch.
        We give a useless company_ids arg, but provide the PREVIEW_ID arg that will prepare the template for
        '_get_css_for_preview' processing later.
        :return:
        """
        template_style = self.env.ref('web.styles_company_report',
                                      raise_if_not_found=False)
        if not template_style:
            return b''

        company_styles = template_style._render({
            'company_ids': self,
        })

        return company_styles

    @api.model
    def _get_css_for_preview(self, scss, new_id):
        """
        Compile the scss into css.
        """
        css_code = self._compile_scss(scss)
        return css_code

    @api.model
    def _compile_scss(self, scss_source):
        """
        This code will compile valid scss into css.
        Parameters are the same from flectra/addons/base/models/assetsbundle.py
        Simply copied and adapted slightly
        """

        # No scss ? still valid, returns empty css
        if not scss_source.strip():
            return ""

        precision = 8
        output_style = 'expanded'
        bootstrap_path = get_resource_path('web', 'static', 'lib', 'bootstrap',
                                           'scss')

        try:
            return libsass.compile(
                string=scss_source,
                include_paths=[
                    bootstrap_path,
                ],
                output_style=output_style,
                precision=precision,
            )
        except libsass.CompileError as e:
            raise libsass.CompileError(e.args[0])
Example #10
0
class ProductTemplate(models.Model):
    _inherit = 'product.template'

    responsible_id = fields.Many2one('res.users',
                                     string='Responsible',
                                     default=lambda self: self.env.uid,
                                     required=True)
    type = fields.Selection(selection_add=[('product', 'Stockable Product')])
    property_stock_production = fields.Many2one(
        'stock.location',
        "Production Location",
        company_dependent=True,
        domain=[('usage', 'like', 'production')],
        help=
        "This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."
    )
    property_stock_inventory = fields.Many2one(
        'stock.location',
        "Inventory Location",
        company_dependent=True,
        domain=[('usage', 'like', 'inventory')],
        help=
        "This stock location will be used, instead of the default one, as the source location for stock moves generated when you do an inventory."
    )
    sale_delay = fields.Float(
        'Customer Lead Time',
        default=0,
        help=
        "The average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."
    )
    tracking = fields.Selection([('serial', 'By Unique Serial Number'),
                                 ('lot', 'By Lots'), ('none', 'No Tracking')],
                                string="Tracking",
                                default='none',
                                required=True)
    description_picking = fields.Text('Description on Picking', translate=True)
    description_pickingout = fields.Text('Description on Delivery Orders',
                                         translate=True)
    description_pickingin = fields.Text('Description on Receptions',
                                        translate=True)
    qty_available = fields.Float(
        'Quantity On Hand',
        compute='_compute_quantities',
        search='_search_qty_available',
        digits=dp.get_precision('Product Unit of Measure'))
    virtual_available = fields.Float(
        'Forecasted Quantity',
        compute='_compute_quantities',
        search='_search_virtual_available',
        digits=dp.get_precision('Product Unit of Measure'))
    incoming_qty = fields.Float(
        'Incoming',
        compute='_compute_quantities',
        search='_search_incoming_qty',
        digits=dp.get_precision('Product Unit of Measure'))
    outgoing_qty = fields.Float(
        'Outgoing',
        compute='_compute_quantities',
        search='_search_outgoing_qty',
        digits=dp.get_precision('Product Unit of Measure'))
    # The goal of these fields is not to be able to search a location_id/warehouse_id but
    # to properly make these fields "dummy": only used to put some keys in context from
    # the search view in order to influence computed field
    location_id = fields.Many2one('stock.location',
                                  'Location',
                                  store=False,
                                  search=lambda operator, operand, vals: [])
    warehouse_id = fields.Many2one('stock.warehouse',
                                   'Warehouse',
                                   store=False,
                                   search=lambda operator, operand, vals: [])
    route_ids = fields.Many2many(
        'stock.location.route',
        'stock_route_product',
        'product_id',
        'route_id',
        'Routes',
        domain=[('product_selectable', '=', True)],
        help=
        "Depending on the modules installed, this will allow you to define the route of the product: whether it will be bought, manufactured, MTO/MTS,..."
    )
    nbr_reordering_rules = fields.Integer(
        'Reordering Rules', compute='_compute_nbr_reordering_rules')
    # TDE FIXME: really used ?
    reordering_min_qty = fields.Float(compute='_compute_nbr_reordering_rules')
    reordering_max_qty = fields.Float(compute='_compute_nbr_reordering_rules')
    # TDE FIXME: seems only visible in a view - remove me ?
    route_from_categ_ids = fields.Many2many(relation="stock.location.route",
                                            string="Category Routes",
                                            related='categ_id.total_route_ids')

    def _is_cost_method_standard(self):
        return True

    def _compute_quantities(self):
        res = self._compute_quantities_dict()
        for template in self:
            template.qty_available = res[template.id]['qty_available']
            template.virtual_available = res[template.id]['virtual_available']
            template.incoming_qty = res[template.id]['incoming_qty']
            template.outgoing_qty = res[template.id]['outgoing_qty']

    def _product_available(self, name, arg):
        return self._compute_quantities_dict()

    def _compute_quantities_dict(self):
        # TDE FIXME: why not using directly the function fields ?
        variants_available = self.mapped(
            'product_variant_ids')._product_available()
        prod_available = {}
        for template in self:
            qty_available = 0
            virtual_available = 0
            incoming_qty = 0
            outgoing_qty = 0
            for p in template.product_variant_ids:
                qty_available += variants_available[p.id]["qty_available"]
                virtual_available += variants_available[
                    p.id]["virtual_available"]
                incoming_qty += variants_available[p.id]["incoming_qty"]
                outgoing_qty += variants_available[p.id]["outgoing_qty"]
            prod_available[template.id] = {
                "qty_available": qty_available,
                "virtual_available": virtual_available,
                "incoming_qty": incoming_qty,
                "outgoing_qty": outgoing_qty,
            }
        return prod_available

    def _search_qty_available(self, operator, value):
        domain = [('qty_available', operator, value)]
        product_variant_ids = self.env['product.product'].search(domain)
        return [('product_variant_ids', 'in', product_variant_ids.ids)]

    def _search_virtual_available(self, operator, value):
        domain = [('virtual_available', operator, value)]
        product_variant_ids = self.env['product.product'].search(domain)
        return [('product_variant_ids', 'in', product_variant_ids.ids)]

    def _search_incoming_qty(self, operator, value):
        domain = [('incoming_qty', operator, value)]
        product_variant_ids = self.env['product.product'].search(domain)
        return [('product_variant_ids', 'in', product_variant_ids.ids)]

    def _search_outgoing_qty(self, operator, value):
        domain = [('outgoing_qty', operator, value)]
        product_variant_ids = self.env['product.product'].search(domain)
        return [('product_variant_ids', 'in', product_variant_ids.ids)]

    def _compute_nbr_reordering_rules(self):
        res = {
            k: {
                'nbr_reordering_rules': 0,
                'reordering_min_qty': 0,
                'reordering_max_qty': 0
            }
            for k in self.ids
        }
        product_data = self.env['stock.warehouse.orderpoint'].read_group(
            [('product_id.product_tmpl_id', 'in', self.ids)],
            ['product_id', 'product_min_qty', 'product_max_qty'],
            ['product_id'])
        for data in product_data:
            product = self.env['product.product'].browse(
                [data['product_id'][0]])
            product_tmpl_id = product.product_tmpl_id.id
            res[product_tmpl_id]['nbr_reordering_rules'] += int(
                data['product_id_count'])
            res[product_tmpl_id]['reordering_min_qty'] = data[
                'product_min_qty']
            res[product_tmpl_id]['reordering_max_qty'] = data[
                'product_max_qty']
        for template in self:
            template.nbr_reordering_rules = res[
                template.id]['nbr_reordering_rules']
            template.reordering_min_qty = res[
                template.id]['reordering_min_qty']
            template.reordering_max_qty = res[
                template.id]['reordering_max_qty']

    @api.onchange('tracking')
    def onchange_tracking(self):
        return self.mapped('product_variant_ids').onchange_tracking()

    def write(self, vals):
        if 'uom_id' in vals:
            new_uom = self.env['product.uom'].browse(vals['uom_id'])
            updated = self.filtered(
                lambda template: template.uom_id != new_uom)
            done_moves = self.env['stock.move'].search(
                [('product_id', 'in', updated.with_context(
                    active_test=False).mapped('product_variant_ids').ids)],
                limit=1)
            if done_moves:
                raise UserError(
                    _("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product."
                      ))
        if 'type' in vals and vals['type'] != 'product' and sum(
                self.mapped('nbr_reordering_rules')) != 0:
            raise UserError(
                _('You still have some active reordering rules on this product. Please archive or delete them first.'
                  ))
        if any('type' in vals and vals['type'] != prod_tmpl.type
               for prod_tmpl in self):
            existing_move_lines = self.env['stock.move.line'].search([
                ('product_id', 'in', self.mapped('product_variant_ids').ids),
                ('state', 'in', ['partially_available', 'assigned']),
            ])
            if existing_move_lines:
                raise UserError(
                    _("You can not change the type of a product that is currently reserved on a stock move. If you need to change the type, you should first unreserve the stock move."
                      ))
        return super(ProductTemplate, self).write(vals)

    def action_view_routes(self):
        routes = self.mapped('route_ids') | self.mapped('categ_id').mapped(
            'total_route_ids') | self.env['stock.location.route'].search(
                [('warehouse_selectable', '=', True)])
        action = self.env.ref('stock.action_routes_form').read()[0]
        action['domain'] = [('id', 'in', routes.ids)]
        return action

    def action_open_quants(self):
        products = self.mapped('product_variant_ids')
        action = self.env.ref('stock.product_open_quants').read()[0]
        action['domain'] = [('product_id', 'in', products.ids)]
        action['context'] = {'search_default_internal_loc': 1}
        return action

    def action_view_orderpoints(self):
        products = self.mapped('product_variant_ids')
        action = self.env.ref('stock.product_open_orderpoint').read()[0]
        if products and len(products) == 1:
            action['context'] = {
                'default_product_id': products.ids[0],
                'search_default_product_id': products.ids[0]
            }
        else:
            action['domain'] = [('product_id', 'in', products.ids)]
            action['context'] = {}
        return action

    def action_view_stock_move_lines(self):
        self.ensure_one()
        action = self.env.ref('stock.stock_move_line_action').read()[0]
        action['domain'] = [('product_id.product_tmpl_id', 'in', self.ids)]
        return action

    def action_open_product_lot(self):
        self.ensure_one()
        action = self.env.ref('stock.action_production_lot_form').read()[0]
        action['domain'] = [('product_id.product_tmpl_id', '=', self.id)]
        if self.product_variant_count == 1:
            action['context'] = {
                'default_product_id': self.product_variant_id.id,
            }

        return action
Example #11
0
class MailMail(models.Model):
    """ Model holding RFC2822 email messages to send. This model also provides
        facilities to queue and send new email messages.  """
    _name = 'mail.mail'
    _description = 'Outgoing Mails'
    _inherits = {'mail.message': 'mail_message_id'}
    _order = 'id desc'
    _rec_name = 'subject'

    # content
    mail_message_id = fields.Many2one('mail.message',
                                      'Message',
                                      required=True,
                                      ondelete='cascade',
                                      index=True,
                                      auto_join=True)
    body_html = fields.Text('Rich-text Contents',
                            help="Rich-text/HTML message")
    references = fields.Text(
        'References',
        help='Message references, such as identifiers of previous messages',
        readonly=1)
    headers = fields.Text('Headers', copy=False)
    # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
    # and during unlink() we will not cascade delete the parent and its attachments
    notification = fields.Boolean(
        'Is Notification',
        help=
        'Mail has been created to notify people of an existing mail.message')
    # recipients: include inactive partners (they may have been archived after
    # the message was sent, but they should remain visible in the relation)
    email_to = fields.Text('To', help='Message recipients (emails)')
    email_cc = fields.Char('Cc', help='Carbon copy message recipients')
    recipient_ids = fields.Many2many('res.partner',
                                     string='To (Partners)',
                                     context={'active_test': False})
    # process
    state = fields.Selection([
        ('outgoing', 'Outgoing'),
        ('sent', 'Sent'),
        ('received', 'Received'),
        ('exception', 'Delivery Failed'),
        ('cancel', 'Cancelled'),
    ],
                             'Status',
                             readonly=True,
                             copy=False,
                             default='outgoing')
    auto_delete = fields.Boolean(
        'Auto Delete',
        help=
        "This option permanently removes any track of email after it's been sent, including from the Technical menu in the Settings, in order to preserve storage space of your Flectra database."
    )
    failure_reason = fields.Text(
        'Failure Reason',
        readonly=1,
        help=
        "Failure reason. This is usually the exception thrown by the email server, stored to ease the debugging of mailing issues."
    )
    scheduled_date = fields.Char(
        'Scheduled Send Date',
        help=
        "If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible."
    )

    @api.model_create_multi
    def create(self, values_list):
        # notification field: if not set, set if mail comes from an existing mail.message
        for values in values_list:
            if 'notification' not in values and values.get('mail_message_id'):
                values['notification'] = True

        new_mails = super(MailMail, self).create(values_list)

        new_mails_w_attach = self
        for mail, values in zip(new_mails, values_list):
            if values.get('attachment_ids'):
                new_mails_w_attach += mail
        if new_mails_w_attach:
            new_mails_w_attach.mapped('attachment_ids').check(mode='read')

        return new_mails

    def write(self, vals):
        res = super(MailMail, self).write(vals)
        if vals.get('attachment_ids'):
            for mail in self:
                mail.attachment_ids.check(mode='read')
        return res

    def unlink(self):
        # cascade-delete the parent message for all mails that are not created for a notification
        mail_msg_cascade_ids = [
            mail.mail_message_id.id for mail in self if not mail.notification
        ]
        res = super(MailMail, self).unlink()
        if mail_msg_cascade_ids:
            self.env['mail.message'].browse(mail_msg_cascade_ids).unlink()
        return res

    @api.model
    def default_get(self, fields):
        # protection for `default_type` values leaking from menu action context (e.g. for invoices)
        # To remove when automatic context propagation is removed in web client
        if self._context.get('default_type') not in type(
                self).message_type.base_field.selection:
            self = self.with_context(dict(self._context, default_type=None))
        return super(MailMail, self).default_get(fields)

    def mark_outgoing(self):
        return self.write({'state': 'outgoing'})

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

    @api.model
    def process_email_queue(self, ids=None):
        """Send immediately queued messages, committing after each
           message is sent - this is not transactional and should
           not be called during another transaction!

           :param list ids: optional list of emails ids to send. If passed
                            no search is performed, and these ids are used
                            instead.
           :param dict context: if a 'filters' key is present in context,
                                this value will be used as an additional
                                filter to further restrict the outgoing
                                messages to send (by default all 'outgoing'
                                messages are sent).
        """
        filters = [
            '&', ('state', '=', 'outgoing'), '|',
            ('scheduled_date', '<', datetime.datetime.now()),
            ('scheduled_date', '=', False)
        ]
        if 'filters' in self._context:
            filters.extend(self._context['filters'])
        # TODO: make limit configurable
        filtered_ids = self.search(filters, limit=10000).ids
        if not ids:
            ids = filtered_ids
        else:
            ids = list(set(filtered_ids) & set(ids))
        ids.sort()

        res = None
        try:
            # auto-commit except in testing mode
            auto_commit = not getattr(threading.currentThread(), 'testing',
                                      False)
            res = self.browse(ids).send(auto_commit=auto_commit)
        except Exception:
            _logger.exception("Failed processing mail queue")
        return res

    def _postprocess_sent_message(self,
                                  success_pids,
                                  failure_reason=False,
                                  failure_type=None):
        """Perform any post-processing necessary after sending ``mail``
        successfully, including deleting it completely along with its
        attachment if the ``auto_delete`` flag of the mail was set.
        Overridden by subclasses for extra post-processing behaviors.

        :return: True
        """
        notif_mails_ids = [mail.id for mail in self if mail.notification]
        if notif_mails_ids:
            notifications = self.env['mail.notification'].search([
                ('notification_type', '=', 'email'),
                ('mail_id', 'in', notif_mails_ids),
                ('notification_status', 'not in', ('sent', 'canceled'))
            ])
            if notifications:
                # find all notification linked to a failure
                failed = self.env['mail.notification']
                if failure_type:
                    failed = notifications.filtered(
                        lambda notif: notif.res_partner_id not in success_pids)
                (notifications - failed).sudo().write({
                    'notification_status': 'sent',
                    'failure_type': '',
                    'failure_reason': '',
                })
                if failed:
                    failed.sudo().write({
                        'notification_status': 'exception',
                        'failure_type': failure_type,
                        'failure_reason': failure_reason,
                    })
                    messages = notifications.mapped(
                        'mail_message_id').filtered(
                            lambda m: m.is_thread_message())
                    # TDE TODO: could be great to notify message-based, not notifications-based, to lessen number of notifs
                    messages._notify_message_notification_update(
                    )  # notify user that we have a failure
        if not failure_type or failure_type == 'RECIPIENT':  # if we have another error, we want to keep the mail.
            mail_to_delete_ids = [mail.id for mail in self if mail.auto_delete]
            self.browse(mail_to_delete_ids).sudo().unlink()
        return True

    # ------------------------------------------------------
    # mail_mail formatting, tools and send mechanism
    # ------------------------------------------------------

    def _send_prepare_body(self):
        """Return a specific ir_email body. The main purpose of this method
        is to be inherited to add custom content depending on some module."""
        self.ensure_one()
        return self.body_html or ''

    def _send_prepare_values(self, partner=None):
        """Return a dictionary for specific email values, depending on a
        partner, or generic to the whole recipients given by mail.email_to.

            :param Model partner: specific recipient partner
        """
        self.ensure_one()
        body = self._send_prepare_body()
        body_alternative = tools.html2plaintext(body)
        if partner:
            email_to = [
                tools.formataddr((partner.name or 'False', partner.email
                                  or 'False'))
            ]
        else:
            email_to = tools.email_split_and_format(self.email_to)
        res = {
            'body': body,
            'body_alternative': body_alternative,
            'email_to': email_to,
        }
        return res

    def _split_by_server(self):
        """Returns an iterator of pairs `(mail_server_id, record_ids)` for current recordset.

        The same `mail_server_id` may repeat in order to limit batch size according to
        the `mail.session.batch.size` system parameter.
        """
        groups = defaultdict(list)
        # Turn prefetch OFF to avoid MemoryError on very large mail queues, we only care
        # about the mail server ids in this case.
        for mail in self.with_context(prefetch_fields=False):
            groups[mail.mail_server_id.id].append(mail.id)
        sys_params = self.env['ir.config_parameter'].sudo()
        batch_size = int(sys_params.get_param('mail.session.batch.size', 1000))
        for server_id, record_ids in groups.items():
            for mail_batch in tools.split_every(batch_size, record_ids):
                yield server_id, mail_batch

    def send(self, auto_commit=False, raise_exception=False):
        """ Sends the selected emails immediately, ignoring their current
            state (mails that have already been sent should not be passed
            unless they should actually be re-sent).
            Emails successfully delivered are marked as 'sent', and those
            that fail to be deliver are marked as 'exception', and the
            corresponding error mail is output in the server logs.

            :param bool auto_commit: whether to force a commit of the mail status
                after sending each mail (meant only for scheduler processing);
                should never be True during normal transactions (default: False)
            :param bool raise_exception: whether to raise an exception if the
                email sending process has failed
            :return: True
        """
        for server_id, batch_ids in self._split_by_server():
            smtp_session = None
            try:
                smtp_session = self.env['ir.mail_server'].connect(
                    mail_server_id=server_id)
            except Exception as exc:
                if raise_exception:
                    # To be consistent and backward compatible with mail_mail.send() raised
                    # exceptions, it is encapsulated into an Flectra MailDeliveryException
                    raise MailDeliveryException(
                        _('Unable to connect to SMTP Server'), exc)
                else:
                    batch = self.browse(batch_ids)
                    batch.write({'state': 'exception', 'failure_reason': exc})
                    batch._postprocess_sent_message(success_pids=[],
                                                    failure_type="SMTP")
            else:
                self.browse(batch_ids)._send(auto_commit=auto_commit,
                                             raise_exception=raise_exception,
                                             smtp_session=smtp_session)
                _logger.info('Sent batch %s emails via mail server ID #%s',
                             len(batch_ids), server_id)
            finally:
                if smtp_session:
                    smtp_session.quit()

    def _send(self,
              auto_commit=False,
              raise_exception=False,
              smtp_session=None):
        IrMailServer = self.env['ir.mail_server']
        IrAttachment = self.env['ir.attachment']
        for mail_id in self.ids:
            success_pids = []
            failure_type = None
            processing_pid = None
            mail = None
            try:
                mail = self.browse(mail_id)
                if mail.state != 'outgoing':
                    if mail.state != 'exception' and mail.auto_delete:
                        mail.sudo().unlink()
                    continue

                # remove attachments if user send the link with the access_token
                body = mail.body_html or ''
                attachments = mail.attachment_ids
                for link in re.findall(r'/web/(?:content|image)/([0-9]+)',
                                       body):
                    attachments = attachments - IrAttachment.browse(int(link))

                # load attachment binary data with a separate read(), as prefetching all
                # `datas` (binary field) could bloat the browse cache, triggerring
                # soft/hard mem limits with temporary data.
                attachments = [(a['name'], base64.b64decode(a['datas']),
                                a['mimetype'])
                               for a in attachments.sudo().read(
                                   ['name', 'datas', 'mimetype'])
                               if a['datas'] is not False]

                # specific behavior to customize the send email for notified partners
                email_list = []
                if mail.email_to:
                    email_list.append(mail._send_prepare_values())
                for partner in mail.recipient_ids:
                    values = mail._send_prepare_values(partner=partner)
                    values['partner_id'] = partner
                    email_list.append(values)

                # headers
                headers = {}
                ICP = self.env['ir.config_parameter'].sudo()
                bounce_alias = ICP.get_param("mail.bounce.alias")
                catchall_domain = ICP.get_param("mail.catchall.domain")
                if bounce_alias and catchall_domain:
                    if mail.mail_message_id.is_thread_message():
                        headers['Return-Path'] = '%s+%d-%s-%d@%s' % (
                            bounce_alias, mail.id, mail.model, mail.res_id,
                            catchall_domain)
                    else:
                        headers['Return-Path'] = '%s+%d@%s' % (
                            bounce_alias, mail.id, catchall_domain)
                if mail.headers:
                    try:
                        headers.update(ast.literal_eval(mail.headers))
                    except Exception:
                        pass

                # Writing on the mail object may fail (e.g. lock on user) which
                # would trigger a rollback *after* actually sending the email.
                # To avoid sending twice the same email, provoke the failure earlier
                mail.write({
                    'state':
                    'exception',
                    'failure_reason':
                    _('Error without exception. Probably due do sending an email without computed recipients.'
                      ),
                })
                # Update notification in a transient exception state to avoid concurrent
                # update in case an email bounces while sending all emails related to current
                # mail record.
                notifs = self.env['mail.notification'].search([
                    ('notification_type', '=', 'email'),
                    ('mail_id', 'in', mail.ids),
                    ('notification_status', 'not in', ('sent', 'canceled'))
                ])
                if notifs:
                    notif_msg = _(
                        'Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.'
                    )
                    notifs.sudo().write({
                        'notification_status': 'exception',
                        'failure_type': 'UNKNOWN',
                        'failure_reason': notif_msg,
                    })
                    # `test_mail_bounce_during_send`, force immediate update to obtain the lock.
                    # see rev. 56596e5240ef920df14d99087451ce6f06ac6d36
                    notifs.flush(fnames=[
                        'notification_status', 'failure_type', 'failure_reason'
                    ],
                                 records=notifs)

                # build an RFC2822 email.message.Message object and send it without queuing
                res = None
                for email in email_list:
                    msg = IrMailServer.build_email(
                        email_from=mail.email_from,
                        email_to=email.get('email_to'),
                        subject=mail.subject,
                        body=email.get('body'),
                        body_alternative=email.get('body_alternative'),
                        email_cc=tools.email_split(mail.email_cc),
                        reply_to=mail.reply_to,
                        attachments=attachments,
                        message_id=mail.message_id,
                        references=mail.references,
                        object_id=mail.res_id
                        and ('%s-%s' % (mail.res_id, mail.model)),
                        subtype='html',
                        subtype_alternative='plain',
                        headers=headers)
                    processing_pid = email.pop("partner_id", None)
                    try:
                        res = IrMailServer.send_email(
                            msg,
                            mail_server_id=mail.mail_server_id.id,
                            smtp_session=smtp_session)
                        if processing_pid:
                            success_pids.append(processing_pid)
                        processing_pid = None
                    except AssertionError as error:
                        if str(error) == IrMailServer.NO_VALID_RECIPIENT:
                            failure_type = "RECIPIENT"
                            # No valid recipient found for this particular
                            # mail item -> ignore error to avoid blocking
                            # delivery to next recipients, if any. If this is
                            # the only recipient, the mail will show as failed.
                            _logger.info(
                                "Ignoring invalid recipients for mail.mail %s: %s",
                                mail.message_id, email.get('email_to'))
                        else:
                            raise
                if res:  # mail has been sent at least once, no major exception occured
                    mail.write({
                        'state': 'sent',
                        'message_id': res,
                        'failure_reason': False
                    })
                    _logger.info(
                        'Mail with ID %r and Message-Id %r successfully sent',
                        mail.id, mail.message_id)
                    # /!\ can't use mail.state here, as mail.refresh() will cause an error
                    # see revid:[email protected] in 6.1
                mail._postprocess_sent_message(success_pids=success_pids,
                                               failure_type=failure_type)
            except MemoryError:
                # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
                # instead of marking the mail as failed
                _logger.exception(
                    'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
                    mail.id, mail.message_id)
                # mail status will stay on ongoing since transaction will be rollback
                raise
            except (psycopg2.Error, smtplib.SMTPServerDisconnected):
                # If an error with the database or SMTP session occurs, chances are that the cursor
                # or SMTP session are unusable, causing further errors when trying to save the state.
                _logger.exception(
                    'Exception while processing mail with ID %r and Msg-Id %r.',
                    mail.id, mail.message_id)
                raise
            except Exception as e:
                failure_reason = tools.ustr(e)
                _logger.exception('failed sending mail (id: %s) due to %s',
                                  mail.id, failure_reason)
                mail.write({
                    'state': 'exception',
                    'failure_reason': failure_reason
                })
                mail._postprocess_sent_message(success_pids=success_pids,
                                               failure_reason=failure_reason,
                                               failure_type='UNKNOWN')
                if raise_exception:
                    if isinstance(e, (AssertionError, UnicodeEncodeError)):
                        if isinstance(e, UnicodeEncodeError):
                            value = "Invalid text: %s" % e.object
                        else:
                            value = '. '.join(e.args)
                        raise MailDeliveryException(value)
                    raise

            if auto_commit is True:
                self._cr.commit()
        return True
Example #12
0
class SalonOrder(models.Model):
    _name = 'salon.order'

    @api.depends('order_line.price_subtotal')
    def sub_total_update(self):
        for order in self:
            amount_untaxed = 0.0
            for line in order.order_line:
                amount_untaxed += line.price_subtotal
            order.price_subtotal = amount_untaxed
        for order in self:
            total_time_taken = 0.0
            for line in order.order_line:
                total_time_taken += line.time_taken
            order.time_taken_total = total_time_taken
        time_takes = total_time_taken
        hours = int(time_takes)
        minutes = (time_takes - hours) * 60
        start_time_store = datetime.strptime(self.start_time,
                                             "%Y-%m-%d %H:%M:%S")
        self.write({
            'end_time':
            start_time_store + timedelta(hours=hours, minutes=minutes)
        })
        if self.end_time:
            self.write({'end_time_only': str(self.end_time)[11:16]})
        if self.start_time:
            salon_start_time = self.start_time
            salon_start_time_date = salon_start_time[0:10]
            self.write({'start_date_only': salon_start_time_date})
            self.write({'start_time_only': str(self.start_time)[11:16]})

    name = fields.Char(string='Salon',
                       required=True,
                       copy=False,
                       readonly=True,
                       default='Draft Salon Order')
    start_time = fields.Datetime(string="Start time",
                                 default=date.today(),
                                 required=True)
    end_time = fields.Datetime(string="End time")
    date = fields.Datetime(string="Date", required=True, default=date.today())
    color = fields.Integer(string="Colour", default=6)
    partner_id = fields.Many2one(
        'res.partner',
        string="Customer",
        required=False,
        help="If the customer is a regular customer, "
        "then you can add the customer in your database")
    customer_name = fields.Char(string="Name", required=True)
    amount = fields.Float(string="Amount")
    chair_id = fields.Many2one('salon.chair', string="Chair", required=True)
    price_subtotal = fields.Float(string='Total',
                                  compute='sub_total_update',
                                  readonly=True,
                                  store=True)
    time_taken_total = fields.Float(string="Total time taken")
    note = fields.Text('Terms and conditions')
    order_line = fields.One2many('salon.order.lines',
                                 'salon_order',
                                 string="Order Lines")
    stage_id = fields.Many2one('salon.stages', string="Stages", default=1)
    inv_stage_identifier = fields.Boolean(string="Stage Identifier")
    invoice_number = fields.Integer(string="Invoice Number")
    validation_controller = fields.Boolean(string="Validation controller",
                                           default=False)
    start_date_only = fields.Date(string="Date Only")
    booking_identifier = fields.Boolean(string="Booking Identifier")
    start_time_only = fields.Char(string="Start Time Only")
    end_time_only = fields.Char(string="End Time Only")
    chair_user = fields.Many2one('res.users', string="Chair User")
    salon_order_created_user = fields.Integer(
        string="Salon Order Created User", default=lambda self: self._uid)

    @api.onchange('start_time')
    def start_date_change(self):
        salon_start_time = self.start_time
        salon_start_time_date = salon_start_time[0:10]
        self.write({'start_date_only': salon_start_time_date})

    @api.multi
    def action_view_invoice_salon(self):
        imd = self.env['ir.model.data']
        action = imd.xmlid_to_object('account.action_invoice_tree1')
        list_view_id = imd.xmlid_to_res_id('account.invoice_tree')
        form_view_id = imd.xmlid_to_res_id('account.invoice_form')
        result = {
            'name':
            action.name,
            'help':
            action.help,
            'type':
            action.type,
            'views': [[form_view_id, 'form'], [list_view_id, 'tree'],
                      [False, 'graph'], [False, 'kanban'], [False, 'calendar'],
                      [False, 'pivot']],
            'target':
            action.target,
            'context':
            action.context,
            'res_model':
            action.res_model,
            'res_id':
            self.invoice_number,
        }
        return result

    @api.multi
    def write(self, cr):
        if 'stage_id' in cr.keys():
            if self.stage_id.id == 3 and cr['stage_id'] != 4:
                raise ValidationError(_("You can't perform that move !"))
            if self.stage_id.id == 1 and cr['stage_id'] not in [2, 5]:
                raise ValidationError(_("You can't perform that move!"))
            if self.stage_id.id == 4:
                raise ValidationError(
                    _("You can't move a salon order from closed stage !"))
            if self.stage_id.id == 5:
                raise ValidationError(
                    _("You can't move a salon order from cancel stage !"))
            if self.stage_id.id == 2 and (cr['stage_id'] == 1
                                          or cr['stage_id'] == 4):
                raise ValidationError(_("You can't perform that move !"))
            if self.stage_id.id == 2 and cr[
                    'stage_id'] == 3 and self.inv_stage_identifier is False:
                self.salon_invoice_create()
        if 'stage_id' in cr.keys() and self.name == "Draft Salon Order":
            if cr['stage_id'] == 2:
                self.salon_confirm()
        return super(SalonOrder, self).write(cr)

    @api.multi
    def salon_confirm(self):
        sequence_code = 'salon.order.sequence'
        order_date = self.date
        order_date = order_date[0:10]
        self.name = self.env['ir.sequence'].with_context(
            ir_sequence_date=order_date).next_by_code(sequence_code)
        if self.partner_id:
            self.partner_id.partner_salon = True
        self.stage_id = 2
        self.chair_id.number_of_orders = len(self.env['salon.order'].search([
            ("chair_id", "=", self.chair_id.id), ("stage_id", "in", [2, 3])
        ]))
        self.chair_id.total_time_taken_chair = self.chair_id.total_time_taken_chair + self.time_taken_total
        self.chair_user = self.chair_id.user_of_chair

    @api.multi
    def salon_validate(self):
        self.validation_controller = True

    @api.multi
    def salon_close(self):
        self.stage_id = 4
        self.chair_id.number_of_orders = len(self.env['salon.order'].search([
            ("chair_id", "=", self.chair_id.id), ("stage_id", "in", [2, 3])
        ]))
        self.chair_id.total_time_taken_chair = self.chair_id.total_time_taken_chair - self.time_taken_total

    @api.multi
    def salon_cancel(self):
        self.stage_id = 5
        self.chair_id.number_of_orders = len(self.env['salon.order'].search([
            ("chair_id", "=", self.chair_id.id), ("stage_id", "in", [2, 3])
        ]))
        if self.stage_id.id != 1:
            self.chair_id.total_time_taken_chair = self.chair_id.total_time_taken_chair - self.time_taken_total

    @api.multi
    def button_total_update(self):
        for order in self:
            amount_untaxed = 0.0
            for line in order.order_line:
                amount_untaxed += line.price_subtotal
            order.price_subtotal = amount_untaxed

    @api.onchange('chair_id')
    def onchange_chair(self):
        if 'active_id' in self._context.keys():
            self.chair_id = self._context['active_id']

    @api.multi
    def salon_invoice_create(self):
        inv_obj = self.env['account.invoice']
        inv_line_obj = self.env['account.invoice.line']
        if self.partner_id:
            supplier = self.partner_id
        else:
            supplier = self.partner_id.search([("name", "=",
                                                "Salon Default Customer")])
        company_id = self.env['res.users'].browse(1).company_id
        currency_salon = company_id.currency_id.id

        inv_data = {
            'name': supplier.name,
            'reference': supplier.name,
            'account_id': supplier.property_account_payable_id.id,
            'partner_id': supplier.id,
            'currency_id': currency_salon,
            'journal_id': 1,
            'origin': self.name,
            'company_id': company_id.id,
        }
        inv_id = inv_obj.create(inv_data)
        self.invoice_number = inv_id
        product_id = self.env['product.product'].search([("name", "=",
                                                          "Salon Service")])
        for records in self.order_line:
            if product_id.property_account_income_id.id:
                income_account = product_id.property_account_income_id.id
            elif product_id.categ_id.property_account_income_categ_id.id:
                income_account = product_id.categ_id.property_account_income_categ_id.id
            else:
                raise UserError(
                    _('Please define income account for this product: "%s" (id:%d).'
                      ) % (product_id.name, product_id.id))
            inv_line_data = {
                'name': records.service_id.name,
                'account_id': income_account,
                'price_unit': records.price,
                'quantity': 1,
                'product_id': product_id.id,
                'invoice_id': inv_id.id,
            }
            inv_line_obj.create(inv_line_data)

        imd = self.env['ir.model.data']
        action = imd.xmlid_to_object('account.action_invoice_tree1')
        list_view_id = imd.xmlid_to_res_id('account.invoice_tree')
        form_view_id = imd.xmlid_to_res_id('account.invoice_form')

        result = {
            'name':
            action.name,
            'help':
            action.help,
            'type':
            'ir.actions.act_window',
            'views': [[list_view_id, 'tree'], [form_view_id, 'form'],
                      [False, 'graph'], [False, 'kanban'], [False, 'calendar'],
                      [False, 'pivot']],
            'target':
            action.target,
            'context':
            action.context,
            'res_model':
            'account.invoice',
        }
        if len(inv_id) > 1:
            result['domain'] = "[('id','in',%s)]" % inv_id.ids
        elif len(inv_id) == 1:
            result['views'] = [(form_view_id, 'form')]
            result['res_id'] = inv_id.ids[0]
        else:
            result = {'type': 'ir.actions.act_window_close'}
        self.inv_stage_identifier = True
        self.stage_id = 3
        invoiced_records = self.env['salon.order'].search([
            ('stage_id', 'in', [3, 4]), ('chair_id', '=', self.chair_id.id)
        ])
        total = 0
        for rows in invoiced_records:
            invoiced_date = rows.date
            invoiced_date = invoiced_date[0:10]
            if invoiced_date == str(date.today()):
                total = total + rows.price_subtotal
        self.chair_id.collection_today = total
        self.chair_id.number_of_orders = len(self.env['salon.order'].search([
            ("chair_id", "=", self.chair_id.id), ("stage_id", "in", [2, 3])
        ]))
        return result

    @api.multi
    def unlink(self):
        for order in self:
            if order.stage_id.id == 3 or order.stage_id.id == 4:
                raise UserError(_("You can't delete an invoiced salon order!"))
        return super(SalonOrder, self).unlink()
Example #13
0
class HrExpense(models.Model):

    _name = "hr.expense"
    _inherit = ['mail.thread']
    _description = "Expense"
    _order = "date desc, id desc"

    name = fields.Char(string='Expense Description',
                       readonly=True,
                       required=True,
                       states={
                           'draft': [('readonly', False)],
                           'refused': [('readonly', False)]
                       })
    date = fields.Date(readonly=True,
                       states={
                           'draft': [('readonly', False)],
                           'refused': [('readonly', False)]
                       },
                       default=fields.Date.context_today,
                       string="Expense Date")
    employee_id = fields.Many2one(
        'hr.employee',
        string="Employee",
        required=True,
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'refused': [('readonly', False)]
        },
        default=lambda self: self.env['hr.employee'].search(
            [('user_id', '=', self.env.uid)], limit=1))
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 readonly=True,
                                 states={
                                     'draft': [('readonly', False)],
                                     'refused': [('readonly', False)]
                                 },
                                 domain=[('can_be_expensed', '=', True)],
                                 required=True)
    product_uom_id = fields.Many2one(
        'product.uom',
        string='Unit of Measure',
        required=True,
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'refused': [('readonly', False)]
        },
        default=lambda self: self.env['product.uom'].search(
            [], limit=1, order='id'))
    unit_amount = fields.Float(string='Unit Price',
                               readonly=True,
                               required=True,
                               states={
                                   'draft': [('readonly', False)],
                                   'refused': [('readonly', False)]
                               },
                               digits=dp.get_precision('Product Price'))
    quantity = fields.Float(required=True,
                            readonly=True,
                            states={
                                'draft': [('readonly', False)],
                                'refused': [('readonly', False)]
                            },
                            digits=dp.get_precision('Product Unit of Measure'),
                            default=1)
    tax_ids = fields.Many2many('account.tax',
                               'expense_tax',
                               'expense_id',
                               'tax_id',
                               string='Taxes',
                               states={
                                   'done': [('readonly', True)],
                                   'post': [('readonly', True)]
                               })
    untaxed_amount = fields.Float(string='Subtotal',
                                  store=True,
                                  compute='_compute_amount',
                                  digits=dp.get_precision('Account'))
    total_amount = fields.Float(string='Total',
                                store=True,
                                compute='_compute_amount',
                                digits=dp.get_precision('Account'))
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True,
                                 states={
                                     'draft': [('readonly', False)],
                                     'refused': [('readonly', False)]
                                 },
                                 default=lambda self: self.env.user.company_id)
    currency_id = fields.Many2one(
        'res.currency',
        string='Currency',
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'refused': [('readonly', False)]
        },
        default=lambda self: self.env.user.company_id.currency_id)
    analytic_account_id = fields.Many2one('account.analytic.account',
                                          string='Analytic Account',
                                          states={
                                              'post': [('readonly', True)],
                                              'done': [('readonly', True)]
                                          },
                                          oldname='analytic_account')
    account_id = fields.Many2one(
        'account.account',
        string='Account',
        states={
            'post': [('readonly', True)],
            'done': [('readonly', True)]
        },
        default=lambda self: self.env['ir.property'].get(
            'property_account_expense_categ_id', 'product.category'),
        help="An expense account is expected")
    description = fields.Text()
    payment_mode = fields.Selection(
        [("own_account", "Employee (to reimburse)"),
         ("company_account", "Company")],
        default='own_account',
        states={
            'done': [('readonly', True)],
            'post': [('readonly', True)],
            'submitted': [('readonly', True)]
        },
        string="Payment By")
    attachment_number = fields.Integer(compute='_compute_attachment_number',
                                       string='Number of Attachments')
    state = fields.Selection([('draft', 'To Submit'), ('reported', 'Reported'),
                              ('done', 'Posted'), ('refused', 'Refused')],
                             compute='_compute_state',
                             string='Status',
                             copy=False,
                             index=True,
                             readonly=True,
                             store=True,
                             help="Status of the expense.")
    sheet_id = fields.Many2one('hr.expense.sheet',
                               string="Expense Report",
                               readonly=True,
                               copy=False)
    reference = fields.Char(string="Bill Reference")
    is_refused = fields.Boolean(
        string="Explicitely Refused by manager or acccountant",
        readonly=True,
        copy=False)

    @api.depends('sheet_id', 'sheet_id.account_move_id', 'sheet_id.state')
    def _compute_state(self):
        for expense in self:
            if not expense.sheet_id:
                expense.state = "draft"
            elif expense.sheet_id.state == "cancel":
                expense.state = "refused"
            elif not expense.sheet_id.account_move_id:
                expense.state = "reported"
            else:
                expense.state = "done"

    @api.depends('quantity', 'unit_amount', 'tax_ids', 'currency_id')
    def _compute_amount(self):
        for expense in self:
            expense.untaxed_amount = expense.unit_amount * expense.quantity
            taxes = expense.tax_ids.compute_all(
                expense.unit_amount, expense.currency_id, expense.quantity,
                expense.product_id, expense.employee_id.user_id.partner_id)
            expense.total_amount = taxes.get('total_included')

    @api.multi
    def _compute_attachment_number(self):
        attachment_data = self.env['ir.attachment'].read_group(
            [('res_model', '=', 'hr.expense'),
             ('res_id', 'in', self.ids)], ['res_id'], ['res_id'])
        attachment = dict(
            (data['res_id'], data['res_id_count']) for data in attachment_data)
        for expense in self:
            expense.attachment_number = attachment.get(expense.id, 0)

    @api.onchange('product_id')
    def _onchange_product_id(self):
        if self.product_id:
            if not self.name:
                self.name = self.product_id.display_name or ''
            self.unit_amount = self.product_id.price_compute('standard_price')[
                self.product_id.id]
            self.product_uom_id = self.product_id.uom_id
            self.tax_ids = self.product_id.supplier_taxes_id
            account = self.product_id.product_tmpl_id._get_product_accounts(
            )['expense']
            if account:
                self.account_id = account

    @api.onchange('product_uom_id')
    def _onchange_product_uom_id(self):
        if self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id:
            raise UserError(
                _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure'
                  ))

    @api.multi
    def view_sheet(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'hr.expense.sheet',
            'target': 'current',
            'res_id': self.sheet_id.id
        }

    @api.multi
    def submit_expenses(self):
        if any(expense.state != 'draft' for expense in self):
            raise UserError(_("You cannot report twice the same line!"))
        if len(self.mapped('employee_id')) != 1:
            raise UserError(
                _("You cannot report expenses for different employees in the same report!"
                  ))
        return {
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'hr.expense.sheet',
            'target': 'current',
            'context': {
                'default_expense_line_ids': [line.id for line in self],
                'default_employee_id': self[0].employee_id.id,
                'default_name': self[0].name if len(self.ids) == 1 else ''
            }
        }

    def _prepare_move_line(self, line):
        '''
        This function prepares move line of account.move related to an expense
        '''
        partner_id = self.employee_id.address_home_id.commercial_partner_id.id
        return {
            'date_maturity':
            line.get('date_maturity'),
            'partner_id':
            partner_id,
            'name':
            line['name'][:64],
            'debit':
            line['price'] > 0 and line['price'],
            'credit':
            line['price'] < 0 and -line['price'],
            'account_id':
            line['account_id'],
            'analytic_line_ids':
            line.get('analytic_line_ids'),
            'amount_currency':
            line['price'] > 0 and abs(line.get('amount_currency'))
            or -abs(line.get('amount_currency')),
            'currency_id':
            line.get('currency_id'),
            'tax_line_id':
            line.get('tax_line_id'),
            'tax_ids':
            line.get('tax_ids'),
            'quantity':
            line.get('quantity', 1.00),
            'product_id':
            line.get('product_id'),
            'product_uom_id':
            line.get('uom_id'),
            'analytic_account_id':
            line.get('analytic_account_id'),
            'payment_id':
            line.get('payment_id'),
            'expense_id':
            line.get('expense_id'),
        }

    @api.multi
    def _compute_expense_totals(self, company_currency, account_move_lines,
                                move_date):
        '''
        internal method used for computation of total amount of an expense in the company currency and
        in the expense currency, given the account_move_lines that will be created. It also do some small
        transformations at these account_move_lines (for multi-currency purposes)

        :param account_move_lines: list of dict
        :rtype: tuple of 3 elements (a, b ,c)
            a: total in company currency
            b: total in hr.expense currency
            c: account_move_lines potentially modified
        '''
        self.ensure_one()
        total = 0.0
        total_currency = 0.0
        for line in account_move_lines:
            line['currency_id'] = False
            line['amount_currency'] = False
            if self.currency_id != company_currency:
                line['currency_id'] = self.currency_id.id
                line['amount_currency'] = line['price']
                line['price'] = self.currency_id.with_context(
                    date=move_date or fields.Date.context_today(self)).compute(
                        line['price'], company_currency)
            total -= line['price']
            total_currency -= line['amount_currency'] or line['price']
        return total, total_currency, account_move_lines

    @api.multi
    def action_move_create(self):
        '''
        main function that is called when trying to create the accounting entries related to an expense
        '''
        move_group_by_sheet = {}
        for expense in self:
            journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
            #create the move that will contain the accounting entries
            acc_date = expense.sheet_id.accounting_date or expense.date
            if not expense.sheet_id.id in move_group_by_sheet:
                move = self.env['account.move'].create({
                    'journal_id':
                    journal.id,
                    'company_id':
                    self.env.user.company_id.id,
                    'date':
                    acc_date,
                    'ref':
                    expense.sheet_id.name,
                    # force the name to the default value, to avoid an eventual 'default_name' in the context
                    # to set it to '' which cause no number to be given to the account.move when posted.
                    'name':
                    '/',
                })
                move_group_by_sheet[expense.sheet_id.id] = move
            else:
                move = move_group_by_sheet[expense.sheet_id.id]
            company_currency = expense.company_id.currency_id
            diff_currency_p = expense.currency_id != company_currency
            #one account.move.line per expense (+taxes..)
            move_lines = expense._move_line_get()

            #create one more move line, a counterline for the total on payable account
            payment_id = False
            total, total_currency, move_lines = expense._compute_expense_totals(
                company_currency, move_lines, acc_date)
            if expense.payment_mode == 'company_account':
                if not expense.sheet_id.bank_journal_id.default_credit_account_id:
                    raise UserError(
                        _("No credit account found for the %s journal, please configure one."
                          ) % (expense.sheet_id.bank_journal_id.name))
                emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id
                journal = expense.sheet_id.bank_journal_id
                #create payment
                payment_methods = (
                    total < 0
                ) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids
                journal_currency = journal.currency_id or journal.company_id.currency_id
                payment = self.env['account.payment'].create({
                    'payment_method_id':
                    payment_methods and payment_methods[0].id or False,
                    'payment_type':
                    total < 0 and 'outbound' or 'inbound',
                    'partner_id':
                    expense.employee_id.address_home_id.commercial_partner_id.
                    id,
                    'partner_type':
                    'supplier',
                    'journal_id':
                    journal.id,
                    'payment_date':
                    expense.date,
                    'state':
                    'reconciled',
                    'currency_id':
                    diff_currency_p and expense.currency_id.id
                    or journal_currency.id,
                    'amount':
                    diff_currency_p and abs(total_currency) or abs(total),
                    'name':
                    expense.name,
                })
                payment_id = payment.id
            else:
                if not expense.employee_id.address_home_id:
                    raise UserError(
                        _("No Home Address found for the employee %s, please configure one."
                          ) % (expense.employee_id.name))
                emp_account = expense.employee_id.address_home_id.property_account_payable_id.id

            aml_name = expense.employee_id.name + ': ' + expense.name.split(
                '\n')[0][:64]
            move_lines.append({
                'type':
                'dest',
                'name':
                aml_name,
                'price':
                total,
                'account_id':
                emp_account,
                'date_maturity':
                acc_date,
                'amount_currency':
                diff_currency_p and total_currency or False,
                'currency_id':
                diff_currency_p and expense.currency_id.id or False,
                'payment_id':
                payment_id,
                'expense_id':
                expense.id,
            })

            #convert eml into an osv-valid format
            lines = [(0, 0, expense._prepare_move_line(x)) for x in move_lines]
            move.with_context(dont_create_taxes=True).write(
                {'line_ids': lines})
            expense.sheet_id.write({'account_move_id': move.id})
            if expense.payment_mode == 'company_account':
                expense.sheet_id.paid_expense_sheets()
        for move in move_group_by_sheet.values():
            move.post()
        return True

    @api.multi
    def _prepare_move_line_value(self):
        self.ensure_one()
        if self.account_id:
            account = self.account_id
        elif self.product_id:
            account = self.product_id.product_tmpl_id._get_product_accounts(
            )['expense']
            if not account:
                raise UserError(
                    _("No Expense account found for the product %s (or for its category), please configure one."
                      ) % (self.product_id.name))
        else:
            account = self.env['ir.property'].with_context(
                force_company=self.company_id.id).get(
                    'property_account_expense_categ_id', 'product.category')
            if not account:
                raise UserError(
                    _('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.'
                      ))
        aml_name = self.employee_id.name + ': ' + self.name.split('\n')[0][:64]
        move_line = {
            'type': 'src',
            'name': aml_name,
            'price_unit': self.unit_amount,
            'quantity': self.quantity,
            'price': self.total_amount,
            'account_id': account.id,
            'product_id': self.product_id.id,
            'uom_id': self.product_uom_id.id,
            'analytic_account_id': self.analytic_account_id.id,
            'expense_id': self.id,
        }
        return move_line

    @api.multi
    def _move_line_get(self):
        account_move = []
        for expense in self:
            move_line = expense._prepare_move_line_value()
            account_move.append(move_line)

            # Calculate tax lines and adjust base line
            taxes = expense.tax_ids.with_context(round=True).compute_all(
                expense.unit_amount, expense.currency_id, expense.quantity,
                expense.product_id)
            account_move[-1]['price'] = taxes['total_excluded']
            account_move[-1]['tax_ids'] = [(6, 0, expense.tax_ids.ids)]
            for tax in taxes['taxes']:
                account_move.append({
                    'type':
                    'tax',
                    'name':
                    tax['name'],
                    'price_unit':
                    tax['amount'],
                    'quantity':
                    1,
                    'price':
                    tax['amount'],
                    'account_id':
                    tax['account_id'] or move_line['account_id'],
                    'tax_line_id':
                    tax['id'],
                    'expense_id':
                    expense.id,
                })
        return account_move

    @api.multi
    def unlink(self):
        for expense in self:
            if expense.state in ['done']:
                raise UserError(_('You cannot delete a posted expense.'))
        super(HrExpense, self).unlink()

    @api.multi
    def action_get_attachment_view(self):
        self.ensure_one()
        res = self.env['ir.actions.act_window'].for_xml_id(
            'base', 'action_attachment')
        res['domain'] = [('res_model', '=', 'hr.expense'),
                         ('res_id', 'in', self.ids)]
        res['context'] = {
            'default_res_model': 'hr.expense',
            'default_res_id': self.id
        }
        return res

    @api.multi
    def refuse_expense(self, reason):
        self.write({'is_refused': True})
        self.sheet_id.write({'state': 'cancel'})
        self.sheet_id.message_post_with_view(
            'hr_expense.hr_expense_template_refuse_reason',
            values={
                'reason': reason,
                'is_sheet': False,
                'name': self.name
            })

    @api.model
    def get_empty_list_help(self, help_message):
        if help_message:
            use_mailgateway = self.env['ir.config_parameter'].sudo().get_param(
                'hr_expense.use_mailgateway')
            alias_record = use_mailgateway and self.env.ref(
                'hr_expense.mail_alias_expense') or False
            if alias_record and alias_record.alias_domain and alias_record.alias_name:
                link = "<a id='o_mail_test' href='mailto:%(email)s?subject=Lunch%%20with%%20customer%%3A%%20%%2412.32'>%(email)s</a>" % {
                    'email':
                    '%s@%s' %
                    (alias_record.alias_name, alias_record.alias_domain)
                }
                return '<p class="oe_view_nocontent_create">%s<br/>%s</p>%s' % (
                    _('Click to add a new expense,'),
                    _('or send receipts by email to %s.') %
                    (link, ), help_message)
        return super(HrExpense, self).get_empty_list_help(help_message)

    @api.model
    def message_new(self, msg_dict, custom_values=None):
        if custom_values is None:
            custom_values = {}

        email_address = email_split(msg_dict.get('email_from', False))[0]

        employee = self.env['hr.employee'].search([
            '|', ('work_email', 'ilike', email_address),
            ('user_id.email', 'ilike', email_address)
        ],
                                                  limit=1)

        expense_description = msg_dict.get('subject', '')

        # Match the first occurence of '[]' in the string and extract the content inside it
        # Example: '[foo] bar (baz)' becomes 'foo'. This is potentially the product code
        # of the product to encode on the expense. If not, take the default product instead
        # which is 'Fixed Cost'
        default_product = self.env.ref('hr_expense.product_product_fixed_cost')
        pattern = '\[([^)]*)\]'
        product_code = re.search(pattern, expense_description)
        if product_code is None:
            product = default_product
        else:
            expense_description = expense_description.replace(
                product_code.group(), '')
            product = self.env['product.product'].search([
                ('default_code', 'ilike', product_code.group(1))
            ]) or default_product

        pattern = '[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?'
        # Match the last occurence of a float in the string
        # Example: '[foo] 50.3 bar 34.5' becomes '34.5'. This is potentially the price
        # to encode on the expense. If not, take 1.0 instead
        expense_price = re.findall(pattern, expense_description)
        # TODO: International formatting
        if not expense_price:
            price = 1.0
        else:
            price = expense_price[-1][0]
            expense_description = expense_description.replace(price, '')
            try:
                price = float(price)
            except ValueError:
                price = 1.0

        custom_values.update({
            'name': expense_description.strip(),
            'employee_id': employee.id,
            'product_id': product.id,
            'product_uom_id': product.uom_id.id,
            'quantity': 1,
            'unit_amount': price,
            'company_id': employee.company_id.id,
        })
        return super(HrExpense, self).message_new(msg_dict, custom_values)
Example #14
0
class Partner(models.Model):
    _description = 'Contact'
    _inherit = ['format.address.mixin']
    _name = "res.partner"
    _order = "display_name"

    def _default_category(self):
        return self.env['res.partner.category'].browse(
            self._context.get('category_id'))

    def _default_company(self):
        return self.env['res.company']._company_default_get('res.partner')

    name = fields.Char(index=True)
    display_name = fields.Char(compute='_compute_display_name',
                               store=True,
                               index=True)
    date = fields.Date(index=True)
    title = fields.Many2one('res.partner.title')
    parent_id = fields.Many2one('res.partner',
                                string='Related Company',
                                index=True)
    parent_name = fields.Char(related='parent_id.name',
                              readonly=True,
                              string='Parent name')
    child_ids = fields.One2many(
        'res.partner',
        'parent_id',
        string='Contacts',
        domain=[('active', '=', True)
                ])  # force "active_test" domain to bypass _search() override
    ref = fields.Char(string='Internal Reference', index=True)
    lang = fields.Selection(
        _lang_get,
        string='Language',
        default=lambda self: self.env.lang,
        help=
        "If the selected language is loaded in the system, all documents related to "
        "this contact will be printed in this language. If not, it will be English."
    )
    tz = fields.Selection(
        _tz_get,
        string='Timezone',
        default=lambda self: self._context.get('tz'),
        help=
        "The partner's timezone, used to output proper date and time values "
        "inside printed reports. It is important to set a value for this field. "
        "You should use the same timezone that is otherwise used to pick and "
        "render date and time values: your computer's timezone.")
    tz_offset = fields.Char(compute='_compute_tz_offset',
                            string='Timezone offset',
                            invisible=True)
    user_id = fields.Many2one(
        'res.users',
        string='Salesperson',
        help=
        'The internal user that is in charge of communicating with this contact if any.'
    )
    vat = fields.Char(string='TIN',
                      help="Tax Identification Number. "
                      "Fill it if the company is subjected to taxes. "
                      "Used by the some of the legal statements.")
    bank_ids = fields.One2many('res.partner.bank',
                               'partner_id',
                               string='Banks')
    website = fields.Char(help="Website of Partner or Company")
    comment = fields.Text(string='Notes')

    category_id = fields.Many2many('res.partner.category',
                                   column1='partner_id',
                                   column2='category_id',
                                   string='Tags',
                                   default=_default_category)
    credit_limit = fields.Float(string='Credit Limit')
    barcode = fields.Char(oldname='ean13')
    active = fields.Boolean(default=True)
    customer = fields.Boolean(
        string='Is a Customer',
        default=True,
        help="Check this box if this contact is a customer.")
    supplier = fields.Boolean(
        string='Is a Vendor',
        help="Check this box if this contact is a vendor. "
        "If it's not checked, purchase people will not see it when encoding a purchase order."
    )
    employee = fields.Boolean(
        help="Check this box if this contact is an Employee.")
    function = fields.Char(string='Job Position')
    type = fields.Selection(
        [('contact', 'Contact'), ('invoice', 'Invoice address'),
         ('delivery', 'Shipping address'), ('other', 'Other address')],
        string='Address Type',
        default='contact',
        help=
        "Used to select automatically the right address according to the context in sales and purchases documents."
    )
    street = fields.Char()
    street2 = fields.Char()
    zip = fields.Char(change_default=True)
    city = fields.Char()
    state_id = fields.Many2one("res.country.state",
                               string='State',
                               ondelete='restrict')
    country_id = fields.Many2one('res.country',
                                 string='Country',
                                 ondelete='restrict')
    email = fields.Char()
    email_formatted = fields.Char(
        'Formatted Email',
        compute='_compute_email_formatted',
        help='Format email address "Name <email@domain>"')
    phone = fields.Char()
    mobile = fields.Char()
    is_company = fields.Boolean(
        string='Is a Company',
        default=False,
        help="Check if the contact is a company, otherwise it is a person")
    industry_id = fields.Many2one('res.partner.industry', 'Industry')
    # company_type is only an interface field, do not use it in business logic
    company_type = fields.Selection(string='Company Type',
                                    selection=[('person', 'Individual'),
                                               ('company', 'Company')],
                                    compute='_compute_company_type',
                                    inverse='_write_company_type')
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 index=True,
                                 default=_default_company)
    color = fields.Integer(string='Color Index', default=0)
    user_ids = fields.One2many('res.users',
                               'partner_id',
                               string='Users',
                               auto_join=True)
    partner_share = fields.Boolean(
        'Share Partner',
        compute='_compute_partner_share',
        store=True,
        help=
        "Either customer (no user), either shared user. Indicated the current partner is a customer without "
        "access or with a limited access created for sharing data.")
    contact_address = fields.Char(compute='_compute_contact_address',
                                  string='Complete Address')

    # technical field used for managing commercial fields
    commercial_partner_id = fields.Many2one(
        'res.partner',
        compute='_compute_commercial_partner',
        string='Commercial Entity',
        store=True,
        index=True)
    commercial_partner_country_id = fields.Many2one(
        'res.country', related='commercial_partner_id.country_id', store=True)
    commercial_company_name = fields.Char(
        'Company Name Entity',
        compute='_compute_commercial_company_name',
        store=True)
    company_name = fields.Char('Company Name')

    # image: all image fields are base64 encoded and PIL-supported
    image = fields.Binary(
        "Image",
        attachment=True,
        help=
        "This field holds the image used as avatar for this contact, limited to 1024x1024px",
    )
    image_medium = fields.Binary("Medium-sized image", attachment=True,
        help="Medium-sized image of this contact. It is automatically "\
             "resized as a 128x128px image, with aspect ratio preserved. "\
             "Use this field in form views or some kanban views.")
    image_small = fields.Binary("Small-sized image", attachment=True,
        help="Small-sized image of this contact. It is automatically "\
             "resized as a 64x64px image, with aspect ratio preserved. "\
             "Use this field anywhere a small image is required.")
    # hack to allow using plain browse record in qweb views, and used in ir.qweb.field.contact
    self = fields.Many2one(comodel_name=_name, compute='_compute_get_ids')

    _sql_constraints = [
        ('check_name',
         "CHECK( (type='contact' AND name IS NOT NULL) or (type!='contact') )",
         'Contacts require a name.'),
    ]

    @api.depends('is_company', 'name', 'parent_id.name', 'type',
                 'company_name')
    def _compute_display_name(self):
        diff = dict(show_address=None, show_address_only=None, show_email=None)
        names = dict(self.with_context(**diff).name_get())
        for partner in self:
            partner.display_name = names.get(partner.id)

    @api.depends('tz')
    def _compute_tz_offset(self):
        for partner in self:
            partner.tz_offset = datetime.datetime.now(
                pytz.timezone(partner.tz or 'GMT')).strftime('%z')

    @api.depends('user_ids.share')
    def _compute_partner_share(self):
        for partner in self:
            partner.partner_share = not partner.user_ids or any(
                user.share for user in partner.user_ids)

    @api.depends(lambda self: self._display_address_depends())
    def _compute_contact_address(self):
        for partner in self:
            partner.contact_address = partner._display_address()

    @api.one
    def _compute_get_ids(self):
        self.self = self.id

    @api.depends('is_company', 'parent_id.commercial_partner_id')
    def _compute_commercial_partner(self):
        for partner in self:
            if partner.is_company or not partner.parent_id:
                partner.commercial_partner_id = partner
            else:
                partner.commercial_partner_id = partner.parent_id.commercial_partner_id

    @api.depends('company_name', 'parent_id.is_company',
                 'commercial_partner_id.name')
    def _compute_commercial_company_name(self):
        for partner in self:
            p = partner.commercial_partner_id
            partner.commercial_company_name = p.is_company and p.name or partner.company_name

    @api.model
    def _get_default_image(self, partner_type, is_company, parent_id):
        if getattr(threading.currentThread(), 'testing',
                   False) or self._context.get('install_mode'):
            return False

        colorize, img_path, image = False, False, False

        if partner_type in ['other'] and parent_id:
            parent_image = self.browse(parent_id).image
            image = parent_image and base64.b64decode(parent_image) or None

        if not image and partner_type == 'invoice':
            img_path = get_module_resource('base', 'static/src/img',
                                           'money.png')
        elif not image and partner_type == 'delivery':
            img_path = get_module_resource('base', 'static/src/img',
                                           'truck.png')
        elif not image and is_company:
            img_path = get_module_resource('base', 'static/src/img',
                                           'company_image.png')
        elif not image:
            img_path = get_module_resource('base', 'static/src/img',
                                           'avatar.png')
            colorize = True

        if img_path:
            with open(img_path, 'rb') as f:
                image = f.read()
        if image and colorize:
            image = tools.image_colorize(image)

        return tools.image_resize_image_big(base64.b64encode(image))

    @api.model
    def _fields_view_get(self,
                         view_id=None,
                         view_type='form',
                         toolbar=False,
                         submenu=False):
        if (not view_id) and (view_type
                              == 'form') and self._context.get('force_email'):
            view_id = self.env.ref('base.view_partner_simple_form').id
        res = super(Partner, self)._fields_view_get(view_id=view_id,
                                                    view_type=view_type,
                                                    toolbar=toolbar,
                                                    submenu=submenu)
        if view_type == 'form':
            res['arch'] = self._fields_view_get_address(res['arch'])
        return res

    @api.constrains('parent_id')
    def _check_parent_id(self):
        if not self._check_recursion():
            raise ValidationError(
                _('You cannot create recursive Partner hierarchies.'))

    @api.multi
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {}, name=_('%s (copy)') % self.name)
        return super(Partner, self).copy(default)

    @api.onchange('parent_id')
    def onchange_parent_id(self):
        # return values in result, as this method is used by _fields_sync()
        if not self.parent_id:
            return
        result = {}
        partner = getattr(self, '_origin', self)
        if partner.parent_id and partner.parent_id != self.parent_id:
            result['warning'] = {
                'title':
                _('Warning'),
                'message':
                _('Changing the company of a contact should only be done if it '
                  'was never correctly set. If an existing contact starts working for a new '
                  'company then a new contact should be created under that new '
                  'company. You can use the "Discard" button to abandon this change.'
                  )
            }
        if partner.type == 'contact' or self.type == 'contact':
            # for contacts: copy the parent address, if set (aka, at least one
            # value is set in the address: otherwise, keep the one from the
            # contact)
            address_fields = self._address_fields()
            if any(self.parent_id[key] for key in address_fields):

                def convert(value):
                    return value.id if isinstance(value,
                                                  models.BaseModel) else value

                result['value'] = {
                    key: convert(self.parent_id[key])
                    for key in address_fields
                }
        return result

    @api.onchange('country_id')
    def _onchange_country_id(self):
        if self.country_id:
            return {
                'domain': {
                    'state_id': [('country_id', '=', self.country_id.id)]
                }
            }
        else:
            return {'domain': {'state_id': []}}

    @api.onchange('email')
    def onchange_email(self):
        if not self.image and self._context.get(
                'gravatar_image') and self.email:
            self.image = self._get_gravatar_image(self.email)

    @api.depends('name', 'email')
    def _compute_email_formatted(self):
        for partner in self:
            partner.email_formatted = formataddr(
                (partner.name or u"False", partner.email or u"False"))

    @api.depends('is_company')
    def _compute_company_type(self):
        for partner in self:
            partner.company_type = 'company' if partner.is_company else 'person'

    def _write_company_type(self):
        for partner in self:
            partner.is_company = partner.company_type == 'company'

    @api.onchange('company_type')
    def onchange_company_type(self):
        self.is_company = (self.company_type == 'company')

    @api.multi
    def _update_fields_values(self, fields):
        """ Returns dict of write() values for synchronizing ``fields`` """
        values = {}
        for fname in fields:
            field = self._fields[fname]
            if field.type == 'many2one':
                values[fname] = self[fname].id
            elif field.type == 'one2many':
                raise AssertionError(
                    _('One2Many fields cannot be synchronized as part of `commercial_fields` or `address fields`'
                      ))
            elif field.type == 'many2many':
                values[fname] = [(6, 0, self[fname].ids)]
            else:
                values[fname] = self[fname]
        return values

    @api.model
    def _address_fields(self):
        """Returns the list of address fields that are synced from the parent."""
        return list(ADDRESS_FIELDS)

    @api.multi
    def update_address(self, vals):
        addr_vals = {
            key: vals[key]
            for key in self._address_fields() if key in vals
        }
        if addr_vals:
            return super(Partner, self).write(addr_vals)

    @api.model
    def _commercial_fields(self):
        """ Returns the list of fields that are managed by the commercial entity
        to which a partner belongs. These fields are meant to be hidden on
        partners that aren't `commercial entities` themselves, and will be
        delegated to the parent `commercial entity`. The list is meant to be
        extended by inheriting classes. """
        return ['vat', 'credit_limit']

    @api.multi
    def _commercial_sync_from_company(self):
        """ Handle sync of commercial fields when a new parent commercial entity is set,
        as if they were related fields """
        commercial_partner = self.commercial_partner_id
        if commercial_partner != self:
            sync_vals = commercial_partner._update_fields_values(
                self._commercial_fields())
            self.write(sync_vals)

    @api.multi
    def _commercial_sync_to_children(self):
        """ Handle sync of commercial fields to descendants """
        commercial_partner = self.commercial_partner_id
        sync_vals = commercial_partner._update_fields_values(
            self._commercial_fields())
        sync_children = self.child_ids.filtered(lambda c: not c.is_company)
        for child in sync_children:
            child._commercial_sync_to_children()
        sync_children._compute_commercial_partner()
        return sync_children.write(sync_vals)

    @api.multi
    def _fields_sync(self, values):
        """ Sync commercial fields and address fields from company and to children after create/update,
        just as if those were all modeled as fields.related to the parent """
        # 1. From UPSTREAM: sync from parent
        if values.get('parent_id') or values.get('type', 'contact'):
            # 1a. Commercial fields: sync if parent changed
            if values.get('parent_id'):
                self._commercial_sync_from_company()
            # 1b. Address fields: sync if parent or use_parent changed *and* both are now set
            if self.parent_id and self.type == 'contact':
                onchange_vals = self.onchange_parent_id().get('value', {})
                self.update_address(onchange_vals)

        # 2. To DOWNSTREAM: sync children
        if self.child_ids:
            # 2a. Commercial Fields: sync if commercial entity
            if self.commercial_partner_id == self:
                commercial_fields = self._commercial_fields()
                if any(field in values for field in commercial_fields):
                    self._commercial_sync_to_children()
            for child in self.child_ids.filtered(lambda c: not c.is_company):
                if child.commercial_partner_id != self.commercial_partner_id:
                    self._commercial_sync_to_children()
                    break
            # 2b. Address fields: sync if address changed
            address_fields = self._address_fields()
            if any(field in values for field in address_fields):
                contacts = self.child_ids.filtered(
                    lambda c: c.type == 'contact')
                contacts.update_address(values)

    @api.multi
    def _handle_first_contact_creation(self):
        """ On creation of first contact for a company (or root) that has no address, assume contact address
        was meant to be company address """
        parent = self.parent_id
        address_fields = self._address_fields()
        if (parent.is_company or not parent.parent_id) and len(parent.child_ids) == 1 and \
            any(self[f] for f in address_fields) and not any(parent[f] for f in address_fields):
            addr_vals = self._update_fields_values(address_fields)
            parent.update_address(addr_vals)

    def _clean_website(self, website):
        url = urls.url_parse(website)
        if not url.scheme:
            if not url.netloc:
                url = url.replace(netloc=url.path, path='')
            website = url.replace(scheme='http').to_url()
        return website

    @api.multi
    def write(self, vals):
        # res.partner must only allow to set the company_id of a partner if it
        # is the same as the company of all users that inherit from this partner
        # (this is to allow the code from res_users to write to the partner!) or
        # if setting the company_id to False (this is compatible with any user
        # company)
        if vals.get('website'):
            vals['website'] = self._clean_website(vals['website'])
        if vals.get('parent_id'):
            vals['company_name'] = False
        if vals.get('company_id'):
            company = self.env['res.company'].browse(vals['company_id'])
            for partner in self:
                if partner.user_ids:
                    companies = set(user.company_id
                                    for user in partner.user_ids)
                    if len(companies) > 1 or company not in companies:
                        raise UserError(
                            _("You can not change the company as the partner/user has multiple user linked with different companies."
                              ))
        tools.image_resize_images(vals)

        result = True
        # To write in SUPERUSER on field is_company and avoid access rights problems.
        if 'is_company' in vals and self.user_has_groups(
                'base.group_partner_manager'
        ) and not self.env.uid == SUPERUSER_ID:
            result = super(Partner, self).sudo().write(
                {'is_company': vals.get('is_company')})
            del vals['is_company']
        result = result and super(Partner, self).write(vals)
        for partner in self:
            if any(
                    u.has_group('base.group_user') for u in partner.user_ids
                    if u != self.env.user):
                self.env['res.users'].check_access_rights('write')
            partner._fields_sync(vals)
        return result

    @api.model
    def create(self, vals):
        if vals.get('website'):
            vals['website'] = self._clean_website(vals['website'])
        if vals.get('parent_id'):
            vals['company_name'] = False
        # compute default image in create, because computing gravatar in the onchange
        # cannot be easily performed if default images are in the way
        if not vals.get('image'):
            vals['image'] = self._get_default_image(vals.get('type'),
                                                    vals.get('is_company'),
                                                    vals.get('parent_id'))
        tools.image_resize_images(vals)
        partner = super(Partner, self).create(vals)
        partner._fields_sync(vals)
        partner._handle_first_contact_creation()
        return partner

    @api.multi
    def create_company(self):
        self.ensure_one()
        if self.company_name:
            # Create parent company
            values = dict(name=self.company_name, is_company=True)
            values.update(self._update_fields_values(self._address_fields()))
            new_company = self.create(values)
            # Set new company as my parent
            self.write({
                'parent_id':
                new_company.id,
                'child_ids': [(1, partner_id, dict(parent_id=new_company.id))
                              for partner_id in self.child_ids.ids]
            })
        return True

    @api.multi
    def open_commercial_entity(self):
        """ Utility method used to add an "Open Company" button in partner views """
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'res.partner',
            'view_mode': 'form',
            'res_id': self.commercial_partner_id.id,
            'target': 'current',
            'flags': {
                'form': {
                    'action_buttons': True
                }
            }
        }

    @api.multi
    def open_parent(self):
        """ Utility method used to add an "Open Parent" button in partner views """
        self.ensure_one()
        address_form_id = self.env.ref('base.view_partner_address_form').id
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'res.partner',
            'view_mode': 'form',
            'views': [(address_form_id, 'form')],
            'res_id': self.parent_id.id,
            'target': 'new',
            'flags': {
                'form': {
                    'action_buttons': True
                }
            }
        }

    @api.multi
    def name_get(self):
        res = []
        for partner in self:
            name = partner.name or ''

            if partner.company_name or partner.parent_id:
                if not name and partner.type in [
                        'invoice', 'delivery', 'other'
                ]:
                    name = dict(
                        self.fields_get(['type'
                                         ])['type']['selection'])[partner.type]
                if not partner.is_company:
                    name = "%s, %s" % (partner.commercial_company_name
                                       or partner.parent_id.name, name)
            if self._context.get('show_address_only'):
                name = partner._display_address(without_company=True)
            if self._context.get('show_address'):
                name = name + "\n" + partner._display_address(
                    without_company=True)
            name = name.replace('\n\n', '\n')
            name = name.replace('\n\n', '\n')
            if self._context.get('show_email') and partner.email:
                name = "%s <%s>" % (name, partner.email)
            if self._context.get('html_format'):
                name = name.replace('\n', '<br/>')
            res.append((partner.id, name))
        return res

    def _parse_partner_name(self, text, context=None):
        """ Supported syntax:
            - 'Raoul <*****@*****.**>': will find name and email address
            - otherwise: default, everything is set as the name """
        emails = tools.email_split(text.replace(' ', ','))
        if emails:
            email = emails[0]
            name = text[:text.index(email)].replace('"',
                                                    '').replace('<',
                                                                '').strip()
        else:
            name, email = text, ''
        return name, email

    @api.model
    def name_create(self, name):
        """ Override of orm's name_create method for partners. The purpose is
            to handle some basic formats to create partners using the
            name_create.
            If only an email address is received and that the regex cannot find
            a name, the name will have the email value.
            If 'force_email' key in context: must find the email address. """
        name, email = self._parse_partner_name(name)
        if self._context.get('force_email') and not email:
            raise UserError(
                _("Couldn't create contact without email address!"))
        if not name and email:
            name = email
        partner = self.create({
            self._rec_name:
            name or email,
            'email':
            email or self.env.context.get('default_email', False)
        })
        return partner.name_get()[0]

    @api.model
    def _search(self,
                args,
                offset=0,
                limit=None,
                order=None,
                count=False,
                access_rights_uid=None):
        """ Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will
        always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """
        # a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions
        if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in') \
                and args[0][2] != [False]:
            self = self.with_context(active_test=False)
        return super(Partner,
                     self)._search(args,
                                   offset=offset,
                                   limit=limit,
                                   order=order,
                                   count=count,
                                   access_rights_uid=access_rights_uid)

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        if args is None:
            args = []
        if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
            self.check_access_rights('read')
            where_query = self._where_calc(args)
            self._apply_ir_rules(where_query, 'read')
            from_clause, where_clause, where_clause_params = where_query.get_sql(
            )
            where_str = where_clause and (" WHERE %s AND " %
                                          where_clause) or ' WHERE '

            # search on the name of the contacts and of its company
            search_name = name
            if operator in ('ilike', 'like'):
                search_name = '%%%s%%' % name
            if operator in ('=ilike', '=like'):
                operator = operator[1:]

            unaccent = get_unaccent_wrapper(self.env.cr)

            query = """SELECT id
                         FROM res_partner
                      {where} ({email} {operator} {percent}
                           OR {display_name} {operator} {percent}
                           OR {reference} {operator} {percent}
                           OR {vat} {operator} {percent})
                           -- don't panic, trust postgres bitmap
                     ORDER BY {display_name} {operator} {percent} desc,
                              {display_name}
                    """.format(
                where=where_str,
                operator=operator,
                email=unaccent('email'),
                display_name=unaccent('display_name'),
                reference=unaccent('ref'),
                percent=unaccent('%s'),
                vat=unaccent('vat'),
            )

            where_clause_params += [search_name] * 5
            if limit:
                query += ' limit %s'
                where_clause_params.append(limit)
            self.env.cr.execute(query, where_clause_params)
            partner_ids = [row[0] for row in self.env.cr.fetchall()]

            if partner_ids:
                return self.browse(partner_ids).name_get()
            else:
                return []
        return super(Partner, self).name_search(name,
                                                args,
                                                operator=operator,
                                                limit=limit)

    @api.model
    def find_or_create(self, email):
        """ Find a partner with the given ``email`` or use :py:method:`~.name_create`
            to create one

            :param str email: email-like string, which should contain at least one email,
                e.g. ``"Raoul Grosbedon <*****@*****.**>"``"""
        assert email, 'an email is required for find_or_create to work'
        emails = tools.email_split(email)
        if emails:
            email = emails[0]
        partners = self.search([('email', '=ilike', email)], limit=1)
        return partners.id or self.name_create(email)[0]

    def _get_gravatar_image(self, email):
        email_hash = hashlib.md5(email.lower().encode('utf-8')).hexdigest()
        url = "https://www.gravatar.com/avatar/" + email_hash
        try:
            res = requests.get(url, params={'d': '404', 's': '128'}, timeout=5)
            if res.status_code != requests.codes.ok:
                return False
        except requests.exceptions.ConnectionError as e:
            return False
        return base64.b64encode(res.content)

    @api.multi
    def _email_send(self, email_from, subject, body, on_error=None):
        for partner in self.filtered('email'):
            tools.email_send(email_from, [partner.email], subject, body,
                             on_error)
        return True

    @api.multi
    def address_get(self, adr_pref=None):
        """ Find contacts/addresses of the right type(s) by doing a depth-first-search
        through descendants within company boundaries (stop at entities flagged ``is_company``)
        then continuing the search at the ancestors that are within the same company boundaries.
        Defaults to partners of type ``'default'`` when the exact type is not found, or to the
        provided partner itself if no type ``'default'`` is found either. """
        adr_pref = set(adr_pref or [])
        if 'contact' not in adr_pref:
            adr_pref.add('contact')
        result = {}
        visited = set()
        for partner in self:
            current_partner = partner
            while current_partner:
                to_scan = [current_partner]
                # Scan descendants, DFS
                while to_scan:
                    record = to_scan.pop(0)
                    visited.add(record)
                    if record.type in adr_pref and not result.get(record.type):
                        result[record.type] = record.id
                    if len(result) == len(adr_pref):
                        return result
                    to_scan = [
                        c for c in record.child_ids if c not in visited
                        if not c.is_company
                    ] + to_scan

                # Continue scanning at ancestor if current_partner is not a commercial entity
                if current_partner.is_company or not current_partner.parent_id:
                    break
                current_partner = current_partner.parent_id

        # default to type 'contact' or the partner itself
        default = result.get('contact', self.id or False)
        for adr_type in adr_pref:
            result[adr_type] = result.get(adr_type) or default
        return result

    @api.model
    def view_header_get(self, view_id, view_type):
        res = super(Partner, self).view_header_get(view_id, view_type)
        if res: return res
        if not self._context.get('category_id'):
            return False
        return _('Partners: ') + self.env['res.partner.category'].browse(
            self._context['category_id']).name

    @api.model
    @api.returns('self')
    def main_partner(self):
        ''' Return the main partner '''
        return self.env.ref('base.main_partner')

    @api.model
    def _get_default_address_format(self):
        return "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s"

    @api.multi
    def _display_address(self, without_company=False):
        '''
        The purpose of this function is to build and return an address formatted accordingly to the
        standards of the country where it belongs.

        :param address: browse record of the res.partner to format
        :returns: the address formatted in a display that fit its country habits (or the default ones
            if not country is specified)
        :rtype: string
        '''
        # get the information that will be injected into the display format
        # get the address format
        address_format = self.country_id.address_format or \
            self._get_default_address_format()
        args = {
            'state_code': self.state_id.code or '',
            'state_name': self.state_id.name or '',
            'country_code': self.country_id.code or '',
            'country_name': self.country_id.name or '',
            'company_name': self.commercial_company_name or '',
        }
        for field in self._address_fields():
            args[field] = getattr(self, field) or ''
        if without_company:
            args['company_name'] = ''
        elif self.commercial_company_name:
            address_format = '%(company_name)s\n' + address_format
        return address_format % args

    def _display_address_depends(self):
        # field dependencies of method _display_address()
        return self._address_fields() + [
            'country_id.address_format',
            'country_id.code',
            'country_id.name',
            'company_name',
            'state_id.code',
            'state_id.name',
        ]
Example #15
0
class IrAttachment(models.Model):
    """Attachments are used to link binary files or url to any openerp document.

    External attachment storage
    ---------------------------

    The computed field ``datas`` is implemented using ``_file_read``,
    ``_file_write`` and ``_file_delete``, which can be overridden to implement
    other storage engines. Such methods should check for other location pseudo
    uri (example: hdfs://hadoopserver).

    The default implementation is the file:dirname location that stores files
    on the local filesystem using name based on their sha1 hash
    """
    _name = 'ir.attachment'
    _order = 'id desc'

    @api.depends('res_model', 'res_id')
    def _compute_res_name(self):
        for attachment in self:
            if attachment.res_model and attachment.res_id:
                record = self.env[attachment.res_model].browse(attachment.res_id)
                attachment.res_name = record.display_name

    @api.model
    def _storage(self):
        return self.env['ir.config_parameter'].sudo().get_param('ir_attachment.location', 'file')

    @api.model
    def _filestore(self):
        return config.filestore(self._cr.dbname)

    @api.model
    def force_storage(self):
        """Force all attachments to be stored in the currently configured storage"""
        if not self.env.user._is_admin():
            raise AccessError(_('Only administrators can execute this action.'))

        # domain to retrieve the attachments to migrate
        domain = {
            'db': [('store_fname', '!=', False)],
            'file': [('db_datas', '!=', False)],
        }[self._storage()]

        for attach in self.search(domain):
            attach.write({'datas': attach.datas})
        return True

    @api.model
    def _full_path(self, path):
        # sanitize path
        path = re.sub('[.]', '', path)
        path = path.strip('/\\')
        return os.path.join(self._filestore(), path)

    @api.model
    def _get_path(self, bin_data, sha):
        # retro compatibility
        fname = sha[:3] + '/' + sha
        full_path = self._full_path(fname)
        if os.path.isfile(full_path):
            return fname, full_path        # keep existing path

        # scatter files across 256 dirs
        # we use '/' in the db (even on windows)
        fname = sha[:2] + '/' + sha
        full_path = self._full_path(fname)
        dirname = os.path.dirname(full_path)
        if not os.path.isdir(dirname):
            os.makedirs(dirname)
        return fname, full_path

    @api.model
    def _file_read(self, fname, bin_size=False):
        full_path = self._full_path(fname)
        r = ''
        try:
            if bin_size:
                r = human_size(os.path.getsize(full_path))
            else:
                r = base64.b64encode(open(full_path,'rb').read())
        except (IOError, OSError):
            _logger.info("_read_file reading %s", full_path, exc_info=True)
        return r

    @api.model
    def _file_write(self, value, checksum):
        bin_value = base64.b64decode(value)
        fname, full_path = self._get_path(bin_value, checksum)
        if not os.path.exists(full_path):
            try:
                with open(full_path, 'wb') as fp:
                    fp.write(bin_value)
                # add fname to checklist, in case the transaction aborts
                self._mark_for_gc(fname)
            except IOError:
                _logger.info("_file_write writing %s", full_path, exc_info=True)
        return fname

    @api.model
    def _file_delete(self, fname):
        # simply add fname to checklist, it will be garbage-collected later
        self._mark_for_gc(fname)

    def _mark_for_gc(self, fname):
        """ Add ``fname`` in a checklist for the filestore garbage collection. """
        # we use a spooldir: add an empty file in the subdirectory 'checklist'
        full_path = os.path.join(self._full_path('checklist'), fname)
        if not os.path.exists(full_path):
            dirname = os.path.dirname(full_path)
            if not os.path.isdir(dirname):
                with tools.ignore(OSError):
                    os.makedirs(dirname)
            open(full_path, 'ab').close()

    @api.model
    def _file_gc(self):
        """ Perform the garbage collection of the filestore. """
        if self._storage() != 'file':
            return

        # Continue in a new transaction. The LOCK statement below must be the
        # first one in the current transaction, otherwise the database snapshot
        # used by it may not contain the most recent changes made to the table
        # ir_attachment! Indeed, if concurrent transactions create attachments,
        # the LOCK statement will wait until those concurrent transactions end.
        # But this transaction will not see the new attachements if it has done
        # other requests before the LOCK (like the method _storage() above).
        cr = self._cr
        cr.commit()

        # prevent all concurrent updates on ir_attachment while collecting!
        cr.execute("LOCK ir_attachment IN SHARE MODE")

        # retrieve the file names from the checklist
        checklist = {}
        for dirpath, _, filenames in os.walk(self._full_path('checklist')):
            dirname = os.path.basename(dirpath)
            for filename in filenames:
                fname = "%s/%s" % (dirname, filename)
                checklist[fname] = os.path.join(dirpath, filename)

        # determine which files to keep among the checklist
        whitelist = set()
        for names in cr.split_for_in_conditions(checklist):
            cr.execute("SELECT store_fname FROM ir_attachment WHERE store_fname IN %s", [names])
            whitelist.update(row[0] for row in cr.fetchall())

        # remove garbage files, and clean up checklist
        removed = 0
        for fname, filepath in checklist.items():
            if fname not in whitelist:
                try:
                    os.unlink(self._full_path(fname))
                    removed += 1
                except (OSError, IOError):
                    _logger.info("_file_gc could not unlink %s", self._full_path(fname), exc_info=True)
            with tools.ignore(OSError):
                os.unlink(filepath)

        # commit to release the lock
        cr.commit()
        _logger.info("filestore gc %d checked, %d removed", len(checklist), removed)

    @api.depends('store_fname', 'db_datas')
    def _compute_datas(self):
        bin_size = self._context.get('bin_size')
        for attach in self:
            if attach.store_fname:
                attach.datas = self._file_read(attach.store_fname, bin_size)
            else:
                attach.datas = attach.db_datas

    def _inverse_datas(self):
        location = self._storage()
        for attach in self:
            # compute the fields that depend on datas
            value = attach.datas
            bin_data = base64.b64decode(value) if value else b''
            vals = {
                'file_size': len(bin_data),
                'checksum': self._compute_checksum(bin_data),
                'index_content': self._index(bin_data, attach.datas_fname, attach.mimetype),
                'store_fname': False,
                'db_datas': value,
            }
            if value and location != 'db':
                # save it to the filestore
                vals['store_fname'] = self._file_write(value, vals['checksum'])
                vals['db_datas'] = False

            # take current location in filestore to possibly garbage-collect it
            fname = attach.store_fname
            # write as superuser, as user probably does not have write access
            super(IrAttachment, attach.sudo()).write(vals)
            if fname:
                self._file_delete(fname)

    def _compute_checksum(self, bin_data):
        """ compute the checksum for the given datas
            :param bin_data : datas in its binary form
        """
        # an empty file has a checksum too (for caching)
        return hashlib.sha1(bin_data or b'').hexdigest()

    def _compute_mimetype(self, values):
        """ compute the mimetype of the given values
            :param values : dict of values to create or write an ir_attachment
            :return mime : string indicating the mimetype, or application/octet-stream by default
        """
        mimetype = None
        if values.get('mimetype'):
            mimetype = values['mimetype']
        if not mimetype and values.get('datas_fname'):
            mimetype = mimetypes.guess_type(values['datas_fname'])[0]
        if not mimetype and values.get('url'):
            mimetype = mimetypes.guess_type(values['url'])[0]
        if values.get('datas') and (not mimetype or mimetype == 'application/octet-stream'):
            mimetype = guess_mimetype(base64.b64decode(values['datas']))
        return mimetype or 'application/octet-stream'

    def _check_contents(self, values):
        mimetype = values['mimetype'] = self._compute_mimetype(values)
        xml_like = 'ht' in mimetype or 'xml' in mimetype # hta, html, xhtml, etc.
        force_text = (xml_like and (not self.env.user._is_admin() or
            self.env.context.get('attachments_mime_plainxml')))
        if force_text:
            values['mimetype'] = 'text/plain'
        return values

    @api.model
    def _index(self, bin_data, datas_fname, file_type):
        """ compute the index content of the given filename, or binary data.
            This is a python implementation of the unix command 'strings'.
            :param bin_data : datas in binary form
            :return index_content : string containing all the printable character of the binary data
        """
        index_content = False
        if file_type:
            index_content = file_type.split('/')[0]
            if index_content == 'text': # compute index_content only for text type
                words = re.findall(b"[\x20-\x7E]{4,}", bin_data)
                index_content = b"\n".join(words).decode('ascii')
        return index_content

    @api.model
    def get_serving_groups(self):
        """ An ir.attachment record may be used as a fallback in the
        http dispatch if its type field is set to "binary" and its url
        field is set as the request's url. Only the groups returned by
        this method are allowed to create and write on such records.
        """
        return ['base.group_system']

    name = fields.Char('Attachment Name', required=True)
    datas_fname = fields.Char('File Name')
    description = fields.Text('Description')
    res_name = fields.Char('Resource Name', compute='_compute_res_name', store=True)
    res_model = fields.Char('Resource Model', readonly=True, help="The database object this attachment will be attached to.")
    res_field = fields.Char('Resource Field', readonly=True)
    res_id = fields.Integer('Resource ID', readonly=True, help="The record id this is attached to.")
    create_date = fields.Datetime('Date Created', readonly=True)
    create_uid = fields.Many2one('res.users', string='Owner', readonly=True)
    company_id = fields.Many2one('res.company', string='Company', change_default=True,
                                 default=lambda self: self.env['res.company']._company_default_get('ir.attachment'))
    type = fields.Selection([('url', 'URL'), ('binary', 'File')],
                            string='Type', required=True, default='binary', change_default=True,
                            help="You can either upload a file from your computer or copy/paste an internet link to your file.")
    url = fields.Char('Url', index=True, size=1024)
    public = fields.Boolean('Is public document')

    # for external access
    access_token = fields.Char('Access Token', groups="base.group_user")

    # the field 'datas' is computed and may use the other fields below
    datas = fields.Binary(string='File Content', compute='_compute_datas', inverse='_inverse_datas')
    db_datas = fields.Binary('Database Data')
    store_fname = fields.Char('Stored Filename')
    file_size = fields.Integer('File Size', readonly=True)
    checksum = fields.Char("Checksum/SHA1", size=40, index=True, readonly=True)
    mimetype = fields.Char('Mime Type', readonly=True)
    index_content = fields.Text('Indexed Content', readonly=True, prefetch=False)

    @api.model_cr_context
    def _auto_init(self):
        res = super(IrAttachment, self)._auto_init()
        tools.create_index(self._cr, 'ir_attachment_res_idx',
                           self._table, ['res_model', 'res_id'])
        return res

    @api.one
    @api.constrains('type', 'url')
    def _check_serving_attachments(self):
        # restrict writing on attachments that could be served by the
        # ir.http's dispatch exception handling
        if self.env.user._is_superuser():
            return
        if self.type == 'binary' and self.url:
            has_group = self.env.user.has_group
            if not any([has_group(g) for g in self.get_serving_groups()]):
                raise ValidationError("Sorry, you are not allowed to write on this document")

    @api.model
    def check(self, mode, values=None):
        """Restricts the access to an ir.attachment, according to referred model
        In the 'document' module, it is overriden to relax this hard rule, since
        more complex ones apply there.
        """
        # collect the records to check (by model)
        model_ids = defaultdict(set)            # {model_name: set(ids)}
        require_employee = False
        if self:
            self._cr.execute('SELECT res_model, res_id, create_uid, public FROM ir_attachment WHERE id IN %s', [tuple(self.ids)])
            for res_model, res_id, create_uid, public in self._cr.fetchall():
                if public and mode == 'read':
                    continue
                if not (res_model and res_id):
                    if create_uid != self._uid:
                        require_employee = True
                    continue
                model_ids[res_model].add(res_id)
        if values and values.get('res_model') and values.get('res_id'):
            model_ids[values['res_model']].add(values['res_id'])

        # check access rights on the records
        for res_model, res_ids in model_ids.items():
            # ignore attachments that are not attached to a resource anymore
            # when checking access rights (resource was deleted but attachment
            # was not)
            if res_model not in self.env:
                require_employee = True
                continue
            elif res_model == 'res.users' and len(res_ids) == 1 and self._uid == list(res_ids)[0]:
                # by default a user cannot write on itself, despite the list of writeable fields
                # e.g. in the case of a user inserting an image into his image signature
                # we need to bypass this check which would needlessly throw us away
                continue
            records = self.env[res_model].browse(res_ids).exists()
            if len(records) < len(res_ids):
                require_employee = True
            # For related models, check if we can write to the model, as unlinking
            # and creating attachments can be seen as an update to the model
            records.check_access_rights('write' if mode in ('create', 'unlink') else mode)
            records.check_access_rule(mode)

        if require_employee:
            if not (self.env.user._is_admin() or self.env.user.has_group('base.group_user')):
                raise AccessError(_("Sorry, you are not allowed to access this document."))

    @api.model
    def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
        # add res_field=False in domain if not present; the arg[0] trick below
        # works for domain items and '&'/'|'/'!' operators too
        if not any(arg[0] in ('id', 'res_field') for arg in args):
            args.insert(0, ('res_field', '=', False))

        ids = super(IrAttachment, self)._search(args, offset=offset, limit=limit, order=order,
                                                count=False, access_rights_uid=access_rights_uid)

        if self._uid == SUPERUSER_ID:
            # rules do not apply for the superuser
            return len(ids) if count else ids

        if not ids:
            return 0 if count else []

        # Work with a set, as list.remove() is prohibitive for large lists of documents
        # (takes 20+ seconds on a db with 100k docs during search_count()!)
        orig_ids = ids
        ids = set(ids)

        # For attachments, the permissions of the document they are attached to
        # apply, so we must remove attachments for which the user cannot access
        # the linked document.
        # Use pure SQL rather than read() as it is about 50% faster for large dbs (100k+ docs),
        # and the permissions are checked in super() and below anyway.
        model_attachments = defaultdict(lambda: defaultdict(set))   # {res_model: {res_id: set(ids)}}
        self._cr.execute("""SELECT id, res_model, res_id, public FROM ir_attachment WHERE id IN %s""", [tuple(ids)])
        for row in self._cr.dictfetchall():
            if not row['res_model'] or row['public']:
                continue
            # model_attachments = {res_model: {res_id: set(ids)}}
            model_attachments[row['res_model']][row['res_id']].add(row['id'])

        # To avoid multiple queries for each attachment found, checks are
        # performed in batch as much as possible.
        for res_model, targets in model_attachments.items():
            if res_model not in self.env:
                continue
            if not self.env[res_model].check_access_rights('read', False):
                # remove all corresponding attachment ids
                ids.difference_update(itertools.chain(*targets.values()))
                continue
            # filter ids according to what access rules permit
            target_ids = list(targets)
            allowed = self.env[res_model].with_context(active_test=False).search([('id', 'in', target_ids)])
            for res_id in set(target_ids).difference(allowed.ids):
                ids.difference_update(targets[res_id])

        # sort result according to the original sort ordering
        result = [id for id in orig_ids if id in ids]
        return len(result) if count else list(result)

    @api.multi
    def read(self, fields=None, load='_classic_read'):
        self.check('read')
        return super(IrAttachment, self).read(fields, load=load)

    @api.multi
    def write(self, vals):
        self.check('write', values=vals)
        # remove computed field depending of datas
        for field in ('file_size', 'checksum'):
            vals.pop(field, False)
        if 'mimetype' in vals or 'datas' in vals:
            vals = self._check_contents(vals)
        return super(IrAttachment, self).write(vals)

    @api.multi
    def copy(self, default=None):
        self.check('write')
        return super(IrAttachment, self).copy(default)

    @api.multi
    def unlink(self):
        self.check('unlink')

        # First delete in the database, *then* in the filesystem if the
        # database allowed it. Helps avoid errors when concurrent transactions
        # are deleting the same file, and some of the transactions are
        # rolled back by PostgreSQL (due to concurrent updates detection).
        to_delete = set(attach.store_fname for attach in self if attach.store_fname)
        res = super(IrAttachment, self).unlink()
        for file_path in to_delete:
            self._file_delete(file_path)

        return res

    @api.model
    def create(self, values):
        # remove computed field depending of datas
        for field in ('file_size', 'checksum'):
            values.pop(field, False)
        values = self._check_contents(values)
        self.browse().check('write', values=values)
        return super(IrAttachment, self).create(values)

    @api.one
    def generate_access_token(self):
        if self.access_token:
            return self.access_token
        access_token = str(uuid.uuid4())
        self.write({'access_token': access_token})
        return access_token

    @api.model
    def action_get(self):
        return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
Example #16
0
class Groups(models.Model):
    _name = "res.groups"
    _description = "Access Groups"
    _rec_name = 'full_name'
    _order = 'name'

    name = fields.Char(required=True, translate=True)
    users = fields.Many2many('res.users', 'res_groups_users_rel', 'gid', 'uid')
    model_access = fields.One2many('ir.model.access',
                                   'group_id',
                                   string='Access Controls',
                                   copy=True)
    rule_groups = fields.Many2many('ir.rule',
                                   'rule_group_rel',
                                   'group_id',
                                   'rule_group_id',
                                   string='Rules',
                                   domain=[('global', '=', False)])
    menu_access = fields.Many2many('ir.ui.menu',
                                   'ir_ui_menu_group_rel',
                                   'gid',
                                   'menu_id',
                                   string='Access Menu')
    view_access = fields.Many2many('ir.ui.view',
                                   'ir_ui_view_group_rel',
                                   'group_id',
                                   'view_id',
                                   string='Views')
    comment = fields.Text(translate=True)
    category_id = fields.Many2one('ir.module.category',
                                  string='Application',
                                  index=True)
    color = fields.Integer(string='Color Index')
    full_name = fields.Char(compute='_compute_full_name',
                            string='Group Name',
                            search='_search_full_name')
    share = fields.Boolean(
        string='Share Group',
        help=
        "Group created to set access rights for sharing data with some users.")
    is_portal = fields.Boolean(
        'Portal', help="If checked, this group is usable as a portal.")

    _sql_constraints = [
        ('name_uniq', 'unique (category_id, name)',
         'The name of the group must be unique within an application!')
    ]

    @api.depends('category_id.name', 'name')
    def _compute_full_name(self):
        # Important: value must be stored in environment of group, not group1!
        for group, group1 in pycompat.izip(self, self.sudo()):
            if group1.category_id:
                group.full_name = '%s / %s' % (group1.category_id.name,
                                               group1.name)
            else:
                group.full_name = group1.name

    def _search_full_name(self, operator, operand):
        lst = True
        if isinstance(operand, bool):
            domains = [[('name', operator, operand)],
                       [('category_id.name', operator, operand)]]
            if operator in expression.NEGATIVE_TERM_OPERATORS == (not operand):
                return expression.AND(domains)
            else:
                return expression.OR(domains)
        if isinstance(operand, pycompat.string_types):
            lst = False
            operand = [operand]
        where = []
        for group in operand:
            values = [v for v in group.split('/') if v]
            group_name = values.pop().strip()
            category_name = values and '/'.join(values).strip() or group_name
            group_domain = [('name', operator, lst and [group_name]
                             or group_name)]
            category_domain = [('category_id.name', operator,
                                lst and [category_name] or category_name)]
            if operator in expression.NEGATIVE_TERM_OPERATORS and not values:
                category_domain = expression.OR(
                    [category_domain, [('category_id', '=', False)]])
            if (operator
                    in expression.NEGATIVE_TERM_OPERATORS) == (not values):
                sub_where = expression.AND([group_domain, category_domain])
            else:
                sub_where = expression.OR([group_domain, category_domain])
            if operator in expression.NEGATIVE_TERM_OPERATORS:
                where = expression.AND([where, sub_where])
            else:
                where = expression.OR([where, sub_where])
        return where

    @api.model
    def search(self, args, offset=0, limit=None, order=None, count=False):
        # add explicit ordering if search is sorted on full_name
        if order and order.startswith('full_name'):
            groups = super(Groups, self).search(args)
            groups = groups.sorted('full_name', reverse=order.endswith('DESC'))
            groups = groups[offset:offset +
                            limit] if limit else groups[offset:]
            return len(groups) if count else groups.ids
        return super(Groups, self).search(args,
                                          offset=offset,
                                          limit=limit,
                                          order=order,
                                          count=count)

    @api.multi
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {}, name=_('%s (copy)') % self.name)
        return super(Groups, self).copy(default)

    @api.multi
    def write(self, vals):
        if 'name' in vals:
            if vals['name'].startswith('-'):
                raise UserError(
                    _('The name of the group can not start with "-"'))
        # invalidate caches before updating groups, since the recomputation of
        # field 'share' depends on method has_group()
        self.env['ir.model.access'].call_cache_clearing_methods()
        self.env['res.users'].has_group.clear_cache(self.env['res.users'])
        return super(Groups, self).write(vals)
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, id
                                    ORDER BY date, id) AS b
                        WHERE a.id = b.stmt_id;"""

        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 = '#875A7B' 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)

        return {
            'number_to_reconcile':
            number_to_reconcile,
            'account_balance':
            formatLang(self.env,
                       account_sum,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'last_balance':
            formatLang(self.env,
                       last_balance,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'difference': (last_balance - account_sum) and formatLang(
                self.env,
                last_balance - account_sum,
                currency_obj=self.currency_id or self.company_id.currency_id)
            or False,
            'number_draft':
            number_draft,
            'number_waiting':
            number_waiting,
            'number_late':
            number_late,
            'sum_draft':
            formatLang(self.env,
                       sum_draft or 0.0,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'sum_waiting':
            formatLang(self.env,
                       sum_waiting or 0.0,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'sum_late':
            formatLang(self.env,
                       sum_late or 0.0,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'currency_id':
            self.currency_id and self.currency_id.id
            or self.company_id.currency_id.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
                  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
                  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
            rslt_sum += 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,
            'search_default_journal_id': self.id,
            'default_type': invoice_type,
            'type': invoice_type
        })

        [action] = self.env.ref('account.%s' % action_name).read()
        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
Example #18
0
class ResConfigSettings(models.TransientModel):
    _inherit = 'res.config.settings'

    sale_note = fields.Text(related='company_id.sale_note',
                            string="Terms & Conditions")
    use_sale_note = fields.Boolean(string='Default Terms & Conditions',
                                   oldname='default_use_sale_note')
    group_discount_per_so_line = fields.Boolean(
        "Discounts", implied_group='sale.group_discount_per_so_line')
    module_sale_margin = fields.Boolean("Margins")
    group_sale_layout = fields.Boolean("Sections on Sales Orders",
                                       implied_group='sale.group_sale_layout')
    group_warning_sale = fields.Boolean(
        "Warnings", implied_group='sale.group_warning_sale')
    portal_confirmation = fields.Boolean('Online Signature & Payment')
    portal_confirmation_options = fields.Selection(
        [('sign', 'Signature'), ('pay', 'Payment')],
        string="Online Signature & Payment options")
    module_sale_payment = fields.Boolean(
        "Online Signature & Payment",
        help='Technical field implied by user choice of online_confirmation')
    module_website_quote = fields.Boolean("Quotations Templates")
    group_sale_delivery_address = fields.Boolean(
        "Customer Addresses",
        implied_group='sale.group_delivery_invoice_address')
    multi_sales_price = fields.Boolean("Multiple Sales Prices per Product")
    multi_sales_price_method = fields.Selection(
        [('percentage',
          'Multiple prices per product (e.g. customer segments, currencies)'),
         ('formula',
          'Prices computed from formulas (discounts, margins, roundings)')],
        default='percentage',
        string="Pricelists")
    sale_pricelist_setting = fields.Selection(
        [('fixed', 'A single sales price per product'),
         ('percentage',
          'Multiple prices per product (e.g. customer segments, currencies)'),
         ('formula',
          'Price computed from formulas (discounts, margins, roundings)')],
        string="Pricelists")
    group_show_price_subtotal = fields.Boolean(
        "Show subtotal",
        implied_group='sale.group_show_price_subtotal',
        group='base.group_portal,base.group_user,base.group_public')
    group_show_price_total = fields.Boolean(
        "Show total",
        implied_group='sale.group_show_price_total',
        group='base.group_portal,base.group_user,base.group_public')
    group_proforma_sales = fields.Boolean(
        string="Pro-Forma Invoice",
        implied_group='sale.group_proforma_sales',
        help="Allows you to send pro-forma invoice.")
    sale_show_tax = fields.Selection([('subtotal', 'Tax-Excluded Prices'),
                                      ('total', 'Tax-Included Prices')],
                                     string="Tax Display",
                                     required=True)
    default_invoice_policy = fields.Selection(
        [('order', 'Invoice what is ordered'),
         ('delivery', 'Invoice what is delivered')],
        'Invoicing Policy',
        default='order',
        default_model='product.template')
    default_deposit_product_id = fields.Many2one(
        'product.product',
        'Deposit Product',
        domain="[('type', '=', 'service')]",
        oldname='deposit_product_id_setting',
        help='Default product used for payment advances')
    auto_done_setting = fields.Boolean("Lock Confirmed Orders")
    module_website_sale_digital = fields.Boolean(
        "Sell digital products - provide downloadable content on your customer portal"
    )

    auth_signup_uninvited = fields.Selection([
        ('b2b', 'On invitation (B2B)'),
        ('b2c', 'Free sign up (B2C)'),
    ],
                                             string='Customer Account')

    module_delivery = fields.Boolean("Shipping Costs")
    module_delivery_dhl = fields.Boolean("DHL")
    module_delivery_fedex = fields.Boolean("FedEx")
    module_delivery_ups = fields.Boolean("UPS")
    module_delivery_usps = fields.Boolean("USPS")
    module_delivery_bpost = fields.Boolean("bpost")

    module_product_email_template = fields.Boolean("Specific Email")
    module_sale_coupon = fields.Boolean("Coupons & Promotions")

    @api.onchange('multi_sales_price', 'multi_sales_price_method')
    def _onchange_sale_price(self):
        if self.multi_sales_price and not self.multi_sales_price_method:
            self.update({
                'multi_sales_price_method': 'percentage',
            })
        self.sale_pricelist_setting = self.multi_sales_price and self.multi_sales_price_method or 'fixed'

    @api.onchange('sale_show_tax')
    def _onchange_sale_tax(self):
        if self.sale_show_tax == "subtotal":
            self.update({
                'group_show_price_total': False,
                'group_show_price_subtotal': True,
            })
        else:
            self.update({
                'group_show_price_total': True,
                'group_show_price_subtotal': False,
            })

    @api.onchange('sale_pricelist_setting')
    def _onchange_sale_pricelist_setting(self):
        if self.sale_pricelist_setting == 'percentage':
            self.update({
                'group_product_pricelist': True,
                'group_sale_pricelist': True,
                'group_pricelist_item': False,
            })
        elif self.sale_pricelist_setting == 'formula':
            self.update({
                'group_product_pricelist': False,
                'group_sale_pricelist': True,
                'group_pricelist_item': True,
            })
        else:
            self.update({
                'group_product_pricelist': False,
                'group_sale_pricelist': False,
                'group_pricelist_item': False,
            })

    @api.onchange('portal_confirmation')
    def _onchange_portal_confirmation(self):
        if not self.portal_confirmation:
            self.portal_confirmation_options = False
        elif not self.portal_confirmation_options:
            self.portal_confirmation_options = 'sign'

    @api.onchange('portal_confirmation_options')
    def _onchange_portal_confirmation_options(self):
        if self.portal_confirmation_options == 'pay':
            self.module_sale_payment = True

    @api.model
    def get_values(self):
        res = super(ResConfigSettings, self).get_values()
        ICPSudo = self.env['ir.config_parameter'].sudo()
        sale_pricelist_setting = ICPSudo.get_param(
            'sale.sale_pricelist_setting')
        sale_portal_confirmation_options = ICPSudo.get_param(
            'sale.sale_portal_confirmation_options', default='none')
        res.update(
            auth_signup_uninvited='b2c' if ICPSudo.get_param(
                'auth_signup.allow_uninvited',
                'False').lower() == 'true' else 'b2b',
            use_sale_note=ICPSudo.get_param('sale.use_sale_note',
                                            default=False),
            auto_done_setting=ICPSudo.get_param('sale.auto_done_setting'),
            default_deposit_product_id=int(
                ICPSudo.get_param('sale.default_deposit_product_id')),
            sale_show_tax=ICPSudo.get_param('sale.sale_show_tax',
                                            default='subtotal'),
            multi_sales_price=sale_pricelist_setting
            in ['percentage', 'formula'],
            multi_sales_price_method=sale_pricelist_setting
            in ['percentage', 'formula'] and sale_pricelist_setting or False,
            sale_pricelist_setting=sale_pricelist_setting,
            portal_confirmation=sale_portal_confirmation_options
            in ('pay', 'sign'),
            portal_confirmation_options=sale_portal_confirmation_options
            if sale_portal_confirmation_options in ('pay', 'sign') else False,
        )
        return res

    @api.multi
    def set_values(self):
        super(ResConfigSettings, self).set_values()
        ICPSudo = self.env['ir.config_parameter'].sudo()
        ICPSudo.set_param('auth_signup.allow_uninvited',
                          repr(self.auth_signup_uninvited == 'b2c'))
        ICPSudo.set_param("sale.use_sale_note", self.use_sale_note)
        ICPSudo.set_param("sale.auto_done_setting", self.auto_done_setting)
        ICPSudo.set_param("sale.default_deposit_product_id",
                          self.default_deposit_product_id.id)
        ICPSudo.set_param('sale.sale_pricelist_setting',
                          self.sale_pricelist_setting)
        ICPSudo.set_param('sale.sale_show_tax', self.sale_show_tax)
        ICPSudo.set_param(
            'sale.sale_portal_confirmation_options',
            self.portal_confirmation_options
            if self.portal_confirmation_options in ('pay', 'sign') else 'none')
Example #19
0
class StockLandedCost(models.Model):
    _name = 'stock.landed.cost'
    _description = 'Stock Landed Cost'
    _inherit = ['mail.thread', 'mail.activity.mixin']

    def _default_account_journal_id(self):
        """Take the journal configured in the company, else fallback on the stock journal."""
        lc_journal = self.env['account.journal']
        if self.env.company.lc_journal_id:
            lc_journal = self.env.company.lc_journal_id
        else:
            lc_journal = self.env['ir.property']._get("property_stock_journal", "product.category")
        return lc_journal

    name = fields.Char(
        'Name', default=lambda self: _('New'),
        copy=False, readonly=True, tracking=True)
    date = fields.Date(
        'Date', default=fields.Date.context_today,
        copy=False, required=True, states={'done': [('readonly', True)]}, tracking=True)
    target_model = fields.Selection(
        [('picking', 'Transfers')], string="Apply On",
        required=True, default='picking',
        copy=False, states={'done': [('readonly', True)]})
    picking_ids = fields.Many2many(
        'stock.picking', string='Transfers',
        copy=False, states={'done': [('readonly', True)]})
    allowed_picking_ids = fields.Many2many('stock.picking', compute='_compute_allowed_picking_ids')
    cost_lines = fields.One2many(
        'stock.landed.cost.lines', 'cost_id', 'Cost Lines',
        copy=True, states={'done': [('readonly', True)]})
    valuation_adjustment_lines = fields.One2many(
        'stock.valuation.adjustment.lines', 'cost_id', 'Valuation Adjustments',
        states={'done': [('readonly', True)]})
    description = fields.Text(
        'Item Description', states={'done': [('readonly', True)]})
    amount_total = fields.Monetary(
        'Total', compute='_compute_total_amount',
        store=True, tracking=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('done', 'Posted'),
        ('cancel', 'Cancelled')], 'State', default='draft',
        copy=False, readonly=True, tracking=True)
    account_move_id = fields.Many2one(
        'account.move', 'Journal Entry',
        copy=False, readonly=True)
    account_journal_id = fields.Many2one(
        'account.journal', 'Account Journal',
        required=True, states={'done': [('readonly', True)]}, default=lambda self: self._default_account_journal_id())
    company_id = fields.Many2one('res.company', string="Company",
        related='account_journal_id.company_id')
    stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_landed_cost_id')
    vendor_bill_id = fields.Many2one(
        'account.move', 'Vendor Bill', copy=False, domain=[('move_type', '=', 'in_invoice')])
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id')

    @api.depends('cost_lines.price_unit')
    def _compute_total_amount(self):
        for cost in self:
            cost.amount_total = sum(line.price_unit for line in cost.cost_lines)

    @api.depends('company_id')
    def _compute_allowed_picking_ids(self):
        # Backport of f329de26: allowed_picking_ids is useless, view_stock_landed_cost_form no longer uses it,
        # the field and its compute are kept since this is a stable version. Still, this compute has been made
        # more resilient to MemoryErrors.
        valued_picking_ids_per_company = defaultdict(list)
        if self.company_id:
            self.env.cr.execute("""SELECT sm.picking_id, sm.company_id
                                     FROM stock_move AS sm
                               INNER JOIN stock_valuation_layer AS svl ON svl.stock_move_id = sm.id
                                    WHERE sm.picking_id IS NOT NULL AND sm.company_id IN %s
                                 GROUP BY sm.picking_id, sm.company_id""", [tuple(self.company_id.ids)])
            for res in self.env.cr.fetchall():
                valued_picking_ids_per_company[res[1]].append(res[0])
        for cost in self:
            n = 5000
            cost.allowed_picking_ids = valued_picking_ids_per_company[cost.company_id.id][:n]
            for ids_chunk in tools.split_every(n, valued_picking_ids_per_company[cost.company_id.id][n:]):
                cost.allowed_picking_ids = [(4, id_) for id_ in ids_chunk]

    @api.onchange('target_model')
    def _onchange_target_model(self):
        if self.target_model != 'picking':
            self.picking_ids = False

    @api.model
    def create(self, vals):
        if vals.get('name', _('New')) == _('New'):
            vals['name'] = self.env['ir.sequence'].next_by_code('stock.landed.cost')
        return super().create(vals)

    def unlink(self):
        self.button_cancel()
        return super().unlink()

    def _track_subtype(self, init_values):
        if 'state' in init_values and self.state == 'done':
            return self.env.ref('stock_landed_costs.mt_stock_landed_cost_open')
        return super()._track_subtype(init_values)

    def button_cancel(self):
        if any(cost.state == 'done' for cost in self):
            raise UserError(
                _('Validated landed costs cannot be cancelled, but you could create negative landed costs to reverse them'))
        return self.write({'state': 'cancel'})

    def button_validate(self):
        self._check_can_validate()
        cost_without_adjusment_lines = self.filtered(lambda c: not c.valuation_adjustment_lines)
        if cost_without_adjusment_lines:
            cost_without_adjusment_lines.compute_landed_cost()
        if not self._check_sum():
            raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.'))

        for cost in self:
            cost = cost.with_company(cost.company_id)
            move = self.env['account.move']
            move_vals = {
                'journal_id': cost.account_journal_id.id,
                'date': cost.date,
                'ref': cost.name,
                'line_ids': [],
                'move_type': 'entry',
            }
            valuation_layer_ids = []
            cost_to_add_byproduct = defaultdict(lambda: 0.0)
            for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id):
                remaining_qty = sum(line.move_id.stock_valuation_layer_ids.mapped('remaining_qty'))
                linked_layer = line.move_id.stock_valuation_layer_ids[:1]

                # Prorate the value at what's still in stock
                cost_to_add = (remaining_qty / line.move_id.product_qty) * line.additional_landed_cost
                if not cost.company_id.currency_id.is_zero(cost_to_add):
                    valuation_layer = self.env['stock.valuation.layer'].create({
                        'value': cost_to_add,
                        'unit_cost': 0,
                        'quantity': 0,
                        'remaining_qty': 0,
                        'stock_valuation_layer_id': linked_layer.id,
                        'description': cost.name,
                        'stock_move_id': line.move_id.id,
                        'product_id': line.move_id.product_id.id,
                        'stock_landed_cost_id': cost.id,
                        'company_id': cost.company_id.id,
                    })
                    linked_layer.remaining_value += cost_to_add
                    valuation_layer_ids.append(valuation_layer.id)
                # Update the AVCO
                product = line.move_id.product_id
                if product.cost_method == 'average':
                    cost_to_add_byproduct[product] += cost_to_add
                # Products with manual inventory valuation are ignored because they do not need to create journal entries.
                if product.valuation != "real_time":
                    continue
                # `remaining_qty` is negative if the move is out and delivered proudcts that were not
                # in stock.
                qty_out = 0
                if line.move_id._is_in():
                    qty_out = line.move_id.product_qty - remaining_qty
                elif line.move_id._is_out():
                    qty_out = line.move_id.product_qty
                move_vals['line_ids'] += line._create_accounting_entries(move, qty_out)

            # batch standard price computation avoid recompute quantity_svl at each iteration
            products = self.env['product.product'].browse(p.id for p in cost_to_add_byproduct.keys())
            for product in products:  # iterate on recordset to prefetch efficiently quantity_svl
                if not float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding):
                    product.with_company(cost.company_id).sudo().with_context(disable_auto_svl=True).standard_price += cost_to_add_byproduct[product] / product.quantity_svl

            move_vals['stock_valuation_layer_ids'] = [(6, None, valuation_layer_ids)]
            # We will only create the accounting entry when there are defined lines (the lines will be those linked to products of real_time valuation category).
            cost_vals = {'state': 'done'}
            if move_vals.get("line_ids"):
                move = move.create(move_vals)
                cost_vals.update({'account_move_id': move.id})
            cost.write(cost_vals)
            if cost.account_move_id:
                move._post()

            if cost.vendor_bill_id and cost.vendor_bill_id.state == 'posted' and cost.company_id.anglo_saxon_accounting:
                all_amls = cost.vendor_bill_id.line_ids | cost.account_move_id.line_ids
                for product in cost.cost_lines.product_id:
                    accounts = product.product_tmpl_id.get_product_accounts()
                    input_account = accounts['stock_input']
                    all_amls.filtered(lambda aml: aml.account_id == input_account and not aml.reconciled).reconcile()

        return True

    def get_valuation_lines(self):
        self.ensure_one()
        lines = []

        for move in self._get_targeted_move_ids():
            # it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost
            if move.product_id.cost_method not in ('fifo', 'average') or move.state == 'cancel' or not move.product_qty:
                continue
            vals = {
                'product_id': move.product_id.id,
                'move_id': move.id,
                'quantity': move.product_qty,
                'former_cost': sum(move.stock_valuation_layer_ids.mapped('value')),
                'weight': move.product_id.weight * move.product_qty,
                'volume': move.product_id.volume * move.product_qty
            }
            lines.append(vals)

        if not lines:
            target_model_descriptions = dict(self._fields['target_model']._description_selection(self.env))
            raise UserError(_("You cannot apply landed costs on the chosen %s(s). Landed costs can only be applied for products with FIFO or average costing method.", target_model_descriptions[self.target_model]))
        return lines

    def compute_landed_cost(self):
        AdjustementLines = self.env['stock.valuation.adjustment.lines']
        AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink()

        towrite_dict = {}
        for cost in self.filtered(lambda cost: cost._get_targeted_move_ids()):
            rounding = cost.currency_id.rounding
            total_qty = 0.0
            total_cost = 0.0
            total_weight = 0.0
            total_volume = 0.0
            total_line = 0.0
            all_val_line_values = cost.get_valuation_lines()
            for val_line_values in all_val_line_values:
                for cost_line in cost.cost_lines:
                    val_line_values.update({'cost_id': cost.id, 'cost_line_id': cost_line.id})
                    self.env['stock.valuation.adjustment.lines'].create(val_line_values)
                total_qty += val_line_values.get('quantity', 0.0)
                total_weight += val_line_values.get('weight', 0.0)
                total_volume += val_line_values.get('volume', 0.0)

                former_cost = val_line_values.get('former_cost', 0.0)
                # round this because former_cost on the valuation lines is also rounded
                total_cost += cost.currency_id.round(former_cost)

                total_line += 1

            for line in cost.cost_lines:
                value_split = 0.0
                for valuation in cost.valuation_adjustment_lines:
                    value = 0.0
                    if valuation.cost_line_id and valuation.cost_line_id.id == line.id:
                        if line.split_method == 'by_quantity' and total_qty:
                            per_unit = (line.price_unit / total_qty)
                            value = valuation.quantity * per_unit
                        elif line.split_method == 'by_weight' and total_weight:
                            per_unit = (line.price_unit / total_weight)
                            value = valuation.weight * per_unit
                        elif line.split_method == 'by_volume' and total_volume:
                            per_unit = (line.price_unit / total_volume)
                            value = valuation.volume * per_unit
                        elif line.split_method == 'equal':
                            value = (line.price_unit / total_line)
                        elif line.split_method == 'by_current_cost_price' and total_cost:
                            per_unit = (line.price_unit / total_cost)
                            value = valuation.former_cost * per_unit
                        else:
                            value = (line.price_unit / total_line)

                        if rounding:
                            value = tools.float_round(value, precision_rounding=rounding, rounding_method='UP')
                            fnc = min if line.price_unit > 0 else max
                            value = fnc(value, line.price_unit - value_split)
                            value_split += value

                        if valuation.id not in towrite_dict:
                            towrite_dict[valuation.id] = value
                        else:
                            towrite_dict[valuation.id] += value
        for key, value in towrite_dict.items():
            AdjustementLines.browse(key).write({'additional_landed_cost': value})
        return True

    def action_view_stock_valuation_layers(self):
        self.ensure_one()
        domain = [('id', 'in', self.stock_valuation_layer_ids.ids)]
        action = self.env["ir.actions.actions"]._for_xml_id("stock_account.stock_valuation_layer_action")
        return dict(action, domain=domain)

    def _get_targeted_move_ids(self):
        return self.picking_ids.move_lines

    def _check_can_validate(self):
        if any(cost.state != 'draft' for cost in self):
            raise UserError(_('Only draft landed costs can be validated'))
        for cost in self:
            if not cost._get_targeted_move_ids():
                target_model_descriptions = dict(self._fields['target_model']._description_selection(self.env))
                raise UserError(_('Please define %s on which those additional costs should apply.', target_model_descriptions[cost.target_model]))

    def _check_sum(self):
        """ Check if each cost line its valuation lines sum to the correct amount
        and if the overall total amount is correct also """
        prec_digits = self.env.company.currency_id.decimal_places
        for landed_cost in self:
            total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost'))
            if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits):
                return False

            val_to_cost_lines = defaultdict(lambda: 0.0)
            for val_line in landed_cost.valuation_adjustment_lines:
                val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost
            if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits)
                   for cost_line, val_amount in val_to_cost_lines.items()):
                return False
        return True
Example #20
0
class Contract(models.Model):

    _name = 'hr.contract'
    _description = 'Contract'
    _inherit = ['mail.thread']

    name = fields.Char('Contract Reference', required=True)
    employee_id = fields.Many2one('hr.employee', string='Employee')
    department_id = fields.Many2one('hr.department', string="Department")
    type_id = fields.Many2one(
        'hr.contract.type',
        string="Contract Type",
        required=True,
        default=lambda self: self.env['hr.contract.type'].search([], limit=1))
    job_id = fields.Many2one('hr.job', string='Job Position')
    date_start = fields.Date('Start Date',
                             required=True,
                             default=fields.Date.today,
                             help="Start date of the contract.")
    date_end = fields.Date(
        'End Date',
        help="End date of the contract (if it's a fixed-term contract).")
    trial_date_end = fields.Date(
        'End of Trial Period',
        help="End date of the trial period (if there is one).")
    resource_calendar_id = fields.Many2one(
        'resource.calendar',
        'Working Schedule',
        default=lambda self: self.env['res.company']._company_default_get(
        ).resource_calendar_id.id)
    wage = fields.Monetary('Wage',
                           digits=(16, 2),
                           required=True,
                           track_visibility="onchange",
                           help="Employee's monthly gross wage.")
    advantages = fields.Text('Advantages')
    notes = fields.Text('Notes')
    state = fields.Selection([('draft', 'New'), ('open', 'Running'),
                              ('pending', 'To Renew'), ('close', 'Expired'),
                              ('cancel', 'Cancelled')],
                             string='Status',
                             group_expand='_expand_states',
                             track_visibility='onchange',
                             help='Status of the contract',
                             default='draft')
    company_id = fields.Many2one('res.company',
                                 default=lambda self: self.env.user.company_id)
    currency_id = fields.Many2one(string="Currency",
                                  related='company_id.currency_id',
                                  readonly=True)
    permit_no = fields.Char('Work Permit No', related="employee_id.permit_no")
    visa_no = fields.Char('Visa No', related="employee_id.visa_no")
    visa_expire = fields.Date('Visa Expire Date',
                              related="employee_id.visa_expire")

    def _expand_states(self, states, domain, order):
        return [key for key, val in type(self).state.selection]

    @api.onchange('employee_id')
    def _onchange_employee_id(self):
        if self.employee_id:
            self.job_id = self.employee_id.job_id
            self.department_id = self.employee_id.department_id
            self.resource_calendar_id = self.employee_id.resource_calendar_id

    @api.constrains('date_start', 'date_end')
    def _check_dates(self):
        if self.filtered(lambda c: c.date_end and c.date_start > c.date_end):
            raise ValidationError(
                _('Contract start date must be less than contract end date.'))

    @api.model
    def update_state(self):
        self.search([
            ('state', '=', 'open'),
            '|',
            '&',
            ('date_end', '<=',
             fields.Date.to_string(date.today() + relativedelta(days=7))),
            ('date_end', '>=',
             fields.Date.to_string(date.today() + relativedelta(days=1))),
            '&',
            ('visa_expire', '<=',
             fields.Date.to_string(date.today() + relativedelta(days=60))),
            ('visa_expire', '>=',
             fields.Date.to_string(date.today() + relativedelta(days=1))),
        ]).write({'state': 'pending'})

        self.search([
            ('state', 'in', ('open', 'pending')),
            '|',
            ('date_end', '<=',
             fields.Date.to_string(date.today() + relativedelta(days=1))),
            ('visa_expire', '<=',
             fields.Date.to_string(date.today() + relativedelta(days=1))),
        ]).write({'state': 'close'})

        return True

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'state' in init_values and self.state == 'pending':
            return 'hr_contract.mt_contract_pending'
        elif 'state' in init_values and self.state == 'close':
            return 'hr_contract.mt_contract_close'
        return super(Contract, self)._track_subtype(init_values)
class Property(models.Model):
    _name = 'ir.property'

    name = fields.Char(index=True)
    res_id = fields.Char(
        string='Resource',
        index=True,
        help="If not set, acts as a default value for new resources",
    )
    company_id = fields.Many2one('res.company', string='Company', index=True)
    fields_id = fields.Many2one('ir.model.fields',
                                string='Field',
                                ondelete='cascade',
                                required=True,
                                index=True)
    value_float = fields.Float()
    value_integer = fields.Integer()
    value_text = fields.Text()  # will contain (char, text)
    value_binary = fields.Binary()
    value_reference = fields.Char()
    value_datetime = fields.Datetime()
    type = fields.Selection([
        ('char', 'Char'),
        ('float', 'Float'),
        ('boolean', 'Boolean'),
        ('integer', 'Integer'),
        ('text', 'Text'),
        ('binary', 'Binary'),
        ('many2one', 'Many2One'),
        ('date', 'Date'),
        ('datetime', 'DateTime'),
        ('selection', 'Selection'),
    ],
                            required=True,
                            default='many2one',
                            index=True)

    @api.multi
    def _update_values(self, values):
        if 'value' not in values:
            return values
        value = values.pop('value')

        prop = None
        type_ = values.get('type')
        if not type_:
            if self:
                prop = self[0]
                type_ = prop.type
            else:
                type_ = self._fields['type'].default(self)

        field = TYPE2FIELD.get(type_)
        if not field:
            raise UserError(_('Invalid type'))

        if field == 'value_reference':
            if isinstance(value, models.BaseModel):
                value = '%s,%d' % (value._name, value.id)
            elif isinstance(value, pycompat.integer_types):
                field_id = values.get('fields_id')
                if not field_id:
                    if not prop:
                        raise ValueError()
                    field_id = prop.fields_id
                else:
                    field_id = self.env['ir.model.fields'].browse(field_id)

                value = '%s,%d' % (field_id.sudo().relation, value)

        values[field] = value
        return values

    @api.multi
    def write(self, values):
        return super(Property, self).write(self._update_values(values))

    @api.model
    def create(self, values):
        return super(Property, self).create(self._update_values(values))

    @api.multi
    def get_by_record(self):
        self.ensure_one()
        if self.type in ('char', 'text', 'selection'):
            return self.value_text
        elif self.type == 'float':
            return self.value_float
        elif self.type == 'boolean':
            return bool(self.value_integer)
        elif self.type == 'integer':
            return self.value_integer
        elif self.type == 'binary':
            return self.value_binary
        elif self.type == 'many2one':
            if not self.value_reference:
                return False
            model, resource_id = self.value_reference.split(',')
            return self.env[model].browse(int(resource_id)).exists()
        elif self.type == 'datetime':
            return self.value_datetime
        elif self.type == 'date':
            if not self.value_datetime:
                return False
            return fields.Date.to_string(
                fields.Datetime.from_string(self.value_datetime))
        return False

    @api.model
    def get(self, name, model, res_id=False):
        domain = self._get_domain(name, model)
        if domain is not None:
            domain = [('res_id', '=', res_id)] + domain
            #make the search with company_id asc to make sure that properties specific to a company are given first
            prop = self.search(domain, limit=1, order='company_id')
            if prop:
                return prop.get_by_record()
        return False

    def _get_domain(self, prop_name, model):
        self._cr.execute(
            "SELECT id FROM ir_model_fields WHERE name=%s AND model=%s",
            (prop_name, model))
        res = self._cr.fetchone()
        if not res:
            return None
        company_id = self._context.get(
            'force_company') or self.env['res.company']._company_default_get(
                model, res[0]).id
        return [('fields_id', '=', res[0]),
                ('company_id', 'in', [company_id, False])]

    @api.model
    def get_multi(self, name, model, ids):
        """ Read the property field `name` for the records of model `model` with
            the given `ids`, and return a dictionary mapping `ids` to their
            corresponding value.
        """
        if not ids:
            return {}

        field = self.env[model]._fields[name]
        field_id = self.env['ir.model.fields']._get(model, name).id
        company_id = (self._context.get('force_company')
                      or self.env['res.company']._company_default_get(
                          model, field_id).id)

        if field.type == 'many2one':
            comodel = self.env[field.comodel_name]
            model_pos = len(model) + 2
            value_pos = len(comodel._name) + 2
            # retrieve values: both p.res_id and p.value_reference are formatted
            # as "<rec._name>,<rec.id>"; the purpose of the LEFT JOIN is to
            # return the value id if it exists, NULL otherwise
            query = """
                SELECT substr(p.res_id, %s)::integer, r.id
                FROM ir_property p
                LEFT JOIN {} r ON substr(p.value_reference, %s)::integer=r.id
                WHERE p.fields_id=%s
                    AND (p.company_id=%s OR p.company_id IS NULL)
                    AND (p.res_id IN %s OR p.res_id IS NULL)
                ORDER BY p.company_id NULLS FIRST
            """.format(comodel._table)
            params = [model_pos, value_pos, field_id, company_id]
            clean = comodel.browse

        elif field.type in TYPE2FIELD:
            model_pos = len(model) + 2
            # retrieve values: p.res_id is formatted as "<rec._name>,<rec.id>"
            query = """
                SELECT substr(p.res_id, %s)::integer, p.{}
                FROM ir_property p
                WHERE p.fields_id=%s
                    AND (p.company_id=%s OR p.company_id IS NULL)
                    AND (p.res_id IN %s OR p.res_id IS NULL)
                ORDER BY p.company_id NULLS FIRST
            """.format(TYPE2FIELD[field.type])
            params = [model_pos, field_id, company_id]
            clean = TYPE2CLEAN[field.type]

        else:
            return dict.fromkeys(ids, False)

        # retrieve values
        cr = self.env.cr
        result = {}
        refs = {"%s,%s" % (model, id) for id in ids}
        for sub_refs in cr.split_for_in_conditions(refs):
            cr.execute(query, params + [sub_refs])
            result.update(cr.fetchall())

        # remove default value, add missing values, and format them
        default = result.pop(None, None)
        for id in ids:
            result[id] = clean(result.get(id, default))

        return result

    @api.model
    def set_multi(self, name, model, values, default_value=None):
        """ Assign the property field `name` for the records of model `model`
            with `values` (dictionary mapping record ids to their value).
            If the value for a given record is the same as the default
            value, the property entry will not be stored, to avoid bloating
            the database.
            If `default_value` is provided, that value will be used instead
            of the computed default value, to determine whether the value
            for a record should be stored or not.
        """
        def clean(value):
            return value.id if isinstance(value, models.BaseModel) else value

        if not values:
            return

        if default_value is None:
            domain = self._get_domain(name, model)
            if domain is None:
                raise Exception()
            # retrieve the default value for the field
            default_value = clean(self.get(name, model))

        # retrieve the properties corresponding to the given record ids
        self._cr.execute(
            "SELECT id FROM ir_model_fields WHERE name=%s AND model=%s",
            (name, model))
        field_id = self._cr.fetchone()[0]
        company_id = self.env.context.get(
            'force_company') or self.env['res.company']._company_default_get(
                model, field_id).id
        refs = {('%s,%s' % (model, id)): id for id in values}
        props = self.search([
            ('fields_id', '=', field_id),
            ('company_id', '=', company_id),
            ('res_id', 'in', list(refs)),
        ])

        # modify existing properties
        for prop in props:
            id = refs.pop(prop.res_id)
            value = clean(values[id])
            if value == default_value:
                # avoid prop.unlink(), as it clears the record cache that can
                # contain the value of other properties to set on record!
                prop.check_access_rights('unlink')
                prop.check_access_rule('unlink')
                self._cr.execute("DELETE FROM ir_property WHERE id=%s",
                                 [prop.id])
            elif value != clean(prop.get_by_record()):
                prop.write({'value': value})

        # create new properties for records that do not have one yet
        for ref, id in refs.items():
            value = clean(values[id])
            if value != default_value:
                self.create({
                    'fields_id': field_id,
                    'company_id': company_id,
                    'res_id': ref,
                    'name': name,
                    'value': value,
                    'type': self.env[model]._fields[name].type,
                })

    @api.model
    def search_multi(self, name, model, operator, value):
        """ Return a domain for the records that match the given condition. """
        default_matches = False
        include_zero = False

        field = self.env[model]._fields[name]
        if field.type == 'many2one':
            comodel = field.comodel_name

            def makeref(value):
                return value and '%s,%s' % (comodel, value)

            if operator == "=":
                value = makeref(value)
                # if searching properties not set, search those not in those set
                if value is False:
                    default_matches = True
            elif operator in ('!=', '<=', '<', '>', '>='):
                value = makeref(value)
            elif operator in ('in', 'not in'):
                value = [makeref(v) for v in value]
            elif operator in ('=like', '=ilike', 'like', 'not like', 'ilike',
                              'not ilike'):
                # most probably inefficient... but correct
                target = self.env[comodel]
                target_names = target.name_search(value,
                                                  operator=operator,
                                                  limit=None)
                target_ids = [n[0] for n in target_names]
                operator, value = 'in', [makeref(v) for v in target_ids]
        elif field.type in ('integer', 'float'):
            # No record is created in ir.property if the field's type is float or integer with a value
            # equal to 0. Then to match with the records that are linked to a property field equal to 0,
            # the negation of the operator must be taken  to compute the goods and the domain returned
            # to match the searched records is just the opposite.
            if value == 0 and operator == '=':
                operator = '!='
                include_zero = True
            elif value <= 0 and operator == '>=':
                operator = '<'
                include_zero = True
            elif value < 0 and operator == '>':
                operator = '<='
                include_zero = True
            elif value >= 0 and operator == '<=':
                operator = '>'
                include_zero = True
            elif value > 0 and operator == '<':
                operator = '>='
                include_zero = True

        # retrieve the properties that match the condition
        domain = self._get_domain(name, model)
        if domain is None:
            raise Exception()
        props = self.search(domain +
                            [(TYPE2FIELD[field.type], operator, value)])

        # retrieve the records corresponding to the properties that match
        good_ids = []
        for prop in props:
            if prop.res_id:
                res_model, res_id = prop.res_id.split(',')
                good_ids.append(int(res_id))
            else:
                default_matches = True

        if include_zero:
            return [('id', 'not in', good_ids)]
        elif default_matches:
            # exclude all records with a property that does not match
            all_ids = []
            props = self.search(domain + [('res_id', '!=', False)])
            for prop in props:
                res_model, res_id = prop.res_id.split(',')
                all_ids.append(int(res_id))
            bad_ids = list(set(all_ids) - set(good_ids))
            return [('id', 'not in', bad_ids)]
        else:
            return [('id', 'in', good_ids)]
Example #22
0
class PurchaseRequisition(models.Model):
    _name = "purchase.requisition"
    _description = "Purchase Requisition"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = "id desc"

    def _get_type_id(self):
        return self.env['purchase.requisition.type'].search([], limit=1)

    name = fields.Char(string='Reference', required=True, copy=False, default='New', readonly=True)
    origin = fields.Char(string='Source Document')
    order_count = fields.Integer(compute='_compute_orders_number', string='Number of Orders')
    vendor_id = fields.Many2one('res.partner', string="Vendor", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    type_id = fields.Many2one('purchase.requisition.type', string="Agreement Type", required=True, default=_get_type_id)
    ordering_date = fields.Date(string="Ordering Date", tracking=True)
    date_end = fields.Datetime(string='Agreement Deadline', tracking=True)
    schedule_date = fields.Date(string='Delivery Date', index=True, help="The expected and scheduled delivery date where all the products are received", tracking=True)
    user_id = fields.Many2one(
        'res.users', string='Purchase Representative',
        default=lambda self: self.env.user, check_company=True)
    description = fields.Text()
    company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
    purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]})
    line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True)
    product_id = fields.Many2one('product.product', related='line_ids.product_id', string='Product', readonly=False)
    state = fields.Selection(PURCHASE_REQUISITION_STATES,
                              'Status', tracking=True, required=True,
                              copy=False, default='draft')
    state_blanket_order = fields.Selection(PURCHASE_REQUISITION_STATES, compute='_set_state')
    is_quantity_copy = fields.Selection(related='type_id.quantity_copy', readonly=True)
    currency_id = fields.Many2one('res.currency', 'Currency', required=True,
        default=lambda self: self.env.company.currency_id.id)

    @api.depends('state')
    def _set_state(self):
        for requisition in self:
            requisition.state_blanket_order = requisition.state

    @api.onchange('vendor_id')
    def _onchange_vendor(self):
        self = self.with_company(self.company_id)
        if not self.vendor_id:
            self.currency_id = self.env.company.currency_id.id
        else:
            self.currency_id = self.vendor_id.property_purchase_currency_id.id or self.env.company.currency_id.id

        requisitions = self.env['purchase.requisition'].search([
            ('vendor_id', '=', self.vendor_id.id),
            ('state', '=', 'ongoing'),
            ('type_id.quantity_copy', '=', 'none'),
            ('company_id', '=', self.company_id.id),
        ])
        if any(requisitions):
            title = _("Warning for %s", self.vendor_id.name)
            message = _("There is already an open blanket order for this supplier. We suggest you complete this open blanket order, instead of creating a new one.")
            warning = {
                'title': title,
                'message': message
            }
            return {'warning': warning}

    @api.depends('purchase_ids')
    def _compute_orders_number(self):
        for requisition in self:
            requisition.order_count = len(requisition.purchase_ids)

    def action_cancel(self):
        # try to set all associated quotations to cancel state
        for requisition in self:
            for requisition_line in requisition.line_ids:
                requisition_line.supplier_info_ids.unlink()
            requisition.purchase_ids.button_cancel()
            for po in requisition.purchase_ids:
                po.message_post(body=_('Cancelled by the agreement associated to this quotation.'))
        self.write({'state': 'cancel'})

    def action_in_progress(self):
        self.ensure_one()
        if not self.line_ids:
            raise UserError(_("You cannot confirm agreement '%s' because there is no product line.", self.name))
        if self.type_id.quantity_copy == 'none' and self.vendor_id:
            for requisition_line in self.line_ids:
                if requisition_line.price_unit <= 0.0:
                    raise UserError(_('You cannot confirm the blanket order without price.'))
                if requisition_line.product_qty <= 0.0:
                    raise UserError(_('You cannot confirm the blanket order without quantity.'))
                requisition_line.create_supplier_info()
            self.write({'state': 'ongoing'})
        else:
            self.write({'state': 'in_progress'})
        # Set the sequence number regarding the requisition type
        if self.name == 'New':
            if self.is_quantity_copy != 'none':
                self.name = self.env['ir.sequence'].next_by_code('purchase.requisition.purchase.tender')
            else:
                self.name = self.env['ir.sequence'].next_by_code('purchase.requisition.blanket.order')

    def action_open(self):
        self.write({'state': 'open'})

    def action_draft(self):
        self.ensure_one()
        self.name = 'New'
        self.write({'state': 'draft'})

    def action_done(self):
        """
        Generate all purchase order based on selected lines, should only be called on one agreement at a time
        """
        if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')):
            raise UserError(_('You have to cancel or validate every RfQ before closing the purchase requisition.'))
        for requisition in self:
            for requisition_line in requisition.line_ids:
                requisition_line.supplier_info_ids.unlink()
        self.write({'state': 'done'})

    def unlink(self):
        if any(requisition.state not in ('draft', 'cancel') for requisition in self):
            raise UserError(_('You can only delete draft requisitions.'))
        # Draft requisitions could have some requisition lines.
        self.mapped('line_ids').unlink()
        return super(PurchaseRequisition, self).unlink()
Example #23
0
class FleetVehicleLogContract(models.Model):
    _inherit = ['mail.thread']
    _inherits = {'fleet.vehicle.cost': 'cost_id'}
    _name = 'fleet.vehicle.log.contract'
    _description = 'Contract information on a vehicle'
    _order = 'state desc,expiration_date'

    def compute_next_year_date(self, strdate):
        oneyear = relativedelta(years=1)
        start_date = fields.Date.from_string(strdate)
        return fields.Date.to_string(start_date + oneyear)

    @api.model
    def default_get(self, default_fields):
        res = super(FleetVehicleLogContract, self).default_get(default_fields)
        contract = self.env.ref('fleet.type_contract_leasing',
                                raise_if_not_found=False)
        res.update({
            'date': fields.Date.context_today(self),
            'cost_subtype_id': contract and contract.id or False,
            'cost_type': 'contract'
        })
        return res

    name = fields.Text(compute='_compute_contract_name', store=True)
    active = fields.Boolean(default=True)
    start_date = fields.Date(
        'Contract Start Date',
        default=fields.Date.context_today,
        help='Date when the coverage of the contract begins')
    expiration_date = fields.Date(
        'Contract Expiration Date',
        default=lambda self: self.compute_next_year_date(
            fields.Date.context_today(self)),
        help=
        'Date when the coverage of the contract expirates (by default, one year after begin date)'
    )
    days_left = fields.Integer(compute='_compute_days_left',
                               string='Warning Date')
    insurer_id = fields.Many2one('res.partner', 'Vendor')
    purchaser_id = fields.Many2one(
        'res.partner',
        'Contractor',
        default=lambda self: self.env.user.partner_id.id,
        help='Person to which the contract is signed for')
    ins_ref = fields.Char('Contract Reference', size=64, copy=False)
    state = fields.Selection(
        [('futur', 'Incoming'), ('open', 'In Progress'),
         ('expired', 'Expired'), ('diesoon', 'Expiring Soon'),
         ('closed', 'Closed')],
        'Status',
        default='open',
        readonly=True,
        help='Choose whether the contract is still valid or not',
        track_visibility="onchange",
        copy=False)
    notes = fields.Text(
        'Terms and Conditions',
        help=
        'Write here all supplementary information relative to this contract',
        copy=False)
    cost_generated = fields.Float(
        'Recurring Cost Amount',
        help="Costs paid at regular intervals, depending on the cost frequency. "
        "If the cost frequency is set to unique, the cost will be logged at the start date"
    )
    cost_frequency = fields.Selection([('no', 'No'), ('daily', 'Daily'),
                                       ('weekly', 'Weekly'),
                                       ('monthly', 'Monthly'),
                                       ('yearly', 'Yearly')],
                                      'Recurring Cost Frequency',
                                      default='no',
                                      help='Frequency of the recuring cost',
                                      required=True)
    generated_cost_ids = fields.One2many('fleet.vehicle.cost', 'contract_id',
                                         'Generated Costs')
    sum_cost = fields.Float(compute='_compute_sum_cost',
                            string='Indicative Costs Total')
    cost_id = fields.Many2one('fleet.vehicle.cost',
                              'Cost',
                              required=True,
                              ondelete='cascade')
    # we need to keep this field as a related with store=True because the graph view doesn't support
    # (1) to address fields from inherited table
    # (2) fields that aren't stored in database
    cost_amount = fields.Float(related='cost_id.amount',
                               string='Amount',
                               store=True)
    odometer = fields.Float(
        string='Odometer at creation',
        help=
        'Odometer measure of the vehicle at the moment of the contract creation'
    )

    @api.depends('vehicle_id', 'cost_subtype_id', 'date')
    def _compute_contract_name(self):
        for record in self:
            name = record.vehicle_id.name
            if record.cost_subtype_id.name:
                name += ' / ' + record.cost_subtype_id.name
            if record.date:
                name += ' / ' + record.date
            record.name = name

    @api.depends('expiration_date', 'state')
    def _compute_days_left(self):
        """return a dict with as value for each contract an integer
        if contract is in an open state and is overdue, return 0
        if contract is in a closed state, return -1
        otherwise return the number of days before the contract expires
        """
        for record in self:
            if (record.expiration_date
                    and (record.state == 'open' or record.state == 'expired')):
                today = fields.Date.from_string(fields.Date.today())
                renew_date = fields.Date.from_string(record.expiration_date)
                diff_time = (renew_date - today).days
                record.days_left = diff_time > 0 and diff_time or 0
            else:
                record.days_left = -1

    @api.depends('cost_ids.amount')
    def _compute_sum_cost(self):
        for contract in self:
            contract.sum_cost = sum(contract.cost_ids.mapped('amount'))

    @api.onchange('vehicle_id')
    def _onchange_vehicle(self):
        if self.vehicle_id:
            self.odometer_unit = self.vehicle_id.odometer_unit

    @api.multi
    def contract_close(self):
        for record in self:
            record.state = 'closed'

    @api.multi
    def contract_open(self):
        for record in self:
            record.state = 'open'

    @api.multi
    def act_renew_contract(self):
        assert len(
            self.ids
        ) == 1, "This operation should only be done for 1 single contract at a time, as it it suppose to open a window as result"
        for element in self:
            # compute end date
            startdate = fields.Date.from_string(element.start_date)
            enddate = fields.Date.from_string(element.expiration_date)
            diffdate = (enddate - startdate)
            default = {
                'date':
                fields.Date.context_today(self),
                'start_date':
                fields.Date.to_string(
                    fields.Date.from_string(element.expiration_date) +
                    relativedelta(days=1)),
                'expiration_date':
                fields.Date.to_string(enddate + diffdate),
            }
            newid = element.copy(default).id
        return {
            'name': _("Renew Contract"),
            'view_mode': 'form',
            'view_id':
            self.env.ref('fleet.fleet_vehicle_log_contract_view_form').id,
            'view_type': 'tree,form',
            'res_model': 'fleet.vehicle.log.contract',
            'type': 'ir.actions.act_window',
            'domain': '[]',
            'res_id': newid,
            'context': {
                'active_id': newid
            },
        }

    @api.model
    def scheduler_manage_auto_costs(self):
        # This method is called by a cron task
        # It creates costs for contracts having the "recurring cost" field setted, depending on their frequency
        # For example, if a contract has a reccuring cost of 200 with a weekly frequency, this method creates a cost of 200 on the
        # first day of each week, from the date of the last recurring costs in the database to today
        # If the contract has not yet any recurring costs in the database, the method generates the recurring costs from the start_date to today
        # The created costs are associated to a contract thanks to the many2one field contract_id
        # If the contract has no start_date, no cost will be created, even if the contract has recurring costs
        VehicleCost = self.env['fleet.vehicle.cost']
        deltas = {
            'yearly': relativedelta(years=+1),
            'monthly': relativedelta(months=+1),
            'weekly': relativedelta(weeks=+1),
            'daily': relativedelta(days=+1)
        }
        contracts = self.env['fleet.vehicle.log.contract'].search(
            [('state', '!=', 'closed')], offset=0, limit=None, order=None)
        for contract in contracts:
            if not contract.start_date or contract.cost_frequency == 'no':
                continue
            found = False
            last_cost_date = contract.start_date
            if contract.generated_cost_ids:
                last_autogenerated_cost = VehicleCost.search(
                    [('contract_id', '=', contract.id),
                     ('auto_generated', '=', True)],
                    offset=0,
                    limit=1,
                    order='date desc')
                if last_autogenerated_cost:
                    found = True
                    last_cost_date = last_autogenerated_cost.date
            startdate = fields.Date.from_string(last_cost_date)
            if found:
                startdate += deltas.get(contract.cost_frequency)
            today = fields.Date.from_string(fields.Date.context_today(self))
            while (startdate <= today) & (startdate <= fields.Date.from_string(
                    contract.expiration_date)):
                data = {
                    'amount': contract.cost_generated,
                    'date': fields.Date.context_today(self),
                    'vehicle_id': contract.vehicle_id.id,
                    'cost_subtype_id': contract.cost_subtype_id.id,
                    'contract_id': contract.id,
                    'auto_generated': True
                }
                self.env['fleet.vehicle.cost'].create(data)
                startdate += deltas.get(contract.cost_frequency)
        return True

    @api.model
    def scheduler_manage_contract_expiration(self):
        # This method is called by a cron task
        # It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status
        date_today = fields.Date.from_string(fields.Date.today())
        in_fifteen_days = fields.Date.to_string(date_today +
                                                relativedelta(days=+15))
        nearly_expired_contracts = self.search([('state', '=', 'open'),
                                                ('expiration_date', '<',
                                                 in_fifteen_days)])
        res = {}
        for contract in nearly_expired_contracts:
            if contract.vehicle_id.id in res:
                res[contract.vehicle_id.id] += 1
            else:
                res[contract.vehicle_id.id] = 1

        Vehicle = self.env['fleet.vehicle']
        for vehicle, value in res.items():
            Vehicle.browse(vehicle).message_post(body=_(
                '%s contract(s) will expire soon and should be renewed and/or closed!'
            ) % value)
        nearly_expired_contracts.write({'state': 'diesoon'})

        expired_contracts = self.search([
            ('state', 'not in', ['expired', 'closed']),
            ('expiration_date', '<', fields.Date.today())
        ])
        expired_contracts.write({'state': 'expired'})

        futur_contracts = self.search([
            ('state', 'not in', ['futur', 'closed']),
            ('start_date', '>', fields.Date.today())
        ])
        futur_contracts.write({'state': 'futur'})

        now_running_contracts = self.search([('state', '=', 'futur'),
                                             ('start_date', '<=',
                                              fields.Date.today())])
        now_running_contracts.write({'state': 'open'})

    @api.model
    def run_scheduler(self):
        self.scheduler_manage_auto_costs()
        self.scheduler_manage_contract_expiration()
Example #24
0
class IrTranslation(models.Model):
    _name = "ir.translation"
    _log_access = False

    name = fields.Char(string='Translated field', required=True)
    res_id = fields.Integer(string='Record ID', index=True)
    lang = fields.Selection(selection='_get_languages', string='Language')
    type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True)
    src = fields.Text(string='Internal Source')  # stored in database, kept for backward compatibility
    source = fields.Text(string='Source term', compute='_compute_source',
                         inverse='_inverse_source', search='_search_source')
    value = fields.Text(string='Translation Value')
    module = fields.Char(index=True, help="Module this term belongs to")

    state = fields.Selection([('to_translate', 'To Translate'),
                              ('inprogress', 'Translation in Progress'),
                              ('translated', 'Translated')],
                             string="Status", default='to_translate',
                             help="Automatically set to let administators find new terms that might need to be translated")

    # aka gettext extracted-comments - we use them to flag openerp-web translation
    # cfr: http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html
    comments = fields.Text(string='Translation comments', index=True)

    _sql_constraints = [
        ('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
         'Language code of translation item must be among known languages'),
    ]

    @api.model
    def _get_languages(self):
        langs = self.env['res.lang'].search([('translatable', '=', True)])
        return [(lang.code, lang.name) for lang in langs]

    @api.depends('type', 'name', 'res_id')
    def _compute_source(self):
        ''' Get source name for the translation. If object type is model, return
        the value stored in db. Otherwise, return value store in src field.
        '''
        for record in self:
            record.source = record.src
            if record.type != 'model':
                continue
            model_name, field_name = record.name.split(',')
            if model_name not in self.env:
                continue
            model = self.env[model_name]
            field = model._fields.get(field_name)
            if field is None:
                continue
            if not callable(field.translate):
                # Pass context without lang, need to read real stored field, not translation
                result = model.browse(record.res_id).with_context(lang=None).read([field_name])
                record.source = result[0][field_name] if result else False

    def _inverse_source(self):
        ''' When changing source term of a translation, change its value in db
        for the associated object, and the src field.
        '''
        self.ensure_one()
        if self.type == 'model':
            model_name, field_name = self.name.split(',')
            model = self.env[model_name]
            field = model._fields[field_name]
            if not callable(field.translate):
                # Make a context without language information, because we want
                # to write on the value stored in db and not on the one
                # associated with the current language. Also not removing lang
                # from context trigger an error when lang is different.
                model.browse(self.res_id).with_context(lang=None).write({field_name: self.source})
        if self.src != self.source:
            self.write({'src': self.source})

    def _search_source(self, operator, value):
        ''' the source term is stored on 'src' field '''
        return [('src', operator, value)]

    @api.model_cr_context
    def _auto_init(self):
        res = super(IrTranslation, self)._auto_init()
        # Add separate md5 index on src (no size limit on values, and good performance).
        tools.create_index(self._cr, 'ir_translation_src_md5', self._table, ['md5(src)'])
        tools.create_index(self._cr, 'ir_translation_ltn', self._table, ['name', 'lang', 'type'])
        return res

    @api.model
    def _check_selection_field_value(self, field, value):
        if field == 'lang':
            return
        return super(IrTranslation, self)._check_selection_field_value(field, value)

    @api.model
    def _get_ids(self, name, tt, lang, ids):
        """ Return the translations of records.

        :param name: a string defined as "<model_name>,<field_name>"
        :param tt: the type of translation (should always be "model")
        :param lang: the language code
        :param ids: the ids of the given records
        """
        translations = dict.fromkeys(ids, False)
        if ids:
            self._cr.execute("""SELECT res_id, value FROM ir_translation
                                WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s""",
                             (lang, tt, name, tuple(ids)))
            for res_id, value in self._cr.fetchall():
                translations[res_id] = value
        return translations

    CACHED_MODELS = {'ir.model.fields', 'ir.ui.view'}

    def _modified_model(self, model_name):
        """ Invalidate the ormcache if necessary, depending on ``model_name``.
        This should be called when modifying translations of type 'model'.
        """
        if model_name in self.CACHED_MODELS:
            self.clear_caches()

    @api.multi
    def _modified(self):
        """ Invalidate the ormcache if necessary, depending on the translations ``self``. """
        for trans in self:
            if trans.type != 'model' or trans.name.split(',')[0] in self.CACHED_MODELS:
                self.clear_caches()
                break

    @api.model
    def _set_ids(self, name, tt, lang, ids, value, src=None):
        """ Update the translations of records.

        :param name: a string defined as "<model_name>,<field_name>"
        :param tt: the type of translation (should always be "model")
        :param lang: the language code
        :param ids: the ids of the given records
        :param value: the value of the translation
        :param src: the source of the translation
        """
        self._modified_model(name.split(',')[0])

        # update existing translations
        self._cr.execute("""UPDATE ir_translation
                            SET value=%s, src=%s, state=%s
                            WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s
                            RETURNING res_id""",
                         (value, src, 'translated', lang, tt, name, tuple(ids)))
        existing_ids = [row[0] for row in self._cr.fetchall()]

        # create missing translations
        for res_id in set(ids) - set(existing_ids):
            self.create({
                'lang': lang,
                'type': tt,
                'name': name,
                'res_id': res_id,
                'value': value,
                'src': src,
                'state': 'translated',
            })
        return len(ids)

    @api.model
    def _get_source_query(self, name, types, lang, source, res_id):
        if source:
            # Note: the extra test on md5(src) is a hint for postgres to use the
            # index ir_translation_src_md5
            query = """SELECT value FROM ir_translation
                       WHERE lang=%s AND type in %s AND src=%s AND md5(src)=md5(%s)"""
            source = tools.ustr(source)
            params = (lang or '', types, source, source)
            if res_id:
                query += " AND res_id in %s"
                params += (res_id,)
            if name:
                query += " AND name=%s"
                params += (tools.ustr(name),)
        else:
            query = """ SELECT value FROM ir_translation
                        WHERE lang=%s AND type in %s AND name=%s """
            params = (lang or '', types, tools.ustr(name))

        return (query, params)

    @tools.ormcache('name', 'types', 'lang', 'source', 'res_id')
    def __get_source(self, name, types, lang, source, res_id):
        # res_id is a tuple or None, otherwise ormcache cannot cache it!
        query, params = self._get_source_query(name, types, lang, source, res_id)
        self._cr.execute(query, params)
        res = self._cr.fetchone()
        trad = res and res[0] or u''
        if source and not trad:
            return tools.ustr(source)
        return trad

    @api.model
    def _get_source(self, name, types, lang, source=None, res_id=None):
        """ Return the translation for the given combination of ``name``,
        ``type``, ``language`` and ``source``. All values passed to this method
        should be unicode (not byte strings), especially ``source``.

        :param name: identification of the term to translate, such as field name (optional if source is passed)
        :param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings)
        :param lang: language code of the desired translation
        :param source: optional source term to translate (should be unicode)
        :param res_id: optional resource id or a list of ids to translate (if used, ``source`` should be set)
        :rtype: unicode
        :return: the request translation, or an empty unicode string if no translation was
                 found and `source` was not passed
        """
        # FIXME: should assert that `source` is unicode and fix all callers to
        # always pass unicode so we can remove the string encoding/decoding.
        if not lang:
            return tools.ustr(source or '')
        if isinstance(types, pycompat.string_types):
            types = (types,)
        if res_id:
            if isinstance(res_id, pycompat.integer_types):
                res_id = (res_id,)
            else:
                res_id = tuple(res_id)
        return self.__get_source(name, types, lang, source, res_id)

    @api.model
    def _get_terms_query(self, field, records):
        """ Utility function that makes the query for field terms. """
        query = """ SELECT * FROM ir_translation
                    WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s """
        name = "%s,%s" % (field.model_name, field.name)
        params = (records.env.lang, 'model', name, tuple(records.ids))
        return query, params

    @api.model
    def _get_terms_mapping(self, field, records):
        """ Return a function mapping a ir_translation row (dict) to a value.
        This method is called before querying the database for translations.
        """
        return lambda data: data['value']

    @api.model
    def _get_terms_translations(self, field, records):
        """ Return the terms and translations of a given `field` on `records`.

        :return: {record_id: {source: value}}
        """
        result = {rid: {} for rid in records.ids}
        if records:
            map_trans = self._get_terms_mapping(field, records)
            query, params = self._get_terms_query(field, records)
            self._cr.execute(query, params)
            for data in self._cr.dictfetchall():
                result[data['res_id']][data['src']] = map_trans(data)
        return result

    @api.model
    def _sync_terms_translations(self, field, records):
        """ Synchronize the translations to the terms to translate, after the
        English value of a field is modified. The algorithm tries to match
        existing translations to the terms to translate, provided the distance
        between modified strings is not too large. It allows to not retranslate
        data where a typo has been fixed in the English value.
        """
        if not callable(field.translate):
            return

        trans = self.env['ir.translation']
        outdated = trans
        discarded = trans

        for record in records:
            # get field value and terms to translate
            value = record[field.name]
            terms = set(field.get_trans_terms(value))
            record_trans = trans.search([
                ('type', '=', 'model'),
                ('name', '=', "%s,%s" % (field.model_name, field.name)),
                ('res_id', '=', record.id),
            ])

            if not terms:
                # discard all translations for that field
                discarded += record_trans
                continue

            # remap existing translations on terms when possible
            for trans in record_trans:
                if trans.src == trans.value:
                    discarded += trans
                elif trans.src not in terms:
                    matches = get_close_matches(trans.src, terms, 1, 0.9)
                    if matches:
                        trans.write({'src': matches[0], 'state': trans.state})
                    else:
                        outdated += trans

        # process outdated and discarded translations
        outdated.write({'state': 'to_translate'})
        discarded.unlink()

    @api.model
    @tools.ormcache_context('model_name', keys=('lang',))
    def get_field_string(self, model_name):
        """ Return the translation of fields strings in the context's language.
        Note that the result contains the available translations only.

        :param model_name: the name of a model
        :return: the model's fields' strings as a dictionary `{field_name: field_string}`
        """
        fields = self.env['ir.model.fields'].sudo().search([('model', '=', model_name)])
        return {field.name: field.field_description for field in fields}

    @api.model
    @tools.ormcache_context('model_name', keys=('lang',))
    def get_field_help(self, model_name):
        """ Return the translation of fields help in the context's language.
        Note that the result contains the available translations only.

        :param model_name: the name of a model
        :return: the model's fields' help as a dictionary `{field_name: field_help}`
        """
        fields = self.env['ir.model.fields'].sudo().search([('model', '=', model_name)])
        return {field.name: field.help for field in fields}

    @api.multi
    def check(self, mode):
        """ Check access rights of operation ``mode`` on ``self`` for the
        current user. Raise an AccessError in case conditions are not met.
        """
        if self.env.user._is_admin():
            return

        # collect translated field records (model_ids) and other translations
        trans_ids = []
        model_ids = defaultdict(list)
        model_fields = defaultdict(list)
        for trans in self:
            if trans.type == 'model':
                mname, fname = trans.name.split(',')
                model_ids[mname].append(trans.res_id)
                model_fields[mname].append(fname)
            else:
                trans_ids.append(trans.id)

        # check for regular access rights on other translations
        if trans_ids:
            records = self.browse(trans_ids)
            records.check_access_rights(mode)
            records.check_access_rule(mode)

        # check for read/write access on translated field records
        fmode = 'read' if mode == 'read' else 'write'
        for mname, ids in model_ids.items():
            records = self.env[mname].browse(ids)
            records.check_access_rights(fmode)
            records.check_field_access_rights(fmode, model_fields[mname])
            records.check_access_rule(fmode)

    @api.constrains('type', 'name', 'value')
    def _check_value(self):
        for trans in self.with_context(lang=None):
            if trans.type == 'model' and trans.value:
                mname, fname = trans.name.split(',')
                record = trans.env[mname].browse(trans.res_id)
                field = record._fields[fname]
                if callable(field.translate):
                    src = trans.src
                    val = trans.value.strip()
                    # check whether applying (src -> val) then (val -> src)
                    # gives the original value back
                    value0 = field.translate(lambda term: None, record[fname])
                    value1 = field.translate({src: val}.get, value0)
                    # don't check the reverse if no translation happened
                    if value0 == value1:
                        continue
                    value2 = field.translate({val: src}.get, value1)
                    if value2 != value0:
                        raise ValidationError(_("Translation is not valid:\n%s") % val)

    @api.model
    def create(self, vals):
        record = super(IrTranslation, self.sudo()).create(vals).with_env(self.env)
        record.check('create')
        record._modified()
        return record

    @api.multi
    def write(self, vals):
        if vals.get('value'):
            vals.setdefault('state', 'translated')
        elif vals.get('src') or not vals.get('value', True):
            vals.setdefault('state', 'to_translate')
        self.check('write')
        result = super(IrTranslation, self.sudo()).write(vals)
        self.check('write')
        self._modified()
        return result

    @api.multi
    def unlink(self):
        self.check('unlink')
        self._modified()
        return super(IrTranslation, self.sudo()).unlink()

    @api.model
    def insert_missing(self, field, records):
        """ Insert missing translations for `field` on `records`. """
        records = records.with_context(lang=None)
        external_ids = records.get_external_id()  # if no xml_id, empty string
        if callable(field.translate):
            # insert missing translations for each term in src
            query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value, module)
                        SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s, %(module)s
                        FROM res_lang l
                        WHERE l.active AND NOT EXISTS (
                            SELECT 1 FROM ir_translation
                            WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND src=%(src)s
                        );
                    """
            for record in records:
                module = external_ids[record.id].split('.')[0]
                src = record[field.name] or None
                for term in set(field.get_trans_terms(src)):
                    self._cr.execute(query, {
                        'name': "%s,%s" % (field.model_name, field.name),
                        'res_id': record.id,
                        'src': term,
                        'module': module
                    })
        else:
            # insert missing translations for src
            query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value, module)
                        SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s, %(module)s
                        FROM res_lang l
                        WHERE l.active AND l.code != 'en_US' AND NOT EXISTS (
                            SELECT 1 FROM ir_translation
                            WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s
                        );
                        UPDATE ir_translation SET src=%(src)s
                        WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s;
                    """
            for record in records:
                module = external_ids[record.id].split('.')[0]
                self._cr.execute(query, {
                    'name': "%s,%s" % (field.model_name, field.name),
                    'res_id': record.id,
                    'src': record[field.name] or None,
                    'module': module
                })
        self._modified_model(field.model_name)

    @api.model
    def translate_fields(self, model, id, field=None):
        """ Open a view for translating the field(s) of the record (model, id). """
        main_lang = 'en_US'
        if not self.env['res.lang'].search_count([('code', '!=', main_lang)]):
            raise UserError(_("Translation features are unavailable until you install an extra translation."))

        # determine domain for selecting translations
        record = self.env[model].with_context(lang=main_lang).browse(id)
        domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')]

        def make_domain(fld, rec):
            name = "%s,%s" % (fld.model_name, fld.name)
            return ['&', ('res_id', '=', rec.id), ('name', '=', name)]

        # insert missing translations, and extend domain for related fields
        for name, fld in record._fields.items():
            if not fld.translate:
                continue

            rec = record
            if fld.related:
                try:
                    # traverse related fields up to their data source
                    while fld.related:
                        rec, fld = fld.traverse_related(rec)
                    if rec:
                        domain = ['|'] + domain + make_domain(fld, rec)
                except AccessError:
                    continue

            assert fld.translate and rec._name == fld.model_name
            self.insert_missing(fld, rec)

        action = {
            'name': 'Translate',
            'res_model': 'ir.translation',
            'type': 'ir.actions.act_window',
            'view_mode': 'tree',
            'view_id': self.env.ref('base.view_translation_dialog_tree').id,
            'target': 'current',
            'flags': {'search_view': True, 'action_buttons': True},
            'domain': domain,
        }
        if field:
            fld = record._fields[field]
            if not fld.related:
                action['context'] = {
                    'search_default_name': "%s,%s" % (fld.model_name, fld.name),
                }
        return action

    @api.model
    def _get_import_cursor(self):
        """ Return a cursor-like object for fast inserting translations """
        return IrTranslationImport(self)

    @api.model_cr_context
    def load_module_terms(self, modules, langs):
        """ Load PO files of the given modules for the given languages. """
        # make sure the given languages are active
        res_lang = self.env['res.lang'].sudo()
        for lang in langs:
            res_lang.load_lang(lang)
        # load i18n files
        for module_name in modules:
            modpath = get_module_path(module_name)
            if not modpath:
                continue
            for lang in langs:
                context = dict(self._context)
                lang_code = tools.get_iso_codes(lang)
                base_lang_code = None
                if '_' in lang_code:
                    base_lang_code = lang_code.split('_')[0]

                # Step 1: for sub-languages, load base language first (e.g. es_CL.po is loaded over es.po)
                if base_lang_code:
                    base_trans_file = get_module_resource(module_name, 'i18n', base_lang_code + '.po')
                    if base_trans_file:
                        _logger.info('module %s: loading base translation file %s for language %s', module_name, base_lang_code, lang)
                        tools.trans_load(self._cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
                        context['overwrite'] = True  # make sure the requested translation will override the base terms later

                    # i18n_extra folder is for additional translations handle manually (eg: for l10n_be)
                    base_trans_extra_file = get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
                    if base_trans_extra_file:
                        _logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang)
                        tools.trans_load(self._cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
                        context['overwrite'] = True  # make sure the requested translation will override the base terms later

                # Step 2: then load the main translation file, possibly overriding the terms coming from the base language
                trans_file = get_module_resource(module_name, 'i18n', lang_code + '.po')
                if trans_file:
                    _logger.info('module %s: loading translation file (%s) for language %s', module_name, lang_code, lang)
                    tools.trans_load(self._cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
                elif lang_code != 'en_US':
                    _logger.info('module %s: no translation for language %s', module_name, lang_code)

                trans_extra_file = get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
                if trans_extra_file:
                    _logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang)
                    tools.trans_load(self._cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
        return True

    @api.model
    def get_technical_translations(self, model_name):
        """ Find the translations for the fields of `model_name`

        Find the technical translations for the fields of the model, including
        string, tooltip and available selections.

        :return: action definition to open the list of available translations
        """
        fields = self.env['ir.model.fields'].search([('model', '=', model_name)])
        view = self.env.ref("base.view_translation_tree", False) or self.env['ir.ui.view']
        return {
            'name': _("Technical Translations"),
            'view_mode': 'tree',
            'views': [(view.id, "list")],
            'res_model': 'ir.translation',
            'type': 'ir.actions.act_window',
            'domain': [
                '|',
                    '&', ('type', '=', 'model'),
                        '&', ('res_id', 'in', fields.ids),
                             ('name', 'like', 'ir.model.fields,'),
                    '&', ('type', '=', 'selection'),
                         ('name', 'like', model_name+','),
            ],
        }
Example #25
0
class PosConfig(models.Model):
    _name = 'pos.config'
    _description = 'Point of Sale Configuration'

    def _default_picking_type_id(self):
        return self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).pos_type_id.id

    def _default_sale_journal(self):
        return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id), ('code', '=', 'POSS')], limit=1)

    def _default_invoice_journal(self):
        return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1)

    def _default_payment_methods(self):
        return self.env['pos.payment.method'].search([('split_transactions', '=', False), ('company_id', '=', self.env.company.id)])

    def _default_pricelist(self):
        return self.env['product.pricelist'].search([('company_id', 'in', (False, self.env.company.id)), ('currency_id', '=', self.env.company.currency_id.id)], limit=1)

    def _get_group_pos_manager(self):
        return self.env.ref('point_of_sale.group_pos_manager')

    def _get_group_pos_user(self):
        return self.env.ref('point_of_sale.group_pos_user')

    def _compute_customer_html(self):
        for config in self:
            config.customer_facing_display_html = self.env['ir.qweb']._render('point_of_sale.customer_facing_display_html')

    name = fields.Char(string='Point of Sale', index=True, required=True, help="An internal identification of the point of sale.")
    is_installed_account_accountant = fields.Boolean(string="Is the Full Accounting Installed",
        compute="_compute_is_installed_account_accountant")
    picking_type_id = fields.Many2one(
        'stock.picking.type',
        string='Operation Type',
        default=_default_picking_type_id,
        required=True,
        domain="[('code', '=', 'outgoing'), ('warehouse_id.company_id', '=', company_id)]",
        ondelete='restrict')
    journal_id = fields.Many2one(
        'account.journal', string='Sales Journal',
        domain=[('type', '=', 'sale')],
        help="Accounting journal used to post sales entries.",
        default=_default_sale_journal,
        ondelete='restrict')
    invoice_journal_id = fields.Many2one(
        'account.journal', string='Invoice Journal',
        domain=[('type', '=', 'sale')],
        help="Accounting journal used to create invoices.",
        default=_default_invoice_journal)
    currency_id = fields.Many2one('res.currency', compute='_compute_currency', string="Currency")
    iface_cashdrawer = fields.Boolean(string='Cashdrawer', help="Automatically open the cashdrawer.")
    iface_electronic_scale = fields.Boolean(string='Electronic Scale', help="Enables Electronic Scale integration.")
    iface_vkeyboard = fields.Boolean(string='Virtual KeyBoard', help=u"Don’t turn this option on if you take orders on smartphones or tablets. \n Such devices already benefit from a native keyboard.")
    iface_customer_facing_display = fields.Boolean(string='Customer Facing Display', help="Show checkout to customers with a remotely-connected screen.")
    iface_print_via_proxy = fields.Boolean(string='Print via Proxy', help="Bypass browser printing and prints via the hardware proxy.")
    iface_scan_via_proxy = fields.Boolean(string='Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner and card swiping with a Vantiv card reader.")
    iface_big_scrollbars = fields.Boolean('Large Scrollbars', help='For imprecise industrial touchscreens.')
    iface_print_auto = fields.Boolean(string='Automatic Receipt Printing', default=False,
        help='The receipt will automatically be printed at the end of each order.')
    iface_print_skip_screen = fields.Boolean(string='Skip Preview Screen', default=True,
        help='The receipt screen will be skipped if the receipt can be printed automatically.')
    iface_tax_included = fields.Selection([('subtotal', 'Tax-Excluded Price'), ('total', 'Tax-Included Price')], string="Tax Display", default='subtotal', required=True)
    iface_start_categ_id = fields.Many2one('pos.category', string='Initial Category',
        help='The point of sale will display this product category by default. If no category is specified, all available products will be shown.')
    iface_available_categ_ids = fields.Many2many('pos.category', string='Available PoS Product Categories',
        help='The point of sale will only display products which are within one of the selected category trees. If no category is specified, all available products will be shown')
    selectable_categ_ids = fields.Many2many('pos.category', compute='_compute_selectable_categories')
    iface_display_categ_images = fields.Boolean(string='Display Category Pictures',
        help="The product categories will be displayed with pictures.")
    restrict_price_control = fields.Boolean(string='Restrict Price Modifications to Managers',
        help="Only users with Manager access rights for PoS app can modify the product prices on orders.")
    cash_control = fields.Boolean(string='Advanced Cash Control', help="Check the amount of the cashbox at opening and closing.")
    receipt_header = fields.Text(string='Receipt Header', help="A short text that will be inserted as a header in the printed receipt.")
    receipt_footer = fields.Text(string='Receipt Footer', help="A short text that will be inserted as a footer in the printed receipt.")
    proxy_ip = fields.Char(string='IP Address', size=45,
        help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty.')
    active = fields.Boolean(default=True)
    uuid = fields.Char(readonly=True, default=lambda self: str(uuid4()), copy=False,
        help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.')
    sequence_id = fields.Many2one('ir.sequence', string='Order IDs Sequence', readonly=True,
        help="This sequence is automatically created by Flectra but you can change it "
        "to customize the reference numbers of your orders.", copy=False, ondelete='restrict')
    sequence_line_id = fields.Many2one('ir.sequence', string='Order Line IDs Sequence', readonly=True,
        help="This sequence is automatically created by Flectra but you can change it "
        "to customize the reference numbers of your orders lines.", copy=False)
    session_ids = fields.One2many('pos.session', 'config_id', string='Sessions')
    current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session")
    current_session_state = fields.Char(compute='_compute_current_session')
    last_session_closing_cash = fields.Float(compute='_compute_last_session')
    last_session_closing_date = fields.Date(compute='_compute_last_session')
    last_session_closing_cashbox = fields.Many2one('account.bank.statement.cashbox', compute='_compute_last_session')
    pos_session_username = fields.Char(compute='_compute_current_session_user')
    pos_session_state = fields.Char(compute='_compute_current_session_user')
    pos_session_duration = fields.Char(compute='_compute_current_session_user')
    pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist', required=True, default=_default_pricelist,
        help="The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured.")
    available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists', default=_default_pricelist,
        help="Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply.")
    allowed_pricelist_ids = fields.Many2many(
        'product.pricelist',
        string='Allowed Pricelists',
        compute='_compute_allowed_pricelist_ids',
        help='This is a technical field used for the domain of pricelist_id.',
    )
    company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
    barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcode Nomenclature',
        help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers.',
        default=lambda self: self.env.company.nomenclature_id, required=True)
    group_pos_manager_id = fields.Many2one('res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager,
        help='This field is there to pass the id of the pos manager group to the point of sale client.')
    group_pos_user_id = fields.Many2one('res.groups', string='Point of Sale User Group', default=_get_group_pos_user,
        help='This field is there to pass the id of the pos user group to the point of sale client.')
    iface_tipproduct = fields.Boolean(string="Product tips")
    tip_product_id = fields.Many2one('product.product', string='Tip Product',
        help="This product is used as reference on customer receipts.")
    fiscal_position_ids = fields.Many2many('account.fiscal.position', string='Fiscal Positions', help='This is useful for restaurants with onsite and take-away services that imply specific tax rates.')
    default_fiscal_position_id = fields.Many2one('account.fiscal.position', string='Default Fiscal Position')
    default_cashbox_id = fields.Many2one('account.bank.statement.cashbox', string='Default Balance')
    customer_facing_display_html = fields.Html(string='Customer facing display content', translate=True, compute=_compute_customer_html)
    use_pricelist = fields.Boolean("Use a pricelist.")
    tax_regime = fields.Boolean("Tax Regime")
    tax_regime_selection = fields.Boolean("Tax Regime Selection value")
    start_category = fields.Boolean("Start Category", default=False)
    limit_categories = fields.Boolean("Restrict Product Categories")
    module_account = fields.Boolean(string='Invoicing', default=True, help='Enables invoice generation from the Point of Sale.')
    module_pos_restaurant = fields.Boolean("Is a Bar/Restaurant")
    module_pos_discount = fields.Boolean("Global Discounts")
    module_pos_loyalty = fields.Boolean("Loyalty Program")
    module_pos_mercury = fields.Boolean(string="Integrated Card Payments")
    manage_orders = fields.Boolean(string="Manage Orders")
    product_configurator = fields.Boolean(string="Product Configurator")
    is_posbox = fields.Boolean("PosBox")
    is_header_or_footer = fields.Boolean("Header & Footer")
    module_pos_hr = fields.Boolean(help="Show employee login screen")
    amount_authorized_diff = fields.Float('Amount Authorized Difference',
        help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when "
             "closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at "
             "the closing of his session saying that he needs to contact his manager.")
    payment_method_ids = fields.Many2many('pos.payment.method', string='Payment Methods', default=lambda self: self._default_payment_methods())
    company_has_template = fields.Boolean(string="Company has chart of accounts", compute="_compute_company_has_template")
    current_user_id = fields.Many2one('res.users', string='Current Session Responsible', compute='_compute_current_session_user')
    other_devices = fields.Boolean(string="Other Devices", help="Connect devices to your PoS without an IoT Box.")
    rounding_method = fields.Many2one('account.cash.rounding', string="Cash rounding")
    cash_rounding = fields.Boolean(string="Cash Rounding")
    only_round_cash_method = fields.Boolean(string="Only apply rounding on cash")
    has_active_session = fields.Boolean(compute='_compute_current_session')
    show_allow_invoicing_alert = fields.Boolean(compute="_compute_show_allow_invoicing_alert")
    manual_discount = fields.Boolean(string="Manual Discounts", default=True)

    @api.depends('use_pricelist', 'available_pricelist_ids')
    def _compute_allowed_pricelist_ids(self):
        for config in self:
            if config.use_pricelist:
                config.allowed_pricelist_ids = config.available_pricelist_ids.ids
            else:
                config.allowed_pricelist_ids = self.env['product.pricelist'].search([]).ids

    @api.depends('company_id')
    def _compute_company_has_template(self):
        for config in self:
            if config.company_id.chart_template_id:
                config.company_has_template = True
            else:
                config.company_has_template = False

    def _compute_is_installed_account_accountant(self):
        account_accountant = self.env['ir.module.module'].sudo().search([('name', '=', 'account_accountant'), ('state', '=', 'installed')])
        for pos_config in self:
            pos_config.is_installed_account_accountant = account_accountant and account_accountant.id

    @api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id', 'company_id', 'company_id.currency_id')
    def _compute_currency(self):
        for pos_config in self:
            if pos_config.journal_id:
                pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id
            else:
                pos_config.currency_id = pos_config.company_id.currency_id.id

    @api.depends('session_ids', 'session_ids.state')
    def _compute_current_session(self):
        """If there is an open session, store it to current_session_id / current_session_State.
        """
        for pos_config in self:
            opened_sessions = pos_config.session_ids.filtered(lambda s: not s.state == 'closed')
            session = pos_config.session_ids.filtered(lambda s: s.user_id.id == self.env.uid and \
                    not s.state == 'closed' and not s.rescue)
            # sessions ordered by id desc
            pos_config.has_active_session = opened_sessions and True or False
            pos_config.current_session_id = session and session[0].id or False
            pos_config.current_session_state = session and session[0].state or False

    @api.depends('module_account', 'manage_orders')
    def _compute_show_allow_invoicing_alert(self):
        for pos_config in self:
            if not pos_config.manage_orders:
                pos_config.show_allow_invoicing_alert = False
            else:
                pos_config.show_allow_invoicing_alert = not pos_config.module_account

    @api.depends('session_ids')
    def _compute_last_session(self):
        PosSession = self.env['pos.session']
        for pos_config in self:
            session = PosSession.search_read(
                [('config_id', '=', pos_config.id), ('state', '=', 'closed')],
                ['cash_register_balance_end_real', 'stop_at', 'cash_register_id'],
                order="stop_at desc", limit=1)
            if session:
                timezone = pytz.timezone(self._context.get('tz') or self.env.user.tz or 'UTC')
                pos_config.last_session_closing_date = session[0]['stop_at'].astimezone(timezone).date()
                if session[0]['cash_register_id']:
                    pos_config.last_session_closing_cash = session[0]['cash_register_balance_end_real']
                    pos_config.last_session_closing_cashbox = self.env['account.bank.statement'].browse(session[0]['cash_register_id'][0]).cashbox_end_id
                else:
                    pos_config.last_session_closing_cash = 0
                    pos_config.last_session_closing_cashbox = False
            else:
                pos_config.last_session_closing_cash = 0
                pos_config.last_session_closing_date = False
                pos_config.last_session_closing_cashbox = False

    @api.depends('session_ids')
    def _compute_current_session_user(self):
        for pos_config in self:
            session = pos_config.session_ids.filtered(lambda s: s.state in ['opening_control', 'opened', 'closing_control'] and not s.rescue)
            if session:
                pos_config.pos_session_username = session[0].user_id.sudo().name
                pos_config.pos_session_state = session[0].state
                pos_config.pos_session_duration = (
                    datetime.now() - session[0].start_at
                ).days if session[0].start_at else 0
                pos_config.current_user_id = session[0].user_id
            else:
                pos_config.pos_session_username = False
                pos_config.pos_session_state = False
                pos_config.pos_session_duration = 0
                pos_config.current_user_id = False

    @api.depends('iface_available_categ_ids')
    def _compute_selectable_categories(self):
        for config in self:
            if config.iface_available_categ_ids:
                config.selectable_categ_ids = config.iface_available_categ_ids
            else:
                config.selectable_categ_ids = self.env['pos.category'].search([])

    @api.constrains('cash_control')
    def _check_session_state(self):
        open_session = self.env['pos.session'].search([('config_id', '=', self.id), ('state', '!=', 'closed')])
        if open_session:
            raise ValidationError(_("You are not allowed to change the cash control status while a session is already opened."))

    @api.constrains('rounding_method')
    def _check_rounding_method_strategy(self):
        if self.cash_rounding and self.rounding_method.strategy != 'add_invoice_line':
            raise ValidationError(_("Cash rounding strategy must be: 'Add a rounding line'"))

    @api.constrains('company_id', 'journal_id')
    def _check_company_journal(self):
        if self.journal_id and self.journal_id.company_id.id != self.company_id.id:
            raise ValidationError(_("The sales journal and the point of sale must belong to the same company."))

    def _check_profit_loss_cash_journal(self):
        if self.cash_control and self.payment_method_ids:
            for method in self.payment_method_ids:
                if method.is_cash_count and (not method.cash_journal_id.loss_account_id or not method.cash_journal_id.profit_account_id):
                    raise ValidationError(_("You need a loss and profit account on your cash journal."))

    @api.constrains('company_id', 'invoice_journal_id')
    def _check_company_invoice_journal(self):
        if self.invoice_journal_id and self.invoice_journal_id.company_id.id != self.company_id.id:
            raise ValidationError(_("The invoice journal and the point of sale must belong to the same company."))

    @api.constrains('company_id', 'payment_method_ids')
    def _check_company_payment(self):
        if self.env['pos.payment.method'].search_count([('id', 'in', self.payment_method_ids.ids), ('company_id', '!=', self.company_id.id)]):
            raise ValidationError(_("The payment methods and the point of sale must belong to the same company."))

    @api.constrains('pricelist_id', 'use_pricelist', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'payment_method_ids')
    def _check_currencies(self):
        for config in self:
            if config.use_pricelist and config.pricelist_id not in config.available_pricelist_ids:
                raise ValidationError(_("The default pricelist must be included in the available pricelists."))
        if any(self.available_pricelist_ids.mapped(lambda pricelist: pricelist.currency_id != self.currency_id)):
            raise ValidationError(_("All available pricelists must be in the same currency as the company or"
                                    " as the Sales Journal set on this point of sale if you use"
                                    " the Accounting application."))
        if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id:
            raise ValidationError(_("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set."))
        if any(
            self.payment_method_ids\
                .filtered(lambda pm: pm.is_cash_count)\
                .mapped(lambda pm: self.currency_id not in (self.company_id.currency_id | pm.cash_journal_id.currency_id))
        ):
            raise ValidationError(_("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set."))

    @api.constrains('payment_method_ids')
    def _check_payment_method_receivable_accounts(self):
        # This is normally not supposed to happen to have a payment method without a receivable account set,
        # as this is a required field. However, it happens the receivable account cannot be found during upgrades
        # and this is a bommer to block the upgrade for that point, given the user can correct this by himself,
        # without requiring a manual intervention from our upgrade support.
        # However, this must be ensured this receivable is well set before opening a POS session.
        invalid_payment_methods = self.payment_method_ids.filtered(lambda method: not method.receivable_account_id)
        if invalid_payment_methods:
            method_names = ", ".join(method.name for method in invalid_payment_methods)
            raise ValidationError(
                _("You must configure an intermediary account for the payment methods: %s.") % method_names
            )

    def _check_payment_method_ids(self):
        self.ensure_one()
        if not self.payment_method_ids:
            raise ValidationError(
                _("You must have at least one payment method configured to launch a session.")
            )

    @api.constrains('company_id', 'available_pricelist_ids')
    def _check_companies(self):
        if any(self.available_pricelist_ids.mapped(lambda pl: pl.company_id.id not in (False, self.company_id.id))):
            raise ValidationError(_("The selected pricelists must belong to no company or the company of the point of sale."))

    @api.onchange('iface_tipproduct')
    def _onchange_tipproduct(self):
        if self.iface_tipproduct:
            self.tip_product_id = self.env.ref('point_of_sale.product_product_tip', False)
        else:
            self.tip_product_id = False

    @api.onchange('iface_print_via_proxy')
    def _onchange_iface_print_via_proxy(self):
        self.iface_print_auto = self.iface_print_via_proxy
        if not self.iface_print_via_proxy:
            self.iface_cashdrawer = False

    @api.onchange('module_account')
    def _onchange_module_account(self):
        if self.module_account and not self.invoice_journal_id:
            self.invoice_journal_id = self._default_invoice_journal()

    @api.onchange('use_pricelist')
    def _onchange_use_pricelist(self):
        """
        If the 'pricelist' box is unchecked, we reset the pricelist_id to stop
        using a pricelist for this iotbox.
        """
        if not self.use_pricelist:
            self.pricelist_id = self._default_pricelist()

    @api.onchange('available_pricelist_ids')
    def _onchange_available_pricelist_ids(self):
        if self.pricelist_id not in self.available_pricelist_ids._origin:
            self.pricelist_id = False

    @api.onchange('is_posbox')
    def _onchange_is_posbox(self):
        if not self.is_posbox:
            self.proxy_ip = False
            self.iface_scan_via_proxy = False
            self.iface_electronic_scale = False
            self.iface_cashdrawer = False
            self.iface_print_via_proxy = False
            self.iface_customer_facing_display = False

    @api.onchange('tax_regime')
    def _onchange_tax_regime(self):
        if not self.tax_regime:
            self.default_fiscal_position_id = False

    @api.onchange('tax_regime_selection')
    def _onchange_tax_regime_selection(self):
        if not self.tax_regime_selection:
            self.fiscal_position_ids = [(5, 0, 0)]

    @api.onchange('start_category')
    def _onchange_start_category(self):
        if not self.start_category:
            self.iface_start_categ_id = False

    @api.onchange('limit_categories', 'iface_available_categ_ids', 'iface_start_categ_id')
    def _onchange_limit_categories(self):
        res = {}
        if not self.limit_categories:
            self.iface_available_categ_ids = False
        if self.iface_available_categ_ids and self.iface_start_categ_id.id not in self.iface_available_categ_ids.ids:
            self.iface_start_categ_id = False
        return res

    @api.onchange('is_header_or_footer')
    def _onchange_header_footer(self):
        if not self.is_header_or_footer:
            self.receipt_header = False
            self.receipt_footer = False

    def name_get(self):
        result = []
        for config in self:
            last_session = self.env['pos.session'].search([('config_id', '=', config.id)], limit=1)
            if (not last_session) or (last_session.state == 'closed'):
                result.append((config.id, _("%(pos_name)s (not used)", pos_name=config.name)))
            else:
                result.append((config.id, "%s (%s)" % (config.name, last_session.user_id.name)))
        return result

    @api.model
    def create(self, values):
        IrSequence = self.env['ir.sequence'].sudo()
        val = {
            'name': _('POS Order %s', values['name']),
            'padding': 4,
            'prefix': "%s/" % values['name'],
            'code': "pos.order",
            'company_id': values.get('company_id', False),
        }
        # force sequence_id field to new pos.order sequence
        values['sequence_id'] = IrSequence.create(val).id

        val.update(name=_('POS order line %s', values['name']), code='pos.order.line')
        values['sequence_line_id'] = IrSequence.create(val).id
        pos_config = super(PosConfig, self).create(values)
        pos_config.sudo()._check_modules_to_install()
        pos_config.sudo()._check_groups_implied()
        # If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
        return pos_config

    def write(self, vals):
        opened_session = self.mapped('session_ids').filtered(lambda s: s.state != 'closed')
        if opened_session:
            forbidden_fields = []
            for key in self._get_forbidden_change_fields():
                if key in vals.keys():
                    field_name = self._fields[key].get_description(self.env)["string"]
                    forbidden_fields.append(field_name)
            if len(forbidden_fields) > 0:
                raise UserError(_(
                    "Unable to modify this PoS Configuration because you can't modify %s while a session is open.",
                    ", ".join(forbidden_fields)
                ))
        result = super(PosConfig, self).write(vals)

        self.sudo()._set_fiscal_position()
        self.sudo()._check_modules_to_install()
        self.sudo()._check_groups_implied()
        return result

    def _get_forbidden_change_fields(self):
        forbidden_keys = ['module_pos_hr', 'cash_control', 'module_pos_restaurant', 'available_pricelist_ids',
                          'limit_categories', 'iface_available_categ_ids', 'use_pricelist', 'module_pos_discount',
                          'payment_method_ids', 'iface_tipproduc']
        return forbidden_keys

    def unlink(self):
        # Delete the pos.config records first then delete the sequences linked to them
        sequences_to_delete = self.sequence_id | self.sequence_line_id
        res = super(PosConfig, self).unlink()
        sequences_to_delete.unlink()
        return res

    def _set_fiscal_position(self):
        for config in self:
            if config.tax_regime and config.default_fiscal_position_id.id not in config.fiscal_position_ids.ids:
                config.fiscal_position_ids = [(4, config.default_fiscal_position_id.id)]
            elif not config.tax_regime_selection and not config.tax_regime and config.fiscal_position_ids.ids:
                config.fiscal_position_ids = [(5, 0, 0)]

    def _check_modules_to_install(self):
        # determine modules to install
        expected = [
            fname[7:]           # 'module_account' -> 'account'
            for fname in self.fields_get_keys()
            if fname.startswith('module_')
            if any(pos_config[fname] for pos_config in self)
        ]
        if expected:
            STATES = ('installed', 'to install', 'to upgrade')
            modules = self.env['ir.module.module'].sudo().search([('name', 'in', expected)])
            modules = modules.filtered(lambda module: module.state not in STATES)
            if modules:
                modules.button_immediate_install()
                # just in case we want to do something if we install a module. (like a refresh ...)
                return True
        return False

    def _check_groups_implied(self):
        for pos_config in self:
            for field_name in [f for f in pos_config.fields_get_keys() if f.startswith('group_')]:
                field = pos_config._fields[field_name]
                if field.type in ('boolean', 'selection') and hasattr(field, 'implied_group'):
                    field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',')
                    field_groups = self.env['res.groups'].concat(*(self.env.ref(it) for it in field_group_xmlids))
                    field_groups.write({'implied_ids': [(4, self.env.ref(field.implied_group).id)]})


    def execute(self):
        return {
             'type': 'ir.actions.client',
             'tag': 'reload',
             'params': {'wait': True}
         }

    def _force_http(self):
        return False

    def _get_pos_base_url(self):
        return '/pos/web' if self._force_http() else '/pos/ui'

    # Methods to open the POS
    def open_ui(self):
        """Open the pos interface with config_id as an extra argument.

        In vanilla PoS each user can only have one active session, therefore it was not needed to pass the config_id
        on opening a session. It is also possible to login to sessions created by other users.

        :returns: dict
        """
        self.ensure_one()
        # check all constraints, raises if any is not met
        self._validate_fields(set(self._fields) - {"cash_control"})
        return {
            'type': 'ir.actions.act_url',
            'url': self._get_pos_base_url() + '?config_id=%d' % self.id,
            'target': 'self',
        }

    def open_session_cb(self, check_coa=True):
        """ new session button

        create one if none exist
        access cash control interface if enabled or start a session
        """
        self.ensure_one()
        if not self.current_session_id:
            self._check_company_journal()
            self._check_company_invoice_journal()
            self._check_company_payment()
            self._check_currencies()
            self._check_profit_loss_cash_journal()
            self._check_payment_method_ids()
            self._check_payment_method_receivable_accounts()
            self.env['pos.session'].create({
                'user_id': self.env.uid,
                'config_id': self.id
            })
        return self.open_ui()

    def open_existing_session_cb(self):
        """ close session button

        access session form to validate entries
        """
        self.ensure_one()
        return self._open_session(self.current_session_id.id)

    def _open_session(self, session_id):
        return {
            'name': _('Session'),
            'view_mode': 'form,tree',
            'res_model': 'pos.session',
            'res_id': session_id,
            'view_id': False,
            'type': 'ir.actions.act_window',
        }

    # All following methods are made to create data needed in POS, when a localisation
    # is installed, or if POS is installed on database having companies that already have
    # a localisation installed
    @api.model
    def post_install_pos_localisation(self, companies=False):
        self = self.sudo()
        if not companies:
            companies = self.env['res.company'].search([])
        for company in companies.filtered('chart_template_id'):
            pos_configs = self.search([('company_id', '=', company.id)])
            pos_configs.setup_defaults(company)

    def setup_defaults(self, company):
        """Extend this method to customize the existing pos.config of the company during the installation
        of a localisation.

        :param self pos.config: pos.config records present in the company during the installation of localisation.
        :param company res.company: the single company where the pos.config defaults will be setup.
        """
        self.assign_payment_journals(company)
        self.generate_pos_journal(company)
        self.setup_invoice_journal(company)

    def assign_payment_journals(self, company):
        for pos_config in self:
            if pos_config.payment_method_ids or pos_config.has_active_session:
                continue
            cash_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('type', '=', 'cash')], limit=1)
            pos_receivable_account = company.account_default_pos_receivable_account_id
            payment_methods = self.env['pos.payment.method']
            if cash_journal:
                payment_methods |= payment_methods.create({
                    'name': _('Cash'),
                    'receivable_account_id': pos_receivable_account.id,
                    'is_cash_count': True,
                    'cash_journal_id': cash_journal.id,
                    'company_id': company.id,
                })
            payment_methods |= payment_methods.create({
                'name': _('Bank'),
                'receivable_account_id': pos_receivable_account.id,
                'is_cash_count': False,
                'company_id': company.id,
            })
            pos_config.write({'payment_method_ids': [(6, 0, payment_methods.ids)]})

    def generate_pos_journal(self, company):
        for pos_config in self:
            if pos_config.journal_id:
                continue
            pos_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'POSS')])
            if not pos_journal:
                pos_journal = self.env['account.journal'].create({
                    'type': 'sale',
                    'name': 'Point of Sale',
                    'code': 'POSS',
                    'company_id': company.id,
                    'sequence': 20
                })
            pos_config.write({'journal_id': pos_journal.id})

    def setup_invoice_journal(self, company):
        for pos_config in self:
            invoice_journal_id = pos_config.invoice_journal_id or self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', company.id)], limit=1)
            if invoice_journal_id:
                pos_config.write({'invoice_journal_id': invoice_journal_id.id})
            else:
                pos_config.write({'module_account': False})
Example #26
0
class Recurring(models.Model):
    _name = "recurring"
    _description = "Recurring"

    @api.model
    def default_get(self, fields):
        res = super(Recurring, self).default_get(fields)
        active_model = self._context.get('active_model')
        active_id = self._context.get('active_id')
        if active_model and active_id:
            record = self.env[active_model].browse(active_id)
            if 'partner_id' in self.env[active_model]._fields:
                res['partner_id'] = record.partner_id.id
            else:
                res['name'] = record.name
                if not res['name']:
                    res['name'] = record.number
        return res

    @api.onchange('partner_id')
    def _onchange_partner_id(self):
        active_model = self._context.get('active_model')
        active_id = self._context.get('active_id')
        if self.partner_id and active_model and active_id:
            record = self.env[active_model].browse(active_id)
            name = record.name
            if not name:
                name = record.number
            if name:
                self.name = name + '-' + self.partner_id.name
            else:
                self.name = self.partner_id.name

    @api.constrains('partner_id', 'doc_source')
    def _check_partner_id_doc_source(self):
        for record in self:
            if record.partner_id and record.doc_source and 'partner_id' in \
                    self.env[record.doc_source._name]._fields and \
                    record.doc_source.partner_id != record.partner_id:
                raise ValidationError(
                    _('Error! Source Document should be related to partner %s'
                      % record.doc_source.partner_id.name))

    name = fields.Char(string='Name')
    active = fields.Boolean(
        help="If the active field is set to False, it will allow you to hide "
        "the recurring without removing it.",
        default=True)
    partner_id = fields.Many2one('res.partner', string='Partner')
    notes = fields.Text(string='Internal Notes')
    user_id = fields.Many2one('res.users',
                              string='User',
                              default=lambda self: self.env.user)
    interval_number = fields.Integer(string='Internal Qty', default=1)
    interval_type = fields.Selection([('minutes', 'Minutes'),
                                      ('hours', 'Hours'), ('days', 'Days'),
                                      ('weeks', 'Weeks'),
                                      ('months', 'Months')],
                                     string='Interval Unit',
                                     default='months')
    exec_init = fields.Integer(string='Number of Documents')
    date_init = fields.Datetime(string='First Date',
                                default=fields.Datetime.now)
    state = fields.Selection([('draft', 'Draft'), ('running', 'Running'),
                              ('done', 'Done')],
                             string='Status',
                             copy=False,
                             default='draft')
    doc_source = fields.Reference(
        selection=_get_document_types,
        string='Source Document',
        help="User can choose the source document on which he wants to "
        "create documents")
    doc_lines = fields.One2many('recurring.history',
                                'recurring_id',
                                string='Documents created')
    cron_id = fields.Many2one('ir.cron',
                              string='Cron Job',
                              help="Scheduler which runs on recurring",
                              states={
                                  'running': [('readonly', True)],
                                  'done': [('readonly', True)]
                              })
    note = fields.Text(string='Notes',
                       help="Description or Summary of Recurring")

    @api.model
    def _auto_end(self):
        super(Recurring, self)._auto_end()
        # drop the FK from recurring to ir.cron, as it would cause deadlocks
        # during cron job execution. When model_copy() tries to write() on
        # the recurring,
        # it has to wait for an ExclusiveLock on the cron job record,
        # but the latter is locked by the cron system for the duration of
        # the job!
        # FIXME: the recurring module should be reviewed to simplify the
        # scheduling process
        # and to use a unique cron job for all recurrings, so that it
        # never needs to be updated during its execution.
        self.env.cr.execute("ALTER TABLE %s DROP CONSTRAINT %s" %
                            (self._table, '%s_cron_id_fkey' % self._table))

    @api.multi
    def create_recurring_type(self):
        rec_doc_obj = self.env['recurring.document']
        ir_model_id = self.env['ir.model'].search([
            ('model', '=', self._context.get('active_model', False))
        ])
        rec_doc_id = rec_doc_obj.search([('model', '=', ir_model_id.id)])
        if not rec_doc_id:
            rec_doc_id = rec_doc_obj.create({
                'name': ir_model_id.name,
                'model': ir_model_id.id,
            })
        return rec_doc_id

    @api.multi
    def btn_recurring(self):
        self.ensure_one()
        rec_doc_id = self.create_recurring_type()
        if rec_doc_id:
            active_model = self._context.get('active_model')
            active_id = self._context.get('active_id')
            if active_id and active_model:
                record = self.env[active_model].browse(active_id)
                self.doc_source = record._name + "," + str(record.id)
                record.recurring_id = self.id
                record.rec_source_id = record.id
            if self._context.get('process') == 'start':
                self.set_process()

    @api.multi
    def set_process(self):
        for recurring in self:
            model = 'recurring'
            cron_data = {
                'name': recurring.name,
                'interval_number': recurring.interval_number,
                'interval_type': recurring.interval_type,
                'numbercall': recurring.exec_init,
                'nextcall': recurring.date_init,
                'model_id':
                self.env['ir.model'].search([('model', '=', model)]).id,
                'priority': 6,
                'user_id': recurring.user_id.id,
                'state': 'code',
                'code': 'model._cron_model_copy(' + repr([recurring.id]) + ')',
            }
            cron = self.env['ir.cron'].sudo().create(cron_data)
            recurring.write({'cron_id': cron.id, 'state': 'running'})

    @api.multi
    def set_recurring_id(self):
        if self.doc_source and 'recurring_id' and 'rec_source_id' in \
                self.env[self.doc_source._name]._fields:
            rec_id = self.env[self.doc_source._name].browse(self.doc_source.id)
            if not rec_id.recurring_id and not rec_id.rec_source_id:
                rec_id.recurring_id = self.id
                rec_id.rec_source_id = self.doc_source.id
            else:
                raise ValidationError(_('Document is already recurring'))

    @api.model
    def create(self, vals):
        if vals.get('doc_source', False) and self.search(
            [('doc_source', '=', vals['doc_source'])]):
            raise ValidationError(
                _('Recurring of the selected Source Document already exist'))
        res = super(Recurring, self).create(vals)
        res.set_recurring_id()
        return res

    @api.multi
    def write(self, values):
        doc_source_id = False
        if values.get('doc_source', False):
            doc_source_id = self.doc_source
        res = super(Recurring, self).write(values)
        if doc_source_id:
            rec_id = self.env[doc_source_id._name].browse(doc_source_id.id)
            rec_id.recurring_id = False
            self.set_recurring_id()
        return res

    @api.multi
    def get_recurring(self, model, active_id):
        result = self.env.ref('recurring.action_recurring_form').read()[0]
        record = self.env[model].browse(active_id)
        rec_ids = self.env['recurring'].search([
            ('doc_source', '=', record._name + "," + str(record.id))
        ])
        result['domain'] = [('id', 'in', rec_ids.ids)]
        return result

    @api.multi
    def get_recurring_documents(self, model, action, recurring_id):
        result = self.env.ref(action).read()[0]
        res_ids = self.env[model].search([('recurring_id', '=',
                                           recurring_id.id)])
        result['domain'] = [('id', 'in', res_ids.ids)]
        return result

    @api.model
    def _cron_model_copy(self, ids):
        self.browse(ids).model_copy()

    @api.multi
    def model_copy(self):
        for recurring in self.filtered(lambda sub: sub.cron_id):
            if not recurring.doc_source.exists():
                raise UserError(
                    _('Please provide another source '
                      'document.\nThis one does not exist!'))

            default = {}
            documents = self.env['recurring.document'].search(
                [('model.model', '=', recurring.doc_source._name)], limit=1)
            fieldnames = dict(
                (f.field.name,
                 f.value == 'date' and fields.Date.today() or False)
                for f in documents.field_ids)
            default.update(fieldnames)
            # if there was only one remaining document to generate
            # the recurring is over and we mark it as being done
            if recurring.cron_id.numbercall == 1:
                recurring.write({'state': 'done'})
            else:
                recurring.write({'state': 'running'})
            copied_doc = recurring.doc_source.copy(default)
            self.env['recurring.history'].create({
                'recurring_id':
                recurring.id,
                'date':
                fields.Datetime.now(),
                'document_id':
                '%s,%s' % (recurring.doc_source._name, copied_doc.id)
            })

    @api.multi
    def unlink(self):
        if any(self.filtered(lambda s: s.state == "running")):
            raise UserError(_('You cannot delete an active recurring!'))
        return super(Recurring, self).unlink()

    @api.multi
    def set_done(self):
        self.mapped('cron_id').write({'active': False})
        self.write({'state': 'done'})

    @api.multi
    def set_draft(self):
        self.write({'state': 'draft'})
class MailMessageSubtype(models.Model):
    """ Class holding subtype definition for messages. Subtypes allow to tune
        the follower subscription, allowing only some subtypes to be pushed
        on the Wall. """
    _name = 'mail.message.subtype'
    _description = 'Message subtypes'
    _order = 'sequence, id'

    name = fields.Char(
        'Message Type',
        required=True,
        translate=True,
        help='Message subtype gives a more precise type on the message, '
        'especially for system notifications. For example, it can be '
        'a notification related to a new record (New), or to a stage '
        'change in a process (Stage change). Message subtypes allow to '
        'precisely tune the notifications the user want to receive on its wall.'
    )
    description = fields.Text(
        'Description',
        translate=True,
        help='Description that will be added in the message posted for this '
        'subtype. If void, the name will be added instead.')
    internal = fields.Boolean(
        'Internal Only',
        help=
        'Messages with internal subtypes will be visible only by employees, aka members of base_user group'
    )
    parent_id = fields.Many2one(
        'mail.message.subtype',
        string='Parent',
        ondelete='set null',
        help=
        'Parent subtype, used for automatic subscription. This field is not '
        'correctly named. For example on a project, the parent_id of project '
        'subtypes refers to task-related subtypes.')
    relation_field = fields.Char(
        'Relation field',
        help='Field used to link the related model to the subtype model when '
        'using automatic subscription on a related document. The field '
        'is used to compute getattr(related_document.relation_field).')
    res_model = fields.Char(
        'Model',
        help=
        "Model the subtype applies to. If False, this subtype applies to all models."
    )
    default = fields.Boolean('Default',
                             default=True,
                             help="Activated by default when subscribing.")
    sequence = fields.Integer('Sequence',
                              default=1,
                              help="Used to order subtypes.")
    hidden = fields.Boolean('Hidden',
                            help="Hide the subtype in the follower options")

    @api.model
    def create(self, vals):
        self.clear_caches()
        return super(MailMessageSubtype, self).create(vals)

    def write(self, vals):
        self.clear_caches()
        return super(MailMessageSubtype, self).write(vals)

    def unlink(self):
        self.clear_caches()
        return super(MailMessageSubtype, self).unlink()

    def auto_subscribe_subtypes(self, model_name):
        """ Retrieve the header subtypes and relations for the given model. """
        subtype_ids, relations = self._auto_subscribe_subtypes(model_name)
        return self.browse(subtype_ids), relations

    @tools.ormcache('self.env.uid', 'model_name')
    def _auto_subscribe_subtypes(self, model_name):
        domain = [
            '|', ('res_model', '=', False),
            ('parent_id.res_model', '=', model_name)
        ]
        subtypes = self.search(domain)
        return subtypes.ids, set(subtype.relation_field for subtype in subtypes
                                 if subtype.relation_field)

    def default_subtypes(self, model_name):
        """ Retrieve the default subtypes (all, internal, external) for the given model. """
        subtype_ids, internal_ids, external_ids = self._default_subtypes(
            model_name)
        return self.browse(subtype_ids), self.browse(
            internal_ids), self.browse(external_ids)

    @tools.ormcache('self.env.uid', 'model_name')
    def _default_subtypes(self, model_name):
        domain = [('default', '=', True), '|', ('res_model', '=', model_name),
                  ('res_model', '=', False)]
        subtypes = self.search(domain)
        internal = subtypes.filtered('internal')
        return subtypes.ids, internal.ids, (subtypes - internal).ids
Example #28
0
class File(dms_base.DMSModel):
    _name = 'muk_dms.file'
    _description = "File"

    _inherit = 'muk_dms.access'

    #----------------------------------------------------------
    # Database
    #----------------------------------------------------------

    name = fields.Char(string="Filename", required=True)

    settings = fields.Many2one('muk_dms.settings',
                               string="Settings",
                               store=True,
                               auto_join=True,
                               ondelete='restrict',
                               compute='_compute_settings')

    content = fields.Binary(string='Content',
                            required=True,
                            compute='_compute_content',
                            inverse='_inverse_content')

    reference = fields.Reference(selection=[('muk_dms.data', _('Data'))],
                                 string="Data Reference",
                                 readonly=True)

    directory = fields.Many2one('muk_dms.directory',
                                string="Directory",
                                ondelete='restrict',
                                auto_join=True,
                                required=True)

    extension = fields.Char(string='Extension',
                            compute='_compute_extension',
                            readonly=True,
                            store=True)

    mimetype = fields.Char(string='Type',
                           compute='_compute_mimetype',
                           readonly=True,
                           store=True)

    size = fields.Integer(string='Size', readonly=True)

    custom_thumbnail = fields.Binary(string="Custom Thumbnail")

    thumbnail = fields.Binary(compute='_compute_thumbnail', string="Thumbnail")

    path = fields.Char(string="Path",
                       store=True,
                       readonly=True,
                       compute='_compute_path')

    relational_path = fields.Text(string="Path",
                                  store=True,
                                  readonly=True,
                                  compute='_compute_relational_path')

    index_content = fields.Text(string='Indexed Content',
                                compute='_compute_index',
                                readonly=True,
                                store=True,
                                prefetch=False)

    locked_by = fields.Reference(string='Locked by',
                                 related='locked.locked_by_ref')

    #----------------------------------------------------------
    # Functions
    #----------------------------------------------------------

    def notify_change(self, values, refresh=False, operation=None):
        super(File, self).notify_change(values, refresh, operation)
        if "index_files" in values:
            self._compute_index()
        if "save_type" in values:
            self._update_reference_type()

    def trigger_computation_up(self, fields):
        self.directory.trigger_computation(fields)

    def trigger_computation(self, fields, refresh=True, operation=None):
        super(File, self).trigger_computation(fields, refresh, operation)
        values = {}
        if "settings" in fields:
            values.update(
                self.with_context(operation=operation)._compute_settings(
                    write=False))
        if "path" in fields:
            values.update(
                self.with_context(operation=operation)._compute_path(
                    write=False))
            values.update(
                self.with_context(
                    operation=operation)._compute_relational_path(write=False))
        if "extension" in fields:
            values.update(
                self.with_context(operation=operation)._compute_extension(
                    write=False))
        if "mimetype" in fields:
            values.update(
                self.with_context(operation=operation)._compute_mimetype(
                    write=False))
        if "index_content" in fields:
            values.update(
                self.with_context(operation=operation)._compute_index(
                    write=False))
        if values:
            self.write(values)
            if "settings" in fields:
                self.notify_change({'save_type': self.settings.save_type})

    @api.model
    def max_upload_size(self):
        config_parameter = self.env['ir.config_parameter'].sudo()
        return config_parameter.get_param('muk_dms.max_upload_size',
                                          default=25)

    #----------------------------------------------------------
    # Read, View
    #----------------------------------------------------------

    def _compute_settings(self, write=True):
        if write:
            for record in self:
                record.settings = record.directory.settings
        else:
            self.ensure_one()
            return {'settings': self.directory.settings.id}

    def _compute_extension(self, write=True):
        if write:
            for record in self:
                record.extension = os.path.splitext(record.name)[1]
        else:
            self.ensure_one()
            return {'extension': os.path.splitext(self.name)[1]}

    def _compute_mimetype(self, write=True):
        def get_mimetype(record):
            mimetype = mimetypes.guess_type(record.name)[0]
            if (not mimetype or mimetype
                    == 'application/octet-stream') and record.content:
                mimetype = guess_mimetype(base64.b64decode(record.content))
            return mimetype or 'application/octet-stream'

        if write:
            for record in self:
                record.mimetype = get_mimetype(record)
        else:
            self.ensure_one()
            return {'mimetype': get_mimetype(self)}

    def _compute_path(self, write=True):
        if write:
            for record in self:
                record.path = "%s%s" % (record.directory.path, record.name)
        else:
            self.ensure_one()
            return {'path': "%s%s" % (self.directory.path, self.name)}

    def _compute_relational_path(self, write=True):
        def get_relational_path(record):
            path = json.loads(record.directory.relational_path)
            path.append({
                'model': record._name,
                'id': record.id,
                'name': record.name
            })
            return json.dumps(path)

        if write:
            for record in self:
                record.relational_path = get_relational_path(record)
        else:
            self.ensure_one()
            return {'relational_path': get_relational_path(self)}

    def _compute_index(self, write=True):
        def get_index(record):
            type = record.mimetype.split(
                '/')[0] if record.mimetype else record._compute_mimetype(
                    write=False)['mimetype']
            index_files = record.settings.index_files if record.settings else record.directory.settings.index_files
            if type and type.split(
                    '/')[0] == 'text' and record.content and index_files:
                words = re.findall(
                    b"[\x20-\x7E]{4,}",
                    base64.b64decode(record.content)
                    if record.content else b'')
                return b"\n".join(words).decode('ascii')
            else:
                return None

        if write:
            for record in self:
                record.index_content = get_index(record)
        else:
            self.ensure_one()
            return {'index_content': get_index(self)}

    def _compute_content(self):
        for record in self:
            record.content = record._get_content()

    @api.depends('custom_thumbnail')
    def _compute_thumbnail(self):
        for record in self:
            if record.custom_thumbnail:
                record.thumbnail = record.with_context({}).custom_thumbnail
            else:
                extension = record.extension and record.extension.strip(
                    ".") or ""
                path = os.path.join(_img_path, "file_%s.png" % extension)
                if not os.path.isfile(path):
                    path = os.path.join(_img_path, "file_unkown.png")
                with open(path, "rb") as image_file:
                    record.thumbnail = base64.b64encode(image_file.read())

    @api.one
    def _compute_perm_create(self):
        try:
            result = super(File, self)._compute_perm_create()
            if self.directory:
                self.perm_create = result and self.directory.check_access(
                    'create')
            else:
                self.perm_create = result
        except AccessError:
            self.perm_create = False

    #----------------------------------------------------------
    # Create, Update, Delete
    #----------------------------------------------------------

    @api.constrains('name')
    def _check_name(self):
        if not self.check_name(self.name):
            raise ValidationError("The file name is invalid.")
        childs = self.sudo().directory.files.mapped(
            lambda rec: [rec.id, rec.name])
        duplicates = [
            rec for rec in childs if rec[1] == self.name and rec[0] != self.id
        ]
        if duplicates:
            raise ValidationError(
                _("A file with the same name already exists."))

    @api.constrains('name')
    def _check_extension(self):
        config_parameter = self.env['ir.config_parameter'].sudo()
        forbidden_extensions = config_parameter.get_param(
            'muk_dms.forbidden_extensions', default="")
        forbidden_extensions = [
            x.strip() for x in forbidden_extensions.split(',')
        ]
        file_extension = self._compute_extension(write=False)['extension']
        if file_extension and file_extension in forbidden_extensions:
            raise ValidationError(
                _("The file has a forbidden file extension."))

    @api.constrains('content')
    def _check_size(self):
        config_parameter = self.env['ir.config_parameter'].sudo()
        max_upload_size = config_parameter.get_param('muk_dms.max_upload_size',
                                                     default=25)
        try:
            max_upload_size = int(max_upload_size)
        except ValueError:
            max_upload_size = 25
        if max_upload_size * 1024 * 1024 < len(base64.b64decode(self.content)):
            raise ValidationError(
                _("The maximum upload size is %s MB).") % max_upload_size)

    def _after_create(self, vals):
        record = super(File, self)._after_create(vals)
        record._check_recomputation(vals)
        return record

    def _after_write_record(self, vals, operation):
        vals = super(File, self)._after_write_record(vals, operation)
        self._check_recomputation(vals, operation)
        return vals

    def _check_recomputation(self, values, operation=None):
        fields = []
        if 'name' in values:
            fields.extend(['extension', 'mimetype', 'path'])
        if 'directory' in values:
            fields.extend(['settings', 'path'])
        if 'content' in values:
            fields.extend(['index_content'])
        if fields:
            self.trigger_computation(fields)
        self._check_reference_values(values)
        if 'size' in values:
            self.trigger_computation_up(['size'])

    def _inverse_content(self):
        for record in self:
            if record.content:
                content = record.content
                directory = record.directory
                settings = record.settings if record.settings else directory.settings
                reference = record.reference
                if reference:
                    record._update_reference_content(content)
                else:
                    reference = record._create_reference(
                        settings, directory.path, record.name, content)
                record.reference = "%s,%s" % (reference._name, reference.id)
                record.size = len(base64.b64decode(content))
            else:
                record._unlink_reference()
                record.reference = None

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or [])
        names = []
        if 'directory' in default:
            directory = self.env['muk_dms.directory'].sudo().browse(
                default['directory'])
            names = directory.files.mapped('name')
        else:
            names = self.sudo().directory.files.mapped('name')
        default.update(
            {'name': self.unique_name(self.name, names, self.extension)})
        vals = self.copy_data(default)[0]
        if 'reference' in vals:
            del vals['reference']
        if not 'content' in vals:
            vals.update({'content': self.content})
        new = self.with_context(lang=None).create(vals)
        self.copy_translations(new)
        return new

    def _before_unlink(self, operation):
        info = super(File, self)._before_unlink(operation)
        references = set(record.reference for record in self
                         if record.reference)
        info['references'] = references
        return info

    def _after_unlink(self, result, info, infos, operation):
        super(File, self)._after_unlink(result, info, infos, operation)
        if 'references' in info:
            for reference in info['references']:
                reference.sudo().delete()
                reference.sudo().unlink()

    #----------------------------------------------------------
    # Reference
    #----------------------------------------------------------

    def _create_reference(self, settings, path, filename, content):
        self.ensure_one()
        self.check_access('create', raise_exception=True)
        if settings.save_type == 'database':
            return self.env['muk_dms.data_database'].sudo().create(
                {'data': content})
        return None

    def _update_reference_content(self, content):
        self.ensure_one()
        self.check_access('write', raise_exception=True)
        self.reference.sudo().update({'content': content})

    def _update_reference_type(self):
        self.ensure_one()
        self.check_access('write', raise_exception=True)
        if self.reference and self.settings.save_type != self.reference.type():
            reference = self._create_reference(self.settings,
                                               self.directory.path, self.name,
                                               self.content)
            self._unlink_reference()
            self.reference = "%s,%s" % (reference._name, reference.id)

    def _check_reference_values(self, values):
        self.ensure_one()
        self.check_access('write', raise_exception=True)
        if 'content' in values:
            self._update_reference_content(values['content'])
        if 'settings' in values:
            self._update_reference_type()

    def _get_content(self):
        self.ensure_one()
        self.check_access('read', raise_exception=True)
        return self.reference.sudo().content() if self.reference else None

    def _unlink_reference(self):
        self.ensure_one()
        self.check_access('unlink', raise_exception=True)
        if self.reference:
            self.reference.sudo().delete()
            self.reference.sudo().unlink()
Example #29
0
class FleetVehicleLogContract(models.Model):
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _name = 'fleet.vehicle.log.contract'
    _description = 'Vehicle Contract'
    _order = 'state desc,expiration_date'

    def compute_next_year_date(self, strdate):
        oneyear = relativedelta(years=1)
        start_date = fields.Date.from_string(strdate)
        return fields.Date.to_string(start_date + oneyear)

    vehicle_id = fields.Many2one('fleet.vehicle',
                                 'Vehicle',
                                 required=True,
                                 help='Vehicle concerned by this log')
    cost_subtype_id = fields.Many2one(
        'fleet.service.type',
        'Type',
        help='Cost type purchased with this cost',
        domain=[('category', '=', 'contract')])
    amount = fields.Monetary('Cost')
    date = fields.Date(help='Date when the cost has been executed')
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env.company)
    currency_id = fields.Many2one('res.currency',
                                  related='company_id.currency_id')
    name = fields.Char(string='Name',
                       compute='_compute_contract_name',
                       store=True)
    active = fields.Boolean(default=True)
    user_id = fields.Many2one('res.users',
                              'Responsible',
                              default=lambda self: self.env.user,
                              index=True)
    start_date = fields.Date(
        'Contract Start Date',
        default=fields.Date.context_today,
        help='Date when the coverage of the contract begins')
    expiration_date = fields.Date(
        'Contract Expiration Date',
        default=lambda self: self.compute_next_year_date(
            fields.Date.context_today(self)),
        help=
        'Date when the coverage of the contract expirates (by default, one year after begin date)'
    )
    days_left = fields.Integer(compute='_compute_days_left',
                               string='Warning Date')
    insurer_id = fields.Many2one('res.partner', 'Vendor')
    purchaser_id = fields.Many2one(related='vehicle_id.driver_id',
                                   string='Current Driver')
    ins_ref = fields.Char('Reference', size=64, copy=False)
    state = fields.Selection(
        [('futur', 'Incoming'), ('open', 'In Progress'),
         ('expired', 'Expired'), ('closed', 'Closed')],
        'Status',
        default='open',
        readonly=True,
        help='Choose whether the contract is still valid or not',
        tracking=True,
        copy=False)
    notes = fields.Text(
        'Terms and Conditions',
        help=
        'Write here all supplementary information relative to this contract',
        copy=False)
    cost_generated = fields.Monetary('Recurring Cost')
    cost_frequency = fields.Selection([('no', 'No'), ('daily', 'Daily'),
                                       ('weekly', 'Weekly'),
                                       ('monthly', 'Monthly'),
                                       ('yearly', 'Yearly')],
                                      'Recurring Cost Frequency',
                                      default='monthly',
                                      help='Frequency of the recuring cost',
                                      required=True)
    service_ids = fields.Many2many('fleet.service.type',
                                   string="Included Services")

    @api.depends('vehicle_id.name', 'cost_subtype_id')
    def _compute_contract_name(self):
        for record in self:
            name = record.vehicle_id.name
            if name and record.cost_subtype_id.name:
                name = record.cost_subtype_id.name + ' ' + name
            record.name = name

    @api.depends('expiration_date', 'state')
    def _compute_days_left(self):
        """return a dict with as value for each contract an integer
        if contract is in an open state and is overdue, return 0
        if contract is in a closed state, return -1
        otherwise return the number of days before the contract expires
        """
        for record in self:
            if record.expiration_date and record.state in ['open', 'expired']:
                today = fields.Date.from_string(fields.Date.today())
                renew_date = fields.Date.from_string(record.expiration_date)
                diff_time = (renew_date - today).days
                record.days_left = diff_time > 0 and diff_time or 0
            else:
                record.days_left = -1

    def write(self, vals):
        res = super(FleetVehicleLogContract, self).write(vals)
        if vals.get('expiration_date') or vals.get('user_id'):
            self.activity_reschedule(
                ['fleet.mail_act_fleet_contract_to_renew'],
                date_deadline=vals.get('expiration_date'),
                new_user_id=vals.get('user_id'))
        return res

    def contract_close(self):
        for record in self:
            record.state = 'closed'

    def contract_draft(self):
        for record in self:
            record.state = 'futur'

    def contract_open(self):
        for record in self:
            record.state = 'open'

    @api.model
    def scheduler_manage_contract_expiration(self):
        # This method is called by a cron task
        # It manages the state of a contract, possibly by posting a message on the vehicle concerned and updating its status
        params = self.env['ir.config_parameter'].sudo()
        delay_alert_contract = int(
            params.get_param('hr_fleet.delay_alert_contract', default=30))
        date_today = fields.Date.from_string(fields.Date.today())
        outdated_days = fields.Date.to_string(date_today + relativedelta(
            days=+delay_alert_contract))
        nearly_expired_contracts = self.search([('state', '=', 'open'),
                                                ('expiration_date', '<',
                                                 outdated_days)])

        for contract in nearly_expired_contracts.filtered(
                lambda contract: contract.user_id):
            contract.activity_schedule(
                'fleet.mail_act_fleet_contract_to_renew',
                contract.expiration_date,
                user_id=contract.user_id.id)

        expired_contracts = self.search([
            ('state', 'not in', ['expired', 'closed']),
            ('expiration_date', '<', fields.Date.today())
        ])
        expired_contracts.write({'state': 'expired'})

        futur_contracts = self.search([
            ('state', 'not in', ['futur', 'closed']),
            ('start_date', '>', fields.Date.today())
        ])
        futur_contracts.write({'state': 'futur'})

        now_running_contracts = self.search([('state', '=', 'futur'),
                                             ('start_date', '<=',
                                              fields.Date.today())])
        now_running_contracts.write({'state': 'open'})

    def run_scheduler(self):
        self.scheduler_manage_contract_expiration()
Example #30
0
class PosConfig(models.Model):
    _name = 'pos.config'

    def _default_sale_journal(self):
        journal = self.env.ref('point_of_sale.pos_sale_journal', raise_if_not_found=False)
        if journal and journal.sudo().company_id == self.env.user.company_id:
            return journal
        return self._default_invoice_journal()

    def _default_invoice_journal(self):
        return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.user.company_id.id)], limit=1)

    def _default_pricelist(self):
        return self.env['product.pricelist'].search([('currency_id', '=', self.env.user.company_id.currency_id.id)], limit=1)

    def _get_default_location(self):
        return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id

    def _get_group_pos_manager(self):
        return self.env.ref('point_of_sale.group_pos_manager')

    def _get_group_pos_user(self):
        return self.env.ref('point_of_sale.group_pos_user')

    def _compute_default_customer_html(self):
        return self.env['ir.qweb'].render('point_of_sale.customer_facing_display_html')

    name = fields.Char(string='Point of Sale Name', index=True, required=True, help="An internal identification of the point of sale.")
    is_installed_account_accountant = fields.Boolean(compute="_compute_is_installed_account_accountant")
    journal_ids = fields.Many2many(
        'account.journal', 'pos_config_journal_rel',
        'pos_config_id', 'journal_id', string='Available Payment Methods',
        domain="[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]",)
    picking_type_id = fields.Many2one('stock.picking.type', string='Operation Type')
    use_existing_lots = fields.Boolean(related='picking_type_id.use_existing_lots')
    stock_location_id = fields.Many2one(
        'stock.location', string='Stock Location',
        domain=[('usage', '=', 'internal')], required=True, default=_get_default_location)
    journal_id = fields.Many2one(
        'account.journal', string='Sales Journal',
        domain=[('type', '=', 'sale')],
        help="Accounting journal used to post sales entries.",
        default=_default_sale_journal)
    invoice_journal_id = fields.Many2one(
        'account.journal', string='Invoice Journal',
        domain=[('type', '=', 'sale')],
        help="Accounting journal used to create invoices.",
        default=_default_invoice_journal)
    currency_id = fields.Many2one('res.currency', compute='_compute_currency', string="Currency")
    iface_cashdrawer = fields.Boolean(string='Cashdrawer', help="Automatically open the cashdrawer.")
    iface_payment_terminal = fields.Boolean(string='Payment Terminal', help="Enables Payment Terminal integration.")
    iface_electronic_scale = fields.Boolean(string='Electronic Scale', help="Enables Electronic Scale integration.")
    iface_vkeyboard = fields.Boolean(string='Virtual KeyBoard', help=u"Don’t turn this option on if you take orders on smartphones or tablets. \n Such devices already benefit from a native keyboard.")
    iface_customer_facing_display = fields.Boolean(string='Customer Facing Display', help="Show checkout to customers with a remotely-connected screen.")
    iface_print_via_proxy = fields.Boolean(string='Print via Proxy', help="Bypass browser printing and prints via the hardware proxy.")
    iface_scan_via_proxy = fields.Boolean(string='Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner.")
    iface_invoicing = fields.Boolean(string='Invoicing', help='Enables invoice generation from the Point of Sale.')
    iface_big_scrollbars = fields.Boolean('Large Scrollbars', help='For imprecise industrial touchscreens.')
    iface_print_auto = fields.Boolean(string='Automatic Receipt Printing', default=False,
        help='The receipt will automatically be printed at the end of each order.')
    iface_print_skip_screen = fields.Boolean(string='Skip Preview Screen', default=True,
        help='The receipt screen will be skipped if the receipt can be printed automatically.')
    iface_precompute_cash = fields.Boolean(string='Prefill Cash Payment',
        help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount.')
    iface_tax_included = fields.Selection([('subtotal', 'Tax-Excluded Prices'), ('total', 'Tax-Included Prices')], "Tax Display", default='subtotal', required=True)
    iface_start_categ_id = fields.Many2one('pos.category', string='Initial Category',
        help='The point of sale will display this product category by default. If no category is specified, all available products will be shown.')
    iface_display_categ_images = fields.Boolean(string='Display Category Pictures',
        help="The product categories will be displayed with pictures.")
    restrict_price_control = fields.Boolean(string='Restrict Price Modifications to Managers',
        help="Only users with Manager access rights for PoS app can modify the product prices on orders.")
    cash_control = fields.Boolean(string='Cash Control', help="Check the amount of the cashbox at opening and closing.")
    receipt_header = fields.Text(string='Receipt Header', help="A short text that will be inserted as a header in the printed receipt.")
    receipt_footer = fields.Text(string='Receipt Footer', help="A short text that will be inserted as a footer in the printed receipt.")
    proxy_ip = fields.Char(string='IP Address', size=45,
        help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty.')
    active = fields.Boolean(default=True)
    uuid = fields.Char(readonly=True, default=lambda self: str(uuid.uuid4()),
        help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.')
    sequence_id = fields.Many2one('ir.sequence', string='Order IDs Sequence', readonly=True,
        help="This sequence is automatically created by Flectra but you can change it "
        "to customize the reference numbers of your orders.", copy=False)
    sequence_line_id = fields.Many2one('ir.sequence', string='Order Line IDs Sequence', readonly=True,
        help="This sequence is automatically created by Flectra but you can change it "
        "to customize the reference numbers of your orders lines.", copy=False)
    session_ids = fields.One2many('pos.session', 'config_id', string='Sessions')
    current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session")
    current_session_state = fields.Char(compute='_compute_current_session')
    last_session_closing_cash = fields.Float(compute='_compute_last_session')
    last_session_closing_date = fields.Date(compute='_compute_last_session')
    pos_session_username = fields.Char(compute='_compute_current_session_user')
    pos_session_state = fields.Char(compute='_compute_current_session_user')
    group_by = fields.Boolean(string='Group Journal Items', default=True,
        help="Check this if you want to group the Journal Items by Product while closing a Session.")
    pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist', required=True, default=_default_pricelist,
        help="The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured.")
    available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists', default=_default_pricelist,
        help="Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply.")
    company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id)
    barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcode Nomenclature',
        help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers.')
    group_pos_manager_id = fields.Many2one('res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager,
        help='This field is there to pass the id of the pos manager group to the point of sale client.')
    group_pos_user_id = fields.Many2one('res.groups', string='Point of Sale User Group', default=_get_group_pos_user,
        help='This field is there to pass the id of the pos user group to the point of sale client.')
    iface_tipproduct = fields.Boolean(string="Product tips")
    tip_product_id = fields.Many2one('product.product', string='Tip Product',
        help="This product is used as reference on customer receipts.")
    fiscal_position_ids = fields.Many2many('account.fiscal.position', string='Fiscal Positions', help='This is useful for restaurants with onsite and take-away services that imply specific tax rates.')
    default_fiscal_position_id = fields.Many2one('account.fiscal.position', string='Default Fiscal Position')
    default_cashbox_lines_ids = fields.One2many('account.cashbox.line', 'default_pos_id', string='Default Balance')
    customer_facing_display_html = fields.Html(string='Customer facing display content', translate=True, default=_compute_default_customer_html)
    use_pricelist = fields.Boolean("Use a pricelist.")
    group_sale_pricelist = fields.Boolean("Use pricelists to adapt your price per customers",
                                          implied_group='product.group_sale_pricelist',
                                          help="""Allows to manage different prices based on rules per category of customers.
                    Example: 10% for retailers, promotion of 5 EUR on this product, etc.""")
    group_pricelist_item = fields.Boolean("Show pricelists to customers",
                                          implied_group='product.group_pricelist_item')
    tax_regime = fields.Boolean("Tax Regime")
    tax_regime_selection = fields.Boolean("Tax Regime Selection value")
    barcode_scanner = fields.Boolean("Barcode Scanner")
    start_category = fields.Boolean("Set Start Category")
    module_pos_restaurant = fields.Boolean("Is a Bar/Restaurant")
    module_pos_discount = fields.Boolean("Global Discounts")
    module_pos_mercury = fields.Boolean(string="Integrated Card Payments")
    module_pos_reprint = fields.Boolean(string="Reprint Receipt")
    is_posbox = fields.Boolean("PosBox")
    is_header_or_footer = fields.Boolean("Header & Footer")

    def _compute_is_installed_account_accountant(self):
        account_accountant = self.env['ir.module.module'].sudo().search([('name', '=', 'account_accountant'), ('state', '=', 'installed')])
        for pos_config in self:
            pos_config.is_installed_account_accountant = account_accountant and account_accountant.id

    @api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id')
    def _compute_currency(self):
        for pos_config in self:
            if pos_config.journal_id:
                pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id
            else:
                pos_config.currency_id = self.env.user.company_id.currency_id.id

    @api.depends('session_ids')
    def _compute_current_session(self):
        for pos_config in self:
            session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and \
                not r.state == 'closed' and \
                not r.rescue)
            # sessions ordered by id desc
            pos_config.current_session_id = session and session[0].id or False
            pos_config.current_session_state = session and session[0].state or False

    @api.depends('session_ids')
    def _compute_last_session(self):
        PosSession = self.env['pos.session']
        for pos_config in self:
            session = PosSession.search_read(
                [('config_id', '=', pos_config.id), ('state', '=', 'closed')],
                ['cash_register_balance_end_real', 'stop_at'],
                order="stop_at desc", limit=1)
            if session:
                pos_config.last_session_closing_cash = session[0]['cash_register_balance_end_real']
                pos_config.last_session_closing_date = session[0]['stop_at']
            else:
                pos_config.last_session_closing_cash = 0
                pos_config.last_session_closing_date = False

    @api.depends('session_ids')
    def _compute_current_session_user(self):
        for pos_config in self:
            session = pos_config.session_ids.filtered(lambda s: s.state in ['opening_control', 'opened', 'closing_control'] and not s.rescue)
            pos_config.pos_session_username = session and session[0].user_id.name or False
            pos_config.pos_session_state = session and session[0].state or False

    @api.constrains('company_id', 'stock_location_id')
    def _check_company_location(self):
        if self.stock_location_id.company_id and self.stock_location_id.company_id.id != self.company_id.id:
            raise ValidationError(_("The company of the stock location is different than the one of point of sale"))

    @api.constrains('company_id', 'journal_id')
    def _check_company_journal(self):
        if self.journal_id and self.journal_id.company_id.id != self.company_id.id:
            raise ValidationError(_("The company of the sales journal is different than the one of point of sale"))

    @api.constrains('company_id', 'invoice_journal_id')
    def _check_company_invoice_journal(self):
        if self.invoice_journal_id and self.invoice_journal_id.company_id.id != self.company_id.id:
            raise ValidationError(_("The invoice journal and the point of sale must belong to the same company"))

    @api.constrains('company_id', 'journal_ids')
    def _check_company_payment(self):
        if self.env['account.journal'].search_count([('id', 'in', self.journal_ids.ids), ('company_id', '!=', self.company_id.id)]):
            raise ValidationError(_("The company of a payment method is different than the one of point of sale"))

    @api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'journal_ids')
    def _check_currencies(self):
        if self.pricelist_id not in self.available_pricelist_ids:
            raise ValidationError(_("The default pricelist must be included in the available pricelists."))
        if any(self.available_pricelist_ids.mapped(lambda pricelist: pricelist.currency_id != self.currency_id)):
            raise ValidationError(_("All available pricelists must be in the same currency as the company or"
                                    " as the Sales Journal set on this point of sale if you use"
                                    " the Accounting application."))
        if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id:
            raise ValidationError(_("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set."))
        if any(self.journal_ids.mapped(lambda journal: journal.currency_id and journal.currency_id != self.currency_id)):
            raise ValidationError(_("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set."))

    @api.onchange('iface_print_via_proxy')
    def _onchange_iface_print_via_proxy(self):
        self.iface_print_auto = self.iface_print_via_proxy

    @api.onchange('picking_type_id')
    def _onchange_picking_type_id(self):
        if self.picking_type_id.default_location_src_id.usage == 'internal' and self.picking_type_id.default_location_dest_id.usage == 'customer':
            self.stock_location_id = self.picking_type_id.default_location_src_id.id

    @api.onchange('use_pricelist')
    def _onchange_use_pricelist(self):
        """
        If the 'pricelist' box is unchecked, we reset the pricelist_id to stop
        using a pricelist for this posbox. 
        """
        if not self.use_pricelist:
            self.pricelist_id = self._default_pricelist()
        else:
            self.update({
                'group_sale_pricelist': True,
                'group_pricelist_item': True,
            })

    @api.onchange('available_pricelist_ids')
    def _onchange_available_pricelist_ids(self):
        if self.pricelist_id not in self.available_pricelist_ids:
            self.pricelist_id = False

    @api.onchange('iface_scan_via_proxy')
    def _onchange_iface_scan_via_proxy(self):
        if self.iface_scan_via_proxy:
            self.barcode_scanner = True
        else:
            self.barcode_scanner = False

    @api.onchange('barcode_scanner')
    def _onchange_barcode_scanner(self):
        if self.barcode_scanner:
            self.barcode_nomenclature_id = self.env['barcode.nomenclature'].search([], limit=1)
        else:
            self.barcode_nomenclature_id = False

    @api.onchange('is_posbox')
    def _onchange_is_posbox(self):
        if not self.is_posbox:
            self.proxy_ip = False
            self.iface_scan_via_proxy = False
            self.iface_electronic_scale = False
            self.iface_cashdrawer = False
            self.iface_print_via_proxy = False
            self.iface_customer_facing_display = False

    @api.onchange('tax_regime')
    def _onchange_tax_regime(self):
        if not self.tax_regime:
            self.default_fiscal_position_id = False

    @api.onchange('tax_regime_selection')
    def _onchange_tax_regime_selection(self):
        if not self.tax_regime_selection:
            self.fiscal_position_ids = [(5, 0, 0)]

    @api.onchange('start_category')
    def _onchange_start_category(self):
        if not self.start_category:
            self.iface_start_categ_id = False

    @api.onchange('is_header_or_footer')
    def _onchange_header_footer(self):
        if not self.is_header_or_footer:
            self.receipt_header = False
            self.receipt_footer = False

    @api.multi
    def name_get(self):
        result = []
        for config in self:
            if (not config.session_ids) or (config.session_ids[0].state == 'closed'):
                result.append((config.id, config.name + ' (' + _('not used') + ')'))
                continue
            result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')'))
        return result

    @api.model
    def create(self, values):
        if values.get('is_posbox') and values.get('iface_customer_facing_display'):
            if values.get('customer_facing_display_html') and not values['customer_facing_display_html'].strip():
                values['customer_facing_display_html'] = self._compute_default_customer_html()
        IrSequence = self.env['ir.sequence'].sudo()
        val = {
            'name': _('POS Order %s') % values['name'],
            'padding': 4,
            'prefix': "%s/" % values['name'],
            'code': "pos.order",
            'company_id': values.get('company_id', False),
        }
        # force sequence_id field to new pos.order sequence
        values['sequence_id'] = IrSequence.create(val).id

        val.update(name=_('POS order line %s') % values['name'], code='pos.order.line')
        values['sequence_line_id'] = IrSequence.create(val).id
        pos_config = super(PosConfig, self).create(values)
        pos_config.sudo()._check_modules_to_install()
        pos_config.sudo()._check_groups_implied()
        # If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
        return pos_config

    @api.multi
    def write(self, vals):
        if (self.is_posbox or vals.get('is_posbox')) and (self.iface_customer_facing_display or vals.get('iface_customer_facing_display')):
            facing_display = (self.customer_facing_display_html or vals.get('customer_facing_display_html') or '').strip()
            if not facing_display:
                vals['customer_facing_display_html'] = self._compute_default_customer_html()
        result = super(PosConfig, self).write(vals)
        self.sudo()._set_fiscal_position()
        self.sudo()._check_modules_to_install()
        self.sudo()._check_groups_implied()
        return result

    @api.multi
    def unlink(self):
        for pos_config in self.filtered(lambda pos_config: pos_config.sequence_id or pos_config.sequence_line_id):
            pos_config.sequence_id.unlink()
            pos_config.sequence_line_id.unlink()
        return super(PosConfig, self).unlink()

    def _set_fiscal_position(self):
        for config in self:
            if config.tax_regime and config.default_fiscal_position_id.id not in config.fiscal_position_ids.ids:
                config.fiscal_position_ids = [(4, config.default_fiscal_position_id.id)]
            elif not config.tax_regime_selection and not config.tax_regime and config.fiscal_position_ids.ids:
                config.fiscal_position_ids = [(5, 0, 0)]

    def _check_modules_to_install(self):
        module_installed = False
        for pos_config in self:
            for field_name in [f for f in pos_config.fields_get_keys() if f.startswith('module_')]:
                module_name = field_name.split('module_')[1]
                module_to_install = self.env['ir.module.module'].sudo().search([('name', '=', module_name)])
                if getattr(pos_config, field_name) and module_to_install.state not in ('installed', 'to install', 'to upgrade'):
                    module_to_install.button_immediate_install()
                    module_installed = True
        # just in case we want to do something if we install a module. (like a refresh ...)
        return module_installed

    def _check_groups_implied(self):
        for pos_config in self:
            for field_name in [f for f in pos_config.fields_get_keys() if f.startswith('group_')]:
                field = pos_config._fields[field_name]
                if field.type in ('boolean', 'selection') and hasattr(field, 'implied_group'):
                    field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',')
                    field_groups = self.env['res.groups'].concat(*(self.env.ref(it) for it in field_group_xmlids))
                    field_groups.write({'implied_ids': [(4, self.env.ref(field.implied_group).id)]})


    def execute(self):
        return {
             'type': 'ir.actions.client',
             'tag': 'reload',
             'params': {'wait': True}
         }

    # Methods to open the POS
    @api.multi
    def open_ui(self):
        """ open the pos interface """
        self.ensure_one()
        return {
            'type': 'ir.actions.act_url',
            'url':   '/pos/web/',
            'target': 'self',
        }

    @api.multi
    def open_session_cb(self):
        """ new session button

        create one if none exist
        access cash control interface if enabled or start a session
        """
        self.ensure_one()
        if not self.current_session_id:
            self.current_session_id = self.env['pos.session'].create({
                'user_id': self.env.uid,
                'config_id': self.id
            })
            if self.current_session_id.state == 'opened':
                return self.open_ui()
            return self._open_session(self.current_session_id.id)
        return self._open_session(self.current_session_id.id)

    @api.multi
    def open_existing_session_cb(self):
        """ close session button

        access session form to validate entries
        """
        self.ensure_one()
        return self._open_session(self.current_session_id.id)

    def _open_session(self, session_id):
        return {
            'name': _('Session'),
            'view_type': 'form',
            'view_mode': 'form,tree',
            'res_model': 'pos.session',
            'res_id': session_id,
            'view_id': False,
            'type': 'ir.actions.act_window',
        }