class Lead2OpportunityPartner(models.TransientModel):
    _name = 'crm.lead2opportunity.partner'
    _description = 'Convert Lead to Opportunity (not in mass)'

    @api.model
    def default_get(self, fields):
        """ Allow support of active_id / active_model instead of jut default_lead_id
        to ease window action definitions, and be backward compatible. """
        result = super(Lead2OpportunityPartner, self).default_get(fields)

        if not result.get('lead_id') and self.env.context.get('active_id'):
            result['lead_id'] = self.env.context.get('active_id')
        return result

    name = fields.Selection([('convert', 'Convert to opportunity'),
                             ('merge', 'Merge with existing opportunities')],
                            'Conversion Action',
                            compute='_compute_name',
                            readonly=False,
                            store=True,
                            compute_sudo=False)
    action = fields.Selection([('create', 'Create a new customer'),
                               ('exist', 'Link to an existing customer'),
                               ('nothing', 'Do not link to a customer')],
                              string='Related Customer',
                              compute='_compute_action',
                              readonly=False,
                              store=True,
                              compute_sudo=False)
    lead_id = fields.Many2one('crm.lead', 'Associated Lead', required=True)
    duplicated_lead_ids = fields.Many2many(
        'crm.lead',
        string='Opportunities',
        context={'active_test': False},
        compute='_compute_duplicated_lead_ids',
        readonly=False,
        store=True,
        compute_sudo=False)
    partner_id = fields.Many2one('res.partner',
                                 'Customer',
                                 compute='_compute_partner_id',
                                 readonly=False,
                                 store=True,
                                 compute_sudo=False)
    user_id = fields.Many2one('res.users',
                              'Salesperson',
                              compute='_compute_user_id',
                              readonly=False,
                              store=True,
                              compute_sudo=False)
    team_id = fields.Many2one('crm.team',
                              'Sales Team',
                              compute='_compute_team_id',
                              readonly=False,
                              store=True,
                              compute_sudo=False)
    force_assignment = fields.Boolean(
        'Force assignment',
        default=True,
        help=
        'If checked, forces salesman to be updated on updated opportunities even if already set.'
    )

    @api.depends('duplicated_lead_ids')
    def _compute_name(self):
        for convert in self:
            convert.name = 'merge' if convert.duplicated_lead_ids and len(
                convert.duplicated_lead_ids) >= 2 else 'convert'

    @api.depends('lead_id')
    def _compute_action(self):
        for convert in self:
            if not convert.lead_id:
                convert.action = 'nothing'
            else:
                partner = convert.lead_id._find_matching_partner()
                if partner:
                    convert.action = 'exist'
                elif convert.lead_id.contact_name:
                    convert.action = 'create'
                else:
                    convert.action = 'nothing'

    @api.depends('lead_id', 'partner_id')
    def _compute_duplicated_lead_ids(self):
        for convert in self:
            if not convert.lead_id:
                convert.duplicated_lead_ids = False
                continue
            convert.duplicated_lead_ids = self.env[
                'crm.lead']._get_lead_duplicates(
                    convert.partner_id,
                    convert.lead_id.partner_id.email
                    if convert.lead_id.partner_id.email else
                    convert.lead_id.email_from,
                    include_lost=True).ids

    @api.depends('action')
    def _compute_partner_id(self):
        for convert in self:
            if convert.action == 'exist':
                convert.partner_id = convert.lead_id._find_matching_partner()
            else:
                convert.partner_id = False

    @api.depends('lead_id')
    def _compute_user_id(self):
        for convert in self:
            convert.user_id = convert.lead_id.user_id if convert.lead_id.user_id else False

    @api.depends('user_id')
    def _compute_team_id(self):
        """ When changing the user, also set a team_id or restrict team id
        to the ones user_id is member of. """
        for convert in self:
            # setting user as void should not trigger a new team computation
            if not convert.user_id:
                continue
            user = convert.user_id
            if convert.team_id and user in convert.team_id.member_ids | convert.team_id.user_id:
                continue
            team_domain = []
            team = self.env['crm.team']._get_default_team_id(
                user_id=user.id, domain=team_domain)
            convert.team_id = team.id

    @api.model
    def view_init(self, fields):
        # JEM TDE FIXME: clean that brol
        """ Check some preconditions before the wizard executes. """
        for lead in self.env['crm.lead'].browse(
                self._context.get('active_ids', [])):
            if lead.probability == 100:
                raise UserError(
                    _("Closed/Dead leads cannot be converted into opportunities."
                      ))
        return False

    def action_apply(self):
        if self.name == 'merge':
            result_opportunity = self._action_merge()
        else:
            result_opportunity = self._action_convert()

        return result_opportunity.redirect_lead_opportunity_view()

    def _action_merge(self):
        to_merge = self.duplicated_lead_ids
        result_opportunity = to_merge.merge_opportunity(auto_unlink=False)
        result_opportunity.action_unarchive()

        if result_opportunity.type == "lead":
            self._convert_and_allocate(result_opportunity, [self.user_id.id],
                                       team_id=self.team_id.id)
        else:
            if not result_opportunity.user_id or self.force_assignment:
                result_opportunity.write({
                    'user_id': self.user_id.id,
                    'team_id': self.team_id.id,
                })
        (to_merge - result_opportunity).unlink()
        return result_opportunity

    def _action_convert(self):
        """ """
        result_opportunities = self.env['crm.lead'].browse(
            self._context.get('active_ids', []))
        self._convert_and_allocate(result_opportunities, [self.user_id.id],
                                   team_id=self.team_id.id)
        return result_opportunities[0]

    def _convert_and_allocate(self, leads, user_ids, team_id=False):
        self.ensure_one()

        for lead in leads:
            if lead.active and self.action != 'nothing':
                self._convert_handle_partner(
                    lead, self.action, self.partner_id.id
                    or lead.partner_id.id)

            lead.convert_opportunity(lead.partner_id.id, [], False)

        leads_to_allocate = leads
        if not self.force_assignment:
            leads_to_allocate = leads_to_allocate.filtered(
                lambda lead: not lead.user_id)

        if user_ids:
            leads_to_allocate.handle_salesmen_assignment(user_ids,
                                                         team_id=team_id)

    def _convert_handle_partner(self, lead, action, partner_id):
        # used to propagate user_id (salesman) on created partners during conversion
        lead.with_context(
            default_user_id=self.user_id.id).handle_partner_assignment(
                force_partner_id=partner_id,
                create_missing=(action == 'create'))
Example #2
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, track_visibility="onchange",
        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', track_visibility="onchange", help='Driver of the vehicle', copy=False)
    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.brand_id.name', 'model_id.name', '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 or _('No 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()
        copy_context = dict(self.env.context)
        copy_context.pop('group_by', None)
        res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
        res.update(
            context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True),
            domain=[('vehicle_id', '=', self.id)]
        )
        return res
Example #3
0
class PartnerActivities(models.Model):
    _name = 'partner.activities'
    _description = 'SII Economical Activities'

    @api.multi
    def name_get(self):
        res = []
        for r in self:
            res.append((r.id, (r.code and '[' + r.code + '] ' + r.name or '') ))
        return res

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        args = args or []
        recs = self.browse()
        if name:
            recs = self.search(['|',('name', '=', name),('code', '=', name)] + args, limit=limit)
        if not recs:
            recs = self.search(['|',('name', operator, name),('code', operator, name)] + args, limit=limit)
        return recs.name_get()

    code = fields.Char(
            string='Activity Code',
            required=True,
            translate=True,
        )
    parent_id = fields.Many2one(
            'partner.activities',
            string='Parent Activity',
            ondelete='cascade',
        )
    name = fields.Char(
            string='Nombre Completo',
            required=True,
            translate=True,
        )
    vat_affected = fields.Selection(
            (
                ('SI', 'Si'),
                ('NO', 'No'),
                ('ND', 'ND'),
            ),
            string='VAT Affected',
            required=True,
            translate=True,
            default='SI',
        )
    tax_category = fields.Selection(
            (
                ('1', '1'),
                ('2', '2'),
                ('ND', 'ND'),
            ),
            string='TAX Category',
            required=True,
            translate=True,
            default='1',
        )
    internet_available = fields.Boolean(
            string='Available at Internet',
            default=True,
        )
    active = fields.Boolean(
            string='Active',
            help="Allows you to hide the activity without removing it.",
            default=True,
        )
    partner_ids = fields.Many2many(
            'res.partner',
            id1='activities_id',
            id2='partner_id',
            string='Partners',
        )
    journal_ids = fields.Many2many(
            'account.journal',
            id1='activities_id',
            id2='journal_id',
            string='Journals',
        )
Example #4
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 #5
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 #6
0
class Invite(models.TransientModel):
    """ Wizard to invite partners (or channels) and make them followers. """
    _name = 'mail.wizard.invite'
    _description = 'Invite wizard'

    @api.model
    def default_get(self, fields):
        result = super(Invite, self).default_get(fields)
        if self._context.get('mail_invite_follower_channel_only'):
            result['send_mail'] = False
        if 'message' not in fields:
            return result

        user_name = self.env.user.display_name
        model = result.get('res_model')
        res_id = result.get('res_id')
        if model and res_id:
            document = self.env['ir.model']._get(model).display_name
            title = self.env[model].browse(res_id).display_name
            msg_fmt = _(
                '%(user_name)s invited you to follow %(document)s document: %(title)s'
            )
        else:
            msg_fmt = _('%(user_name)s invited you to follow a new document.')

        text = msg_fmt % locals()
        message = html.DIV(html.P(_('Hello,')), html.P(text))
        result['message'] = etree.tostring(message)
        return result

    res_model = fields.Char('Related Document Model',
                            required=True,
                            index=True,
                            help='Model of the followed resource')
    res_id = fields.Integer('Related Document ID',
                            index=True,
                            help='Id of the followed resource')
    partner_ids = fields.Many2many(
        'res.partner',
        string='Recipients',
        help=
        "List of partners that will be added as follower of the current document.",
        domain=[('type', '!=', 'private')])
    channel_ids = fields.Many2many(
        'mail.channel',
        string='Channels',
        help=
        'List of channels that will be added as listeners of the current document.',
        domain=[('channel_type', '=', 'channel')])
    message = fields.Html('Message')
    send_mail = fields.Boolean(
        'Send Email',
        default=True,
        help=
        "If checked, the partners will receive an email warning they have been added in the document's followers."
    )

    def add_followers(self):
        if not self.env.user.email:
            raise UserError(
                _("Unable to post message, please configure the sender's email address."
                  ))
        email_from = self.env.user.email_formatted
        for wizard in self:
            Model = self.env[wizard.res_model]
            document = Model.browse(wizard.res_id)

            # filter partner_ids to get the new followers, to avoid sending email to already following partners
            new_partners = wizard.partner_ids - document.sudo(
            ).message_partner_ids
            new_channels = wizard.channel_ids - document.message_channel_ids
            document.message_subscribe(new_partners.ids, new_channels.ids)

            model_name = self.env['ir.model']._get(
                wizard.res_model).display_name
            # send an email if option checked and if a message exists (do not send void emails)
            if wizard.send_mail and wizard.message and not wizard.message == '<br>':  # when deleting the message, cleditor keeps a <br>
                message = self.env['mail.message'].create({
                    'subject':
                    _('Invitation to follow %(document_model)s: %(document_name)s',
                      document_model=model_name,
                      document_name=document.display_name),
                    'body':
                    wizard.message,
                    'record_name':
                    document.display_name,
                    'email_from':
                    email_from,
                    'reply_to':
                    email_from,
                    'model':
                    wizard.res_model,
                    'res_id':
                    wizard.res_id,
                    'no_auto_thread':
                    True,
                    'add_sign':
                    True,
                })
                partners_data = []
                recipient_data = self.env[
                    'mail.followers']._get_recipient_data(
                        document, 'comment', False, pids=new_partners.ids)
                for pid, cid, active, pshare, ctype, notif, groups in recipient_data:
                    pdata = {
                        'id': pid,
                        'share': pshare,
                        'active': active,
                        'notif': 'email',
                        'groups': groups or []
                    }
                    if not pshare and notif:  # has an user and is not shared, is therefore user
                        partners_data.append(dict(pdata, type='user'))
                    elif pshare and notif:  # has an user and is shared, is therefore portal
                        partners_data.append(dict(pdata, type='portal'))
                    else:  # has no user, is therefore customer
                        partners_data.append(dict(pdata, type='customer'))

                document._notify_record_by_email(message, {
                    'partners': partners_data,
                    'channels': []
                },
                                                 send_after_commit=False)
                # in case of failure, the web client must know the message was
                # deleted to discard the related failure notification
                self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner',
                                             self.env.user.partner_id.id), {
                                                 'type': 'deletion',
                                                 'message_ids': message.ids
                                             })
                message.unlink()
        return {'type': 'ir.actions.act_window_close'}
Example #7
0
class IrUiMenu(models.Model):
    _name = 'ir.ui.menu'
    _description = 'Menu'
    _order = "sequence,id"
    _parent_store = True

    def __init__(self, *args, **kwargs):
        super(IrUiMenu, self).__init__(*args, **kwargs)
        self.pool['ir.model.access'].register_cache_clearing_method(
            self._name, 'clear_caches')

    name = fields.Char(string='Menu', required=True, translate=True)
    active = fields.Boolean(default=True)
    sequence = fields.Integer(default=10)
    child_id = fields.One2many('ir.ui.menu', 'parent_id', string='Child IDs')
    parent_id = fields.Many2one('ir.ui.menu',
                                string='Parent Menu',
                                index=True,
                                ondelete="restrict")
    parent_path = fields.Char(index=True)
    groups_id = fields.Many2many('res.groups', 'ir_ui_menu_group_rel',
                                 'menu_id', 'gid', string='Groups',
                                 help="If you have groups, the visibility of this menu will be based on these groups. "\
                                      "If this field is empty, Flectra will compute visibility based on the related object's read access.")
    complete_name = fields.Char(compute='_compute_complete_name',
                                string='Full Path')
    web_icon = fields.Char(string='Web Icon File')
    action = fields.Reference(selection=[(
        'ir.actions.report', 'ir.actions.report'
    ), ('ir.actions.act_window',
        'ir.actions.act_window'), (
            'ir.actions.act_url',
            'ir.actions.act_url'), (
                'ir.actions.server',
                'ir.actions.server'), ('ir.actions.client',
                                       'ir.actions.client')])

    web_icon_data = fields.Binary(string='Web Icon Image', attachment=True)

    @api.depends('name', 'parent_id.complete_name')
    def _compute_complete_name(self):
        for menu in self:
            menu.complete_name = menu._get_full_name()

    def _get_full_name(self, level=6):
        """ Return the full name of ``self`` (up to a certain level). """
        if level <= 0:
            return '...'
        if self.parent_id:
            return self.parent_id._get_full_name(
                level - 1) + MENU_ITEM_SEPARATOR + (self.name or "")
        else:
            return self.name

    def read_image(self, path):
        if not path:
            return False
        path_info = path.split(',')
        icon_path = get_module_resource(path_info[0], path_info[1])
        icon_image = False
        if icon_path:
            with tools.file_open(icon_path, 'rb') as icon_file:
                icon_image = base64.encodebytes(icon_file.read())
        return icon_image

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

    @api.model
    @tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
    def _visible_menu_ids(self, debug=False):
        """ Return the ids of the menu items visible to the user. """
        # retrieve all menus, and determine which ones are visible
        context = {'ir.ui.menu.full_list': True}
        menus = self.with_context(context).search([]).sudo()

        groups = self.env.user.groups_id
        if not debug:
            groups = groups - self.env.ref('base.group_no_one')
        # first discard all menus with groups the user does not have
        menus = menus.filtered(
            lambda menu: not menu.groups_id or menu.groups_id & groups)

        # take apart menus that have an action
        action_menus = menus.filtered(lambda m: m.action and m.action.exists())
        folder_menus = menus - action_menus
        visible = self.browse()

        # process action menus, check whether their action is allowed
        access = self.env['ir.model.access']
        MODEL_GETTER = {
            'ir.actions.act_window': lambda action: action.res_model,
            'ir.actions.report': lambda action: action.model,
            'ir.actions.server': lambda action: action.model_id.model,
        }
        for menu in action_menus:
            get_model = MODEL_GETTER.get(menu.action._name)
            if not get_model or not get_model(menu.action) or \
                    access.check(get_model(menu.action), 'read', False):
                # make menu visible, and its folder ancestors, too
                visible += menu
                menu = menu.parent_id
                while menu and menu in folder_menus and menu not in visible:
                    visible += menu
                    menu = menu.parent_id

        return set(visible.ids)

    @api.returns('self')
    def _filter_visible_menus(self):
        """ Filter `self` to only keep the menu items that should be visible in
            the menu hierarchy of the current user.
            Uses a cache for speeding up the computation.
        """
        visible_ids = self._visible_menu_ids(
            request.session.debug if request else False)
        return self.filtered(lambda menu: menu.id in visible_ids)

    @api.model
    def _search(self,
                args,
                offset=0,
                limit=None,
                order=None,
                count=False,
                access_rights_uid=None):
        menu_ids = super(IrUiMenu,
                         self)._search(args,
                                       offset=0,
                                       limit=None,
                                       order=order,
                                       count=False,
                                       access_rights_uid=access_rights_uid)
        menus = self.browse(menu_ids)
        if menus:
            # menu filtering is done only on main menu tree, not other menu lists
            if not self._context.get('ir.ui.menu.full_list'):
                menus = menus._filter_visible_menus()
            if offset:
                menus = menus[offset:]
            if limit:
                menus = menus[:limit]
        return len(menus) if count else menus.ids

    def name_get(self):
        return [(menu.id, menu._get_full_name()) for menu in self]

    @api.model_create_multi
    def create(self, vals_list):
        self.clear_caches()
        for values in vals_list:
            if 'web_icon' in values:
                values['web_icon_data'] = self._compute_web_icon_data(
                    values.get('web_icon'))
        return super(IrUiMenu, self).create(vals_list)

    def write(self, values):
        self.clear_caches()
        if 'web_icon' in values:
            values['web_icon_data'] = self._compute_web_icon_data(
                values.get('web_icon'))
        return super(IrUiMenu, self).write(values)

    def _compute_web_icon_data(self, web_icon):
        """ Returns the image associated to `web_icon`.
            `web_icon` can either be:
              - an image icon [module, path]
              - a built icon [icon_class, icon_color, background_color]
            and it only has to call `read_image` if it's an image.
        """
        if web_icon and len(web_icon.split(',')) == 2:
            return self.read_image(web_icon)

    def unlink(self):
        # Detach children and promote them to top-level, because it would be unwise to
        # cascade-delete submenus blindly. We also can't use ondelete=set null because
        # that is not supported when _parent_store is used (would silently corrupt it).
        # TODO: ideally we should move them under a generic "Orphans" menu somewhere?
        extra = {'ir.ui.menu.full_list': True, 'active_test': False}
        direct_children = self.with_context(**extra).search([('parent_id',
                                                              'in', self.ids)])
        direct_children.write({'parent_id': False})

        self.clear_caches()
        return super(IrUiMenu, self).unlink()

    def copy(self, default=None):
        record = super(IrUiMenu, self).copy(default=default)
        match = NUMBER_PARENS.search(record.name)
        if match:
            next_num = int(match.group(1)) + 1
            record.name = NUMBER_PARENS.sub('(%d)' % next_num, record.name)
        else:
            record.name = record.name + '(1)'
        return record

    @api.model
    @api.returns('self')
    def get_user_roots(self):
        """ Return all root menu ids visible for the user.

        :return: the root menu ids
        :rtype: list(int)
        """
        return self.search([('parent_id', '=', False)])

    @api.model
    @tools.ormcache_context('self._uid', keys=('lang', ))
    def load_menus_root(self):
        fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon_data']
        menu_roots = self.get_user_roots()
        menu_roots_data = menu_roots.read(fields) if menu_roots else []

        menu_root = {
            'id': False,
            'name': 'root',
            'parent_id': [-1, ''],
            'children': menu_roots_data,
            'all_menu_ids': menu_roots.ids,
        }

        menu_roots._set_menuitems_xmlids(menu_root)

        return menu_root

    @api.model
    @tools.ormcache_context('self._uid', 'debug', keys=('lang', ))
    def load_menus(self, debug):
        """ Loads all menu items (all applications and their sub-menus).

        :return: the menu root
        :rtype: dict('children': menu_nodes)
        """
        fields = [
            'name', 'sequence', 'parent_id', 'action', 'web_icon',
            'web_icon_data'
        ]
        menu_roots = self.get_user_roots()
        menu_roots_data = menu_roots.read(fields) if menu_roots else []
        menu_root = {
            'id': False,
            'name': 'root',
            'parent_id': [-1, ''],
            'children': menu_roots_data,
            'all_menu_ids': menu_roots.ids,
        }

        if not menu_roots_data:
            return menu_root

        # menus are loaded fully unlike a regular tree view, cause there are a
        # limited number of items (752 when all 6.1 addons are installed)
        menus = self.search([('id', 'child_of', menu_roots.ids)])
        menu_items = menus.read(fields)

        # add roots at the end of the sequence, so that they will overwrite
        # equivalent menu items from full menu read when put into id:item
        # mapping, resulting in children being correctly set on the roots.
        menu_items.extend(menu_roots_data)
        menu_root['all_menu_ids'] = menus.ids  # includes menu_roots!

        # make a tree using parent_id
        menu_items_map = {
            menu_item["id"]: menu_item
            for menu_item in menu_items
        }
        for menu_item in menu_items:
            parent = menu_item['parent_id'] and menu_item['parent_id'][0]
            if parent in menu_items_map:
                menu_items_map[parent].setdefault('children',
                                                  []).append(menu_item)

        # sort by sequence a tree using parent_id
        for menu_item in menu_items:
            menu_item.setdefault('children',
                                 []).sort(key=operator.itemgetter('sequence'))

        (menu_roots + menus)._set_menuitems_xmlids(menu_root)

        return menu_root

    def _set_menuitems_xmlids(self, menu_root):
        menuitems = self.env['ir.model.data'].sudo().search([
            ('res_id', 'in', self.ids), ('model', '=', 'ir.ui.menu')
        ])

        xmlids = {menu.res_id: menu.complete_name for menu in menuitems}

        def _set_xmlids(tree, xmlids):
            tree['xmlid'] = xmlids.get(tree['id'], '')
            if 'children' in tree:
                for child in tree['children']:
                    _set_xmlids(child, xmlids)

        _set_xmlids(menu_root, xmlids)
Example #8
0
class AccountTaxReportLine(models.Model):
    _name = "account.tax.report.line"
    _description = 'Account Tax Report Line'
    _order = 'sequence'
    _parent_store = True

    name = fields.Char(string="Name", required=True, help="Complete name for this report line, to be used in report.")
    tag_ids = fields.Many2many(string="Tags", comodel_name='account.account.tag', relation='account_tax_report_line_tags_rel', help="Tax tags populating this line")
    report_action_id = fields.Many2one(string="Report Action", comodel_name='ir.actions.act_window', help="The optional action to call when clicking on this line in accounting reports.")
    children_line_ids = fields.One2many(string="Children Lines", comodel_name='account.tax.report.line', inverse_name='parent_id', help="Lines that should be rendered as children of this one")
    parent_id = fields.Many2one(string="Parent Line", comodel_name='account.tax.report.line')
    sequence = fields.Integer(string='Sequence', required=True,
        help="Sequence determining the order of the lines in the report (smaller ones come first). This order is applied locally per section (so, children of the same line are always rendered one after the other).")
    parent_path = fields.Char(index=True)
    report_id = fields.Many2one(string="Tax Report", required=True, comodel_name='account.tax.report', ondelete='cascade', help="The parent tax report of this line")

    #helper to create tags (positive and negative) on report line creation
    tag_name = fields.Char(string="Tag Name", help="Short name for the tax grid corresponding to this report line. Leave empty if this report line should not correspond to any such grid.")

    #fields used in specific localization reports, where a report line isn't simply the given by the sum of account.move.line with selected tags
    code = fields.Char(string="Code", help="Optional unique code to refer to this line in total formulas")
    formula = fields.Char(string="Formula", help="Python expression used to compute the value of a total line. This field is mutually exclusive with tag_name, setting it turns the line to a total line. Tax report line codes can be used as variables in this expression to refer to the balance of the corresponding lines in the report. A formula cannot refer to another line using a formula.")

    @api.model
    def create(self, vals):
        # Manage tags
        tag_name = vals.get('tag_name', '')
        if tag_name and vals.get('report_id'):
            report = self.env['account.tax.report'].browse(vals['report_id'])
            country = report.country_id

            existing_tags = self.env['account.account.tag']._get_tax_tags(tag_name, country.id)

            if existing_tags:
                # We connect the new report line to the already existing tags
                vals['tag_ids'] = [(6, 0, existing_tags.ids)]
            else:
                # We create new ones
                vals['tag_ids'] = self._get_tags_create_vals(tag_name, country.id)

        return super(AccountTaxReportLine, self).create(vals)

    @api.model
    def _get_tags_create_vals(self, tag_name, country_id):
        minus_tag_vals = {
          'name': '-' + tag_name,
          'applicability': 'taxes',
          'tax_negate': True,
          'country_id': country_id,
        }
        plus_tag_vals = {
          'name': '+' + tag_name,
          'applicability': 'taxes',
          'tax_negate': False,
          'country_id': country_id,
        }
        return [(0, 0, minus_tag_vals), (0, 0, plus_tag_vals)]

    def write(self, vals):
        # If tag_name was set, but not tag_ids, we postpone the write of
        # tag_name, and perform it only after having generated/retrieved the tags.
        # Otherwise, tag_name and tags' name would not match, breaking
        # _validate_tags constaint.
        postponed_vals = {}

        if 'tag_name' in vals and 'tag_ids' not in vals:
            postponed_vals = {'tag_name': vals.pop('tag_name')}
            tag_name_postponed = postponed_vals['tag_name']
            # if tag_name is posponed then we also postpone formula to avoid
            # breaking _validate_formula constraint
            if 'formula' in vals:
                postponed_vals['formula'] = vals.pop('formula')

        rslt = super(AccountTaxReportLine, self).write(vals)

        if postponed_vals:
            # If tag_name modification has been postponed,
            # we need to search for existing tags corresponding to the new tag name
            # (or create them if they don't exist yet) and assign them to the records

            records_by_country = {}
            for record in self.filtered(lambda x: x.tag_name != tag_name_postponed):
                records_by_country[record.report_id.country_id.id] = records_by_country.get(record.report_id.country_id.id, self.env['account.tax.report.line']) + record

            for country_id, records in records_by_country.items():
                if tag_name_postponed:
                    record_tag_names = records.mapped('tag_name')
                    if len(record_tag_names) == 1 and record_tag_names[0]:
                        # If all the records already have the same tag_name before writing,
                        # we simply want to change the name of the existing tags
                        to_update = records.mapped('tag_ids.tax_report_line_ids')
                        tags_to_update = to_update.mapped('tag_ids')
                        minus_child_tags = tags_to_update.filtered(lambda x: x.tax_negate)
                        minus_child_tags.write({'name': '-' + tag_name_postponed})
                        plus_child_tags = tags_to_update.filtered(lambda x: not x.tax_negate)
                        plus_child_tags.write({'name': '+' + tag_name_postponed})
                        super(AccountTaxReportLine, to_update).write(postponed_vals)

                    else:
                        existing_tags = self.env['account.account.tag']._get_tax_tags(tag_name_postponed, country_id)
                        records_to_link = records
                        tags_to_remove = self.env['account.account.tag']

                        if not existing_tags and records_to_link:
                            # If the tag does not exist yet, we first create it by
                            # linking it to the first report line of the record set
                            first_record = records_to_link[0]
                            tags_to_remove += first_record.tag_ids
                            first_record.write({**postponed_vals, 'tag_ids': [(5, 0, 0)] + self._get_tags_create_vals(tag_name_postponed, country_id)})
                            existing_tags = first_record.tag_ids
                            records_to_link -= first_record

                        # All the lines sharing their tags must always be synchronized,
                        tags_to_remove += records_to_link.mapped('tag_ids')
                        records_to_link = tags_to_remove.mapped('tax_report_line_ids')
                        tags_to_remove.mapped('tax_report_line_ids')._remove_tags_used_only_by_self()
                        records_to_link.write({**postponed_vals, 'tag_ids': [(2, tag.id) for tag in tags_to_remove] + [(6, 0, existing_tags.ids)]})

                else:
                    # tag_name was set empty, so we remove the tags on current lines
                    # If some tags are still referenced by other report lines,
                    # we keep them ; else, we delete them from DB
                    line_tags = records.mapped('tag_ids')
                    other_lines_same_tag = line_tags.mapped('tax_report_line_ids').filtered(lambda x: x not in records)
                    if not other_lines_same_tag:
                        self._delete_tags_from_taxes(line_tags.ids)
                    orm_cmd_code = other_lines_same_tag and 3 or 2
                    records.write({**postponed_vals, 'tag_ids': [(orm_cmd_code, tag.id) for tag in line_tags]})

        return rslt

    def unlink(self):
        self._remove_tags_used_only_by_self()
        children = self.mapped('children_line_ids')
        if children:
            children.unlink()
        return super(AccountTaxReportLine, self).unlink()

    def _remove_tags_used_only_by_self(self):
        """ Deletes and removes from taxes and move lines all the
        tags from the provided tax report lines that are not linked
        to any other tax report lines.
        """
        all_tags = self.mapped('tag_ids')
        tags_to_unlink = all_tags.filtered(lambda x: not (x.tax_report_line_ids - self))
        self.write({'tag_ids': [(3, tag.id, 0) for tag in tags_to_unlink]})
        self._delete_tags_from_taxes(tags_to_unlink.ids)

    @api.model
    def _delete_tags_from_taxes(self, tag_ids_to_delete):
        """ Based on a list of tag ids, removes them first from the
        repartition lines they are linked to, then deletes them
        from the account move lines, and finally unlink them.
        """
        if not tag_ids_to_delete:
            # Nothing to do, then!
            return

        self.env.cr.execute("""
            delete from account_account_tag_account_tax_repartition_line_rel
            where account_account_tag_id in %(tag_ids_to_delete)s;

            delete from account_account_tag_account_move_line_rel
            where account_account_tag_id in %(tag_ids_to_delete)s;
        """, {'tag_ids_to_delete': tuple(tag_ids_to_delete)})

        self.env['account.move.line'].invalidate_cache(fnames=['tax_tag_ids'])
        self.env['account.tax.repartition.line'].invalidate_cache(fnames=['tag_ids'])

        self.env['account.account.tag'].browse(tag_ids_to_delete).unlink()

    @api.constrains('formula', 'tag_name')
    def _validate_formula(self):
        for record in self:
            if record.formula and record.tag_name:
                raise ValidationError(_("Tag name and formula are mutually exclusive, they should not be set together on the same tax report line."))

    @api.constrains('tag_name', 'tag_ids')
    def _validate_tags(self):
        for record in self.filtered(lambda x: x.tag_ids):
            neg_tags = record.tag_ids.filtered(lambda x: x.tax_negate)
            pos_tags = record.tag_ids.filtered(lambda x: not x.tax_negate)

            if (len(neg_tags) != 1 or len(pos_tags) != 1):
                raise ValidationError(_("If tags are defined for a tax report line, only two are allowed on it: a positive and a negative one."))

            if neg_tags.name != '-'+record.tag_name or pos_tags.name != '+'+record.tag_name:
                raise ValidationError(_("The tags linked to a tax report line should always match its tag name."))
Example #9
0
class SaleOrder(models.Model):
    _inherit = 'sale.order'

    timesheet_ids = fields.Many2many(
        'account.analytic.line',
        compute='_compute_timesheet_ids',
        string='Timesheet activities associated to this sale')
    timesheet_count = fields.Float(
        string='Timesheet activities',
        compute='_compute_timesheet_ids',
        groups="hr_timesheet.group_hr_timesheet_user")

    tasks_ids = fields.Many2many('project.task',
                                 compute='_compute_tasks_ids',
                                 string='Tasks associated to this sale')
    tasks_count = fields.Integer(string='Tasks',
                                 compute='_compute_tasks_ids',
                                 groups="project.group_project_user")

    project_project_id = fields.Many2one(
        'project.project',
        compute='_compute_project_project_id',
        string='Project associated to this sale')
    project_ids = fields.Many2many('project.project',
                                   compute="_compute_project_ids",
                                   string='Projects',
                                   copy=False,
                                   groups="project.group_project_user",
                                   help="Projects used in this sales order.")

    @api.multi
    @api.depends('analytic_account_id.line_ids')
    def _compute_timesheet_ids(self):
        for order in self:
            if order.analytic_account_id:
                order.timesheet_ids = self.env['account.analytic.line'].search(
                    [('so_line', 'in', order.order_line.ids),
                     ('amount', '<=', 0.0), ('project_id', '!=', False)])
            else:
                order.timesheet_ids = []
            order.timesheet_count = len(order.timesheet_ids)

    @api.multi
    @api.depends('order_line.product_id.project_id')
    def _compute_tasks_ids(self):
        for order in self:
            order.tasks_ids = self.env['project.task'].search([
                ('sale_line_id', 'in', order.order_line.ids)
            ])
            order.tasks_count = len(order.tasks_ids)

    @api.multi
    @api.depends('analytic_account_id.project_ids')
    def _compute_project_project_id(self):
        for order in self:
            order.project_project_id = self.env['project.project'].search([
                ('analytic_account_id', '=', order.analytic_account_id.id)
            ])

    @api.multi
    @api.depends('order_line.product_id', 'project_project_id')
    def _compute_project_ids(self):
        for order in self:
            projects = order.order_line.mapped('product_id.project_id')
            if order.project_project_id:
                projects |= order.project_project_id
            order.project_ids = projects

    @api.multi
    def action_confirm(self):
        """ On SO confirmation, some lines should generate a task or a project. """
        result = super(SaleOrder, self).action_confirm()
        self.order_line._timesheet_service_generation()
        return result

    @api.multi
    def action_view_task(self):
        self.ensure_one()
        action = self.env.ref('project.action_view_task')
        list_view_id = self.env.ref('project.view_task_tree2').id
        form_view_id = self.env.ref('project.view_task_form2').id

        result = {
            'name':
            action.name,
            'help':
            action.help,
            'type':
            action.type,
            'views': [[False, 'kanban'], [list_view_id, 'tree'],
                      [form_view_id, 'form'], [False, 'graph'],
                      [False, 'calendar'], [False, 'pivot'], [False, 'graph']],
            'target':
            action.target,
            'context':
            "{'group_by':'stage_id'}",
            'res_model':
            action.res_model,
        }
        if len(self.tasks_ids) > 1:
            result['domain'] = "[('id','in',%s)]" % self.tasks_ids.ids
        elif len(self.tasks_ids) == 1:
            result['views'] = [(form_view_id, 'form')]
            result['res_id'] = self.tasks_ids.id
        else:
            result = {'type': 'ir.actions.act_window_close'}
        return result

    @api.multi
    def action_view_project_ids(self):
        self.ensure_one()
        if len(self.project_ids) == 1:
            if self.env.user.has_group("hr_timesheet.group_hr_timesheet_user"):
                action = self.project_ids.action_view_timesheet_plan()
            else:
                action = self.env.ref(
                    "project.act_project_project_2_project_task_all").read()[0]
                action['context'] = safe_eval(
                    action.get('context', '{}'), {
                        'active_id': self.project_ids.id,
                        'active_ids': self.project_ids.ids
                    })
        else:
            view_form_id = self.env.ref('project.edit_project').id
            view_kanban_id = self.env.ref('project.view_project_kanban').id
            action = {
                'type': 'ir.actions.act_window',
                'domain': [('id', 'in', self.project_ids.ids)],
                'views': [(view_kanban_id, 'kanban'), (view_form_id, 'form')],
                'view_mode': 'kanban,form',
                'name': _('Projects'),
                'res_model': 'project.project',
            }
        return action

    @api.multi
    def action_view_timesheet(self):
        self.ensure_one()
        action = self.env.ref('hr_timesheet.act_hr_timesheet_line')
        list_view_id = self.env.ref('hr_timesheet.hr_timesheet_line_tree').id
        form_view_id = self.env.ref('hr_timesheet.hr_timesheet_line_form').id

        result = {
            'name': action.name,
            'help': action.help,
            'type': action.type,
            'views': [[list_view_id, 'tree'], [form_view_id, 'form']],
            'target': action.target,
            'context': action.context,
            'res_model': action.res_model,
        }
        if self.timesheet_count > 0:
            result['domain'] = "[('id','in',%s)]" % self.timesheet_ids.ids
        else:
            result = {'type': 'ir.actions.act_window_close'}
        return result
Example #10
0
class DeliveryCarrier(models.Model):
    _name = 'delivery.carrier'
    _description = "Carrier"
    _order = 'sequence, id'
    ''' A Shipping Provider

    In order to add your own external provider, follow these steps:

    1. Create your model MyProvider that _inherit 'delivery.carrier'
    2. Extend the selection of the field "delivery_type" with a pair
       ('<my_provider>', 'My Provider')
    3. Add your methods:
       <my_provider>_rate_shipment
       <my_provider>_send_shipping
       <my_provider>_get_tracking_link
       <my_provider>_cancel_shipment
       (they are documented hereunder)
    '''

    # -------------------------------- #
    # Internals for shipping providers #
    # -------------------------------- #

    name = fields.Char(required=True)
    active = fields.Boolean(default=True)
    sequence = fields.Integer(help="Determine the display order", default=10)
    # This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex')
    delivery_type = fields.Selection([('fixed', 'Fixed Price')],
                                     string='Provider',
                                     default='fixed',
                                     required=True)
    integration_level = fields.Selection(
        [('rate', 'Get Rate'),
         ('rate_and_ship', 'Get Rate and Create Shipment')],
        string="Integration Level",
        default='rate_and_ship',
        help="Action while validating Delivery Orders")
    prod_environment = fields.Boolean(
        "Environment",
        help="Set to True if your credentials are certified for production.")
    debug_logging = fields.Boolean(
        'Debug logging', help="Log requests in order to ease debugging")
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 related='product_id.company_id',
                                 store=True)
    product_id = fields.Many2one('product.product',
                                 string='Delivery Product',
                                 required=True,
                                 ondelete='restrict')

    country_ids = fields.Many2many('res.country',
                                   'delivery_carrier_country_rel',
                                   'carrier_id', 'country_id', 'Countries')
    state_ids = fields.Many2many('res.country.state',
                                 'delivery_carrier_state_rel', 'carrier_id',
                                 'state_id', 'States')
    zip_from = fields.Char('Zip From')
    zip_to = fields.Char('Zip To')

    margin = fields.Integer(
        help='This percentage will be added to the shipping price.')
    free_over = fields.Boolean(
        'Free if order amount is above',
        help=
        "If the order total amount (shipping excluded) is above or equal to this value, the customer benefits from a free shipping",
        default=False,
        oldname='free_if_more_than')
    amount = fields.Float(
        string='Amount',
        help=
        "Amount of the order to benefit from a free shipping, expressed in the company currency"
    )

    _sql_constraints = [
        ('margin_not_under_100_percent', 'CHECK (margin >= -100)',
         'Margin cannot be lower than -100%'),
    ]

    def toggle_prod_environment(self):
        for c in self:
            c.prod_environment = not c.prod_environment

    def toggle_debug(self):
        for c in self:
            c.debug_logging = not c.debug_logging

    @api.multi
    def install_more_provider(self):
        return {
            'name':
            'New Providers',
            'view_mode':
            'kanban',
            'res_model':
            'ir.module.module',
            'domain': [['name', 'ilike', 'delivery_']],
            'type':
            'ir.actions.act_window',
            'help':
            _('''<p class="oe_view_nocontent">
                    Buy Flectra Enterprise now to get more providers.
                </p>'''),
        }

    def available_carriers(self, partner):
        return self.filtered(lambda c: c._match_address(partner))

    def _match_address(self, partner):
        self.ensure_one()
        if self.country_ids and partner.country_id not in self.country_ids:
            return False
        if self.state_ids and partner.state_id not in self.state_ids:
            return False
        if self.zip_from and (partner.zip
                              or '').upper() < self.zip_from.upper():
            return False
        if self.zip_to and (partner.zip or '').upper() > self.zip_to.upper():
            return False
        return True

    @api.onchange('state_ids')
    def onchange_states(self):
        self.country_ids = [
            (6, 0,
             self.country_ids.ids + self.state_ids.mapped('country_id.id'))
        ]

    @api.onchange('country_ids')
    def onchange_countries(self):
        self.state_ids = [
            (6, 0,
             self.state_ids.filtered(lambda state: state.id in self.country_ids
                                     .mapped('state_ids').ids).ids)
        ]

    # -------------------------- #
    # API for external providers #
    # -------------------------- #

    def rate_shipment(self, order):
        ''' Compute the price of the order shipment

        :param order: record of sale.order
        :return dict: {'success': boolean,
                       'price': a float,
                       'error_message': a string containing an error message,
                       'warning_message': a string containing a warning message}
                       # TODO maybe the currency code?
        '''
        self.ensure_one()
        if hasattr(self, '%s_rate_shipment' % self.delivery_type):
            res = getattr(self, '%s_rate_shipment' % self.delivery_type)(order)
            # apply margin on computed price
            res['price'] = res['price'] * (1.0 + (float(self.margin) / 100.0))
            # free when order is large enough
            if res['success'] and self.free_over and order._compute_amount_total_without_delivery(
            ) >= self.amount:
                res['warning_message'] = _(
                    'Info:\nThe shipping is free because the order amount exceeds %.2f.\n(The actual shipping cost is: %.2f)'
                ) % (self.amount, res['price'])
                res['price'] = 0.0
            return res

    def send_shipping(self, pickings):
        ''' Send the package to the service provider

        :param pickings: A recordset of pickings
        :return list: A list of dictionaries (one per picking) containing of the form::
                         { 'exact_price': price,
                           'tracking_number': number }
                           # TODO missing labels per package
                           # TODO missing currency
                           # TODO missing success, error, warnings
        '''
        self.ensure_one()
        if hasattr(self, '%s_send_shipping' % self.delivery_type):
            return getattr(self,
                           '%s_send_shipping' % self.delivery_type)(pickings)

    def get_tracking_link(self, picking):
        ''' Ask the tracking link to the service provider

        :param picking: record of stock.picking
        :return str: an URL containing the tracking link or False
        '''
        self.ensure_one()
        if hasattr(self, '%s_get_tracking_link' % self.delivery_type):
            return getattr(self, '%s_get_tracking_link' %
                           self.delivery_type)(picking)

    def cancel_shipment(self, pickings):
        ''' Cancel a shipment

        :param pickings: A recordset of pickings
        '''
        self.ensure_one()
        if hasattr(self, '%s_cancel_shipment' % self.delivery_type):
            return getattr(self,
                           '%s_cancel_shipment' % self.delivery_type)(pickings)

    def log_xml(self, xml_string, func):
        self.ensure_one()

        if self.debug_logging:
            db_name = self._cr.dbname

            # Use a new cursor to avoid rollback that could be caused by an upper method
            try:
                db_registry = registry(db_name)
                with db_registry.cursor() as cr:
                    env = api.Environment(cr, SUPERUSER_ID, {})
                    IrLogging = env['ir.logging']
                    IrLogging.sudo().create({
                        'name': 'delivery.carrier',
                        'type': 'server',
                        'dbname': db_name,
                        'level': 'DEBUG',
                        'message': xml_string,
                        'path': self.delivery_type,
                        'func': func,
                        'line': 1
                    })
            except psycopg2.Error:
                pass

    # ------------------------------------------------ #
    # Fixed price shipping, aka a very simple provider #
    # ------------------------------------------------ #

    fixed_price = fields.Float(compute='_compute_fixed_price',
                               inverse='_set_product_fixed_price',
                               store=True,
                               string='Fixed Price')

    @api.depends('product_id.list_price',
                 'product_id.product_tmpl_id.list_price')
    def _compute_fixed_price(self):
        for carrier in self:
            carrier.fixed_price = carrier.product_id.list_price

    def _set_product_fixed_price(self):
        for carrier in self:
            carrier.product_id.list_price = carrier.fixed_price

    def fixed_rate_shipment(self, order):
        return {
            'success': True,
            'price': self.fixed_price,
            'error_message': False,
            'warning_message': False
        }

    def fixed_send_shipping(self, pickings):
        res = []
        for p in pickings:
            res = res + [{
                'exact_price': p.carrier_id.fixed_price,
                'tracking_number': False
            }]
        return res

    def fixed_get_tracking_link(self, picking):
        return False

    def fixed_cancel_shipment(self, pickings):
        raise NotImplementedError()
class ProjectTaskTypeDelete(models.TransientModel):
    _name = 'project.task.type.delete.wizard'
    _description = 'Project Stage Delete Wizard'

    project_ids = fields.Many2many('project.project', domain="['|', ('active', '=', False), ('active', '=', True)]", string='Projects', ondelete='cascade')
    stage_ids = fields.Many2many('project.task.type', string='Stages To Delete', ondelete='cascade')
    tasks_count = fields.Integer('Number of tasks', compute='_compute_tasks_count')
    stages_active = fields.Boolean(compute='_compute_stages_active')

    @api.depends('project_ids')
    def _compute_tasks_count(self):
        for wizard in self:
            wizard.tasks_count = self.with_context(active_test=False).env['project.task'].search_count([('stage_id', 'in', wizard.stage_ids.ids)])

    @api.depends('stage_ids')
    def _compute_stages_active(self):
        for wizard in self:
            wizard.stages_active = all(wizard.stage_ids.mapped('active'))

    def action_archive(self):
        if len(self.project_ids) <= 1:
            return self.action_confirm()

        return {
            'name': _('Confirmation'),
            'view_mode': 'form',
            'res_model': 'project.task.type.delete.wizard',
            'views': [(self.env.ref('project.view_project_task_type_delete_confirmation_wizard').id, 'form')],
            'type': 'ir.actions.act_window',
            'res_id': self.id,
            'target': 'new',
            'context': self.env.context,
        }

    def action_confirm(self):
        tasks = self.with_context(active_test=False).env['project.task'].search([('stage_id', 'in', self.stage_ids.ids)])
        tasks.write({'active': False})
        self.stage_ids.write({'active': False})
        return self._get_action()

    def action_unlink(self):
        self.stage_ids.unlink()
        return self._get_action()

    def _get_action(self):
        project_id = self.env.context.get('default_project_id')

        if project_id:
            action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_task")
            action['domain'] = [('project_id', '=', project_id)]
            action['context'] = str({
                'pivot_row_groupby': ['user_id'],
                'default_project_id': project_id,
            })
        elif self.env.context.get('stage_view'):
            action = self.env["ir.actions.actions"]._for_xml_id("project.open_task_type_form")
        else:
            action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_all_task")

        context = dict(ast.literal_eval(action.get('context')), active_test=True)
        action['context'] = context
        action['target'] = 'main'
        return action
Example #12
0
class AccountAnalyticAccount(models.Model):
    _name = 'account.analytic.account'
    _inherit = ['mail.thread', 'ir.branch.company.mixin']
    _description = 'Analytic Account'
    _order = 'code, name asc'

    @api.multi
    def _compute_debit_credit_balance(self):
        analytic_line_obj = self.env['account.analytic.line']
        domain = [('account_id', 'in', self.ids)]
        if self._context.get('from_date', False):
            domain.append(('date', '>=', self._context['from_date']))
        if self._context.get('to_date', False):
            domain.append(('date', '<=', self._context['to_date']))

        credit_groups = analytic_line_obj.read_group(
            domain=domain + [('amount', '>=', 0.0)],
            fields=['account_id', 'amount'],
            groupby=['account_id'])
        data_credit = {l['account_id'][0]: l['amount'] for l in credit_groups}
        debit_groups = analytic_line_obj.read_group(
            domain=domain + [('amount', '<', 0.0)],
            fields=['account_id', 'amount'],
            groupby=['account_id'])
        data_debit = {l['account_id'][0]: l['amount'] for l in debit_groups}

        for account in self:
            account.debit = abs(data_debit.get(account.id, 0.0))
            account.credit = data_credit.get(account.id, 0.0)
            account.balance = account.credit - account.debit

    name = fields.Char(string='Analytic Account',
                       index=True,
                       required=True,
                       track_visibility='onchange')
    code = fields.Char(string='Reference',
                       index=True,
                       track_visibility='onchange')
    active = fields.Boolean(
        'Active',
        help=
        "If the active field is set to False, it will allow you to hide the account without removing it.",
        default=True)

    tag_ids = fields.Many2many('account.analytic.tag',
                               'account_analytic_account_tag_rel',
                               'account_id',
                               'tag_id',
                               string='Tags',
                               copy=True)
    line_ids = fields.One2many('account.analytic.line',
                               'account_id',
                               string="Analytic Lines")

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

    # use auto_join to speed up name_search call
    partner_id = fields.Many2one('res.partner',
                                 string='Customer',
                                 auto_join=True,
                                 track_visibility='onchange')

    balance = fields.Monetary(compute='_compute_debit_credit_balance',
                              string='Balance')
    debit = fields.Monetary(compute='_compute_debit_credit_balance',
                            string='Debit')
    credit = fields.Monetary(compute='_compute_debit_credit_balance',
                             string='Credit')

    currency_id = fields.Many2one(related="company_id.currency_id",
                                  string="Currency",
                                  readonly=True)

    @api.multi
    def name_get(self):
        res = []
        for analytic in self:
            name = analytic.name
            if analytic.code:
                name = '[' + analytic.code + '] ' + name
            if analytic.partner_id.commercial_partner_id.name:
                name = name + ' - ' + analytic.partner_id.commercial_partner_id.name
            res.append((analytic.id, name))
        return res

    @api.model
    def name_search(self, name='', args=None, operator='ilike', limit=100):
        if operator not in ('ilike', 'like', '=', '=like', '=ilike'):
            return super(AccountAnalyticAccount,
                         self).name_search(name, args, operator, limit)
        args = args or []
        domain = ['|', ('code', operator, name), ('name', operator, name)]
        partners = self.env['res.partner'].search([('name', operator, name)],
                                                  limit=limit)
        if partners:
            domain = ['|'] + domain + [('partner_id', 'in', partners.ids)]
        recs = self.search(domain + args, limit=limit)
        return recs.name_get()
Example #13
0
class BlanketOrderLine(models.Model):
    _name = 'purchase.blanket.order.line'
    _description = 'Blanket Order Line'
    _inherit = ['mail.thread', 'mail.activity.mixin']

    @api.depends('original_uom_qty', 'price_unit', 'taxes_id')
    def _compute_amount(self):
        for line in self:
            taxes = line.taxes_id.compute_all(line.price_unit,
                                              line.order_id.currency_id,
                                              line.original_uom_qty,
                                              product=line.product_id,
                                              partner=line.order_id.partner_id)
            line.update({
                'price_tax':
                sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])),
                'price_total':
                taxes['total_included'],
                'price_subtotal':
                taxes['total_excluded'],
            })

    name = fields.Char('Description', track_visibility='onchange')
    sequence = fields.Integer()
    order_id = fields.Many2one('purchase.blanket.order',
                               required=True,
                               ondelete='cascade')
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 required=True,
                                 domain=[('purchase_ok', '=', True)])
    product_uom = fields.Many2one('product.uom',
                                  string='Unit of Measure',
                                  required=True)
    price_unit = fields.Float(string='Price',
                              required=True,
                              digits=dp.get_precision('Product Price'))
    taxes_id = fields.Many2many(
        'account.tax',
        string='Taxes',
        domain=['|', ('active', '=', False), ('active', '=', True)])
    date_schedule = fields.Date(string='Scheduled Date')
    original_uom_qty = fields.Float(
        string='Original quantity',
        required=True,
        default=1.0,
        digits=dp.get_precision('Product Unit of Measure'))
    ordered_uom_qty = fields.Float(
        string='Ordered quantity',
        compute='_compute_quantities',
        store=True,
        digits=dp.get_precision('Product Unit of Measure'))
    invoiced_uom_qty = fields.Float(
        string='Invoiced quantity',
        compute='_compute_quantities',
        store=True,
        digits=dp.get_precision('Product Unit of Measure'))
    remaining_uom_qty = fields.Float(
        string='Remaining quantity',
        compute='_compute_quantities',
        store=True,
        digits=dp.get_precision('Product Unit of Measure'))
    remaining_qty = fields.Float(
        string='Remaining quantity in base UoM',
        compute='_compute_quantities',
        store=True,
        digits=dp.get_precision('Product Unit of Measure'))
    received_uom_qty = fields.Float(
        string='Received quantity',
        compute='_compute_quantities',
        store=True,
        digits=dp.get_precision('Product Unit of Measure'))
    purchase_lines = fields.One2many(comodel_name='purchase.order.line',
                                     inverse_name='blanket_order_line',
                                     string='Purchase Order Lines',
                                     readonly=True,
                                     copy=False)
    company_id = fields.Many2one('res.company',
                                 related='order_id.company_id',
                                 store=True,
                                 readonly=True)
    currency_id = fields.Many2one('res.currency',
                                  related='order_id.currency_id',
                                  readonly=True)
    partner_id = fields.Many2one(related='order_id.partner_id',
                                 string='Vendor',
                                 readonly=True)
    user_id = fields.Many2one(related='order_id.user_id',
                              string='Responsible',
                              readonly=True)
    payment_term_id = fields.Many2one(related='order_id.payment_term_id',
                                      string='Payment Terms',
                                      readonly=True)

    price_subtotal = fields.Monetary(compute='_compute_amount',
                                     string='Subtotal',
                                     store=True)
    price_total = fields.Monetary(compute='_compute_amount',
                                  string='Total',
                                  store=True)
    price_tax = fields.Float(compute='_compute_amount',
                             string='Tax',
                             store=True)

    def _format_date(self, date):
        # format date following user language
        lang_model = self.env['res.lang']
        lang = lang_model._lang_get(self.env.user.lang)
        date_format = lang.date_format
        return datetime.strftime(fields.Date.from_string(date), date_format)

    def name_get(self):
        result = []
        if self.env.context.get('from_purchase_order'):
            for record in self:
                res = "[%s]" % record.order_id.name
                if record.date_schedule:
                    formatted_date = self._format_date(record.date_schedule)
                    res += ' - %s: %s' % (_('Date Scheduled'), formatted_date)
                res += ' (%s: %s %s)' % (_('remaining'),
                                         record.remaining_uom_qty,
                                         record.product_uom.name)
                result.append((record.id, res))
            return result
        return super(BlanketOrderLine, self).name_get()

    @api.multi
    def _get_display_price(self, product):

        seller = product._select_seller(partner_id=self.order_id.partner_id,
                                        quantity=self.original_uom_qty,
                                        date=self.order_id.date_start
                                        and self.order_id.date_start[:10],
                                        uom_id=self.product_uom)

        if not seller:
            return

        price_unit = self.env['account.tax']._fix_tax_included_price_company(
            seller.price, product.supplier_taxes_id,
            self.purchase_lines.taxes_id, self.company_id) if seller else 0.0
        if price_unit and seller and self.order_id.currency_id and \
                seller.currency_id != self.order_id.currency_id:
            price_unit = seller.currency_id.compute(price_unit,
                                                    self.order_id.currency_id)

        if seller and self.product_uom and seller.product_uom != \
                self.product_uom:
            price_unit = seller.product_uom._compute_price(
                price_unit, self.product_uom)

        return price_unit

    @api.multi
    @api.onchange('product_id', 'original_uom_qty')
    def onchange_product(self):
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        if self.product_id:
            name = self.product_id.name
            if not self.product_uom:
                self.product_uom = self.product_id.uom_id.id
            if self.order_id.partner_id and \
                    float_is_zero(self.price_unit, precision_digits=precision):
                self.price_unit = self._get_display_price(self.product_id)
            if self.product_id.code:
                name = '[%s] %s' % (name, self.product_id.code)
            if self.product_id.description_purchase:
                name += '\n' + self.product_id.description_purchase
            self.name = name

            fpos = self.order_id.fiscal_position_id
            if self.env.uid == SUPERUSER_ID:
                company_id = self.env.user.company_id.id
                self.taxes_id = fpos.map_tax(
                    self.product_id.supplier_taxes_id.filtered(
                        lambda r: r.company_id.id == company_id))
            else:
                self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id)

    @api.multi
    @api.depends(
        'purchase_lines.order_id.state',
        'purchase_lines.blanket_order_line',
        'purchase_lines.product_qty',
        'purchase_lines.product_uom',
        'purchase_lines.qty_received',
        'purchase_lines.qty_invoiced',
        'original_uom_qty',
        'product_uom',
    )
    def _compute_quantities(self):
        for line in self:
            purchase_lines = line.purchase_lines
            line.ordered_uom_qty = sum(
                l.product_uom._compute_quantity(
                    l.product_qty, line.product_uom) for l in purchase_lines
                if l.order_id.state != 'cancel'
                and l.product_id == line.product_id)
            line.invoiced_uom_qty = sum(
                l.product_uom._compute_quantity(
                    l.qty_invoiced, line.product_uom) for l in purchase_lines
                if l.order_id.state != 'cancel'
                and l.product_id == line.product_id)
            line.received_uom_qty = sum(
                l.product_uom._compute_quantity(
                    l.qty_received, line.product_uom) for l in purchase_lines
                if l.order_id.state != 'cancel'
                and l.product_id == line.product_id)
            line.remaining_uom_qty = line.original_uom_qty - \
                line.ordered_uom_qty
            line.remaining_qty = line.product_uom._compute_quantity(
                line.remaining_uom_qty, line.product_id.uom_id)

    @api.multi
    def _validate(self):
        try:
            for line in self:
                assert line.price_unit > 0.0, \
                    _("Price must be greater than zero")
                assert line.original_uom_qty > 0.0, \
                    _("Quantity must be greater than zero")
        except AssertionError as e:
            raise UserError(e)
Example #14
0
class wizard_report_kas(models.TransientModel):
    _name = 'siswa_keu_ocb11.wizard_report_kas'

    name = fields.Char('Name', default='Report Kas')
    awal = fields.Date('Periode Awal',
                       default=datetime.today().date(),
                       required=True)
    akhir = fields.Date('Periode Akhir',
                        default=datetime.today().date(),
                        required=True)
    jenis = fields.Selection([(1, 'All'), (2, 'Pendaptan'),
                              (3, 'Pengeluaran')],
                             string='Jenis Kas',
                             required=True,
                             default=1)
    # kas_ids = fields.Many2many('siswa_keu_ocb11.kas', string='Data Kas')
    kas_ids = fields.Many2many('siswa_keu_ocb11.kas',
                               relation='siswa_keu_ocb11_report_kas_rel',
                               column1='report_id',
                               column2='kas_id',
                               string="Data Kas")
    saldo_begining = fields.Float('Saldo Begining', default=0)
    saldo_ending = fields.Float('Saldo Ending', default=0)
    saldo_current = fields.Float('Saldo Current', default=0)
    tipe = fields.Selection([('sum', 'Summary'), ('det', 'Detail')],
                            required=True,
                            default='sum')

    def action_save(self):
        self.ensure_one()

        # if self.tipe == 'det':
        # set kas_ids
        kases = self.env['siswa_keu_ocb11.kas'].search([
            ('tanggal', '>=', self.awal), ('tanggal', '<=', self.akhir)
        ])
        reg_kas = []
        for kas in kases:
            self.write({'kas_ids': [(4, kas.id)]})
        kas_before = self.env['siswa_keu_ocb11.kas'].search([('tanggal', '<',
                                                              self.awal)])
        saldo_begining = sum(x.jumlah for x in kas_before)

        # saldo begining wth cr execute
        self.env.cr.execute(
            "select coalesce(sum(debet),0) - coalesce(sum(kredit),0) \
                from siswa_keu_ocb11_kas \
                where tanggal < '%s'" % self.awal)
        saldo_begining = self.env.cr.fetchone()[0]

        kas_after = self.env['siswa_keu_ocb11.kas'].search([('tanggal', '>',
                                                             self.akhir)])
        saldo_ending = sum(x.jumlah for x in kas_after)

        saldo_current = sum(x.jumlah for x in kases)

        self.write({
            'saldo_begining': saldo_begining,
            'saldo_ending': saldo_ending,
            'saldo_current': saldo_current,
        })
        # else :
        #     self.env.cr.execute("select distinct on (kas_kategori_id) \
#         *, (select sum(cld.jumlah) from siswa_keu_ocb11_kas cld \
#         where kas_kategori_id = pr.kas_kategori_id \
#         and cld.tanggal >= '" + str(self.awal) + "' \
#         and cld.tanggal <= '" + str(self.akhir) + "' ) as total_jumlah \
#         from siswa_keu_ocb11_kas as pr \
#         where tanggal >= '" + str(self.awal) + "' and tanggal <= '" + str(self.akhir) + "'")
#     sum_kas = self.env.cr.fetchall()

#     pprint(sum_kas)

    def get_summary_data(self):
        self.env.cr.execute("select distinct on (kas_kategori_id) \
	            kas_kategori_id, kat.name as kategori, kat.tipe, (select sum(cld.jumlah) from siswa_keu_ocb11_kas cld \
		        where kas_kategori_id = pr.kas_kategori_id \
		        and cld.tanggal >= '" + str(self.awal) + "' \
		        and cld.tanggal <= '" + str(self.akhir) + "' ) as total_jumlah \
                from siswa_keu_ocb11_kas as pr \
                join siswa_keu_ocb11_kas_kategori as kat on pr.kas_kategori_id = kat.id \
                where tanggal >= '" + str(self.awal) + "' and tanggal <= '" +
                            str(self.akhir) + "'")
        sum_kas = self.env.cr.fetchall()
        # print('Loop on get summary data')
        # for sk in sum_kas:
        #     pprint(sk['total_jumlah'])

        return sum_kas

    def get_summary_pendapatan(self):
        self.env.cr.execute("select distinct on (kas_kategori_id) \
	            kas_kategori_id, kat.name as kategori, kat.tipe, (select sum(cld.jumlah) from siswa_keu_ocb11_kas cld \
		        where kas_kategori_id = pr.kas_kategori_id \
		        and cld.tanggal >= '" + str(self.awal) + "' \
		        and cld.tanggal <= '" + str(self.akhir) + "' ) as total_jumlah, \
                tanggal \
                from siswa_keu_ocb11_kas as pr \
                join siswa_keu_ocb11_kas_kategori as kat on pr.kas_kategori_id = kat.id \
                where kat.tipe = 'in' and \
                tanggal >= '" + str(self.awal) + "' and tanggal <= '" +
                            str(self.akhir) + "'")
        sum_pendapatan = self.env.cr.fetchall()

        return sum_pendapatan

    def get_summary_pengeluaran(self):
        self.env.cr.execute("select distinct on (kas_kategori_id) \
	            kas_kategori_id, kat.name as kategori, kat.tipe, (select sum(cld.jumlah) from siswa_keu_ocb11_kas cld \
		        where kas_kategori_id = pr.kas_kategori_id \
		        and cld.tanggal >= '" + str(self.awal) + "' \
		        and cld.tanggal <= '" + str(self.akhir) + "' ) as total_jumlah \
                from siswa_keu_ocb11_kas as pr \
                join siswa_keu_ocb11_kas_kategori as kat on pr.kas_kategori_id = kat.id \
                where kat.tipe = 'out' and \
                tanggal >= '" + str(self.awal) + "' and tanggal <= '" +
                            str(self.akhir) + "'")
        sum_pengeluaran = self.env.cr.fetchall()

        return sum_pengeluaran

    def action_print_kas(self):
        self.action_save()
        return self.env.ref('siswa_keu_ocb11.report_kas_action').report_action(
            self)

    def action_print_rekap(self):
        self.action_save()

        if self.tipe == 'sum':
            return self.env.ref(
                'siswa_keu_ocb11.report_rekap_kas_summary_action'
            ).report_action(self)
        else:
            return self.env.ref(
                'siswa_keu_ocb11.report_rekap_kas_action').report_action(self)
class OpResultTemplate(models.Model):
    _name = 'op.result.template'
    _inherit = ['mail.thread']
    _description = 'Result Template'
    _rec_name = 'name'

    exam_session_id = fields.Many2one(
        'op.exam.session', 'Exam Session',
        required=True, track_visibility='onchange')
    evaluation_type = fields.Selection(
        related='exam_session_id.evaluation_type',
        store=True, track_visibility='onchange')
    name = fields.Char("Name", size=254,
                       required=True, track_visibility='onchange')
    result_date = fields.Date(
        'Result Date', required=True,
        default=fields.Date.today(), track_visibility='onchange')
    grade_ids = fields.Many2many(
        'op.grade.configuration', string='Grade Configuration')
    state = fields.Selection(
        [('draft', 'Draft'), ('result_generated', 'Result Generated')],
        'State', default='draft', track_visibility='onchange')

    @api.multi
    @api.constrains('exam_session_id')
    def _check_exam_session(self):
        for record in self:
            for exam in record.exam_session_id.exam_ids:
                if exam.state != 'done':
                    raise ValidationError(
                        _('All subject exam should be done.'))

    @api.multi
    @api.constrains('grade_ids')
    def _check_min_max_per(self):
        for record in self:
            count = 0
            for grade in record.grade_ids:
                for sub_grade in record.grade_ids:
                    if grade != sub_grade:
                        if (sub_grade.min_per <= grade.min_per and
                                sub_grade.max_per >= grade.min_per) or \
                                (sub_grade.min_per <= grade.max_per and
                                 sub_grade.max_per >= grade.max_per):
                            count += 1
            if count > 0:
                raise ValidationError(
                    _('Percentage range conflict with other record.'))

    @api.multi
    def generate_result(self):
        for record in self:
            marksheet_reg_id = self.env['op.marksheet.register'].create({
                'name': 'Mark Sheet for %s' % record.exam_session_id.name,
                'exam_session_id': record.exam_session_id.id,
                'generated_date': fields.Date.today(),
                'generated_by': self.env.uid,
                'status': 'draft',
                'result_template_id': record.id
            })
            student_dict = {}
            for exam in record.exam_session_id.exam_ids:
                for attendee in exam.attendees_line:
                    result_line_id = self.env['op.result.line'].create({
                        'student_id': attendee.student_id.id,
                        'exam_id': exam.id,
                        'marks': str(attendee.marks and attendee.marks or 0),
                    })
                    if attendee.student_id.id not in student_dict:
                        student_dict[attendee.student_id.id] = []
                    student_dict[attendee.student_id.id].append(result_line_id)
            for student in student_dict:
                marksheet_line_id = self.env['op.marksheet.line'].create({
                    'student_id': student,
                    'marksheet_reg_id': marksheet_reg_id.id,
                })
                for result_line in student_dict[student]:
                    result_line.marksheet_line_id = marksheet_line_id
            record.state = 'result_generated'
Example #16
0
class Channel(models.Model):
    """ A mail.channel is a discussion group that may behave like a listener
    on documents. """
    _description = 'Discussion channel'
    _name = 'mail.channel'
    _mail_flat_thread = False
    _mail_post_access = 'read'
    _inherit = ['mail.thread', 'mail.alias.mixin']

    MAX_BOUNCE_LIMIT = 10

    def _get_default_image(self):
        image_path = modules.get_module_resource('mail', 'static/src/img',
                                                 'groupdefault.png')
        return tools.image_resize_image_big(
            base64.b64encode(open(image_path, 'rb').read()))

    @api.model
    def default_get(self, fields):
        res = super(Channel, self).default_get(fields)
        if not res.get('alias_contact') and (not fields
                                             or 'alias_contact' in fields):
            res['alias_contact'] = 'everyone' if res.get(
                'public', 'private') == 'public' else 'followers'
        return res

    name = fields.Char('Name', required=True, translate=True)
    channel_type = fields.Selection([('chat', 'Chat Discussion'),
                                     ('channel', 'Channel')],
                                    'Channel Type',
                                    default='channel')
    description = fields.Text('Description')
    uuid = fields.Char('UUID',
                       size=50,
                       index=True,
                       default=lambda self: str(uuid4()),
                       copy=False)
    email_send = fields.Boolean('Send messages by email', default=False)
    # multi users channel
    channel_last_seen_partner_ids = fields.One2many('mail.channel.partner',
                                                    'channel_id',
                                                    string='Last Seen')
    channel_partner_ids = fields.Many2many('res.partner',
                                           'mail_channel_partner',
                                           'channel_id',
                                           'partner_id',
                                           string='Listeners')
    channel_message_ids = fields.Many2many('mail.message',
                                           'mail_message_mail_channel_rel')
    is_member = fields.Boolean('Is a member', compute='_compute_is_member')
    # access
    public = fields.Selection(
        [('public', 'Everyone'), ('private', 'Invited people only'),
         ('groups', 'Selected group of users')],
        'Privacy',
        required=True,
        default='groups',
        help=
        'This group is visible by non members. Invisible groups can add members through the invite button.'
    )
    group_public_id = fields.Many2one(
        'res.groups',
        string='Authorized Group',
        default=lambda self: self.env.ref('base.group_user'))
    group_ids = fields.Many2many(
        'res.groups',
        string='Auto Subscription',
        help="Members of those groups will automatically added as followers. "
        "Note that they will be able to manage their subscription manually "
        "if necessary.")
    # image: all image fields are base64 encoded and PIL-supported
    image = fields.Binary(
        "Photo",
        default=_get_default_image,
        attachment=True,
        help=
        "This field holds the image used as photo for the group, limited to 1024x1024px."
    )
    image_medium = fields.Binary(
        'Medium-sized photo',
        attachment=True,
        help="Medium-sized photo of the group. 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 photo',
        attachment=True,
        help="Small-sized photo of the group. It is automatically "
        "resized as a 64x64px image, with aspect ratio preserved. "
        "Use this field anywhere a small image is required.")
    is_subscribed = fields.Boolean('Is Subscribed',
                                   compute='_compute_is_subscribed')

    @api.one
    @api.depends('channel_partner_ids')
    def _compute_is_subscribed(self):
        self.is_subscribed = self.env.user.partner_id in self.channel_partner_ids

    @api.multi
    def _compute_is_member(self):
        memberships = self.env['mail.channel.partner'].sudo().search([
            ('channel_id', 'in', self.ids),
            ('partner_id', '=', self.env.user.partner_id.id),
        ])
        membership_ids = memberships.mapped('channel_id')
        for record in self:
            record.is_member = record in membership_ids

    @api.onchange('public')
    def _onchange_public(self):
        if self.public == 'public':
            self.alias_contact = 'everyone'
        else:
            self.alias_contact = 'followers'

    @api.model
    def create(self, vals):
        tools.image_resize_images(vals)
        # Create channel and alias
        channel = super(
            Channel,
            self.with_context(alias_model_name=self._name,
                              alias_parent_model_name=self._name,
                              mail_create_nolog=True,
                              mail_create_nosubscribe=True)).create(vals)
        channel.alias_id.write({
            "alias_force_thread_id": channel.id,
            'alias_parent_thread_id': channel.id
        })

        if vals.get('group_ids'):
            channel._subscribe_users()

        # make channel listen itself: posting on a channel notifies the channel
        if not self._context.get('mail_channel_noautofollow'):
            channel.message_subscribe(channel_ids=[channel.id])

        return channel

    @api.multi
    def unlink(self):
        aliases = self.mapped('alias_id')

        # Delete mail.channel
        try:
            all_emp_group = self.env.ref('mail.channel_all_employees')
        except ValueError:
            all_emp_group = None
        if all_emp_group and all_emp_group in self:
            raise UserError(
                _('You cannot delete those groups, as the Whole Company group is required by other modules.'
                  ))
        res = super(Channel, self).unlink()
        # Cascade-delete mail aliases as well, as they should not exist without the mail.channel.
        aliases.sudo().unlink()
        return res

    @api.multi
    def write(self, vals):
        tools.image_resize_images(vals)
        result = super(Channel, self).write(vals)
        if vals.get('group_ids'):
            self._subscribe_users()
        return result

    def get_alias_model_name(self, vals):
        return vals.get('alias_model', 'mail.channel')

    def _subscribe_users(self):
        for mail_channel in self:
            mail_channel.write({
                'channel_partner_ids':
                [(4, pid) for pid in mail_channel.mapped('group_ids').mapped(
                    'users').mapped('partner_id').ids]
            })

    @api.multi
    def action_follow(self):
        self.ensure_one()
        channel_partner = self.mapped(
            'channel_last_seen_partner_ids').filtered(
                lambda cp: cp.partner_id == self.env.user.partner_id)
        if not channel_partner:
            return self.write({
                'channel_last_seen_partner_ids': [(0, 0, {
                    'partner_id':
                    self.env.user.partner_id.id
                })]
            })

    @api.multi
    def action_unfollow(self):
        return self._action_unfollow(self.env.user.partner_id)

    @api.multi
    def _action_unfollow(self, partner):
        channel_info = self.channel_info('unsubscribe')[
            0]  # must be computed before leaving the channel (access rights)
        result = self.write({'channel_partner_ids': [(3, partner.id)]})
        self.env['bus.bus'].sendone(
            (self._cr.dbname, 'res.partner', partner.id), channel_info)
        if not self.email_send:
            notification = _(
                '<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>'
            ) % (
                self.id,
                self.name,
            )
            # post 'channel left' message as root since the partner just unsubscribed from the channel
            self.sudo().message_post(body=notification,
                                     message_type="notification",
                                     subtype="mail.mt_comment",
                                     author_id=partner.id)
        return result

    @api.multi
    def _notification_recipients(self, message, groups):
        """ All recipients of a message on a channel are considered as partners.
        This means they will receive a minimal email, without a link to access
        in the backend. Mailing lists should indeed send minimal emails to avoid
        the noise. """
        groups = super(Channel, self)._notification_recipients(message, groups)
        for (index, (group_name, group_func, group_data)) in enumerate(groups):
            if group_name != 'customer':
                groups[index] = (group_name, lambda partner: False, group_data)
        return groups

    @api.multi
    def message_get_email_values(self, notif_mail=None):
        self.ensure_one()
        res = super(Channel,
                    self).message_get_email_values(notif_mail=notif_mail)
        headers = {}
        if res.get('headers'):
            try:
                headers.update(safe_eval(res['headers']))
            except Exception:
                pass
        headers['Precedence'] = 'list'
        # avoid out-of-office replies from MS Exchange
        # http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx
        headers['X-Auto-Response-Suppress'] = 'OOF'
        if self.alias_domain and self.alias_name:
            headers['List-Id'] = '<%s.%s>' % (self.alias_name,
                                              self.alias_domain)
            headers['List-Post'] = '<mailto:%s@%s>' % (self.alias_name,
                                                       self.alias_domain)
            # Avoid users thinking it was a personal message
            # X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server
            list_to = '"%s" <%s@%s>' % (self.name, self.alias_name,
                                        self.alias_domain)
            headers['X-Forge-To'] = list_to
        res['headers'] = repr(headers)
        return res

    @api.multi
    def message_receive_bounce(self, email, partner, mail_id=None):
        """ Override bounce management to unsubscribe bouncing addresses """
        for p in partner:
            if p.message_bounce >= self.MAX_BOUNCE_LIMIT:
                self._action_unfollow(p)
        return super(Channel, self).message_receive_bounce(email,
                                                           partner,
                                                           mail_id=mail_id)

    @api.multi
    def message_get_recipient_values(self,
                                     notif_message=None,
                                     recipient_ids=None):
        # real mailing list: multiple recipients (hidden by X-Forge-To)
        if self.alias_domain and self.alias_name:
            return {
                'email_to':
                ','.join(
                    formataddr((partner.name, partner.email)) for partner in
                    self.env['res.partner'].sudo().browse(recipient_ids)),
                'recipient_ids': [],
            }
        return super(Channel, self).message_get_recipient_values(
            notif_message=notif_message, recipient_ids=recipient_ids)

    @api.multi
    @api.returns('self', lambda value: value.id)
    def message_post(self,
                     body='',
                     subject=None,
                     message_type='notification',
                     subtype=None,
                     parent_id=False,
                     attachments=None,
                     content_subtype='html',
                     **kwargs):
        # auto pin 'direct_message' channel partner
        self.filtered(lambda channel: channel.channel_type == 'chat').mapped(
            'channel_last_seen_partner_ids').write({'is_pinned': True})
        message = super(
            Channel,
            self.with_context(mail_create_nosubscribe=True)).message_post(
                body=body,
                subject=subject,
                message_type=message_type,
                subtype=subtype,
                parent_id=parent_id,
                attachments=attachments,
                content_subtype=content_subtype,
                **kwargs)
        return message

    def _alias_check_contact(self, message, message_dict, alias):
        if alias.alias_contact == 'followers' and self.ids:
            author = self.env['res.partner'].browse(
                message_dict.get('author_id', False))
            if not author or author not in self.channel_partner_ids:
                return {
                    'error_message': _('restricted to channel members'),
                }
            return True
        return super(Channel,
                     self)._alias_check_contact(message, message_dict, alias)

    @api.model_cr
    def init(self):
        self._cr.execute(
            'SELECT indexname FROM pg_indexes WHERE indexname = %s',
            ('mail_channel_partner_seen_message_id_idx', ))
        if not self._cr.fetchone():
            self._cr.execute(
                'CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)'
            )

    #------------------------------------------------------
    # Instant Messaging API
    #------------------------------------------------------
    # A channel header should be broadcasted:
    #   - when adding user to channel (only to the new added partners)
    #   - when folding/minimizing a channel (only to the user making the action)
    # A message should be broadcasted:
    #   - when a message is posted on a channel (to the channel, using _notify() method)

    # Anonymous method
    @api.multi
    def _broadcast(self, partner_ids):
        """ Broadcast the current channel header to the given partner ids
            :param partner_ids : the partner to notify
        """
        notifications = self._channel_channel_notifications(partner_ids)
        self.env['bus.bus'].sendmany(notifications)

    @api.multi
    def _channel_channel_notifications(self, partner_ids):
        """ Generate the bus notifications of current channel for the given partner ids
            :param partner_ids : the partner to send the current channel header
            :returns list of bus notifications (tuple (bus_channe, message_content))
        """
        notifications = []
        for partner in self.env['res.partner'].browse(partner_ids):
            user_id = partner.user_ids and partner.user_ids[0] or False
            if user_id:
                for channel_info in self.sudo(user_id).channel_info():
                    notifications.append([(self._cr.dbname, 'res.partner',
                                           partner.id), channel_info])
        return notifications

    @api.multi
    def _notify(self, message):
        """ Broadcast the given message on the current channels.
            Send the message on the Bus Channel (uuid for public mail.channel, and partner private bus channel (the tuple)).
            A partner will receive only on message on its bus channel, even if this message belongs to multiple mail channel. Then 'channel_ids' field
            of the received message indicates on wich mail channel the message should be displayed.
            :param : mail.message to broadcast
        """
        if not self:
            return
        message.ensure_one()
        notifications = self._channel_message_notifications(message)
        self.env['bus.bus'].sendmany(notifications)

    @api.multi
    def _channel_message_notifications(self, message):
        """ Generate the bus notifications for the given message
            :param message : the mail.message to sent
            :returns list of bus notifications (tuple (bus_channe, message_content))
        """
        message_values = message.message_format()[0]
        notifications = []
        for channel in self:
            notifications.append([(self._cr.dbname, 'mail.channel',
                                   channel.id),
                                  dict(message_values)])
            # add uuid to allow anonymous to listen
            if channel.public == 'public':
                notifications.append([channel.uuid, dict(message_values)])
        return notifications

    @api.multi
    def channel_info(self, extra_info=False):
        """ Get the informations header for the current channels
            :returns a list of channels values
            :rtype : list(dict)
        """
        channel_infos = []
        partner_channels = self.env['mail.channel.partner']
        # find the channel partner state, if logged user
        if self.env.user and self.env.user.partner_id:
            partner_channels = self.env['mail.channel.partner'].search([
                ('partner_id', '=', self.env.user.partner_id.id),
                ('channel_id', 'in', self.ids)
            ])
        # for each channel, build the information header and include the logged partner information
        for channel in self:
            info = {
                'id': channel.id,
                'name': channel.name,
                'uuid': channel.uuid,
                'state': 'open',
                'is_minimized': False,
                'channel_type': channel.channel_type,
                'public': channel.public,
                'mass_mailing': channel.email_send,
                'group_based_subscription': bool(channel.group_ids),
            }
            if extra_info:
                info['info'] = extra_info
            # add the partner for 'direct mesage' channel
            if channel.channel_type == 'chat':
                info['direct_partner'] = (channel.sudo().with_context(
                    active_test=False).channel_partner_ids.filtered(
                        lambda p: p.id != self.env.user.partner_id.id).read(
                            ['id', 'name', 'im_status']))

            # add last message preview (only used in mobile)
            if self._context.get('isMobile', False):
                last_message = channel.channel_fetch_preview()
                if last_message:
                    info['last_message'] = last_message[0].get('last_message')

            # add user session state, if available and if user is logged
            if partner_channels.ids:
                partner_channel = partner_channels.filtered(
                    lambda c: channel.id == c.channel_id.id)
                if len(partner_channel) >= 1:
                    partner_channel = partner_channel[0]
                    info['state'] = partner_channel.fold_state or 'open'
                    info['is_minimized'] = partner_channel.is_minimized
                    info[
                        'seen_message_id'] = partner_channel.seen_message_id.id
                # add needaction and unread counter, since the user is logged
                info[
                    'message_needaction_counter'] = channel.message_needaction_counter
                info['message_unread_counter'] = channel.message_unread_counter
            channel_infos.append(info)
        return channel_infos

    @api.multi
    def channel_fetch_message(self, last_id=False, limit=20):
        """ Return message values of the current channel.
            :param last_id : last message id to start the research
            :param limit : maximum number of messages to fetch
            :returns list of messages values
            :rtype : list(dict)
        """
        self.ensure_one()
        domain = [("channel_ids", "in", self.ids)]
        if last_id:
            domain.append(("id", "<", last_id))
        return self.env['mail.message'].message_fetch(domain=domain,
                                                      limit=limit)

    # User methods
    @api.model
    def channel_get(self, partners_to, pin=True):
        """ Get the canonical private channel between some partners, create it if needed.
            To reuse an old channel (conversation), this one must be private, and contains
            only the given partners.
            :param partners_to : list of res.partner ids to add to the conversation
            :param pin : True if getting the channel should pin it for the current user
            :returns a channel header, or False if the users_to was False
            :rtype : dict
        """
        if partners_to:
            partners_to.append(self.env.user.partner_id.id)
            # determine type according to the number of partner in the channel
            self.env.cr.execute(
                """
                SELECT P.channel_id as channel_id
                FROM mail_channel C, mail_channel_partner P
                WHERE P.channel_id = C.id
                    AND C.public LIKE 'private'
                    AND P.partner_id IN %s
                    AND channel_type LIKE 'chat'
                GROUP BY P.channel_id
                HAVING array_agg(P.partner_id ORDER BY P.partner_id) = %s
            """, (
                    tuple(partners_to),
                    sorted(list(partners_to)),
                ))
            result = self.env.cr.dictfetchall()
            if result:
                # get the existing channel between the given partners
                channel = self.browse(result[0].get('channel_id'))
                # pin up the channel for the current partner
                if pin:
                    self.env['mail.channel.partner'].search([
                        ('partner_id', '=', self.env.user.partner_id.id),
                        ('channel_id', '=', channel.id)
                    ]).write({'is_pinned': True})
            else:
                # create a new one
                channel = self.create({
                    'channel_partner_ids':
                    [(4, partner_id) for partner_id in partners_to],
                    'public':
                    'private',
                    'channel_type':
                    'chat',
                    'email_send':
                    False,
                    'name':
                    ', '.join(self.env['res.partner'].sudo().browse(
                        partners_to).mapped('name')),
                })
                # broadcast the channel header to the other partner (not me)
                channel._broadcast(partners_to)
            return channel.channel_info()[0]
        return False

    @api.model
    def channel_get_and_minimize(self, partners_to):
        channel = self.channel_get(partners_to)
        if channel:
            self.channel_minimize(channel['uuid'])
        return channel

    @api.model
    def channel_fold(self, uuid, state=None):
        """ Update the fold_state of the given session. In order to syncronize web browser
            tabs, the change will be broadcast to himself (the current user channel).
            Note: the user need to be logged
            :param state : the new status of the session for the current user.
        """
        domain = [('partner_id', '=', self.env.user.partner_id.id),
                  ('channel_id.uuid', '=', uuid)]
        for session_state in self.env['mail.channel.partner'].search(domain):
            if not state:
                state = session_state.fold_state
                if session_state.fold_state == 'open':
                    state = 'folded'
                else:
                    state = 'open'
            session_state.write({
                'fold_state': state,
                'is_minimized': bool(state != 'closed'),
            })
            self.env['bus.bus'].sendone(
                (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
                session_state.channel_id.channel_info()[0])

    @api.model
    def channel_minimize(self, uuid, minimized=True):
        values = {
            'fold_state': minimized and 'open' or 'closed',
            'is_minimized': minimized
        }
        domain = [('partner_id', '=', self.env.user.partner_id.id),
                  ('channel_id.uuid', '=', uuid)]
        channel_partners = self.env['mail.channel.partner'].search(domain,
                                                                   limit=1)
        channel_partners.write(values)
        self.env['bus.bus'].sendone(
            (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
            channel_partners.channel_id.channel_info()[0])

    @api.model
    def channel_pin(self, uuid, pinned=False):
        # add the person in the channel, and pin it (or unpin it)
        channel = self.search([('uuid', '=', uuid)])
        channel_partners = self.env['mail.channel.partner'].search([
            ('partner_id', '=', self.env.user.partner_id.id),
            ('channel_id', '=', channel.id)
        ])
        if not pinned:
            self.env['bus.bus'].sendone(
                (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
                channel.channel_info('unsubscribe')[0])
        if channel_partners:
            channel_partners.write({'is_pinned': pinned})

    @api.multi
    def channel_seen(self):
        self.ensure_one()
        if self.channel_message_ids.ids:
            last_message_id = self.channel_message_ids.ids[
                0]  # zero is the index of the last message
            self.env['mail.channel.partner'].search([
                ('channel_id', 'in', self.ids),
                ('partner_id', '=', self.env.user.partner_id.id)
            ]).write({'seen_message_id': last_message_id})
            self.env['bus.bus'].sendone(
                (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
                {
                    'info': 'channel_seen',
                    'id': self.id,
                    'last_message_id': last_message_id
                })
            return last_message_id

    @api.multi
    def channel_invite(self, partner_ids):
        """ Add the given partner_ids to the current channels and broadcast the channel header to them.
            :param partner_ids : list of partner id to add
        """
        partners = self.env['res.partner'].browse(partner_ids)
        # add the partner
        for channel in self:
            partners_to_add = partners - channel.channel_partner_ids
            channel.write({
                'channel_last_seen_partner_ids': [(0, 0, {
                    'partner_id': partner_id
                }) for partner_id in partners_to_add.ids]
            })
            for partner in partners_to_add:
                if partner.id != self.env.user.partner_id.id:
                    notification = _(
                        '<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>'
                    ) % {
                        'author': self.env.user.display_name,
                        'new_partner': partner.display_name,
                        'channel_id': channel.id,
                        'channel_name': channel.name,
                    }
                else:
                    notification = _(
                        '<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>'
                    ) % (
                        channel.id,
                        channel.name,
                    )
                self.message_post(body=notification,
                                  message_type="notification",
                                  subtype="mail.mt_comment",
                                  author_id=partner.id)

        # broadcast the channel header to the added partner
        self._broadcast(partner_ids)

    #------------------------------------------------------
    # Instant Messaging View Specific (Slack Client Action)
    #------------------------------------------------------
    @api.model
    def channel_fetch_slot(self):
        """ Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and
            the mapping between partner_id/channel_id for direct_message channels.
            :returns dict : the grouped channels and the mapping
        """
        values = {}
        my_partner_id = self.env.user.partner_id.id
        pinned_channels = self.env['mail.channel.partner'].search([
            ('partner_id', '=', my_partner_id), ('is_pinned', '=', True)
        ]).mapped('channel_id')

        # get the group/public channels
        values['channel_channel'] = self.search([
            ('channel_type', '=', 'channel'),
            ('public', 'in', ['public', 'groups']),
            ('channel_partner_ids', 'in', [my_partner_id])
        ]).channel_info()

        # get the pinned 'direct message' channel
        direct_message_channels = self.search([('channel_type', '=', 'chat'),
                                               ('id', 'in',
                                                pinned_channels.ids)])
        values[
            'channel_direct_message'] = direct_message_channels.channel_info()

        # get the private group
        values['channel_private_group'] = self.search([
            ('channel_type', '=', 'channel'), ('public', '=', 'private'),
            ('channel_partner_ids', 'in', [my_partner_id])
        ]).channel_info()
        return values

    @api.model
    def channel_search_to_join(self, name=None, domain=None):
        """ Return the channel info of the channel the current partner can join
            :param name : the name of the researched channels
            :param domain : the base domain of the research
            :returns dict : channel dict
        """
        if not domain:
            domain = []
        domain = expression.AND([[('channel_type', '=', 'channel')],
                                 [('channel_partner_ids', 'not in',
                                   [self.env.user.partner_id.id])],
                                 [('public', '!=', 'private')], domain])
        if name:
            domain = expression.AND(
                [domain, [('name', 'ilike', '%' + name + '%')]])
        return self.search(domain).read(
            ['name', 'public', 'uuid', 'channel_type'])

    @api.multi
    def channel_join_and_get_info(self):
        self.ensure_one()
        if self.channel_type == 'channel' and not self.email_send:
            notification = _(
                '<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>'
            ) % (
                self.id,
                self.name,
            )
            self.message_post(body=notification,
                              message_type="notification",
                              subtype="mail.mt_comment")
        self.action_follow()

        channel_info = self.channel_info()[0]
        self.env['bus.bus'].sendone(
            (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
            channel_info)
        return channel_info

    @api.model
    def channel_create(self, name, privacy='public'):
        """ Create a channel and add the current partner, broadcast it (to make the user directly
            listen to it when polling)
            :param name : the name of the channel to create
            :param privacy : privacy of the channel. Should be 'public' or 'private'.
            :return dict : channel header
        """
        # create the channel
        new_channel = self.create({
            'name':
            name,
            'public':
            privacy,
            'email_send':
            False,
            'channel_partner_ids': [(4, self.env.user.partner_id.id)]
        })
        notification = _(
            '<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>'
        ) % (
            new_channel.id,
            new_channel.name,
        )
        new_channel.message_post(body=notification,
                                 message_type="notification",
                                 subtype="mail.mt_comment")
        channel_info = new_channel.channel_info('creation')[0]
        self.env['bus.bus'].sendone(
            (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
            channel_info)
        return channel_info

    @api.model
    def get_mention_suggestions(self, search, limit=8):
        """ Return 'limit'-first channels' id, name and public fields such that the name matches a
            'search' string. Exclude channels of type chat (DM), and private channels the current
            user isn't registered to. """
        domain = expression.AND([[('name', 'ilike', search)],
                                 [('channel_type', '=', 'channel')],
                                 expression.OR(
                                     [[('public', '!=', 'private')],
                                      [('channel_partner_ids', 'in',
                                        [self.env.user.partner_id.id])]])])
        return self.search_read(domain, ['id', 'name', 'public'], limit=limit)

    @api.model
    def channel_fetch_listeners(self, uuid):
        """ Return the id, name and email of partners listening to the given channel """
        self._cr.execute(
            """
            SELECT P.id, P.name, P.email
            FROM mail_channel_partner CP
                INNER JOIN res_partner P ON CP.partner_id = P.id
                INNER JOIN mail_channel C ON CP.channel_id = C.id
            WHERE C.uuid = %s""", (uuid, ))
        return self._cr.dictfetchall()

    @api.multi
    def channel_fetch_preview(self):
        """ Return the last message of the given channels """
        self._cr.execute(
            """
            SELECT mail_channel_id AS id, MAX(mail_message_id) AS message_id
            FROM mail_message_mail_channel_rel
            WHERE mail_channel_id IN %s
            GROUP BY mail_channel_id
            """, (tuple(self.ids), ))
        channels_preview = dict(
            (r['message_id'], r) for r in self._cr.dictfetchall())
        last_messages = self.env['mail.message'].browse(
            channels_preview).message_format()
        for message in last_messages:
            channel = channels_preview[message['id']]
            del (channel['message_id'])
            channel['last_message'] = message
        return list(channels_preview.values())

    #------------------------------------------------------
    # Commands
    #------------------------------------------------------
    @api.model
    @ormcache()
    def get_mention_commands(self):
        """ Returns the allowed commands in channels """
        commands = []
        for n in dir(self):
            match = re.search('^_define_command_(.+?)$', n)
            if match:
                command = getattr(self, n)()
                command['name'] = match.group(1)
                commands.append(command)
        return commands

    @api.multi
    def execute_command(self, command='', **kwargs):
        """ Executes a given command """
        self.ensure_one()
        command_callback = getattr(self, '_execute_command_' + command, False)
        if command_callback:
            command_callback(**kwargs)

    def _send_transient_message(self, partner_to, content):
        """ Notifies partner_to that a message (not stored in DB) has been
            written in this channel """
        self.env['bus.bus'].sendone(
            (self._cr.dbname, 'res.partner', partner_to.id), {
                'body':
                "<span class='o_mail_notification'>" + content + "</span>",
                'channel_ids': [self.id],
                'info': 'transient_message',
            })

    def _define_command_help(self):
        return {'help': _("Show an helper message")}

    def _execute_command_help(self, **kwargs):
        partner = self.env.user.partner_id
        if self.channel_type == 'channel':
            msg = _("You are in channel <b>#%s</b>.") % self.name
            if self.public == 'private':
                msg += _(
                    " This channel is private. People must be invited to join it."
                )
        else:
            channel_partners = self.env['mail.channel.partner'].search([
                ('partner_id', '!=', partner.id), ('channel_id', '=', self.id)
            ])
            msg = _("You are in a private conversation with <b>@%s</b>.") % (
                channel_partners[0].partner_id.name
                if channel_partners else _('Anonymous'))
        msg += _("""<br><br>
            You can mention someone by typing <b>@username</b>, this will grab its attention.<br>
            You can mention a channel by typing <b>#channel</b>.<br>
            You can execute a command by typing <b>/command</b>.<br>
            You can insert canned responses in your message by typing <b>:shortcut</b>.<br>"""
                 )

        self._send_transient_message(partner, msg)

    def _define_command_leave(self):
        return {'help': _("Leave this channel")}

    def _execute_command_leave(self, **kwargs):
        if self.channel_type == 'channel':
            self.action_unfollow()
        else:
            self.channel_pin(self.uuid, False)

    def _define_command_who(self):
        return {
            'channel_types': ['channel', 'chat'],
            'help': _("List users in the current channel")
        }

    def _execute_command_who(self, **kwargs):
        partner = self.env.user.partner_id
        members = [
            '<a href="#" data-oe-id=' + str(p.id) +
            ' data-oe-model="res.partner">@' + p.name + '</a>'
            for p in self.channel_partner_ids[:30] if p != partner
        ]
        if len(members) == 0:
            msg = _("You are alone in this channel.")
        else:
            dots = "..." if len(members) != len(
                self.channel_partner_ids) - 1 else ""
            msg = _("Users in this channel: %s %s and you.") % (
                ", ".join(members), dots)

        self._send_transient_message(partner, msg)
Example #17
0
class StockPicking(models.Model):
    _inherit = 'stock.picking'

    def _default_uom(self):
        weight_uom_id = self.env.ref('product.product_uom_kgm',
                                     raise_if_not_found=False)
        if not weight_uom_id:
            uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id
            weight_uom_id = self.env['product.uom'].search(
                [('category_id', '=', uom_categ_id), ('factor', '=', 1)],
                limit=1)
        return weight_uom_id

    @api.one
    @api.depends('move_line_ids')
    def _compute_packages(self):
        self.ensure_one()
        packs = set()
        for move_line in self.move_line_ids:
            if move_line.result_package_id:
                packs.add(move_line.result_package_id.id)
        self.package_ids = list(packs)

    @api.one
    @api.depends('move_line_ids')
    def _compute_bulk_weight(self):
        weight = 0.0
        for move_line in self.move_line_ids:
            if move_line.product_id and not move_line.result_package_id:
                weight += move_line.product_uom_id._compute_quantity(
                    move_line.qty_done,
                    move_line.product_id.uom_id) * move_line.product_id.weight
        self.weight_bulk = weight

    @api.one
    @api.depends('package_ids', 'weight_bulk')
    def _compute_shipping_weight(self):
        self.shipping_weight = self.weight_bulk + sum(
            [pack.shipping_weight for pack in self.package_ids])

    carrier_price = fields.Float(string="Shipping Cost")
    delivery_type = fields.Selection(related='carrier_id.delivery_type',
                                     readonly=True)
    carrier_id = fields.Many2one("delivery.carrier", string="Carrier")
    volume = fields.Float(copy=False)
    weight = fields.Float(compute='_cal_weight',
                          digits=dp.get_precision('Stock Weight'),
                          store=True)
    carrier_tracking_ref = fields.Char(string='Tracking Reference', copy=False)
    carrier_tracking_url = fields.Char(string='Tracking URL',
                                       compute='_compute_carrier_tracking_url')
    number_of_packages = fields.Integer(string='Number of Packages',
                                        copy=False)
    weight_uom_id = fields.Many2one('product.uom',
                                    string='Unit of Measure',
                                    required=True,
                                    readonly="1",
                                    help="Unit of measurement for Weight",
                                    default=_default_uom)
    package_ids = fields.Many2many('stock.quant.package',
                                   compute='_compute_packages',
                                   string='Packages')
    weight_bulk = fields.Float('Bulk Weight', compute='_compute_bulk_weight')
    shipping_weight = fields.Float("Weight for Shipping",
                                   compute='_compute_shipping_weight')

    @api.depends('carrier_id', 'carrier_tracking_ref')
    def _compute_carrier_tracking_url(self):
        for picking in self:
            picking.carrier_tracking_url = picking.carrier_id.get_tracking_link(
                picking
            ) if picking.carrier_id and picking.carrier_tracking_ref else False

    @api.depends('product_id', 'move_lines')
    def _cal_weight(self):
        for picking in self:
            picking.weight = sum(move.weight for move in picking.move_lines
                                 if move.state != 'cancel')

    @api.multi
    def do_transfer(self):
        # TDE FIXME: should work in batch
        self.ensure_one()
        res = super(StockPicking, self).do_transfer()

        if self.carrier_id and self.carrier_id.integration_level == 'rate_and_ship':
            self.send_to_shipper()

        if self.carrier_id:
            self._add_delivery_cost_to_so()

        return res

    @api.multi
    def put_in_pack(self):
        if self.carrier_id and self.carrier_id.delivery_type not in [
                'base_on_rule', 'fixed'
        ]:
            view_id = self.env.ref(
                'delivery.choose_delivery_package_view_form').id
            return {
                'name': _('Package Details'),
                'type': 'ir.actions.act_window',
                'view_mode': 'form',
                'res_model': 'choose.delivery.package',
                'view_id': view_id,
                'views': [(view_id, 'form')],
                'target': 'new',
                'context': {
                    'current_package_carrier_type':
                    self.carrier_id.delivery_type,
                }
            }
        else:
            return self._put_in_pack()

    @api.multi
    def action_send_confirmation_email(self):
        self.ensure_one()
        delivery_template_id = self.env.ref(
            'delivery.mail_template_data_delivery_confirmation').id
        compose_form_id = self.env.ref(
            'mail.email_compose_message_wizard_form').id
        ctx = dict(
            default_composition_mode='comment',
            default_res_id=self.id,
            default_model='stock.picking',
            default_use_template=bool(delivery_template_id),
            default_template_id=delivery_template_id,
            custom_layout='delivery.mail_template_data_delivery_notification')
        return {
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'mail.compose.message',
            'view_id': compose_form_id,
            'target': 'new',
            'context': ctx,
        }

    @api.multi
    def send_to_shipper(self):
        self.ensure_one()
        res = self.carrier_id.send_shipping(self)[0]
        self.carrier_price = res['exact_price']
        self.carrier_tracking_ref = res['tracking_number']
        order_currency = self.sale_id.currency_id or self.company_id.currency_id
        msg = _(
            "Shipment sent to carrier %s for shipping with tracking number %s<br/>Cost: %.2f %s"
        ) % (self.carrier_id.name, self.carrier_tracking_ref,
             self.carrier_price, order_currency.name)
        self.message_post(body=msg)

    @api.multi
    def _add_delivery_cost_to_so(self):
        self.ensure_one()
        sale_order = self.sale_id
        if sale_order.invoice_shipping_on_delivery:
            sale_order._create_delivery_line(self.carrier_id,
                                             self.carrier_price)

    @api.multi
    def open_website_url(self):
        self.ensure_one()
        if not self.carrier_tracking_url:
            raise UserError(
                _("Your delivery method has no redirect on courier provider's website to track this order."
                  ))

        client_action = {
            'type': 'ir.actions.act_url',
            'name': "Shipment Tracking Page",
            'target': 'new',
            'url': self.carrier_tracking_url,
        }
        return client_action

    @api.one
    def cancel_shipment(self):
        self.carrier_id.cancel_shipment(self)
        msg = "Shipment %s cancelled" % self.carrier_tracking_ref
        self.message_post(body=msg)
        self.carrier_tracking_ref = False

    @api.multi
    def check_packages_are_identical(self):
        '''Some shippers require identical packages in the same shipment. This utility checks it.'''
        self.ensure_one()
        if self.package_ids:
            packages = [p.packaging_id for p in self.package_ids]
            if len(set(packages)) != 1:
                package_names = ', '.join([str(p.name) for p in packages])
                raise UserError(
                    _('You are shipping different packaging types in the same shipment.\nPackaging Types: %s'
                      % package_names))
        return True
Example #18
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',
        }
Example #19
0
class MassMailing(models.Model):
    """ MassMailing models a wave of emails for a mass mailign campaign.
    A mass mailing is an occurence of sending emails. """
    _name = 'mailing.mailing'
    _description = 'Mass Mailing'
    _inherit = ['mail.thread', 'mail.activity.mixin', 'mail.render.mixin']
    _order = 'sent_date DESC'
    _inherits = {'utm.source': 'source_id'}
    _rec_name = "subject"

    @api.model
    def default_get(self, fields):
        vals = super(MassMailing, self).default_get(fields)
        if 'contact_list_ids' in fields and not vals.get(
                'contact_list_ids') and vals.get('mailing_model_id'):
            if vals.get('mailing_model_id') == self.env['ir.model']._get(
                    'mailing.list').id:
                mailing_list = self.env['mailing.list'].search([], limit=2)
                if len(mailing_list) == 1:
                    vals['contact_list_ids'] = [(6, 0, [mailing_list.id])]
        return vals

    @api.model
    def _get_default_mail_server_id(self):
        server_id = self.env['ir.config_parameter'].sudo().get_param(
            'mass_mailing.mail_server_id')
        try:
            server_id = literal_eval(server_id) if server_id else False
            return self.env['ir.mail_server'].search([('id', '=', server_id)
                                                      ]).id
        except ValueError:
            return False

    active = fields.Boolean(default=True, tracking=True)
    subject = fields.Char('Subject',
                          help='Subject of your Mailing',
                          required=True,
                          translate=True)
    preview = fields.Char(
        'Preview',
        translate=True,
        help=
        'Catchy preview sentence that encourages recipients to open this email.\n'
        'In most inboxes, this is displayed next to the subject.\n'
        'Keep it empty if you prefer the first characters of your email content to appear instead.'
    )
    email_from = fields.Char(
        string='Send From',
        required=True,
        default=lambda self: self.env.user.email_formatted)
    sent_date = fields.Datetime(string='Sent Date', copy=False)
    schedule_date = fields.Datetime(string='Scheduled for', tracking=True)
    # don't translate 'body_arch', the translations are only on 'body_html'
    body_arch = fields.Html(string='Body', translate=False)
    body_html = fields.Html(string='Body converted to be sent by mail',
                            sanitize_attributes=False)
    attachment_ids = fields.Many2many('ir.attachment',
                                      'mass_mailing_ir_attachments_rel',
                                      'mass_mailing_id',
                                      'attachment_id',
                                      string='Attachments')
    keep_archives = fields.Boolean(string='Keep Archives')
    campaign_id = fields.Many2one('utm.campaign',
                                  string='UTM Campaign',
                                  index=True)
    source_id = fields.Many2one(
        'utm.source',
        string='Source',
        required=True,
        ondelete='cascade',
        help=
        "This is the link source, e.g. Search Engine, another domain, or name of email list"
    )
    medium_id = fields.Many2one(
        'utm.medium',
        string='Medium',
        compute='_compute_medium_id',
        readonly=False,
        store=True,
        help="UTM Medium: delivery method (email, sms, ...)")
    state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'),
                              ('sending', 'Sending'), ('done', 'Sent')],
                             string='Status',
                             required=True,
                             tracking=True,
                             copy=False,
                             default='draft',
                             group_expand='_group_expand_states')
    color = fields.Integer(string='Color Index')
    user_id = fields.Many2one('res.users',
                              string='Responsible',
                              tracking=True,
                              default=lambda self: self.env.user)
    # mailing options
    mailing_type = fields.Selection([('mail', 'Email')],
                                    string="Mailing Type",
                                    default="mail",
                                    required=True)
    reply_to_mode = fields.Selection(
        [('thread', 'Recipient Followers'),
         ('email', 'Specified Email Address')],
        string='Reply-To Mode',
        compute='_compute_reply_to_mode',
        readonly=False,
        store=True,
        help=
        'Thread: replies go to target document. Email: replies are routed to a given email.'
    )
    reply_to = fields.Char(string='Reply To',
                           compute='_compute_reply_to',
                           readonly=False,
                           store=True,
                           help='Preferred Reply-To Address')
    # recipients
    mailing_model_real = fields.Char(string='Recipients Real Model',
                                     compute='_compute_model')
    mailing_model_id = fields.Many2one(
        'ir.model',
        string='Recipients Model',
        ondelete='cascade',
        required=True,
        domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
        default=lambda self: self.env.ref('mass_mailing.model_mailing_list'
                                          ).id)
    mailing_model_name = fields.Char(string='Recipients Model Name',
                                     related='mailing_model_id.model',
                                     readonly=True,
                                     related_sudo=True)
    mailing_domain = fields.Char(string='Domain',
                                 compute='_compute_mailing_domain',
                                 readonly=False,
                                 store=True)
    mail_server_id = fields.Many2one(
        'ir.mail_server',
        string='Mail Server',
        default=_get_default_mail_server_id,
        help=
        "Use a specific mail server in priority. Otherwise Flectra relies on the first outgoing mail server available (based on their sequencing) as it does for normal mails."
    )
    contact_list_ids = fields.Many2many('mailing.list',
                                        'mail_mass_mailing_list_rel',
                                        string='Mailing Lists')
    contact_ab_pc = fields.Integer(
        string='A/B Testing percentage',
        help=
        'Percentage of the contacts that will be mailed. Recipients will be taken randomly.',
        default=100)
    unique_ab_testing = fields.Boolean(
        string='Allow A/B Testing',
        default=False,
        help=
        'If checked, recipients will be mailed only once for the whole campaign. '
        'This lets you send different mailings to randomly selected recipients and test '
        'the effectiveness of the mailings, without causing duplicate messages.'
    )
    kpi_mail_required = fields.Boolean('KPI mail required', copy=False)
    # statistics data
    mailing_trace_ids = fields.One2many('mailing.trace',
                                        'mass_mailing_id',
                                        string='Emails Statistics')
    total = fields.Integer(compute="_compute_total")
    scheduled = fields.Integer(compute="_compute_statistics")
    expected = fields.Integer(compute="_compute_statistics")
    ignored = fields.Integer(compute="_compute_statistics")
    sent = fields.Integer(compute="_compute_statistics")
    delivered = fields.Integer(compute="_compute_statistics")
    opened = fields.Integer(compute="_compute_statistics")
    clicked = fields.Integer(compute="_compute_statistics")
    replied = fields.Integer(compute="_compute_statistics")
    bounced = fields.Integer(compute="_compute_statistics")
    failed = fields.Integer(compute="_compute_statistics")
    received_ratio = fields.Integer(compute="_compute_statistics",
                                    string='Received Ratio')
    opened_ratio = fields.Integer(compute="_compute_statistics",
                                  string='Opened Ratio')
    replied_ratio = fields.Integer(compute="_compute_statistics",
                                   string='Replied Ratio')
    bounced_ratio = fields.Integer(compute="_compute_statistics",
                                   string='Bounced Ratio')
    clicks_ratio = fields.Integer(compute="_compute_clicks_ratio",
                                  string="Number of Clicks")
    next_departure = fields.Datetime(compute="_compute_next_departure",
                                     string='Scheduled date')

    def _compute_total(self):
        for mass_mailing in self:
            total = self.env[mass_mailing.mailing_model_real].search_count(
                mass_mailing._parse_mailing_domain())
            if mass_mailing.contact_ab_pc < 100:
                total = int(total / 100.0 * mass_mailing.contact_ab_pc)
            mass_mailing.total = total

    def _compute_clicks_ratio(self):
        self.env.cr.execute(
            """
            SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mailing_trace_id)) AS nb_clicks, stats.mass_mailing_id AS id
            FROM mailing_trace AS stats
            LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mailing_trace_id = stats.id
            WHERE stats.mass_mailing_id IN %s
            GROUP BY stats.mass_mailing_id
        """, [tuple(self.ids) or (None, )])
        mass_mailing_data = self.env.cr.dictfetchall()
        mapped_data = dict([(m['id'], 100 * m['nb_clicks'] / m['nb_mails'])
                            for m in mass_mailing_data])
        for mass_mailing in self:
            mass_mailing.clicks_ratio = mapped_data.get(mass_mailing.id, 0)

    def _compute_statistics(self):
        """ Compute statistics of the mass mailing """
        for key in (
                'scheduled',
                'expected',
                'ignored',
                'sent',
                'delivered',
                'opened',
                'clicked',
                'replied',
                'bounced',
                'failed',
                'received_ratio',
                'opened_ratio',
                'replied_ratio',
                'bounced_ratio',
        ):
            self[key] = False
        if not self.ids:
            return
        # ensure traces are sent to db
        self.flush()
        self.env.cr.execute(
            """
            SELECT
                m.id as mailing_id,
                COUNT(s.id) AS expected,
                COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null AND s.bounced is null THEN 1 ELSE null END) AS scheduled,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
                COUNT(CASE WHEN s.sent is not null AND s.exception is null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
                COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
                COUNT(CASE WHEN s.clicked is not null THEN 1 ELSE null END) AS clicked,
                COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied,
                COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced,
                COUNT(CASE WHEN s.exception is not null THEN 1 ELSE null END) AS failed
            FROM
                mailing_trace s
            RIGHT JOIN
                mailing_mailing m
                ON (m.id = s.mass_mailing_id)
            WHERE
                m.id IN %s
            GROUP BY
                m.id
        """, (tuple(self.ids), ))
        for row in self.env.cr.dictfetchall():
            total = (row['expected'] - row['ignored']) or 1
            row['received_ratio'] = 100.0 * row['delivered'] / total
            row['opened_ratio'] = 100.0 * row['opened'] / total
            row['replied_ratio'] = 100.0 * row['replied'] / total
            row['bounced_ratio'] = 100.0 * row['bounced'] / total
            self.browse(row.pop('mailing_id')).update(row)

    def _compute_next_departure(self):
        cron_next_call = self.env.ref(
            'mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall
        str2dt = fields.Datetime.from_string
        cron_time = str2dt(cron_next_call)
        for mass_mailing in self:
            if mass_mailing.schedule_date:
                schedule_date = str2dt(mass_mailing.schedule_date)
                mass_mailing.next_departure = max(schedule_date, cron_time)
            else:
                mass_mailing.next_departure = cron_time

    @api.depends('mailing_type')
    def _compute_medium_id(self):
        for mailing in self:
            if mailing.mailing_type == 'mail' and not mailing.medium_id:
                mailing.medium_id = self.env.ref('utm.utm_medium_email').id

    @api.depends('mailing_model_id')
    def _compute_model(self):
        for record in self:
            record.mailing_model_real = (
                record.mailing_model_id.model != 'mailing.list'
            ) and record.mailing_model_id.model or 'mailing.contact'

    @api.depends('mailing_model_id')
    def _compute_reply_to_mode(self):
        """ For main models not really using chatter to gather answers (contacts
        and mailing contacts), set reply-to as email-based. Otherwise answers
        by default go on the original discussion thread (business document). Note
        that mailing_model being mailing.list means contacting mailing.contact
        (see mailing_model_name versus mailing_model_real). """
        for mailing in self:
            if mailing.mailing_model_id.model in [
                    'res.partner', 'mailing.list'
            ]:
                mailing.reply_to_mode = 'email'
            else:
                mailing.reply_to_mode = 'thread'

    @api.depends('reply_to_mode')
    def _compute_reply_to(self):
        for mailing in self:
            if mailing.reply_to_mode == 'email' and not mailing.reply_to:
                mailing.reply_to = self.env.user.email_formatted
            elif mailing.reply_to_mode == 'thread':
                mailing.reply_to = False

    @api.depends('mailing_model_id', 'contact_list_ids', 'mailing_type')
    def _compute_mailing_domain(self):
        for mailing in self:
            if not mailing.mailing_model_id:
                mailing.mailing_domain = ''
            else:
                mailing.mailing_domain = repr(
                    mailing._get_default_mailing_domain())

    # ------------------------------------------------------
    # ORM
    # ------------------------------------------------------

    @api.model
    def create(self, values):
        if values.get('subject') and not values.get('name'):
            values['name'] = "%s %s" % (
                values['subject'],
                datetime.strftime(fields.datetime.now(),
                                  tools.DEFAULT_SERVER_DATETIME_FORMAT))
        if values.get('body_html'):
            values['body_html'] = self._convert_inline_images_to_urls(
                values['body_html'])
        return super().create(values)\
            ._fix_attachment_ownership()

    def write(self, values):
        if values.get('body_html'):
            values['body_html'] = self._convert_inline_images_to_urls(
                values['body_html'])
        super().write(values)
        self._fix_attachment_ownership()
        return True

    def _fix_attachment_ownership(self):
        for record in self:
            record.attachment_ids.write({
                'res_model': record._name,
                'res_id': record.id
            })
        return self

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {},
                       name=_('%s (copy)', self.name),
                       contact_list_ids=self.contact_list_ids.ids)
        return super(MassMailing, self).copy(default=default)

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

    # ------------------------------------------------------
    # ACTIONS
    # ------------------------------------------------------

    def action_duplicate(self):
        self.ensure_one()
        mass_mailing_copy = self.copy()
        if mass_mailing_copy:
            context = dict(self.env.context)
            context['form_view_initial_mode'] = 'edit'
            return {
                'type': 'ir.actions.act_window',
                'view_mode': 'form',
                'res_model': 'mailing.mailing',
                'res_id': mass_mailing_copy.id,
                'context': context,
            }
        return False

    def action_test(self):
        self.ensure_one()
        ctx = dict(self.env.context, default_mass_mailing_id=self.id)
        return {
            'name': _('Test Mailing'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'mailing.mailing.test',
            'target': 'new',
            'context': ctx,
        }

    def action_schedule(self):
        self.ensure_one()
        action = self.env["ir.actions.actions"]._for_xml_id(
            "mass_mailing.mailing_mailing_schedule_date_action")
        action['context'] = dict(self.env.context,
                                 default_mass_mailing_id=self.id)
        return action

    def action_put_in_queue(self):
        self.write({'state': 'in_queue'})

    def action_cancel(self):
        self.write({
            'state': 'draft',
            'schedule_date': False,
            'next_departure': False
        })

    def action_retry_failed(self):
        failed_mails = self.env['mail.mail'].sudo().search([
            ('mailing_id', 'in', self.ids), ('state', '=', 'exception')
        ])
        failed_mails.mapped('mailing_trace_ids').unlink()
        failed_mails.unlink()
        self.write({'state': 'in_queue'})

    def action_view_traces_scheduled(self):
        return self._action_view_traces_filtered('scheduled')

    def action_view_traces_ignored(self):
        return self._action_view_traces_filtered('ignored')

    def action_view_traces_failed(self):
        return self._action_view_traces_filtered('failed')

    def action_view_traces_sent(self):
        return self._action_view_traces_filtered('sent')

    def _action_view_traces_filtered(self, view_filter):
        action = self.env["ir.actions.actions"]._for_xml_id(
            "mass_mailing.mailing_trace_action")
        action['name'] = _('%s Traces') % (self.name)
        action['context'] = {
            'search_default_mass_mailing_id': self.id,
        }
        filter_key = 'search_default_filter_%s' % (view_filter)
        action['context'][filter_key] = True
        return action

    def action_view_clicked(self):
        model_name = self.env['ir.model']._get('link.tracker').display_name
        return {
            'name': model_name,
            'type': 'ir.actions.act_window',
            'view_mode': 'tree',
            'res_model': 'link.tracker',
            'domain': [('mass_mailing_id.id', '=', self.id)],
            'context': dict(self._context, create=False)
        }

    def action_view_opened(self):
        return self._action_view_documents_filtered('opened')

    def action_view_replied(self):
        return self._action_view_documents_filtered('replied')

    def action_view_bounced(self):
        return self._action_view_documents_filtered('bounced')

    def action_view_delivered(self):
        return self._action_view_documents_filtered('delivered')

    def _action_view_documents_filtered(self, view_filter):
        if view_filter in ('opened', 'replied', 'bounced'):
            opened_stats = self.mailing_trace_ids.filtered(
                lambda stat: stat[view_filter])
        elif view_filter == ('delivered'):
            opened_stats = self.mailing_trace_ids.filtered(
                lambda stat: stat.sent and not stat.bounced)
        else:
            opened_stats = self.env['mailing.trace']
        res_ids = opened_stats.mapped('res_id')
        model_name = self.env['ir.model']._get(
            self.mailing_model_real).display_name
        return {
            'name': model_name,
            'type': 'ir.actions.act_window',
            'view_mode': 'tree',
            'res_model': self.mailing_model_real,
            'domain': [('id', 'in', res_ids)],
            'context': dict(self._context, create=False)
        }

    def update_opt_out(self, email, list_ids, value):
        if len(list_ids) > 0:
            model = self.env['mailing.contact'].with_context(active_test=False)
            records = model.search([('email_normalized', '=',
                                     tools.email_normalize(email))])
            opt_out_records = self.env['mailing.contact.subscription'].search([
                ('contact_id', 'in', records.ids), ('list_id', 'in', list_ids),
                ('opt_out', '!=', value)
            ])

            opt_out_records.write({'opt_out': value})
            message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \
                if value else _('The recipient <strong>subscribed to %s</strong> mailing list(s)')
            for record in records:
                # filter the list_id by record
                record_lists = opt_out_records.filtered(
                    lambda rec: rec.contact_id.id == record.id)
                if len(record_lists) > 0:
                    record.sudo().message_post(body=message % ', '.join(
                        str(list.name)
                        for list in record_lists.mapped('list_id')))

    # ------------------------------------------------------
    # Email Sending
    # ------------------------------------------------------

    def _get_opt_out_list(self):
        """Returns a set of emails opted-out in target model"""
        self.ensure_one()
        opt_out = {}
        target = self.env[self.mailing_model_real]
        if self.mailing_model_real == "mailing.contact":
            # if user is opt_out on One list but not on another
            # or if two user with same email address, one opted in and the other one opted out, send the mail anyway
            # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
            target_list_contacts = self.env[
                'mailing.contact.subscription'].search([
                    ('list_id', 'in', self.contact_list_ids.ids)
                ])
            opt_out_contacts = target_list_contacts.filtered(
                lambda rel: rel.opt_out).mapped('contact_id.email_normalized')
            opt_in_contacts = target_list_contacts.filtered(
                lambda rel: not rel.opt_out).mapped(
                    'contact_id.email_normalized')
            opt_out = set(c for c in opt_out_contacts
                          if c not in opt_in_contacts)

            _logger.info("Mass-mailing %s targets %s, blacklist: %s emails",
                         self, target._name, len(opt_out))
        else:
            _logger.info(
                "Mass-mailing %s targets %s, no opt out list available", self,
                target._name)
        return opt_out

    def _get_link_tracker_values(self):
        self.ensure_one()
        vals = {'mass_mailing_id': self.id}

        if self.campaign_id:
            vals['campaign_id'] = self.campaign_id.id
        if self.source_id:
            vals['source_id'] = self.source_id.id
        if self.medium_id:
            vals['medium_id'] = self.medium_id.id
        return vals

    def _get_seen_list(self):
        """Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
        self.ensure_one()
        target = self.env[self.mailing_model_real]

        # avoid loading a large number of records in memory
        # + use a basic heuristic for extracting emails
        query = """
            SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
              FROM mailing_trace s
              JOIN %(target)s t ON (s.res_id = t.id)
             WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
        """

        # Apply same 'get email field' rule from mail_thread.message_get_default_recipients
        if 'partner_id' in target._fields:
            mail_field = 'email'
            query = """
                SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
                  FROM mailing_trace s
                  JOIN %(target)s t ON (s.res_id = t.id)
                  JOIN res_partner p ON (t.partner_id = p.id)
                 WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
            """
        elif issubclass(type(target), self.pool['mail.thread.blacklist']):
            mail_field = 'email_normalized'
        elif 'email_from' in target._fields:
            mail_field = 'email_from'
        elif 'partner_email' in target._fields:
            mail_field = 'partner_email'
        elif 'email' in target._fields:
            mail_field = 'email'
        else:
            raise UserError(
                _("Unsupported mass mailing model %s",
                  self.mailing_model_id.name))

        if self.unique_ab_testing:
            query += """
               AND s.campaign_id = %%(mailing_campaign_id)s;
            """
        else:
            query += """
               AND s.mass_mailing_id = %%(mailing_id)s
               AND s.model = %%(target_model)s;
            """
        query = query % {'target': target._table, 'mail_field': mail_field}
        params = {
            'mailing_id': self.id,
            'mailing_campaign_id': self.campaign_id.id,
            'target_model': self.mailing_model_real
        }
        self._cr.execute(query, params)
        seen_list = set(m[0] for m in self._cr.fetchall())
        _logger.info("Mass-mailing %s has already reached %s %s emails", self,
                     len(seen_list), target._name)
        return seen_list

    def _get_mass_mailing_context(self):
        """Returns extra context items with pre-filled blacklist and seen list for massmailing"""
        return {
            'mass_mailing_opt_out_list': self._get_opt_out_list(),
            'mass_mailing_seen_list': self._get_seen_list(),
            'post_convert_links': self._get_link_tracker_values(),
        }

    def _get_recipients(self):
        mailing_domain = self._parse_mailing_domain()
        res_ids = self.env[self.mailing_model_real].search(mailing_domain).ids

        # randomly choose a fragment
        if self.contact_ab_pc < 100:
            contact_nbr = self.env[self.mailing_model_real].search_count(
                mailing_domain)
            topick = int(contact_nbr / 100.0 * self.contact_ab_pc)
            if self.campaign_id and self.unique_ab_testing:
                already_mailed = self.campaign_id._get_mailing_recipients()[
                    self.campaign_id.id]
            else:
                already_mailed = set([])
            remaining = set(res_ids).difference(already_mailed)
            if topick > len(remaining):
                topick = len(remaining)
            res_ids = random.sample(remaining, topick)
        return res_ids

    def _get_remaining_recipients(self):
        res_ids = self._get_recipients()
        already_mailed = self.env['mailing.trace'].search_read(
            [('model', '=', self.mailing_model_real),
             ('res_id', 'in', res_ids), ('mass_mailing_id', '=', self.id)],
            ['res_id'])
        done_res_ids = {record['res_id'] for record in already_mailed}
        return [rid for rid in res_ids if rid not in done_res_ids]

    def _get_unsubscribe_url(self, email_to, res_id):
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        url = werkzeug.urls.url_join(
            base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
                'mailing_id':
                self.id,
                'params':
                werkzeug.urls.url_encode(
                    {
                        'res_id': res_id,
                        'email': email_to,
                        'token': self._unsubscribe_token(res_id, email_to),
                    }),
            })
        return url

    def _get_view_url(self, email_to, res_id):
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        url = werkzeug.urls.url_join(
            base_url, 'mailing/%(mailing_id)s/view?%(params)s' % {
                'mailing_id':
                self.id,
                'params':
                werkzeug.urls.url_encode(
                    {
                        'res_id': res_id,
                        'email': email_to,
                        'token': self._unsubscribe_token(res_id, email_to),
                    }),
            })
        return url

    def action_send_mail(self, res_ids=None):
        author_id = self.env.user.partner_id.id

        # If no recipient is passed, we don't want to use the recipients of the first
        # mailing for all the others
        initial_res_ids = res_ids
        for mailing in self:
            if not initial_res_ids:
                res_ids = mailing._get_remaining_recipients()
            if not res_ids:
                raise UserError(_('There are no recipients selected.'))

            composer_values = {
                'author_id':
                author_id,
                'attachment_ids':
                [(4, attachment.id) for attachment in mailing.attachment_ids],
                'body':
                mailing._prepend_preview(mailing.body_html, mailing.preview),
                'subject':
                mailing.subject,
                'model':
                mailing.mailing_model_real,
                'email_from':
                mailing.email_from,
                'record_name':
                False,
                'composition_mode':
                'mass_mail',
                'mass_mailing_id':
                mailing.id,
                'mailing_list_ids':
                [(4, l.id) for l in mailing.contact_list_ids],
                'no_auto_thread':
                mailing.reply_to_mode != 'thread',
                'template_id':
                None,
                'mail_server_id':
                mailing.mail_server_id.id,
            }
            if mailing.reply_to_mode == 'email':
                composer_values['reply_to'] = mailing.reply_to

            composer = self.env['mail.compose.message'].with_context(
                active_ids=res_ids).create(composer_values)
            extra_context = mailing._get_mass_mailing_context()
            composer = composer.with_context(active_ids=res_ids,
                                             **extra_context)
            # auto-commit except in testing mode
            auto_commit = not getattr(threading.currentThread(), 'testing',
                                      False)
            composer.send_mail(auto_commit=auto_commit)
            mailing.write({
                'state': 'done',
                'sent_date': fields.Datetime.now(),
                # send the KPI mail only if it's the first sending
                'kpi_mail_required': not mailing.sent_date,
            })
        return True

    def convert_links(self):
        res = {}
        for mass_mailing in self:
            html = mass_mailing.body_html if mass_mailing.body_html else ''

            vals = {'mass_mailing_id': mass_mailing.id}

            if mass_mailing.campaign_id:
                vals['campaign_id'] = mass_mailing.campaign_id.id
            if mass_mailing.source_id:
                vals['source_id'] = mass_mailing.source_id.id
            if mass_mailing.medium_id:
                vals['medium_id'] = mass_mailing.medium_id.id

            res[mass_mailing.id] = mass_mailing._shorten_links(
                html, vals, blacklist=['/unsubscribe_from_list', '/view'])

        return res

    @api.model
    def _process_mass_mailing_queue(self):
        mass_mailings = self.search([('state', 'in', ('in_queue', 'sending')),
                                     '|',
                                     ('schedule_date', '<',
                                      fields.Datetime.now()),
                                     ('schedule_date', '=', False)])
        for mass_mailing in mass_mailings:
            user = mass_mailing.write_uid or self.env.user
            mass_mailing = mass_mailing.with_context(
                **user.with_user(user).context_get())
            if len(mass_mailing._get_remaining_recipients()) > 0:
                mass_mailing.state = 'sending'
                mass_mailing.action_send_mail()
            else:
                mass_mailing.write({
                    'state':
                    'done',
                    'sent_date':
                    fields.Datetime.now(),
                    # send the KPI mail only if it's the first sending
                    'kpi_mail_required':
                    not mass_mailing.sent_date,
                })

        mailings = self.env['mailing.mailing'].search([
            ('kpi_mail_required', '=', True),
            ('state', '=', 'done'),
            ('sent_date', '<=', fields.Datetime.now() - relativedelta(days=1)),
            ('sent_date', '>=', fields.Datetime.now() - relativedelta(days=5)),
        ])
        if mailings:
            mailings._action_send_statistics()

    # ------------------------------------------------------
    # STATISTICS
    # ------------------------------------------------------
    def _action_send_statistics(self):
        """Send an email to the responsible of each finished mailing with the statistics."""
        self.kpi_mail_required = False

        for mailing in self:
            user = mailing.user_id
            mailing = mailing.with_context(
                lang=user.lang or self._context.get('lang'))

            link_trackers = self.env['link.tracker'].search([
                ('mass_mailing_id', '=', mailing.id)
            ]).sorted('count', reverse=True)
            link_trackers_body = self.env['ir.qweb']._render(
                'mass_mailing.mass_mailing_kpi_link_trackers',
                {
                    'object': mailing,
                    'link_trackers': link_trackers
                },
            )

            rendered_body = self.env['ir.qweb']._render(
                'digest.digest_mail_main',
                {
                    'body': tools.html_sanitize(link_trackers_body),
                    'company': user.company_id,
                    'user': user,
                    'display_mobile_banner': True,
                    **mailing._prepare_statistics_email_values()
                },
            )

            full_mail = self.env['mail.render.mixin']._render_encapsulate(
                'digest.digest_mail_layout',
                rendered_body,
            )

            mail_values = {
                'subject': _('24H Stats of mailing "%s"') % mailing.subject,
                'email_from': user.email_formatted,
                'email_to': user.email_formatted,
                'body_html': full_mail,
                'auto_delete': True,
            }
            mail = self.env['mail.mail'].sudo().create(mail_values)
            mail.send(raise_exception=False)

    def _prepare_statistics_email_values(self):
        """Return some statistics that will be displayed in the mailing statistics email.

        Each item in the returned list will be displayed as a table, with a title and
        1, 2 or 3 columns.
        """
        self.ensure_one()

        random_tip = self.env['digest.tip'].search([
            ('group_id.category_id', '=',
             self.env.ref('base.module_category_marketing_email_marketing').id)
        ])
        if random_tip:
            random_tip = random.choice(random_tip).tip_description

        formatted_date = tools.format_datetime(
            self.env, self.sent_date, self.user_id.tz, 'MMM dd, YYYY',
            self.user_id.lang) if self.sent_date else False

        web_base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')

        return {
            'title':
            _('24H Stats of mailing'),
            'sub_title':
            '"%s"' % self.subject,
            'top_button_label':
            _('More Info'),
            'top_button_url':
            url_join(
                web_base_url,
                f'/web#id={self.id}&model=mailing.mailing&view_type=form'),
            'kpi_data': [
                {
                    'kpi_fullname':
                    _('Engagement on %i Emails Sent') % self.sent,
                    'kpi_action': None,
                    'kpi_col1': {
                        'value':
                        f'{self.received_ratio}%',
                        'col_subtitle':
                        '%s (%i)' % (_('RECEIVED'), self.delivered),
                    },
                    'kpi_col2': {
                        'value': f'{self.opened_ratio}%',
                        'col_subtitle': '%s (%i)' % (_('OPENED'), self.opened),
                    },
                    'kpi_col3': {
                        'value': f'{self.replied_ratio}%',
                        'col_subtitle':
                        '%s (%i)' % (_('REPLIED'), self.replied),
                    },
                },
                {
                    'kpi_fullname':
                    _('Business Benefits on %i Emails Sent') % self.sent,
                    'kpi_action':
                    None,
                    'kpi_col1': {},
                    'kpi_col2': {},
                    'kpi_col3': {},
                },
            ],
            'tips': [random_tip] if random_tip else False,
            'formatted_date':
            formatted_date,
        }

    # ------------------------------------------------------
    # TOOLS
    # ------------------------------------------------------

    def _get_default_mailing_domain(self):
        mailing_domain = []
        if self.mailing_model_name == 'mailing.list' and self.contact_list_ids:
            mailing_domain = [('list_ids', 'in', self.contact_list_ids.ids)]

        if self.mailing_type == 'mail' and 'is_blacklisted' in self.env[
                self.mailing_model_name]._fields:
            mailing_domain = expression.AND([[('is_blacklisted', '=', False)],
                                             mailing_domain])

        return mailing_domain

    def _parse_mailing_domain(self):
        self.ensure_one()
        try:
            mailing_domain = literal_eval(self.mailing_domain)
        except Exception:
            mailing_domain = [('id', 'in', [])]
        return mailing_domain

    def _unsubscribe_token(self, res_id, email):
        """Generate a secure hash for this mailing list and parameters.

        This is appended to the unsubscription URL and then checked at
        unsubscription time to ensure no malicious unsubscriptions are
        performed.

        :param int res_id:
            ID of the resource that will be unsubscribed.

        :param str email:
            Email of the resource that will be unsubscribed.
        """
        secret = self.env["ir.config_parameter"].sudo().get_param(
            "database.secret")
        token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email))
        return hmac.new(secret.encode('utf-8'),
                        repr(token).encode('utf-8'),
                        hashlib.sha512).hexdigest()

    def _convert_inline_images_to_urls(self, body_html):
        """
        Find inline base64 encoded images, make an attachement out of
        them and replace the inline image with an url to the attachement.
        """
        def _image_to_url(b64image: bytes):
            """Store an image in an attachement and returns an url"""
            attachment = self.env['ir.attachment'].create({
                'datas':
                b64image,
                'name':
                "cropped_image_mailing_{}".format(self.id),
                'type':
                'binary',
            })

            attachment.generate_access_token()

            return '/web/image/%s?access_token=%s' % (attachment.id,
                                                      attachment.access_token)

        modified = False
        root = lxml.html.fromstring(body_html)
        for node in root.iter('img'):
            match = image_re.match(node.attrib.get('src', ''))
            if match:
                mime = match.group(1)  # unsed
                image = match.group(2).encode()  # base64 image as bytes

                node.attrib['src'] = _image_to_url(image)
                modified = True

        if modified:
            return lxml.html.tostring(root)

        return body_html
Example #20
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 #21
0
class Digest(models.Model):
    _name = 'digest.digest'
    _description = 'Digest'

    # Digest description
    name = fields.Char(string='Name', required=True, translate=True)
    user_ids = fields.Many2many('res.users',
                                string='Recipients',
                                domain="[('share', '=', False)]")
    periodicity = fields.Selection([('daily', 'Daily'), ('weekly', 'Weekly'),
                                    ('monthly', 'Monthly'),
                                    ('quarterly', 'Quarterly')],
                                   string='Periodicity',
                                   default='weekly',
                                   required=True)
    next_run_date = fields.Date(string='Next Send Date')
    template_id = fields.Many2one(
        'mail.template',
        string='Email Template',
        domain="[('model','=','digest.digest')]",
        default=lambda self: self.env.ref('digest.digest_mail_template'),
        required=True)
    currency_id = fields.Many2one(related="company_id.currency_id",
                                  string='Currency',
                                  readonly=False)
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        default=lambda self: self.env.user.company_id.id)
    available_fields = fields.Char(compute='_compute_available_fields')
    is_subscribed = fields.Boolean('Is user subscribed',
                                   compute='_compute_is_subscribed')
    state = fields.Selection([('activated', 'Activated'),
                              ('deactivated', 'Deactivated')],
                             string='Status',
                             readonly=True,
                             default='activated')
    # First base-related KPIs
    kpi_res_users_connected = fields.Boolean('Connected Users')
    kpi_res_users_connected_value = fields.Integer(
        compute='_compute_kpi_res_users_connected_value')
    kpi_mail_message_total = fields.Boolean('Messages')
    kpi_mail_message_total_value = fields.Integer(
        compute='_compute_kpi_mail_message_total_value')

    def _compute_is_subscribed(self):
        for digest in self:
            digest.is_subscribed = self.env.user in digest.user_ids

    def _compute_available_fields(self):
        for digest in self:
            kpis_values_fields = []
            for field_name, field in digest._fields.items():
                if field.type == 'boolean' and field_name.startswith(
                    ('kpi_', 'x_kpi_')) and digest[field_name]:
                    kpis_values_fields += [field_name + '_value']
            digest.available_fields = ', '.join(kpis_values_fields)

    def _get_kpi_compute_parameters(self):
        return fields.Date.to_string(
            self._context.get('start_date')), fields.Date.to_string(
                self._context.get('end_date')), self._context.get('company')

    def _compute_kpi_res_users_connected_value(self):
        for record in self:
            start, end, company = record._get_kpi_compute_parameters()
            user_connected = self.env['res.users'].search_count([
                ('company_id', '=', company.id), ('login_date', '>=', start),
                ('login_date', '<', end)
            ])
            record.kpi_res_users_connected_value = user_connected

    def _compute_kpi_mail_message_total_value(self):
        for record in self:
            start, end, company = record._get_kpi_compute_parameters()
            total_messages = self.env['mail.message'].search_count([
                ('create_date', '>=', start), ('create_date', '<', end)
            ])
            record.kpi_mail_message_total_value = total_messages

    @api.onchange('periodicity')
    def _onchange_periodicity(self):
        self.next_run_date = self._get_next_run_date()

    @api.model
    def create(self, vals):
        vals['next_run_date'] = date.today() + relativedelta(days=3)
        return super(Digest, self).create(vals)

    @api.multi
    def action_subscribe(self):
        if self.env.user not in self.user_ids:
            self.sudo().user_ids |= self.env.user

    @api.multi
    def action_unsubcribe(self):
        if self.env.user in self.user_ids:
            self.sudo().user_ids -= self.env.user

    @api.multi
    def action_activate(self):
        self.state = 'activated'

    @api.multi
    def action_deactivate(self):
        self.state = 'deactivated'

    def action_send(self):
        for digest in self:
            for user in digest.user_ids:
                subject = '%s: %s' % (user.company_id.name, digest.name)
                digest.template_id.with_context(
                    user=user,
                    company=user.company_id).send_mail(digest.id,
                                                       force_send=True,
                                                       raise_exception=True,
                                                       email_values={
                                                           'email_to':
                                                           user.email,
                                                           'subject': subject
                                                       })
            digest.next_run_date = digest._get_next_run_date()

    def compute_kpis(self, company, user):
        self.ensure_one()
        if not company:
            company = self.env.user.company_id
        if not user:
            user = self.env.user
        res = {}
        for tf_name, tf in self._compute_timeframes(company).items():
            digest = self.with_context(start_date=tf[0][0],
                                       end_date=tf[0][1],
                                       company=company).sudo(user.id)
            previous_digest = self.with_context(start_date=tf[1][0],
                                                end_date=tf[1][1],
                                                company=company).sudo(user.id)
            kpis = {}
            for field_name, field in self._fields.items():
                if field.type == 'boolean' and field_name.startswith(
                    ('kpi_', 'x_kpi_')) and self[field_name]:

                    try:
                        compute_value = digest[field_name + '_value']
                        previous_value = previous_digest[field_name + '_value']
                    except AccessError:  # no access rights -> just skip that digest details from that user's digest email
                        continue
                    margin = self._get_margin_value(compute_value,
                                                    previous_value)
                    if self._fields[field_name + '_value'].type == 'monetary':
                        converted_amount = self._format_human_readable_amount(
                            compute_value)
                        kpis.update({
                            field_name: {
                                field_name:
                                self._format_currency_amount(
                                    converted_amount, company.currency_id),
                                'margin':
                                margin
                            }
                        })
                    else:
                        kpis.update({
                            field_name: {
                                field_name: compute_value,
                                'margin': margin
                            }
                        })

                res.update({tf_name: kpis})
        return res

    def compute_tips(self, company, user):
        tip = self.env['digest.tip'].search(
            [('user_ids', '!=', user.id), '|',
             ('group_id', 'in', user.groups_id.ids), ('group_id', '=', False)],
            limit=1)
        if not tip:
            return False
        tip.user_ids = [4, user.id]
        body = tools.html_sanitize(tip.tip_description)
        tip_description = self.env['mail.template'].render_template(
            body, 'digest.tip', self.id)
        return tip_description

    def compute_kpis_actions(self, company, user):
        """ Give an optional action to display in digest email linked to some KPIs.

        :return dict: key: kpi name (field name), value: an action that will be
          concatenated with /web#action={action}
        """
        return {}

    def _get_next_run_date(self):
        self.ensure_one()
        if self.periodicity == 'daily':
            delta = relativedelta(days=1)
        elif self.periodicity == 'weekly':
            delta = relativedelta(weeks=1)
        elif self.periodicity == 'monthly':
            delta = relativedelta(months=1)
        elif self.periodicity == 'quarterly':
            delta = relativedelta(months=3)
        return date.today() + delta

    def _compute_timeframes(self, company):
        now = datetime.utcnow()
        tz_name = company.resource_calendar_id.tz
        if tz_name:
            now = pytz.timezone(tz_name).localize(now)
        start_date = now.date()
        return {
            'yesterday': ((start_date + relativedelta(days=-1), start_date),
                          (start_date + relativedelta(days=-2),
                           start_date + relativedelta(days=-1))),
            'lastweek': ((start_date + relativedelta(weeks=-1), start_date),
                         (start_date + relativedelta(weeks=-2),
                          start_date + relativedelta(weeks=-1))),
            'lastmonth': ((start_date + relativedelta(months=-1), start_date),
                          (start_date + relativedelta(months=-2),
                           start_date + relativedelta(months=-1))),
        }

    def _get_margin_value(self, value, previous_value=0.0):
        margin = 0.0
        if (value != previous_value) and (value != 0.0
                                          and previous_value != 0.0):
            margin = float_round(
                (float(value - previous_value) / previous_value or 1) * 100,
                precision_digits=2)
        return margin

    def _format_currency_amount(self, amount, currency_id):
        pre = post = u''
        if currency_id.position == 'before':
            pre = u'{symbol}\N{NO-BREAK SPACE}'.format(
                symbol=currency_id.symbol or '')
        else:
            post = u'\N{NO-BREAK SPACE}{symbol}'.format(
                symbol=currency_id.symbol or '')
        return u'{pre}{0}{post}'.format(amount, pre=pre, post=post)

    def _format_human_readable_amount(self, amount, suffix=''):
        for unit in ['', 'K', 'M', 'G']:
            if abs(amount) < 1000.0:
                return "%3.1f%s%s" % (amount, unit, suffix)
            amount /= 1000.0
        return "%.1f%s%s" % (amount, 'T', suffix)

    @api.model
    def _cron_send_digest_email(self):
        digests = self.search([('next_run_date', '=', fields.Date.today()),
                               ('state', '=', 'activated')])
        for digest in digests:
            try:
                digest.action_send()
            except MailDeliveryException as e:
                _logger.warning(
                    'MailDeliveryException while sending digest %d. Digest is now scheduled for next cron update.'
                )
Example #22
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 #23
0
class ProductReplenish(models.TransientModel):
    _name = 'product.replenish'
    _description = 'Product Replenish'

    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 required=True)
    product_tmpl_id = fields.Many2one('product.template',
                                      string='Product Template',
                                      required=True)
    product_has_variants = fields.Boolean('Has variants',
                                          default=False,
                                          required=True)
    product_uom_category_id = fields.Many2one(
        'uom.category',
        related='product_id.uom_id.category_id',
        readonly=True,
        required=True)
    product_uom_id = fields.Many2one('uom.uom',
                                     string='Unity of measure',
                                     required=True)
    quantity = fields.Float('Quantity', default=1, required=True)
    date_planned = fields.Datetime(
        'Scheduled Date',
        required=True,
        help="Date at which the replenishment should take place.")
    warehouse_id = fields.Many2one('stock.warehouse',
                                   string='Warehouse',
                                   required=True,
                                   domain="[('company_id', '=', company_id)]")
    route_ids = fields.Many2many(
        'stock.location.route',
        string='Preferred Routes',
        help=
        "Apply specific route(s) for the replenishment instead of product's default routes.",
        domain=
        "['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    company_id = fields.Many2one('res.company')

    @api.model
    def default_get(self, fields):
        res = super(ProductReplenish, self).default_get(fields)
        product_tmpl_id = self.env['product.template']
        if 'product_id' in fields:
            if self.env.context.get('default_product_id'):
                product_id = self.env['product.product'].browse(
                    self.env.context['default_product_id'])
                product_tmpl_id = product_id.product_tmpl_id
                res['product_tmpl_id'] = product_id.product_tmpl_id.id
                res['product_id'] = product_id.id
            elif self.env.context.get('default_product_tmpl_id'):
                product_tmpl_id = self.env['product.template'].browse(
                    self.env.context['default_product_tmpl_id'])
                res['product_tmpl_id'] = product_tmpl_id.id
                res['product_id'] = product_tmpl_id.product_variant_id.id
                if len(product_tmpl_id.product_variant_ids) > 1:
                    res['product_has_variants'] = True
        company = product_tmpl_id.company_id or self.env.company
        if 'product_uom_id' in fields:
            res['product_uom_id'] = product_tmpl_id.uom_id.id
        if 'company_id' in fields:
            res['company_id'] = company.id
        if 'warehouse_id' in fields and 'warehouse_id' not in res:
            warehouse = self.env['stock.warehouse'].search(
                [('company_id', '=', company.id)], limit=1)
            res['warehouse_id'] = warehouse.id
        if 'date_planned' in fields:
            res['date_planned'] = datetime.datetime.now()
        return res

    def launch_replenishment(self):
        uom_reference = self.product_id.uom_id
        self.quantity = self.product_uom_id._compute_quantity(
            self.quantity, uom_reference)
        try:
            self.env['procurement.group'].with_context(
                clean_context(self.env.context)).run([
                    self.env['procurement.group'].Procurement(
                        self.product_id,
                        self.quantity,
                        uom_reference,
                        self.warehouse_id.lot_stock_id,  # Location
                        _("Manual Replenishment"),  # Name
                        _("Manual Replenishment"),  # Origin
                        self.warehouse_id.company_id,
                        self._prepare_run_values()  # Values
                    )
                ])
        except UserError as error:
            raise UserError(error)

    def _prepare_run_values(self):
        replenishment = self.env['procurement.group'].create({
            'partner_id':
            self.product_id.with_company(
                self.company_id).responsible_id.partner_id.id,
        })

        values = {
            'warehouse_id': self.warehouse_id,
            'route_ids': self.route_ids,
            'date_planned': self.date_planned,
            'group_id': replenishment,
        }
        return values
Example #24
0
class PartnerCategory(models.Model):
    _description = 'Partner Tags'
    _name = 'res.partner.category'
    _order = 'parent_left, name'
    _parent_store = True
    _parent_order = 'name'

    name = fields.Char(string='Tag Name', required=True, translate=True)
    color = fields.Integer(string='Color Index')
    parent_id = fields.Many2one('res.partner.category',
                                string='Parent Category',
                                index=True,
                                ondelete='cascade')
    child_ids = fields.One2many('res.partner.category',
                                'parent_id',
                                string='Child Tags')
    active = fields.Boolean(
        default=True,
        help=
        "The active field allows you to hide the category without removing it."
    )
    parent_left = fields.Integer(string='Left parent', index=True)
    parent_right = fields.Integer(string='Right parent', index=True)
    partner_ids = fields.Many2many('res.partner',
                                   column1='category_id',
                                   column2='partner_id',
                                   string='Partners')

    @api.constrains('parent_id')
    def _check_parent_id(self):
        if not self._check_recursion():
            raise ValidationError(
                _('Error ! You can not create recursive tags.'))

    @api.multi
    def name_get(self):
        """ Return the categories' display name, including their direct
            parent by default.

            If ``context['partner_category_display']`` is ``'short'``, the short
            version of the category name (without the direct parent) is used.
            The default is the long version.
        """
        if self._context.get('partner_category_display') == 'short':
            return super(PartnerCategory, self).name_get()

        res = []
        for category in self:
            names = []
            current = category
            while current:
                names.append(current.name)
                current = current.parent_id
            res.append((category.id, ' / '.join(reversed(names))))
        return res

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        args = args or []
        if name:
            # Be sure name_search is symetric to name_get
            name = name.split(' / ')[-1]
            args = [('name', operator, name)] + args
        return self.search(args, limit=limit).name_get()
Example #25
0
class MergePartnerAutomatic(models.TransientModel):
    """
        The idea behind this wizard is to create a list of potential partners to
        merge. We use two objects, the first one is the wizard for the end-user.
        And the second will contain the partner list to merge.
    """

    _name = 'base.partner.merge.automatic.wizard'
    _description = 'Merge Partner Wizard'

    @api.model
    def default_get(self, fields):
        res = super(MergePartnerAutomatic, self).default_get(fields)
        active_ids = self.env.context.get('active_ids')
        if self.env.context.get(
                'active_model') == 'res.partner' and active_ids:
            if 'state' in fields:
                res['state'] = 'selection'
            if 'partner_ids' in fields:
                res['partner_ids'] = [(6, 0, active_ids)]
            if 'dst_partner_id' in fields:
                res['dst_partner_id'] = self._get_ordered_partner(
                    active_ids)[-1].id
        return res

    # Group by
    group_by_email = fields.Boolean('Email')
    group_by_name = fields.Boolean('Name')
    group_by_is_company = fields.Boolean('Is Company')
    group_by_vat = fields.Boolean('VAT')
    group_by_parent_id = fields.Boolean('Parent Company')

    state = fields.Selection([('option', 'Option'), ('selection', 'Selection'),
                              ('finished', 'Finished')],
                             readonly=True,
                             required=True,
                             string='State',
                             default='option')

    number_group = fields.Integer('Group of Contacts', readonly=True)
    current_line_id = fields.Many2one('base.partner.merge.line',
                                      string='Current Line')
    line_ids = fields.One2many('base.partner.merge.line',
                               'wizard_id',
                               string='Lines')
    partner_ids = fields.Many2many('res.partner', string='Contacts')
    dst_partner_id = fields.Many2one('res.partner',
                                     string='Destination Contact')

    exclude_contact = fields.Boolean('A user associated to the contact')
    exclude_journal_item = fields.Boolean(
        'Journal Items associated to the contact')
    maximum_group = fields.Integer('Maximum of Group of Contacts')

    # ----------------------------------------
    # Update method. Core methods to merge steps
    # ----------------------------------------

    def _get_fk_on(self, table):
        """ return a list of many2one relation with the given table.
            :param table : the name of the sql table to return relations
            :returns a list of tuple 'table name', 'column name'.
        """
        query = """
            SELECT cl1.relname as table, att1.attname as column
            FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, pg_attribute as att1, pg_attribute as att2
            WHERE con.conrelid = cl1.oid
                AND con.confrelid = cl2.oid
                AND array_lower(con.conkey, 1) = 1
                AND con.conkey[1] = att1.attnum
                AND att1.attrelid = cl1.oid
                AND cl2.relname = %s
                AND att2.attname = 'id'
                AND array_lower(con.confkey, 1) = 1
                AND con.confkey[1] = att2.attnum
                AND att2.attrelid = cl2.oid
                AND con.contype = 'f'
        """
        self._cr.execute(query, (table, ))
        return self._cr.fetchall()

    @api.model
    def _update_foreign_keys(self, src_partners, dst_partner):
        """ Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated.
            :param src_partners : merge source res.partner recordset (does not include destination one)
            :param dst_partner : record of destination res.partner
        """
        _logger.debug(
            '_update_foreign_keys for dst_partner: %s for src_partners: %s',
            dst_partner.id, str(src_partners.ids))

        # find the many2one relation to a partner
        Partner = self.env['res.partner']
        relations = self._get_fk_on('res_partner')

        self.flush()

        for table, column in relations:
            if 'base_partner_merge_' in table:  # ignore two tables
                continue

            # get list of columns of current table (exept the current fk column)
            query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % (
                table)
            self._cr.execute(query, ())
            columns = []
            for data in self._cr.fetchall():
                if data[0] != column:
                    columns.append(data[0])

            # do the update for the current table/column in SQL
            query_dic = {
                'table': table,
                'column': column,
                'value': columns[0],
            }
            if len(columns) <= 1:
                # unique key treated
                query = """
                    UPDATE "%(table)s" as ___tu
                    SET "%(column)s" = %%s
                    WHERE
                        "%(column)s" = %%s AND
                        NOT EXISTS (
                            SELECT 1
                            FROM "%(table)s" as ___tw
                            WHERE
                                "%(column)s" = %%s AND
                                ___tu.%(value)s = ___tw.%(value)s
                        )""" % query_dic
                for partner in src_partners:
                    self._cr.execute(
                        query, (dst_partner.id, partner.id, dst_partner.id))
            else:
                try:
                    with mute_logger('flectra.sql_db'), self._cr.savepoint():
                        query = 'UPDATE "%(table)s" SET "%(column)s" = %%s WHERE "%(column)s" IN %%s' % query_dic
                        self._cr.execute(query, (
                            dst_partner.id,
                            tuple(src_partners.ids),
                        ))

                        # handle the recursivity with parent relation
                        if column == Partner._parent_name and table == 'res_partner':
                            query = """
                                WITH RECURSIVE cycle(id, parent_id) AS (
                                        SELECT id, parent_id FROM res_partner
                                    UNION
                                        SELECT  cycle.id, res_partner.parent_id
                                        FROM    res_partner, cycle
                                        WHERE   res_partner.id = cycle.parent_id AND
                                                cycle.id != cycle.parent_id
                                )
                                SELECT id FROM cycle WHERE id = parent_id AND id = %s
                            """
                            self._cr.execute(query, (dst_partner.id, ))
                            # NOTE JEM : shouldn't we fetch the data ?
                except psycopg2.Error:
                    # updating fails, most likely due to a violated unique constraint
                    # keeping record with nonexistent partner_id is useless, better delete it
                    query = 'DELETE FROM "%(table)s" WHERE "%(column)s" IN %%s' % query_dic
                    self._cr.execute(query, (tuple(src_partners.ids), ))

        self.invalidate_cache()

    @api.model
    def _update_reference_fields(self, src_partners, dst_partner):
        """ Update all reference fields from the src_partner to dst_partner.
            :param src_partners : merge source res.partner recordset (does not include destination one)
            :param dst_partner : record of destination res.partner
        """
        _logger.debug(
            '_update_reference_fields for dst_partner: %s for src_partners: %r',
            dst_partner.id, src_partners.ids)

        def update_records(model, src, field_model='model', field_id='res_id'):
            Model = self.env[model] if model in self.env else None
            if Model is None:
                return
            records = Model.sudo().search([(field_model, '=', 'res.partner'),
                                           (field_id, '=', src.id)])
            try:
                with mute_logger('flectra.sql_db'), self._cr.savepoint(
                ), self.env.clear_upon_failure():
                    records.sudo().write({field_id: dst_partner.id})
                    records.flush()
            except psycopg2.Error:
                # updating fails, most likely due to a violated unique constraint
                # keeping record with nonexistent partner_id is useless, better delete it
                records.sudo().unlink()

        update_records = functools.partial(update_records)

        for partner in src_partners:
            update_records('calendar',
                           src=partner,
                           field_model='model_id.model')
            update_records('ir.attachment',
                           src=partner,
                           field_model='res_model')
            update_records('mail.followers',
                           src=partner,
                           field_model='res_model')
            update_records('mail.activity',
                           src=partner,
                           field_model='res_model')
            update_records('mail.message', src=partner)
            update_records('ir.model.data', src=partner)

        records = self.env['ir.model.fields'].search([('ttype', '=',
                                                       'reference')])
        for record in records.sudo():
            try:
                Model = self.env[record.model]
                field = Model._fields[record.name]
            except KeyError:
                # unknown model or field => skip
                continue

            if field.compute is not None:
                continue

            for partner in src_partners:
                records_ref = Model.sudo().search([
                    (record.name, '=', 'res.partner,%d' % partner.id)
                ])
                values = {
                    record.name: 'res.partner,%d' % dst_partner.id,
                }
                records_ref.sudo().write(values)

        self.flush()

    def _get_summable_fields(self):
        """ Returns the list of fields that should be summed when merging partners
        """
        return []

    @api.model
    def _update_values(self, src_partners, dst_partner):
        """ Update values of dst_partner with the ones from the src_partners.
            :param src_partners : recordset of source res.partner
            :param dst_partner : record of destination res.partner
        """
        _logger.debug(
            '_update_values for dst_partner: %s for src_partners: %r',
            dst_partner.id, src_partners.ids)

        model_fields = dst_partner.fields_get().keys()
        summable_fields = self._get_summable_fields()

        def write_serializer(item):
            if isinstance(item, models.BaseModel):
                return item.id
            else:
                return item

        # get all fields that are not computed or x2many
        values = dict()
        for column in model_fields:
            field = dst_partner._fields[column]
            if field.type not in ('many2many',
                                  'one2many') and field.compute is None:
                for item in itertools.chain(src_partners, [dst_partner]):
                    if item[column]:
                        if column in summable_fields and values.get(column):
                            values[column] += write_serializer(item[column])
                        else:
                            values[column] = write_serializer(item[column])
        # remove fields that can not be updated (id and parent_id)
        values.pop('id', None)
        parent_id = values.pop('parent_id', None)
        dst_partner.write(values)
        # try to update the parent_id
        if parent_id and parent_id != dst_partner.id:
            try:
                dst_partner.write({'parent_id': parent_id})
            except ValidationError:
                _logger.info(
                    'Skip recursive partner hierarchies for parent_id %s of partner: %s',
                    parent_id, dst_partner.id)

    def _merge(self, partner_ids, dst_partner=None, extra_checks=True):
        """ private implementation of merge partner
            :param partner_ids : ids of partner to merge
            :param dst_partner : record of destination res.partner
            :param extra_checks: pass False to bypass extra sanity check (e.g. email address)
        """
        # super-admin can be used to bypass extra checks
        if self.env.is_admin():
            extra_checks = False

        Partner = self.env['res.partner']
        partner_ids = Partner.browse(partner_ids).exists()
        if len(partner_ids) < 2:
            return

        if len(partner_ids) > 3:
            raise UserError(
                _("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed."
                  ))

        # check if the list of partners to merge contains child/parent relation
        child_ids = self.env['res.partner']
        for partner_id in partner_ids:
            child_ids |= Partner.search([('id', 'child_of', [partner_id.id])
                                         ]) - partner_id
        if partner_ids & child_ids:
            raise UserError(
                _("You cannot merge a contact with one of his parent."))

        if extra_checks and len(set(partner.email
                                    for partner in partner_ids)) > 1:
            raise UserError(
                _("All contacts must have the same email. Only the Administrator can merge contacts with different emails."
                  ))

        # remove dst_partner from partners to merge
        if dst_partner and dst_partner in partner_ids:
            src_partners = partner_ids - dst_partner
        else:
            ordered_partners = self._get_ordered_partner(partner_ids.ids)
            dst_partner = ordered_partners[-1]
            src_partners = ordered_partners[:-1]
        _logger.info("dst_partner: %s", dst_partner.id)

        # FIXME: is it still required to make and exception for account.move.line since accounting v9.0 ?
        if extra_checks and 'account.move.line' in self.env and self.env[
                'account.move.line'].sudo().search([('partner_id', 'in', [
                    partner.id for partner in src_partners
                ])]):
            raise UserError(
                _("Only the destination contact may be linked to existing Journal Items. Please ask the Administrator if you need to merge several contacts linked to existing Journal Items."
                  ))

        # Make the company of all related users consistent with destination partner company
        if dst_partner.company_id:
            partner_ids.mapped('user_ids').sudo().write({
                'company_ids': [(4, dst_partner.company_id.id)],
                'company_id':
                dst_partner.company_id.id
            })

        # call sub methods to do the merge
        self._update_foreign_keys(src_partners, dst_partner)
        self._update_reference_fields(src_partners, dst_partner)
        self._update_values(src_partners, dst_partner)

        self._log_merge_operation(src_partners, dst_partner)

        # delete source partner, since they are merged
        src_partners.unlink()

    def _log_merge_operation(self, src_partners, dst_partner):
        _logger.info('(uid = %s) merged the partners %r with %s', self._uid,
                     src_partners.ids, dst_partner.id)

    # ----------------------------------------
    # Helpers
    # ----------------------------------------

    @api.model
    def _generate_query(self, fields, maximum_group=100):
        """ Build the SQL query on res.partner table to group them according to given criteria
            :param fields : list of column names to group by the partners
            :param maximum_group : limit of the query
        """
        # make the list of column to group by in sql query
        sql_fields = []
        for field in fields:
            if field in ['email', 'name']:
                sql_fields.append('lower(%s)' % field)
            elif field in ['vat']:
                sql_fields.append("replace(%s, ' ', '')" % field)
            else:
                sql_fields.append(field)
        group_fields = ', '.join(sql_fields)

        # where clause : for given group by columns, only keep the 'not null' record
        filters = []
        for field in fields:
            if field in ['email', 'name', 'vat']:
                filters.append((field, 'IS NOT', 'NULL'))
        criteria = ' AND '.join('%s %s %s' % (field, operator, value)
                                for field, operator, value in filters)

        # build the query
        text = [
            "SELECT min(id), array_agg(id)",
            "FROM res_partner",
        ]

        if criteria:
            text.append('WHERE %s' % criteria)

        text.extend([
            "GROUP BY %s" % group_fields,
            "HAVING COUNT(*) >= 2",
            "ORDER BY min(id)",
        ])

        if maximum_group:
            text.append("LIMIT %s" % maximum_group, )

        return ' '.join(text)

    @api.model
    def _compute_selected_groupby(self):
        """ Returns the list of field names the partner can be grouped (as merge
            criteria) according to the option checked on the wizard
        """
        groups = []
        group_by_prefix = 'group_by_'

        for field_name in self._fields:
            if field_name.startswith(group_by_prefix):
                if getattr(self, field_name, False):
                    groups.append(field_name[len(group_by_prefix):])

        if not groups:
            raise UserError(
                _("You have to specify a filter for your selection."))

        return groups

    @api.model
    def _partner_use_in(self, aggr_ids, models):
        """ Check if there is no occurence of this group of partner in the selected model
            :param aggr_ids : stringified list of partner ids separated with a comma (sql array_agg)
            :param models : dict mapping a model name with its foreign key with res_partner table
        """
        return any(self.env[model].search_count([(field, 'in', aggr_ids)])
                   for model, field in models.items())

    @api.model
    def _get_ordered_partner(self, partner_ids):
        """ Helper : returns a `res.partner` recordset ordered by create_date/active fields
            :param partner_ids : list of partner ids to sort
        """
        return self.env['res.partner'].browse(partner_ids).sorted(
            key=lambda p: (p.active,
                           (p.create_date or datetime.datetime(1970, 1, 1))),
            reverse=True,
        )

    def _compute_models(self):
        """ Compute the different models needed by the system if you want to exclude some partners. """
        model_mapping = {}
        if self.exclude_contact:
            model_mapping['res.users'] = 'partner_id'
        if 'account.move.line' in self.env and self.exclude_journal_item:
            model_mapping['account.move.line'] = 'partner_id'
        return model_mapping

    # ----------------------------------------
    # Actions
    # ----------------------------------------

    def action_skip(self):
        """ Skip this wizard line. Don't compute any thing, and simply redirect to the new step."""
        if self.current_line_id:
            self.current_line_id.unlink()
        return self._action_next_screen()

    def _action_next_screen(self):
        """ return the action of the next screen ; this means the wizard is set to treat the
            next wizard line. Each line is a subset of partner that can be merged together.
            If no line left, the end screen will be displayed (but an action is still returned).
        """
        self.invalidate_cache()  # FIXME: is this still necessary?
        values = {}
        if self.line_ids:
            # in this case, we try to find the next record.
            current_line = self.line_ids[0]
            current_partner_ids = literal_eval(current_line.aggr_ids)
            values.update({
                'current_line_id':
                current_line.id,
                'partner_ids': [(6, 0, current_partner_ids)],
                'dst_partner_id':
                self._get_ordered_partner(current_partner_ids)[-1].id,
                'state':
                'selection',
            })
        else:
            values.update({
                'current_line_id': False,
                'partner_ids': [],
                'state': 'finished',
            })

        self.write(values)

        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

    def _process_query(self, query):
        """ Execute the select request and write the result in this wizard
            :param query : the SQL query used to fill the wizard line
        """
        self.ensure_one()
        model_mapping = self._compute_models()

        # group partner query
        self._cr.execute(query)  # pylint: disable=sql-injection

        counter = 0
        for min_id, aggr_ids in self._cr.fetchall():
            # To ensure that the used partners are accessible by the user
            partners = self.env['res.partner'].search([('id', 'in', aggr_ids)])
            if len(partners) < 2:
                continue

            # exclude partner according to options
            if model_mapping and self._partner_use_in(partners.ids,
                                                      model_mapping):
                continue

            self.env['base.partner.merge.line'].create({
                'wizard_id':
                self.id,
                'min_id':
                min_id,
                'aggr_ids':
                partners.ids,
            })
            counter += 1

        self.write({
            'state': 'selection',
            'number_group': counter,
        })

        _logger.info("counter: %s", counter)

    def action_start_manual_process(self):
        """ Start the process 'Merge with Manual Check'. Fill the wizard according to the group_by and exclude
            options, and redirect to the first step (treatment of first wizard line). After, for each subset of
            partner to merge, the wizard will be actualized.
                - Compute the selected groups (with duplication)
                - If the user has selected the 'exclude_xxx' fields, avoid the partners
        """
        self.ensure_one()
        groups = self._compute_selected_groupby()
        query = self._generate_query(groups, self.maximum_group)
        self._process_query(query)
        return self._action_next_screen()

    def action_start_automatic_process(self):
        """ Start the process 'Merge Automatically'. This will fill the wizard with the same mechanism as 'Merge
            with Manual Check', but instead of refreshing wizard with the current line, it will automatically process
            all lines by merging partner grouped according to the checked options.
        """
        self.ensure_one()
        self.action_start_manual_process(
        )  # here we don't redirect to the next screen, since it is automatic process
        self.invalidate_cache()  # FIXME: is this still necessary?

        for line in self.line_ids:
            partner_ids = literal_eval(line.aggr_ids)
            self._merge(partner_ids)
            line.unlink()
            self._cr.commit()  # TODO JEM : explain why

        self.write({'state': 'finished'})
        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

    def parent_migration_process_cb(self):
        self.ensure_one()

        query = """
            SELECT
                min(p1.id),
                array_agg(DISTINCT p1.id)
            FROM
                res_partner as p1
            INNER join
                res_partner as p2
            ON
                p1.email = p2.email AND
                p1.name = p2.name AND
                (p1.parent_id = p2.id OR p1.id = p2.parent_id)
            WHERE
                p2.id IS NOT NULL
            GROUP BY
                p1.email,
                p1.name,
                CASE WHEN p1.parent_id = p2.id THEN p2.id
                    ELSE p1.id
                END
            HAVING COUNT(*) >= 2
            ORDER BY
                min(p1.id)
        """

        self._process_query(query)

        for line in self.line_ids:
            partner_ids = literal_eval(line.aggr_ids)
            self._merge(partner_ids)
            line.unlink()
            self._cr.commit()

        self.write({'state': 'finished'})

        self._cr.execute("""
            UPDATE
                res_partner
            SET
                is_company = NULL,
                parent_id = NULL
            WHERE
                parent_id = id
        """)

        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

    def action_update_all_process(self):
        self.ensure_one()
        self.parent_migration_process_cb()

        # NOTE JEM : seems louche to create a new wizard instead of reuse the current one with updated options.
        # since it is like this from the initial commit of this wizard, I don't change it. yet ...
        wizard = self.create({
            'group_by_vat': True,
            'group_by_email': True,
            'group_by_name': True
        })
        wizard.action_start_automatic_process()

        # NOTE JEM : no idea if this query is usefull
        self._cr.execute("""
            UPDATE
                res_partner
            SET
                is_company = NULL
            WHERE
                parent_id IS NOT NULL AND
                is_company IS NOT NULL
        """)

        return self._action_next_screen()

    def action_merge(self):
        """ Merge Contact button. Merge the selected partners, and redirect to
            the end screen (since there is no other wizard line to process.
        """
        if not self.partner_ids:
            self.write({'state': 'finished'})
            return {
                'type': 'ir.actions.act_window',
                'res_model': self._name,
                'res_id': self.id,
                'view_mode': 'form',
                'target': 'new',
            }

        self._merge(self.partner_ids.ids, self.dst_partner_id)

        if self.current_line_id:
            self.current_line_id.unlink()

        return self._action_next_screen()
Example #26
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 #27
0
class Partner(models.Model):
    """ Update partner to add a field about notification preferences. Add a generic opt-out field that can be used
       to restrict usage of automatic email templates. """
    _name = "res.partner"
    _inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin']
    _mail_flat_thread = False

    message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)
    opt_out = fields.Boolean(
        'Opt-Out', help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
                        "Filter 'Available for Mass Mailing' allows users to filter the partners when performing mass mailing.")
    channel_ids = fields.Many2many('mail.channel', 'mail_channel_partner', 'partner_id', 'channel_id', string='Channels', copy=False)

    @api.multi
    def message_get_suggested_recipients(self):
        recipients = super(Partner, self).message_get_suggested_recipients()
        for partner in self:
            partner._message_add_suggested_recipient(recipients, partner=partner, reason=_('Partner Profile'))
        return recipients

    @api.multi
    def message_get_default_recipients(self):
        return dict((res_id, {'partner_ids': [res_id], 'email_to': False, 'email_cc': False}) for res_id in self.ids)

    @api.model
    def _notify_prepare_template_context(self, message):
        # compute signature
        signature = ""
        if message.author_id and message.author_id.user_ids and message.author_id.user_ids[0].signature:
            signature = message.author_id.user_ids[0].signature
        elif message.author_id:
            signature = "<p>-- <br/>%s</p>" % message.author_id.name

        # compute Sent by
        if message.author_id and message.author_id.user_ids:
            user = message.author_id.user_ids[0]
        else:
            user = self.env.user
        if user.company_id.website:
            website_url = 'http://%s' % user.company_id.website if not user.company_id.website.lower().startswith(('http:', 'https:')) else user.company_id.website
        else:
            website_url = False

        model_name = False
        if message.model:
            model_name = self.env['ir.model']._get(message.model).display_name

        record_name = message.record_name

        tracking = []
        for tracking_value in message.tracking_value_ids:
            tracking.append((tracking_value.field_desc,
                             tracking_value.get_old_display_value()[0],
                             tracking_value.get_new_display_value()[0]))

        is_discussion = message.subtype_id.id == self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')

        record = False
        if message.res_id and message.model in self.env:
            record = self.env[message.model].browse(message.res_id)

        company = user.company_id;
        if record and hasattr(record, 'company_id'):
            company = record.company_id;
        company_name = company.name;

        return {
            'signature': signature,
            'website_url': website_url,
            'company': company,
            'company_name': company_name,
            'model_name': model_name,
            'record': record,
            'record_name': record_name,
            'tracking': tracking,
            'is_discussion': is_discussion,
            'subtype': message.subtype_id,
        }

    @api.model
    def _notify_prepare_email_values(self, message):
        # compute email references
        references = message.parent_id.message_id if message.parent_id else False

        # custom values
        custom_values = dict()
        if message.res_id and message.model in self.env and hasattr(self.env[message.model], 'message_get_email_values'):
            custom_values = self.env[message.model].browse(message.res_id).message_get_email_values(message)

        mail_values = {
            'mail_message_id': message.id,
            'mail_server_id': message.mail_server_id.id,
            'auto_delete': self._context.get('mail_auto_delete', True),
            'references': references,
        }
        mail_values.update(custom_values)
        return mail_values

    @api.model
    def _notify_send(self, body, subject, recipients, **mail_values):
        emails = self.env['mail.mail']
        recipients_nbr = len(recipients)
        for email_chunk in split_every(50, recipients.ids):
            # TDE FIXME: missing message parameter. So we will find mail_message_id
            # in the mail_values and browse it. It should already be in the
            # cache so should not impact performances.
            mail_message_id = mail_values.get('mail_message_id')
            message = self.env['mail.message'].browse(mail_message_id) if mail_message_id else None
            if message and message.model and message.res_id and message.model in self.env and hasattr(self.env[message.model], 'message_get_recipient_values'):
                tig = self.env[message.model].browse(message.res_id)
                recipient_values = tig.message_get_recipient_values(notif_message=message, recipient_ids=email_chunk)
            else:
                recipient_values = self.env['mail.thread'].message_get_recipient_values(notif_message=None, recipient_ids=email_chunk)
            create_values = {
                'body_html': body,
                'subject': subject,
            }
            create_values.update(mail_values)
            create_values.update(recipient_values)
            emails |= self.env['mail.mail'].create(create_values)
        return emails, recipients_nbr

    @api.model
    def _notify_udpate_notifications(self, emails):
        for email in emails:
            notifications = self.env['mail.notification'].sudo().search([
                ('mail_message_id', '=', email.mail_message_id.id),
                ('res_partner_id', 'in', email.recipient_ids.ids)])
            notifications.write({
                'is_email': True,
                'is_read': True,  # handle by email discards Inbox notification
                'email_status': 'ready',
            })

    @api.multi
    def _notify(self, message, force_send=False, send_after_commit=True, user_signature=True):
        """ Method to send email linked to notified messages. The recipients are
        the recordset on which this method is called.

        :param boolean force_send: send notification emails now instead of letting the scheduler handle the email queue
        :param boolean send_after_commit: send notification emails after the transaction end instead of durign the
                                          transaction; this option is used only if force_send is True
        :param user_signature: add current user signature to notification emails """
        if not self.ids:
            return True

        # existing custom notification email
        base_template = None
        if message.model and self._context.get('custom_layout', False):
            base_template = self.env.ref(self._context['custom_layout'], raise_if_not_found=False)
        if not base_template:
            base_template = self.env.ref('mail.mail_template_data_notification_email_default')

        base_template_ctx = self._notify_prepare_template_context(message)
        if not user_signature:
            base_template_ctx['signature'] = False
        base_mail_values = self._notify_prepare_email_values(message)

        # classify recipients: actions / no action
        if message.model and message.res_id and hasattr(self.env[message.model], '_message_notification_recipients'):
            recipients = self.env[message.model].browse(message.res_id)._message_notification_recipients(message, self)
        else:
            recipients = self.env['mail.thread']._message_notification_recipients(message, self)

        emails = self.env['mail.mail']
        recipients_nbr, recipients_max = 0, 50
        for email_type, recipient_template_values in recipients.items():
            if recipient_template_values['followers']:
                # generate notification email content
                template_fol_values = dict(base_template_ctx, **recipient_template_values)  # fixme: set button_unfollow to none
                template_fol_values['has_button_follow'] = False
                template_fol = base_template.with_context(**template_fol_values)
                # generate templates for followers and not followers
                fol_values = template_fol.generate_email(message.id, fields=['body_html', 'subject'])
                # send email
                new_emails, new_recipients_nbr = self._notify_send(fol_values['body'], fol_values['subject'], recipient_template_values['followers'], **base_mail_values)
                # update notifications
                self._notify_udpate_notifications(new_emails)

                emails |= new_emails
                recipients_nbr += new_recipients_nbr
            if recipient_template_values['not_followers']:
                # generate notification email content
                template_not_values = dict(base_template_ctx, **recipient_template_values)  # fixme: set button_follow to none
                template_not_values['has_button_unfollow'] = False
                template_not = base_template.with_context(**template_not_values)
                # generate templates for followers and not followers
                not_values = template_not.generate_email(message.id, fields=['body_html', 'subject'])
                # send email
                new_emails, new_recipients_nbr = self._notify_send(not_values['body'], not_values['subject'], recipient_template_values['not_followers'], **base_mail_values)
                # update notifications
                self._notify_udpate_notifications(new_emails)

                emails |= new_emails
                recipients_nbr += new_recipients_nbr

        # NOTE:
        #   1. for more than 50 followers, use the queue system
        #   2. do not send emails immediately if the registry is not loaded,
        #      to prevent sending email during a simple update of the database
        #      using the command-line.
        test_mode = getattr(threading.currentThread(), 'testing', False)
        if force_send and recipients_nbr < recipients_max and \
                (not self.pool._init or test_mode):
            email_ids = emails.ids
            dbname = self.env.cr.dbname
            _context = self._context

            def send_notifications():
                db_registry = registry(dbname)
                with api.Environment.manage(), db_registry.cursor() as cr:
                    env = api.Environment(cr, SUPERUSER_ID, _context)
                    env['mail.mail'].browse(email_ids).send()

            # unless asked specifically, send emails after the transaction to
            # avoid side effects due to emails being sent while the transaction fails
            if not test_mode and send_after_commit:
                self._cr.after('commit', send_notifications)
            else:
                emails.send()

        return True

    @api.multi
    def _notify_by_chat(self, message):
        """ Broadcast the message to all the partner since """
        message_values = message.message_format()[0]
        notifications = []
        for partner in self:
            notifications.append([(self._cr.dbname, 'ir.needaction', partner.id), dict(message_values)])
        self.env['bus.bus'].sendmany(notifications)

    @api.model
    def get_needaction_count(self):
        """ compute the number of needaction of the current user """
        if self.env.user.partner_id:
            self.env.cr.execute("""
                SELECT count(*) as needaction_count
                FROM mail_message_res_partner_needaction_rel R
                WHERE R.res_partner_id = %s AND (R.is_read = false OR R.is_read IS NULL)""", (self.env.user.partner_id.id,))
            return self.env.cr.dictfetchall()[0].get('needaction_count')
        _logger.error('Call to needaction_count without partner_id')
        return 0

    @api.model
    def get_starred_count(self):
        """ compute the number of starred of the current user """
        if self.env.user.partner_id:
            self.env.cr.execute("""
                SELECT count(*) as starred_count
                FROM mail_message_res_partner_starred_rel R
                WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id,))
            return self.env.cr.dictfetchall()[0].get('starred_count')
        _logger.error('Call to starred_count without partner_id')
        return 0

    @api.model
    def get_static_mention_suggestions(self):
        """ To be overwritten to return the id, name and email of partners used as static mention
            suggestions loaded once at webclient initialization and stored client side. """
        return []

    @api.model
    def get_mention_suggestions(self, search, limit=8):
        """ Return 'limit'-first partners' id, name and email such that the name or email matches a
            'search' string. Prioritize users, and then extend the research to all partners. """
        search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]])
        fields = ['id', 'name', 'email']

        # Search users
        domain = expression.AND([[('user_ids.id', '!=', False)], search_dom])
        users = self.search_read(domain, fields, limit=limit)

        # Search partners if less than 'limit' users found
        partners = []
        if len(users) < limit:
            partners = self.search_read(search_dom, fields, limit=limit)
            # Remove duplicates
            partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])] 

        return [users, partners]
class AccountAnalyticDefault(models.Model):
    _name = "account.analytic.default"
    _description = "Analytic Distribution"
    _rec_name = "analytic_id"
    _order = "sequence"

    sequence = fields.Integer(
        string='Sequence',
        help=
        "Gives the sequence order when displaying a list of analytic distribution"
    )
    analytic_id = fields.Many2one('account.analytic.account',
                                  string='Analytic Account')
    analytic_tag_ids = fields.Many2many('account.analytic.tag',
                                        string='Analytic Tags')
    product_id = fields.Many2one(
        'product.product',
        string='Product',
        ondelete='cascade',
        help=
        "Select a product which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this product, it will automatically take this as an analytic account)"
    )
    partner_id = fields.Many2one(
        'res.partner',
        string='Partner',
        ondelete='cascade',
        help=
        "Select a partner which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this partner, it will automatically take this as an analytic account)"
    )
    account_id = fields.Many2one(
        'account.account',
        string='Account',
        ondelete='cascade',
        help=
        "Select an accounting account which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this account, it will automatically take this as an analytic account)"
    )
    user_id = fields.Many2one(
        'res.users',
        string='User',
        ondelete='cascade',
        help=
        "Select a user which will use analytic account specified in analytic default."
    )
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        ondelete='cascade',
        help=
        "Select a company which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this company, it will automatically take this as an analytic account)"
    )
    date_start = fields.Date(
        string='Start Date',
        help="Default start date for this Analytic Account.")
    date_stop = fields.Date(string='End Date',
                            help="Default end date for this Analytic Account.")

    @api.constrains('analytic_id', 'analytic_tag_ids')
    def _check_account_or_tags(self):
        if any(not default.analytic_id and not default.analytic_tag_ids
               for default in self):
            raise ValidationError(
                _('An analytic default requires at least an analytic account or an analytic tag.'
                  ))

    @api.model
    def account_get(self,
                    product_id=None,
                    partner_id=None,
                    account_id=None,
                    user_id=None,
                    date=None,
                    company_id=None):
        domain = []
        if product_id:
            domain += ['|', ('product_id', '=', product_id)]
        domain += [('product_id', '=', False)]
        if partner_id:
            domain += ['|', ('partner_id', '=', partner_id)]
        domain += [('partner_id', '=', False)]
        if account_id:
            domain += ['|', ('account_id', '=', account_id)]
        domain += [('account_id', '=', False)]
        if company_id:
            domain += ['|', ('company_id', '=', company_id)]
        domain += [('company_id', '=', False)]
        if user_id:
            domain += ['|', ('user_id', '=', user_id)]
        domain += [('user_id', '=', False)]
        if date:
            domain += [
                '|', ('date_start', '<=', date), ('date_start', '=', False)
            ]
            domain += [
                '|', ('date_stop', '>=', date), ('date_stop', '=', False)
            ]
        best_index = -1
        res = self.env['account.analytic.default']
        for rec in self.search(domain):
            index = 0
            if rec.product_id: index += 1
            if rec.partner_id: index += 1
            if rec.account_id: index += 1
            if rec.company_id: index += 1
            if rec.user_id: index += 1
            if rec.date_start: index += 1
            if rec.date_stop: index += 1
            if index > best_index:
                res = rec
                best_index = index
        return res
Example #29
0
class PosConfig(models.Model):
    _inherit = 'pos.config'

    iface_splitbill = fields.Boolean(
        string='Bill Splitting',
        help='Enables Bill Splitting in the Point of Sale.')
    iface_printbill = fields.Boolean(
        string='Bill Printing',
        help='Allows to print the Bill before payment.')
    iface_orderline_notes = fields.Boolean(
        string='Notes', help='Allow custom notes on Orderlines.')
    floor_ids = fields.One2many(
        'restaurant.floor',
        'pos_config_id',
        string='Restaurant Floors',
        help='The restaurant floors served by this point of sale.')
    printer_ids = fields.Many2many('restaurant.printer',
                                   'pos_config_printer_rel',
                                   'config_id',
                                   'printer_id',
                                   string='Order Printers')
    is_table_management = fields.Boolean('Floors & Tables')
    is_order_printer = fields.Boolean('Order Printer')
    set_tip_after_payment = fields.Boolean(
        'Set Tip After Payment',
        help=
        "Adjust the amount authorized by payment terminals to add a tip after the customers left or at the end of the day."
    )
    module_pos_restaurant = fields.Boolean(default=True)

    @api.onchange('module_pos_restaurant')
    def _onchange_module_pos_restaurant(self):
        if not self.module_pos_restaurant:
            self.update({
                'iface_printbill': False,
                'iface_splitbill': False,
                'is_order_printer': False,
                'is_table_management': False,
                'iface_orderline_notes': False
            })

    @api.onchange('iface_tipproduct')
    def _onchange_iface_tipproduct(self):
        if not self.iface_tipproduct:
            self.set_tip_after_payment = False

    def get_tables_order_count(self):
        """         """
        self.ensure_one()
        tables = self.env['restaurant.table'].search([
            ('floor_id.pos_config_id', 'in', self.ids)
        ])
        domain = [('state', '=', 'draft'), ('table_id', 'in', tables.ids)]

        order_stats = self.env['pos.order'].read_group(domain, ['table_id'],
                                                       'table_id')
        orders_map = dict(
            (s['table_id'][0], s['table_id_count']) for s in order_stats)

        result = []
        for table in tables:
            result.append({
                'id': table.id,
                'orders': orders_map.get(table.id, 0)
            })
        return result

    def _get_forbidden_change_fields(self):
        forbidden_keys = super(PosConfig, self)._get_forbidden_change_fields()
        forbidden_keys.append('is_table_management')
        forbidden_keys.append('floor_ids')
        return forbidden_keys

    def write(self, vals):
        if ('is_table_management' in vals
                and vals['is_table_management'] == False):
            vals['floor_ids'] = [(5, 0, 0)]
        if ('is_order_printer' in vals and vals['is_order_printer'] == False):
            vals['printer_ids'] = [(5, 0, 0)]
        return super(PosConfig, self).write(vals)
Example #30
0
class Followers(models.Model):
    """ mail_followers holds the data related to the follow mechanism inside
    Flectra. Partners can choose to follow documents (records) of any kind
    that inherits from mail.thread. Following documents allow to receive
    notifications for new messages. A subscription is characterized by:

    :param: res_model: model of the followed objects
    :param: res_id: ID of resource (may be 0 for every objects)
    """
    _name = 'mail.followers'
    _rec_name = 'partner_id'
    _log_access = False
    _description = 'Document Followers'

    # Note. There is no integrity check on model names for performance reasons.
    # However, followers of unlinked models are deleted by models themselves
    # (see 'ir.model' inheritance).
    res_model = fields.Char('Related Document Model Name',
                            required=True,
                            index=True)
    res_id = fields.Integer('Related Document ID',
                            index=True,
                            help='Id of the followed resource')
    partner_id = fields.Many2one('res.partner',
                                 string='Related Partner',
                                 ondelete='cascade',
                                 index=True)
    channel_id = fields.Many2one('mail.channel',
                                 string='Listener',
                                 ondelete='cascade',
                                 index=True)
    subtype_ids = fields.Many2many(
        'mail.message.subtype',
        string='Subtype',
        help=
        "Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall."
    )

    @api.model
    def _add_follower_command(self,
                              res_model,
                              res_ids,
                              partner_data,
                              channel_data,
                              force=True):
        """ Please upate me
        :param force: if True, delete existing followers before creating new one
                      using the subtypes given in the parameters
        """
        force_mode = force or (all(partner_data.values())
                               and all(channel_data.values()))
        generic = []
        specific = {}
        existing = {}  # {res_id: follower_ids}
        p_exist = {}  # {partner_id: res_ids}
        c_exist = {}  # {channel_id: res_ids}

        followers = self.sudo().search([
            '&', '&', ('res_model', '=', res_model), ('res_id', 'in', res_ids),
            '|', ('partner_id', 'in', list(partner_data)),
            ('channel_id', 'in', list(channel_data))
        ])

        if force_mode:
            followers.unlink()
        else:
            for follower in followers:
                existing.setdefault(follower.res_id, list()).append(follower)
                if follower.partner_id:
                    p_exist.setdefault(follower.partner_id.id,
                                       list()).append(follower.res_id)
                if follower.channel_id:
                    c_exist.setdefault(follower.channel_id.id,
                                       list()).append(follower.res_id)

        default_subtypes, _internal_subtypes, external_subtypes = \
            self.env['mail.message.subtype'].default_subtypes(res_model)

        if force_mode:
            employee_pids = self.env['res.users'].sudo().search([
                ('partner_id', 'in', list(partner_data)), ('share', '=', False)
            ]).mapped('partner_id').ids
            for pid, data in partner_data.items():
                if not data:
                    if pid not in employee_pids:
                        partner_data[pid] = external_subtypes.ids
                    else:
                        partner_data[pid] = default_subtypes.ids
            for cid, data in channel_data.items():
                if not data:
                    channel_data[cid] = default_subtypes.ids

        # create new followers, batch ok
        gen_new_pids = [pid for pid in partner_data if pid not in p_exist]
        gen_new_cids = [cid for cid in channel_data if cid not in c_exist]
        for pid in gen_new_pids:
            generic.append([
                0, 0, {
                    'res_model':
                    res_model,
                    'partner_id':
                    pid,
                    'subtype_ids': [(6, 0, partner_data.get(pid)
                                     or default_subtypes.ids)]
                }
            ])
        for cid in gen_new_cids:
            generic.append([
                0, 0, {
                    'res_model':
                    res_model,
                    'channel_id':
                    cid,
                    'subtype_ids': [(6, 0, channel_data.get(cid)
                                     or default_subtypes.ids)]
                }
            ])

        # create new followers, each document at a time because of existing followers to avoid erasing
        if not force_mode:
            for res_id in res_ids:
                command = []
                doc_followers = existing.get(res_id, list())

                new_pids = set(partner_data) - set([
                    sub.partner_id.id
                    for sub in doc_followers if sub.partner_id
                ]) - set(gen_new_pids)
                new_cids = set(channel_data) - set([
                    sub.channel_id.id
                    for sub in doc_followers if sub.channel_id
                ]) - set(gen_new_cids)

                # subscribe new followers
                for new_pid in new_pids:
                    command.append((0, 0, {
                        'res_model':
                        res_model,
                        'partner_id':
                        new_pid,
                        'subtype_ids': [(6, 0, partner_data.get(new_pid)
                                         or default_subtypes.ids)],
                    }))
                for new_cid in new_cids:
                    command.append((0, 0, {
                        'res_model':
                        res_model,
                        'channel_id':
                        new_cid,
                        'subtype_ids': [(6, 0, channel_data.get(new_cid)
                                         or default_subtypes.ids)],
                    }))
                if command:
                    specific[res_id] = command
        return generic, specific

    #
    # Modifying followers change access rights to individual documents. As the
    # cache may contain accessible/inaccessible data, one has to refresh it.
    #
    @api.multi
    def _invalidate_documents(self):
        """ Invalidate the cache of the documents followed by ``self``. """
        for record in self:
            if record.res_id:
                self.env[record.res_model].invalidate_cache(
                    ids=[record.res_id])

    @api.model
    def create(self, vals):
        res = super(Followers, self).create(vals)
        res._invalidate_documents()
        return res

    @api.multi
    def write(self, vals):
        if 'res_model' in vals or 'res_id' in vals:
            self._invalidate_documents()
        res = super(Followers, self).write(vals)
        self._invalidate_documents()
        return res

    @api.multi
    def unlink(self):
        self._invalidate_documents()
        return super(Followers, self).unlink()

    _sql_constraints = [
        ('mail_followers_res_partner_res_model_id_uniq',
         'unique(res_model,res_id,partner_id)',
         'Error, a partner cannot follow twice the same object.'),
        ('mail_followers_res_channel_res_model_id_uniq',
         'unique(res_model,res_id,channel_id)',
         'Error, a channel cannot follow twice the same object.'),
        ('partner_xor_channel',
         'CHECK((partner_id IS NULL) != (channel_id IS NULL))',
         'Error: A follower must be either a partner or a channel (but not both).'
         )
    ]