class StockValuationLayerRevaluation(models.TransientModel):
    _name = 'stock.valuation.layer.revaluation'
    _description = "Wizard model to reavaluate a stock inventory for a product"
    _check_company_auto = True

    @api.model
    def default_get(self, default_fields):
        res = super().default_get(default_fields)
        if res.get('product_id'):
            product = self.env['product.product'].browse(res['product_id'])
            if product.categ_id.property_cost_method == 'standard':
                raise UserError(
                    _("You cannot revalue a product with a standard cost method."
                      ))
            if product.quantity_svl <= 0:
                raise UserError(
                    _("You cannot revalue a product with an empty or negative stock."
                      ))
            if 'account_journal_id' not in res and 'account_journal_id' in default_fields and product.categ_id.property_valuation == 'real_time':
                accounts = product.product_tmpl_id.get_product_accounts()
                res['account_journal_id'] = accounts['stock_journal'].id
        return res

    company_id = fields.Many2one('res.company',
                                 "Company",
                                 readonly=True,
                                 required=True)
    currency_id = fields.Many2one('res.currency',
                                  "Currency",
                                  related='company_id.currency_id',
                                  required=True)

    product_id = fields.Many2one('product.product',
                                 "Related product",
                                 required=True,
                                 check_company=True)
    property_valuation = fields.Selection(
        related='product_id.categ_id.property_valuation')
    product_uom_name = fields.Char("Unit of Measure",
                                   related='product_id.uom_id.name')
    current_value_svl = fields.Float("Current Value",
                                     related="product_id.value_svl")
    current_quantity_svl = fields.Float("Current Quantity",
                                        related="product_id.quantity_svl")

    added_value = fields.Monetary("Added value", required=True)
    new_value = fields.Monetary("New value", compute='_compute_new_value')
    new_value_by_qty = fields.Monetary("New value by quantity",
                                       compute='_compute_new_value')
    reason = fields.Char("Reason", help="Reason of the revaluation")

    account_journal_id = fields.Many2one('account.journal',
                                         "Journal",
                                         check_company=True)
    account_id = fields.Many2one('account.account',
                                 "Counterpart Account",
                                 domain=[('deprecated', '=', False)],
                                 check_company=True)
    date = fields.Date("Accounting Date")

    @api.depends('current_value_svl', 'current_quantity_svl', 'added_value')
    def _compute_new_value(self):
        for reval in self:
            reval.new_value = reval.current_value_svl + reval.added_value
            if not float_is_zero(
                    reval.current_quantity_svl,
                    precision_rounding=self.product_id.uom_id.rounding):
                reval.new_value_by_qty = reval.new_value / reval.current_quantity_svl
            else:
                reval.new_value_by_qty = 0.0

    def action_validate_revaluation(self):
        """ Revaluate the stock for `self.product_id` in `self.company_id`.

        - Change the stardard price with the new valuation by product unit.
        - Create a manual stock valuation layer with the `added_value` of `self`.
        - Distribute the `added_value` on the remaining_value of layers still in stock (with a remaining quantity)
        - If the Inventory Valuation of the product category is automated, create
        related account move.
        """
        self.ensure_one()
        if self.currency_id.is_zero(self.added_value):
            raise UserError(
                _("The added value doesn't have any impact on the stock valuation"
                  ))

        product_id = self.product_id.with_company(self.company_id)

        remaining_svls = self.env['stock.valuation.layer'].search([
            ('product_id', '=', product_id.id),
            ('remaining_qty', '>', 0),
            ('company_id', '=', self.company_id.id),
        ])

        # Create a manual stock valuation layer
        if self.reason:
            description = _("Manual Stock Valuation: %s.", self.reason)
        else:
            description = _("Manual Stock Valuation: No Reason Given.")
        if product_id.categ_id.property_cost_method == 'average':
            description += _(
                " Product cost updated from %(previous)s to %(new_cost)s.",
                previous=product_id.standard_price,
                new_cost=product_id.standard_price +
                self.added_value / self.current_quantity_svl)
        revaluation_svl_vals = {
            'company_id': self.company_id.id,
            'product_id': product_id.id,
            'description': description,
            'value': self.added_value,
            'quantity': 0,
        }

        remaining_qty = sum(remaining_svls.mapped('remaining_qty'))
        remaining_value = self.added_value
        remaining_value_unit_cost = self.currency_id.round(remaining_value /
                                                           remaining_qty)
        for svl in remaining_svls:
            if float_is_zero(
                    svl.remaining_qty - remaining_qty,
                    precision_rounding=self.product_id.uom_id.rounding):
                svl.remaining_value += remaining_value
            else:
                taken_remaining_value = remaining_value_unit_cost * svl.remaining_qty
                svl.remaining_value += taken_remaining_value
                remaining_value -= taken_remaining_value
                remaining_qty -= svl.remaining_qty

        revaluation_svl = self.env['stock.valuation.layer'].create(
            revaluation_svl_vals)

        # Update the stardard price in case of AVCO
        if product_id.categ_id.property_cost_method == 'average':
            product_id.with_context(
                disable_auto_svl=True
            ).standard_price += self.added_value / self.current_quantity_svl

        # If the Inventory Valuation of the product category is automated, create related account move.
        if self.property_valuation != 'real_time':
            return True

        accounts = product_id.product_tmpl_id.get_product_accounts()

        if self.added_value < 0:
            debit_account_id = self.account_id.id
            credit_account_id = accounts.get(
                'stock_valuation') and accounts['stock_valuation'].id
        else:
            debit_account_id = accounts.get(
                'stock_valuation') and accounts['stock_valuation'].id
            credit_account_id = self.account_id.id

        move_vals = {
            'journal_id':
            self.account_journal_id.id or accounts['stock_journal'].id,
            'company_id':
            self.company_id.id,
            'ref':
            _("Revaluation of %s", product_id.display_name),
            'stock_valuation_layer_ids': [(6, None, [revaluation_svl.id])],
            'date':
            self.date or fields.Date.today(),
            'move_type':
            'entry',
            'line_ids':
            [(0, 0,
              {
                  'name':
                  _(
                      '%(user)s changed stock valuation from  %(previous)s to %(new_value)s - %(product)s',
                      user=self.env.user.name,
                      previous=self.current_value_svl,
                      new_value=self.current_value_svl + self.added_value,
                      product=product_id.display_name,
                  ),
                  'account_id':
                  debit_account_id,
                  'debit':
                  abs(self.added_value),
                  'credit':
                  0,
                  'product_id':
                  product_id.id,
              }),
             (0, 0, {
                 'name':
                 _(
                     '%(user)s changed stock valuation from  %(previous)s to %(new_value)s - %(product)s',
                     user=self.env.user.name,
                     previous=self.current_value_svl,
                     new_value=self.current_value_svl + self.added_value,
                     product=product_id.display_name,
                 ),
                 'account_id':
                 credit_account_id,
                 'debit':
                 0,
                 'credit':
                 abs(self.added_value),
                 'product_id':
                 product_id.id,
             })],
        }
        account_move = self.env['account.move'].create(move_vals)
        account_move._post()

        return True
示例#2
0
class StockLandedCost(models.Model):
    _name = 'stock.landed.cost'
    _description = 'Stock Landed Cost'
    _inherit = ['mail.thread', 'mail.activity.mixin']

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return True

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

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

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

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

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

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

                total_line += 1

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

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

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

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

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

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

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

            val_to_cost_lines = defaultdict(lambda: 0.0)
            for val_line in landed_cost.valuation_adjustment_lines:
                val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost
            if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits)
                   for cost_line, val_amount in val_to_cost_lines.items()):
                return False
        return True
示例#3
0
class CrmLeadReportAssign(models.Model):
    """ CRM Lead Report """
    _name = "crm.lead.report.assign"
    _auto = False
    _description = "CRM Lead Report"

    partner_assigned_id = fields.Many2one('res.partner',
                                          'Partner',
                                          readonly=True)
    grade_id = fields.Many2one('res.partner.grade', 'Grade', readonly=True)
    user_id = fields.Many2one('res.users', 'User', readonly=True)
    country_id = fields.Many2one('res.country', 'Country', readonly=True)
    team_id = fields.Many2one('crm.team',
                              'Sales Channel',
                              oldname='section_id',
                              readonly=True)
    company_id = fields.Many2one('res.company', 'Company', readonly=True)
    date_assign = fields.Date('Assign Date', readonly=True)
    create_date = fields.Datetime('Create Date', readonly=True)
    delay_open = fields.Float('Delay to Assign',
                              digits=(16, 2),
                              readonly=True,
                              group_operator="avg",
                              help="Number of Days to open the case")
    delay_close = fields.Float('Delay to Close',
                               digits=(16, 2),
                               readonly=True,
                               group_operator="avg",
                               help="Number of Days to close the case")
    delay_expected = fields.Float('Overpassed Deadline',
                                  digits=(16, 2),
                                  readonly=True,
                                  group_operator="avg")
    probability = fields.Float('Avg Probability',
                               digits=(16, 2),
                               readonly=True,
                               group_operator="avg")
    probability_max = fields.Float('Max Probability',
                                   digits=(16, 2),
                                   readonly=True,
                                   group_operator="max")
    planned_revenue = fields.Float('Planned Revenue',
                                   digits=(16, 2),
                                   readonly=True)
    probable_revenue = fields.Float('Probable Revenue',
                                    digits=(16, 2),
                                    readonly=True)
    tag_ids = fields.Many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id',
                               'tag_id', 'Tags')
    partner_id = fields.Many2one('res.partner', 'Customer', readonly=True)
    opening_date = fields.Datetime('Opening Date', readonly=True)
    date_closed = fields.Datetime('Close Date', readonly=True)
    nbr_cases = fields.Integer('# of Cases', readonly=True, oldname='nbr')
    company_id = fields.Many2one('res.company', 'Company', readonly=True)
    priority = fields.Selection(crm_stage.AVAILABLE_PRIORITIES, 'Priority')
    type = fields.Selection(
        [('lead', 'Lead'), ('opportunity', 'Opportunity')],
        'Type',
        help="Type is used to separate Leads and Opportunities")

    @api.model_cr
    def init(self):
        """
            CRM Lead Report
            @param cr: the current row, from the database cursor
        """
        tools.drop_view_if_exists(self._cr, 'crm_lead_report_assign')
        self._cr.execute("""
            CREATE OR REPLACE VIEW crm_lead_report_assign AS (
                SELECT
                    c.id,
                    c.date_open as opening_date,
                    c.date_closed as date_closed,
                    c.date_assign,
                    c.user_id,
                    c.probability,
                    c.probability as probability_max,
                    c.type,
                    c.company_id,
                    c.priority,
                    c.team_id,
                    c.partner_id,
                    c.country_id,
                    c.planned_revenue,
                    c.partner_assigned_id,
                    p.grade_id,
                    p.date as partner_date,
                    c.planned_revenue*(c.probability/100) as probable_revenue,
                    1 as nbr_cases,
                    c.create_date as create_date,
                    extract('epoch' from (c.write_date-c.create_date))/(3600*24) as  delay_close,
                    extract('epoch' from (c.date_deadline - c.date_closed))/(3600*24) as  delay_expected,
                    extract('epoch' from (c.date_open-c.create_date))/(3600*24) as  delay_open
                FROM
                    crm_lead c
                    left join res_partner p on (c.partner_assigned_id=p.id)
            )""")
示例#4
0
文件: hr.py 项目: vincentchen/flectra
class Employee(models.Model):

    _inherit = "hr.employee"

    remaining_leaves = fields.Float(
        compute='_compute_remaining_leaves',
        string='Remaining Legal Leaves',
        inverse='_inverse_remaining_leaves',
        help=
        'Total number of legal leaves allocated to this employee, change this value to create allocation/leave request. '
        'Total based on all the leave types without overriding limit.')
    current_leave_state = fields.Selection(compute='_compute_leave_status',
                                           string="Current Leave Status",
                                           selection=[
                                               ('draft', 'New'),
                                               ('confirm', 'Waiting Approval'),
                                               ('refuse', 'Refused'),
                                               ('validate1',
                                                'Waiting Second Approval'),
                                               ('validate', 'Approved'),
                                               ('cancel', 'Cancelled')
                                           ])
    current_leave_id = fields.Many2one('hr.holidays.status',
                                       compute='_compute_leave_status',
                                       string="Current Leave Type")
    leave_date_from = fields.Date('From Date', compute='_compute_leave_status')
    leave_date_to = fields.Date('To Date', compute='_compute_leave_status')
    leaves_count = fields.Float('Number of Leaves',
                                compute='_compute_leaves_count')
    show_leaves = fields.Boolean('Able to see Remaining Leaves',
                                 compute='_compute_show_leaves')
    is_absent_totay = fields.Boolean('Absent Today',
                                     compute='_compute_absent_employee',
                                     search='_search_absent_employee')

    def _get_remaining_leaves(self):
        """ Helper to compute the remaining leaves for the current employees
            :returns dict where the key is the employee id, and the value is the remain leaves
        """
        self._cr.execute(
            """
            SELECT
                sum(h.number_of_days) AS days,
                h.employee_id
            FROM
                hr_holidays h
                join hr_holidays_status s ON (s.id=h.holiday_status_id)
            WHERE
                h.state='validate' AND
                s.limit=False AND
                h.employee_id in %s
            GROUP BY h.employee_id""", (tuple(self.ids), ))
        return dict((row['employee_id'], row['days'])
                    for row in self._cr.dictfetchall())

    @api.multi
    def _compute_remaining_leaves(self):
        remaining = self._get_remaining_leaves()
        for employee in self:
            employee.remaining_leaves = remaining.get(employee.id, 0.0)

    @api.multi
    def _inverse_remaining_leaves(self):
        status_list = self.env['hr.holidays.status'].search([('limit', '=',
                                                              False)])
        # Create leaves (adding remaining leaves) or raise (reducing remaining leaves)
        actual_remaining = self._get_remaining_leaves()
        for employee in self.filtered(
                lambda employee: employee.remaining_leaves):
            # check the status list. This is done here and not before the loop to avoid raising
            # exception on employee creation (since we are in a computed field).
            if len(status_list) != 1:
                raise UserError(
                    _("The feature behind the field 'Remaining Legal Leaves' can only be used when there is only one "
                      "leave type with the option 'Allow to Override Limit' unchecked. (%s Found). "
                      "Otherwise, the update is ambiguous as we cannot decide on which leave type the update has to be done. "
                      "\n You may prefer to use the classic menus 'Leave Requests' and 'Allocation Requests' located in Leaves Application "
                      "to manage the leave days of the employees if the configuration does not allow to use this field."
                      ) % (len(status_list)))
            status = status_list[0] if status_list else None
            if not status:
                continue
            # if a status is found, then compute remaing leave for current employee
            difference = employee.remaining_leaves - actual_remaining.get(
                employee.id, 0)
            if difference > 0:
                leave = self.env['hr.holidays'].create({
                    'name':
                    _('Allocation for %s') % employee.name,
                    'employee_id':
                    employee.id,
                    'holiday_status_id':
                    status.id,
                    'type':
                    'add',
                    'holiday_type':
                    'employee',
                    'number_of_days_temp':
                    difference
                })
                leave.action_approve()
                if leave.double_validation:
                    leave.action_validate()
            elif difference < 0:
                raise UserError(
                    _('You cannot reduce validated allocation requests'))

    @api.multi
    def _compute_leave_status(self):
        # Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule
        holidays = self.env['hr.holidays'].sudo().search([
            ('employee_id', 'in', self.ids),
            ('date_from', '<=', fields.Datetime.now()),
            ('date_to', '>=', fields.Datetime.now()), ('type', '=', 'remove'),
            ('state', 'not in', ('cancel', 'refuse'))
        ])
        leave_data = {}
        for holiday in holidays:
            leave_data[holiday.employee_id.id] = {}
            leave_data[
                holiday.employee_id.id]['leave_date_from'] = holiday.date_from
            leave_data[
                holiday.employee_id.id]['leave_date_to'] = holiday.date_to
            leave_data[
                holiday.employee_id.id]['current_leave_state'] = holiday.state
            leave_data[holiday.employee_id.
                       id]['current_leave_id'] = holiday.holiday_status_id.id

        for employee in self:
            employee.leave_date_from = leave_data.get(
                employee.id, {}).get('leave_date_from')
            employee.leave_date_to = leave_data.get(employee.id,
                                                    {}).get('leave_date_to')
            employee.current_leave_state = leave_data.get(
                employee.id, {}).get('current_leave_state')
            employee.current_leave_id = leave_data.get(
                employee.id, {}).get('current_leave_id')

    @api.multi
    def _compute_leaves_count(self):
        leaves = self.env['hr.holidays'].read_group(
            [('employee_id', 'in', self.ids),
             ('holiday_status_id.limit', '=', False),
             ('state', '=', 'validate')],
            fields=['number_of_days', 'employee_id'],
            groupby=['employee_id'])
        mapping = dict([(leave['employee_id'][0], leave['number_of_days'])
                        for leave in leaves])
        for employee in self:
            employee.leaves_count = mapping.get(employee.id)

    @api.multi
    def _compute_show_leaves(self):
        show_leaves = self.env['res.users'].has_group(
            'hr_holidays.group_hr_holidays_user')
        for employee in self:
            if show_leaves or employee.user_id == self.env.user:
                employee.show_leaves = True
            else:
                employee.show_leaves = False

    @api.multi
    def _compute_absent_employee(self):
        today_date = datetime.datetime.utcnow().date()
        today_start = fields.Datetime.to_string(
            today_date)  # get the midnight of the current utc day
        today_end = fields.Datetime.to_string(
            today_date + relativedelta(hours=23, minutes=59, seconds=59))
        data = self.env['hr.holidays'].read_group(
            [('employee_id', 'in', self.ids),
             ('state', 'not in', ['cancel', 'refuse']),
             ('date_from', '<=', today_end), ('date_to', '>=', today_start),
             ('type', '=', 'remove')], ['employee_id'], ['employee_id'])
        result = dict.fromkeys(self.ids, False)
        for item in data:
            if item['employee_id_count'] >= 1:
                result[item['employee_id'][0]] = True
        for employee in self:
            employee.is_absent_totay = result[employee.id]

    @api.multi
    def _search_absent_employee(self, operator, value):
        today_date = datetime.datetime.utcnow().date()
        today_start = fields.Datetime.to_string(
            today_date)  # get the midnight of the current utc day
        today_end = fields.Datetime.to_string(
            today_date + relativedelta(hours=23, minutes=59, seconds=59))
        holidays = self.env['hr.holidays'].sudo().search([
            ('employee_id', '!=', False),
            ('state', 'not in', ['cancel', 'refuse']),
            ('date_from', '<=', today_end), ('date_to', '>=', today_start),
            ('type', '=', 'remove')
        ])
        return [('id', 'in', holidays.mapped('employee_id').ids)]
示例#5
0
class FleetVehicle(models.Model):
    _inherit = 'mail.thread'
    _name = 'fleet.vehicle'
    _description = 'Information on a vehicle'
    _order = 'license_plate asc'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @api.multi
    def act_show_log_cost(self):
        """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
            @return: the costs log view
        """
        self.ensure_one()
        res = self.env['ir.actions.act_window'].for_xml_id(
            'fleet', 'fleet_vehicle_costs_action')
        res.update(context=dict(self.env.context,
                                default_vehicle_id=self.id,
                                search_default_parent_false=True),
                   domain=[('vehicle_id', '=', self.id)])
        return res
class ShipmentReportEpt(models.TransientModel):
    _name = "shipment.report.ept"
    from_date = fields.Date('From')
    to_date = fields.Date('To')         
示例#7
0
class PurchaseRequisitionLine(models.Model):
    _name = "purchase.requisition.line"
    _description = "Purchase Requisition Line"
    _rec_name = 'product_id'

    product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], required=True)
    product_uom_id = fields.Many2one('uom.uom', string='Product Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
    product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
    product_qty = fields.Float(string='Quantity', digits='Product Unit of Measure')
    product_description_variants = fields.Char('Custom Description')
    price_unit = fields.Float(string='Unit Price', digits='Product Price')
    qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities')
    requisition_id = fields.Many2one('purchase.requisition', required=True, string='Purchase Agreement', ondelete='cascade')
    company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default= lambda self: self.env.company)
    account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account')
    analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
    schedule_date = fields.Date(string='Scheduled Date')
    supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id')

    @api.model
    def create(self,vals):
        res = super(PurchaseRequisitionLine, self).create(vals)
        if res.requisition_id.state not in ['draft', 'cancel', 'done'] and res.requisition_id.is_quantity_copy == 'none':
            supplier_infos = self.env['product.supplierinfo'].search([
                ('product_id', '=', vals.get('product_id')),
                ('name', '=', res.requisition_id.vendor_id.id),
            ])
            if not any(s.purchase_requisition_id for s in supplier_infos):
                res.create_supplier_info()
            if vals['price_unit'] <= 0.0:
                raise UserError(_('You cannot confirm the blanket order without price.'))
        return res

    def write(self, vals):
        res = super(PurchaseRequisitionLine, self).write(vals)
        if 'price_unit' in vals:
            if vals['price_unit'] <= 0.0 and any(
                    requisition.state not in ['draft', 'cancel', 'done'] and
                    requisition.is_quantity_copy == 'none' for requisition in self.mapped('requisition_id')):
                raise UserError(_('You cannot confirm the blanket order without price.'))
            # If the price is updated, we have to update the related SupplierInfo
            self.supplier_info_ids.write({'price': vals['price_unit']})
        return res

    def unlink(self):
        to_unlink = self.filtered(lambda r: r.requisition_id.state not in ['draft', 'cancel', 'done'])
        to_unlink.mapped('supplier_info_ids').unlink()
        return super(PurchaseRequisitionLine, self).unlink()

    def create_supplier_info(self):
        purchase_requisition = self.requisition_id
        if purchase_requisition.type_id.quantity_copy == 'none' and purchase_requisition.vendor_id:
            # create a supplier_info only in case of blanket order
            self.env['product.supplierinfo'].create({
                'name': purchase_requisition.vendor_id.id,
                'product_id': self.product_id.id,
                'product_tmpl_id': self.product_id.product_tmpl_id.id,
                'price': self.price_unit,
                'currency_id': self.requisition_id.currency_id.id,
                'purchase_requisition_line_id': self.id,
            })

    @api.depends('requisition_id.purchase_ids.state')
    def _compute_ordered_qty(self):
        line_found = set()
        for line in self:
            total = 0.0
            for po in line.requisition_id.purchase_ids.filtered(lambda purchase_order: purchase_order.state in ['purchase', 'done']):
                for po_line in po.order_line.filtered(lambda order_line: order_line.product_id == line.product_id):
                    if po_line.product_uom != line.product_uom_id:
                        total += po_line.product_uom._compute_quantity(po_line.product_qty, line.product_uom_id)
                    else:
                        total += po_line.product_qty
            if line.product_id not in line_found :
                line.qty_ordered = total
                line_found.add(line.product_id)
            else:
                line.qty_ordered = 0

    @api.onchange('product_id')
    def _onchange_product_id(self):
        if self.product_id:
            self.product_uom_id = self.product_id.uom_po_id
            self.product_qty = 1.0
        if not self.schedule_date:
            self.schedule_date = self.requisition_id.schedule_date

    def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, taxes_ids=False):
        self.ensure_one()
        requisition = self.requisition_id
        if self.product_description_variants:
            name += '\n' + self.product_description_variants
        if requisition.schedule_date:
            date_planned = datetime.combine(requisition.schedule_date, time.min)
        else:
            date_planned = datetime.now()
        return {
            'name': name,
            'product_id': self.product_id.id,
            'product_uom': self.product_id.uom_po_id.id,
            'product_qty': product_qty,
            'price_unit': price_unit,
            'taxes_id': [(6, 0, taxes_ids)],
            'date_planned': date_planned,
            'account_analytic_id': self.account_analytic_id.id,
            'analytic_tag_ids': self.analytic_tag_ids.ids,
        }
示例#8
0
class SalonOrder(models.Model):
    _name = 'salon.order'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @api.multi
    def unlink(self):
        for order in self:
            if order.stage_id.id == 3 or order.stage_id.id == 4:
                raise UserError(_("You can't delete an invoiced salon order!"))
        return super(SalonOrder, self).unlink()
示例#9
0
class StockCycleCount(models.Model):
    _name = 'stock.cycle.count'
    _description = "Stock Cycle Counts"
    _inherit = 'mail.thread'

    @api.multi
    def _count_inventory_adj(self):
        self.ensure_one()
        self.inventory_adj_count = len(self.stock_adjustment_ids)

    @api.model
    def create(self, vals):
        vals['name'] = self.env['ir.sequence'].next_by_code(
            'stock.cycle.count') or ''
        return super(StockCycleCount, self).create(vals)

    @api.model
    def _company_get(self):
        company_id = self.env['res.company']._company_default_get(self._name)
        return company_id

    name = fields.Char(string='Name', readonly=True)
    location_id = fields.Many2one(
        'stock.location', string='Location', required=True, readonly=True,
        states={'draft': [('readonly', False)]})
    responsible_id = fields.Many2one(
        'res.users', string='Assigned to', readonly=True, states={'draft': [
            ('readonly', False)]}, track_visibility='onchange')
    date_deadline = fields.Date(
        string='Required Date', readonly=True,
        states={'draft': [('readonly', False)]}, track_visibility='onchange')
    cycle_count_rule_id = fields.Many2one(
        'stock.cycle.count.rule', string='Cycle count rule',
        required=True, readonly=True, states={'draft': [('readonly', False)]},
        track_visibility='onchange')
    state = fields.Selection(selection=[
        ('draft', 'Planned'),
        ('open', 'Execution'),
        ('cancelled', 'Cancelled'),
        ('done', 'Done')
    ], string='State', default='draft', track_visibility='onchange')
    stock_adjustment_ids = fields.One2many('stock.inventory', 'cycle_count_id',
                                           string='Inventory Adjustment',
                                           track_visibility='onchange')
    inventory_adj_count = fields.Integer(compute=_count_inventory_adj)
    company_id = fields.Many2one(
        'res.company', string='Company', required=True,
        default=_company_get, readonly=True)

    @api.multi
    def do_cancel(self):
        self.ensure_one()
        self.state = 'cancelled'

    @api.model
    def _prepare_inventory_adjustment(self):
        return {
            'name': 'INV/{}'.format(self.name),
            'cycle_count_id': self.id,
            'location_id': self.location_id.id,
            'exclude_sublocation': True
        }

    @api.multi
    def action_create_inventory_adjustment(self):
        self.ensure_one()
        if self.state != 'draft':
            raise UserError(_(
                "You can only confirm cycle counts in state 'Planned'."
            ))
        data = self._prepare_inventory_adjustment()
        self.env['stock.inventory'].create(data)
        self.state = 'open'
        return True

    @api.multi
    def action_view_inventory(self):
        self.ensure_one()
        result = self.env.ref('stock.action_inventory_form').read()[0]
        adjustment_ids = self.mapped('stock_adjustment_ids').ids
        if len(adjustment_ids) > 0:
            result['domain'] = [('id', 'in', adjustment_ids)]
        return result
示例#10
0
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'
示例#11
0
class DateRangeGenerator(models.TransientModel):
    _name = 'date.range.generator'

    @api.model
    def _default_company(self):
        return self.env['res.company']._company_default_get('date.range')

    name_prefix = fields.Char('Range name prefix', required=True)
    date_start = fields.Date(strint='Start date', required=True)
    type_id = fields.Many2one(comodel_name='date.range.type',
                              string='Type',
                              required=True,
                              domain="['|', ('company_id', '=', company_id), "
                              "('company_id', '=', False)]",
                              ondelete='cascade')
    company_id = fields.Many2one(comodel_name='res.company',
                                 string='Company',
                                 default=_default_company)
    unit_of_time = fields.Selection([(YEARLY, 'years'), (MONTHLY, 'months'),
                                     (WEEKLY, 'weeks'), (DAILY, 'days')],
                                    required=True)
    duration_count = fields.Integer('Duration', required=True)
    count = fields.Integer(string="Number of ranges to generate",
                           required=True)

    @api.multi
    def _compute_date_ranges(self):
        self.ensure_one()
        vals = rrule(freq=self.unit_of_time,
                     interval=self.duration_count,
                     dtstart=fields.Date.from_string(self.date_start),
                     count=self.count + 1)
        vals = list(vals)
        date_ranges = []
        count_digits = len(str(self.count))
        for idx, dt_start in enumerate(vals[:-1]):
            date_start = fields.Date.to_string(dt_start.date())
            # always remove 1 day for the date_end since range limits are
            # inclusive
            dt_end = vals[idx + 1].date() - relativedelta(days=1)
            date_end = fields.Date.to_string(dt_end)
            date_ranges.append({
                'name':
                '%s%0*d' % (self.name_prefix, count_digits, idx + 1),
                'date_start':
                date_start,
                'date_end':
                date_end,
                'type_id':
                self.type_id.id,
                'company_id':
                self.company_id.id
            })
        return date_ranges

    @api.onchange('company_id')
    def _onchange_company_id(self):
        if self.company_id and self.type_id.company_id and \
                self.type_id.company_id != self.company_id:
            self._cache.update(
                self._convert_to_cache({'type_id': False}, update=True))

    @api.multi
    @api.constrains('company_id', 'type_id')
    def _check_company_id_type_id(self):
        for rec in self.sudo():
            if rec.company_id and rec.type_id.company_id and\
                    rec.company_id != rec.type_id.company_id:
                raise ValidationError(
                    _('The Company in the Date Range Generator and in '
                      'Date Range Type must be the same.'))

    @api.multi
    def action_apply(self):
        date_ranges = self._compute_date_ranges()
        if date_ranges:
            for dr in date_ranges:
                self.env['date.range'].create(dr)
        return self.env['ir.actions.act_window'].for_xml_id(
            module='date_range', xml_id='date_range_action')
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
示例#13
0
class HrExpenseSheet(models.Model):

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

    name = fields.Char(string='Expense Report Summary', required=True)
    expense_line_ids = fields.One2many('hr.expense',
                                       'sheet_id',
                                       string='Expense Lines',
                                       states={
                                           'approve': [('readonly', True)],
                                           'done': [('readonly', True)],
                                           'post': [('readonly', True)]
                                       },
                                       copy=False)
    state = fields.Selection([('submit', 'Submitted'), ('approve', 'Approved'),
                              ('post', 'Posted'), ('done', 'Paid'),
                              ('cancel', 'Refused')],
                             string='Status',
                             index=True,
                             readonly=True,
                             track_visibility='onchange',
                             copy=False,
                             default='submit',
                             required=True,
                             help='Expense Report State')
    employee_id = fields.Many2one(
        'hr.employee',
        string="Employee",
        required=True,
        readonly=True,
        states={'submit': [('readonly', False)]},
        default=lambda self: self.env['hr.employee'].search(
            [('user_id', '=', self.env.uid)], limit=1))
    address_id = fields.Many2one('res.partner', string="Employee Home Address")
    payment_mode = fields.Selection(
        [("own_account", "Employee (to reimburse)"),
         ("company_account", "Company")],
        related='expense_line_ids.payment_mode',
        default='own_account',
        readonly=True,
        string="Payment By")
    responsible_id = fields.Many2one('res.users',
                                     'Validation By',
                                     readonly=True,
                                     copy=False,
                                     states={
                                         'submit': [('readonly', False)],
                                         'submit': [('readonly', False)]
                                     })
    total_amount = fields.Float(string='Total Amount',
                                store=True,
                                compute='_compute_amount',
                                digits=dp.get_precision('Account'))
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True,
                                 states={'submit': [('readonly', False)]},
                                 default=lambda self: self.env.user.company_id)
    currency_id = fields.Many2one(
        'res.currency',
        string='Currency',
        readonly=True,
        states={'submit': [('readonly', False)]},
        default=lambda self: self.env.user.company_id.currency_id)
    attachment_number = fields.Integer(compute='_compute_attachment_number',
                                       string='Number of Attachments')
    journal_id = fields.Many2one(
        'account.journal',
        string='Expense Journal',
        states={
            'done': [('readonly', True)],
            'post': [('readonly', True)]
        },
        default=lambda self: self.env['ir.model.data'].
        xmlid_to_object('hr_expense.hr_expense_account_journal') or self.env[
            'account.journal'].search([('type', '=', 'purchase')], limit=1),
        help="The journal used when the expense is done.")
    bank_journal_id = fields.Many2one(
        'account.journal',
        string='Bank Journal',
        states={
            'done': [('readonly', True)],
            'post': [('readonly', True)]
        },
        default=lambda self: self.env['account.journal'].search(
            [('type', 'in', ['cash', 'bank'])], limit=1),
        help="The payment method used when the expense is paid by the company."
    )
    accounting_date = fields.Date(string="Date")
    account_move_id = fields.Many2one('account.move',
                                      string='Journal Entry',
                                      ondelete='restrict',
                                      copy=False)
    department_id = fields.Many2one('hr.department',
                                    string='Department',
                                    states={
                                        'post': [('readonly', True)],
                                        'done': [('readonly', True)]
                                    })

    @api.multi
    def check_consistency(self):
        for rec in self:
            expense_lines = rec.expense_line_ids
            if not expense_lines:
                continue
            if any(expense.employee_id != rec.employee_id
                   for expense in expense_lines):
                raise UserError(
                    _("Expenses must belong to the same Employee."))
            if any(expense.payment_mode != expense_lines[0].payment_mode
                   for expense in expense_lines):
                raise UserError(
                    _("Expenses must have been paid by the same entity (Company or employee)"
                      ))

    @api.model
    def create(self, vals):
        self._create_set_followers(vals)
        sheet = super(HrExpenseSheet, self).create(vals)
        sheet.check_consistency()
        return sheet

    @api.multi
    def write(self, vals):
        res = super(HrExpenseSheet, self).write(vals)
        self.check_consistency()
        if vals.get('employee_id'):
            self._add_followers()
        return res

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

    @api.multi
    def set_to_paid(self):
        self.write({'state': 'done'})

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'state' in init_values and self.state == 'approve':
            return 'hr_expense.mt_expense_approved'
        elif 'state' in init_values and self.state == 'submit':
            return 'hr_expense.mt_expense_confirmed'
        elif 'state' in init_values and self.state == 'cancel':
            return 'hr_expense.mt_expense_refused'
        elif 'state' in init_values and self.state == 'done':
            return 'hr_expense.mt_expense_paid'
        return super(HrExpenseSheet, self)._track_subtype(init_values)

    def _get_users_to_subscribe(self, employee=False):
        users = self.env['res.users']
        employee = employee or self.employee_id
        if employee.user_id:
            users |= employee.user_id
        if employee.parent_id:
            users |= employee.parent_id.user_id
        if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id:
            users |= employee.department_id.manager_id.user_id
        return users

    def _add_followers(self):
        users = self._get_users_to_subscribe()
        self.message_subscribe_users(user_ids=users.ids)

    @api.model
    def _create_set_followers(self, values):
        # Add the followers at creation, so they can be notified
        employee_id = values.get('employee_id')
        if not employee_id:
            return

        employee = self.env['hr.employee'].browse(employee_id)
        users = self._get_users_to_subscribe(employee=employee) - self.env.user
        values['message_follower_ids'] = []
        MailFollowers = self.env['mail.followers']
        for partner in users.mapped('partner_id'):
            values[
                'message_follower_ids'] += MailFollowers._add_follower_command(
                    self._name, [], {partner.id: None}, {})[0]

    @api.onchange('employee_id')
    def _onchange_employee_id(self):
        self.address_id = self.employee_id.address_home_id
        self.department_id = self.employee_id.department_id

    @api.one
    @api.depends('expense_line_ids', 'expense_line_ids.total_amount',
                 'expense_line_ids.currency_id')
    def _compute_amount(self):
        total_amount = 0.0
        for expense in self.expense_line_ids:
            total_amount += expense.currency_id.with_context(
                date=expense.date, company_id=expense.company_id.id).compute(
                    expense.total_amount, self.currency_id)
        self.total_amount = total_amount

    @api.one
    def _compute_attachment_number(self):
        self.attachment_number = sum(
            self.expense_line_ids.mapped('attachment_number'))

    @api.multi
    def refuse_sheet(self, reason):
        if not self.user_has_groups('hr_expense.group_hr_expense_user'):
            raise UserError(_("Only HR Officers can refuse expenses"))
        self.write({'state': 'cancel'})
        for sheet in self:
            sheet.message_post_with_view(
                'hr_expense.hr_expense_template_refuse_reason',
                values={
                    'reason': reason,
                    'is_sheet': True,
                    'name': self.name
                })

    @api.multi
    def approve_expense_sheets(self):
        if not self.user_has_groups('hr_expense.group_hr_expense_user'):
            raise UserError(_("Only HR Officers can approve expenses"))
        self.write({'state': 'approve', 'responsible_id': self.env.user.id})

    @api.multi
    def paid_expense_sheets(self):
        self.write({'state': 'done'})

    @api.multi
    def reset_expense_sheets(self):
        self.mapped('expense_line_ids').write({'is_refused': False})
        return self.write({'state': 'submit'})

    @api.multi
    def action_sheet_move_create(self):
        if any(sheet.state != 'approve' for sheet in self):
            raise UserError(
                _("You can only generate accounting entry for approved expense(s)."
                  ))

        if any(not sheet.journal_id for sheet in self):
            raise UserError(
                _("Expenses must have an expense journal specified to generate accounting entries."
                  ))

        expense_line_ids = self.mapped('expense_line_ids')\
            .filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.user.company_id.currency_id).rounding))
        res = expense_line_ids.action_move_create()

        if not self.accounting_date:
            self.accounting_date = self.account_move_id.date

        if self.payment_mode == 'own_account' and expense_line_ids:
            self.write({'state': 'post'})
        else:
            self.write({'state': 'done'})
        return res

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

    @api.one
    @api.constrains('expense_line_ids', 'employee_id')
    def _check_employee(self):
        employee_ids = self.expense_line_ids.mapped('employee_id')
        if len(employee_ids) > 1 or (len(employee_ids) == 1
                                     and employee_ids != self.employee_id):
            raise ValidationError(
                _('You cannot add expense lines of another employee.'))

    @api.one
    @api.constrains('expense_line_ids')
    def _check_payment_mode(self):
        payment_mode = set(self.expense_line_ids.mapped('payment_mode'))
        if len(payment_mode) > 1:
            raise ValidationError(
                _('You cannot report expenses with different payment modes.'))
示例#14
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)
示例#15
0
class ProfitabilityAnalysis(models.Model):

    _name = "project.profitability.report"
    _description = "Project Profitability Report"
    _order = 'project_id, sale_line_id'
    _auto = False

    analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', readonly=True)
    project_id = fields.Many2one('project.project', string='Project', readonly=True)
    currency_id = fields.Many2one('res.currency', string='Project Currency', readonly=True)
    company_id = fields.Many2one('res.company', string='Project Company', readonly=True)
    user_id = fields.Many2one('res.users', string='Project Manager', readonly=True)
    partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
    line_date = fields.Date("Date", readonly=True)
    # cost
    timesheet_unit_amount = fields.Float("Timesheet Duration", digits=(16, 2), readonly=True, group_operator="sum")
    timesheet_cost = fields.Float("Timesheet Cost", digits=(16, 2), readonly=True, group_operator="sum")
    expense_cost = fields.Float("Other Costs", digits=(16, 2), readonly=True, group_operator="sum")
    # sale revenue
    order_confirmation_date = fields.Datetime('Sales Order Confirmation Date', readonly=True)
    sale_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', readonly=True)
    sale_order_id = fields.Many2one('sale.order', string='Sale Order', readonly=True)
    product_id = fields.Many2one('product.product', string='Product', readonly=True)

    amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Invoice", digits=(16, 2), readonly=True, group_operator="sum")
    amount_untaxed_invoiced = fields.Float("Untaxed Amount Invoiced", digits=(16, 2), readonly=True, group_operator="sum")
    expense_amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Re-invoice", digits=(16, 2), readonly=True, group_operator="sum")
    expense_amount_untaxed_invoiced = fields.Float("Untaxed Amount Re-invoiced", digits=(16, 2), readonly=True, group_operator="sum")
    other_revenues = fields.Float("Other Revenues", digits=(16, 2), readonly=True, group_operator="sum",
                                  help="All revenues that are not from timesheets and that are linked to the analytic account of the project.")
    margin = fields.Float("Margin", digits=(16, 2), readonly=True, group_operator="sum")

    _depends = {
        'sale.order.line': [
            'order_id',
            'invoice_status',
            'price_reduce',
            'product_id',
            'qty_invoiced',
            'untaxed_amount_invoiced',
            'untaxed_amount_to_invoice',
            'currency_id',
            'company_id',
            'is_downpayment',
            'project_id',
            'task_id',
            'qty_delivered_method',
        ],
        'sale.order': [
            'date_order',
            'user_id',
            'partner_id',
            'currency_id',
            'analytic_account_id',
            'order_line',
            'invoice_status',
            'amount_untaxed',
            'currency_rate',
            'company_id',
            'project_id',
        ],
    }

    def init(self):
        tools.drop_view_if_exists(self._cr, self._table)
        query = """
            CREATE VIEW %s AS (
                SELECT
                    sub.id as id,
                    sub.project_id as project_id,
                    sub.user_id as user_id,
                    sub.sale_line_id as sale_line_id,
                    sub.analytic_account_id as analytic_account_id,
                    sub.partner_id as partner_id,
                    sub.company_id as company_id,
                    sub.currency_id as currency_id,
                    sub.sale_order_id as sale_order_id,
                    sub.order_confirmation_date as order_confirmation_date,
                    sub.product_id as product_id,
                    sub.sale_qty_delivered_method as sale_qty_delivered_method,
                    sub.expense_amount_untaxed_to_invoice as expense_amount_untaxed_to_invoice,
                    sub.expense_amount_untaxed_invoiced as expense_amount_untaxed_invoiced,
                    sub.amount_untaxed_to_invoice as amount_untaxed_to_invoice,
                    sub.amount_untaxed_invoiced as amount_untaxed_invoiced,
                    sub.timesheet_unit_amount as timesheet_unit_amount,
                    sub.timesheet_cost as timesheet_cost,
                    sub.expense_cost as expense_cost,
                    sub.other_revenues as other_revenues,
                    sub.line_date as line_date,
                    (sub.expense_amount_untaxed_to_invoice + sub.expense_amount_untaxed_invoiced + sub.amount_untaxed_to_invoice +
                        sub.amount_untaxed_invoiced + sub.other_revenues + sub.timesheet_cost + sub.expense_cost)
                        as margin
                FROM (
                    SELECT
                        ROW_NUMBER() OVER (ORDER BY P.id, SOL.id) AS id,
                        P.id AS project_id,
                        P.user_id AS user_id,
                        SOL.id AS sale_line_id,
                        P.analytic_account_id AS analytic_account_id,
                        P.partner_id AS partner_id,
                        C.id AS company_id,
                        C.currency_id AS currency_id,
                        S.id AS sale_order_id,
                        S.date_order AS order_confirmation_date,
                        SOL.product_id AS product_id,
                        SOL.qty_delivered_method AS sale_qty_delivered_method,
                        COST_SUMMARY.expense_amount_untaxed_to_invoice AS expense_amount_untaxed_to_invoice,
                        COST_SUMMARY.expense_amount_untaxed_invoiced AS expense_amount_untaxed_invoiced,
                        COST_SUMMARY.amount_untaxed_to_invoice AS amount_untaxed_to_invoice,
                        COST_SUMMARY.amount_untaxed_invoiced AS amount_untaxed_invoiced,
                        COST_SUMMARY.timesheet_unit_amount AS timesheet_unit_amount,
                        COST_SUMMARY.timesheet_cost AS timesheet_cost,
                        COST_SUMMARY.expense_cost AS expense_cost,
                        COST_SUMMARY.other_revenues AS other_revenues,
                        COST_SUMMARY.line_date::date AS line_date
                    FROM project_project P
                        JOIN res_company C ON C.id = P.company_id
                        LEFT JOIN (
                            -- Each costs and revenues will be retrieved individually by sub-requests
                            -- This is required to able to get the date
                            SELECT
                                project_id,
                                analytic_account_id,
                                sale_line_id,
                                SUM(timesheet_unit_amount) AS timesheet_unit_amount,
                                SUM(timesheet_cost) AS timesheet_cost,
                                SUM(expense_cost) AS expense_cost,
                                SUM(other_revenues) AS other_revenues,
                                SUM(expense_amount_untaxed_to_invoice) AS expense_amount_untaxed_to_invoice,
                                SUM(expense_amount_untaxed_invoiced) AS expense_amount_untaxed_invoiced,
                                SUM(amount_untaxed_to_invoice) AS amount_untaxed_to_invoice,
                                SUM(amount_untaxed_invoiced) AS amount_untaxed_invoiced,
                                line_date AS line_date
                            FROM (
                                -- Get the timesheet costs
                                SELECT
                                    P.id AS project_id,
                                    P.analytic_account_id AS analytic_account_id,
                                    TS.so_line AS sale_line_id,
                                    TS.unit_amount AS timesheet_unit_amount,
                                    TS.amount AS timesheet_cost,
                                    0.0 AS other_revenues,
                                    0.0 AS expense_cost,
                                    0.0 AS expense_amount_untaxed_to_invoice,
                                    0.0 AS expense_amount_untaxed_invoiced,
                                    0.0 AS amount_untaxed_to_invoice,
                                    0.0 AS amount_untaxed_invoiced,
                                    TS.date AS line_date
                                FROM account_analytic_line TS, project_project P
                                WHERE TS.project_id IS NOT NULL AND P.id = TS.project_id AND P.active = 't' AND P.allow_timesheets = 't'

                                UNION ALL

                                -- Get the other revenues (products that are not services)
                                SELECT
                                    P.id AS project_id,
                                    P.analytic_account_id AS analytic_account_id,
                                    AAL.so_line AS sale_line_id,
                                    0.0 AS timesheet_unit_amount,
                                    0.0 AS timesheet_cost,
                                    AAL.amount AS other_revenues,
                                    0.0 AS expense_cost,
                                    0.0 AS expense_amount_untaxed_to_invoice,
                                    0.0 AS expense_amount_untaxed_invoiced,
                                    0.0 AS amount_untaxed_to_invoice,
                                    0.0 AS amount_untaxed_invoiced,
                                    AAL.date AS line_date
                                FROM project_project P
                                    JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
                                    JOIN account_analytic_line AAL ON AAL.account_id = AA.id
                                    LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = AAL.move_id
                                    LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
                                    LEFT JOIN account_move_line AML ON AAL.move_id = AML.id
                                                                   AND AML.parent_state = 'posted'
                                                                   AND AML.exclude_from_invoice_tab = 'f'
                                    -- Check if it's not a Credit Note for a Vendor Bill
                                    LEFT JOIN account_move RBILL ON RBILL.id = AML.move_id
                                    LEFT JOIN account_move_line BILLL ON BILLL.move_id = RBILL.reversed_entry_id
                                                                  AND BILLL.parent_state = 'posted'
                                                                  AND BILLL.exclude_from_invoice_tab = 'f'
                                                                  AND BILLL.product_id = AML.product_id
                                    -- Check if it's not an Invoice reversed by a Credit Note
                                    LEFT JOIN account_move RINV ON RINV.reversed_entry_id = AML.move_id
                                    LEFT JOIN account_move_line RINVL ON RINVL.move_id = RINV.id
                                                                  AND RINVL.parent_state = 'posted'
                                                                  AND RINVL.exclude_from_invoice_tab = 'f'
                                                                  AND RINVL.product_id = AML.product_id
                                WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't'
                                    AND P.allow_timesheets = 't'
                                    AND BILLL.id IS NULL
                                    AND RINVL.id IS NULL
                                    AND (SOL.id IS NULL
                                        OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE AND SOL.is_service IS NOT TRUE))

                                UNION ALL

                                -- Get the expense costs from account analytic line
                                SELECT
                                    P.id AS project_id,
                                    P.analytic_account_id AS analytic_account_id,
                                    AAL.so_line AS sale_line_id,
                                    0.0 AS timesheet_unit_amount,
                                    0.0 AS timesheet_cost,
                                    0.0 AS other_revenues,
                                    AAL.amount AS expense_cost,
                                    0.0 AS expense_amount_untaxed_to_invoice,
                                    0.0 AS expense_amount_untaxed_invoiced,
                                    0.0 AS amount_untaxed_to_invoice,
                                    0.0 AS amount_untaxed_invoiced,
                                    AAL.date AS line_date
                                FROM project_project P
                                    JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
                                    JOIN account_analytic_line AAL ON AAL.account_id = AA.id
                                    LEFT JOIN account_move_line AML ON AAL.move_id = AML.id
                                                                   AND AML.parent_state = 'posted'
                                                                   AND AML.exclude_from_invoice_tab = 'f'
                                    -- Check if it's not a Credit Note for an Invoice
                                    LEFT JOIN account_move RINV ON RINV.id = AML.move_id
                                    LEFT JOIN account_move_line INVL ON INVL.move_id = RINV.reversed_entry_id
                                                                    AND INVL.parent_state = 'posted'
                                                                    AND INVL.exclude_from_invoice_tab = 'f'
                                                                    AND INVL.product_id = AML.product_id
                                    -- Check if it's not a Bill reversed by a Credit Note
                                    LEFT JOIN account_move RBILL ON RBILL.reversed_entry_id = AML.move_id
                                    LEFT JOIN account_move_line RBILLL ON RBILLL.move_id = RBILL.id
                                                                      AND RBILLL.parent_state = 'posted'
                                                                      AND RBILLL.exclude_from_invoice_tab = 'f'
                                                                      AND RBILLL.product_id = AML.product_id
                                    -- Check if the AAL is not related to a consumed downpayment (when the SOL is fully invoiced - with downpayment discounted.)
                                    LEFT JOIN sale_order_line_invoice_rel SOINVDOWN ON SOINVDOWN.invoice_line_id = AML.id
                                    LEFT JOIN sale_order_line SOLDOWN on SOINVDOWN.order_line_id = SOLDOWN.id AND SOLDOWN.is_downpayment = 't'
                                WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL
                                  AND INVL.id IS NULL
                                  AND RBILLL.id IS NULL
                                  AND SOLDOWN.id IS NULL
                                  AND P.active = 't' AND P.allow_timesheets = 't'

                                UNION ALL

                                -- Get the following values: expense amount untaxed to invoice/invoiced, amount untaxed to invoice/invoiced
                                -- These values have to be computed from all the records retrieved just above but grouped by project and sale order line
                                SELECT
                                    AMOUNT_UNTAXED.project_id AS project_id,
                                    AMOUNT_UNTAXED.analytic_account_id AS analytic_account_id,
                                    AMOUNT_UNTAXED.sale_line_id AS sale_line_id,
                                    0.0 AS timesheet_unit_amount,
                                    0.0 AS timesheet_cost,
                                    0.0 AS other_revenues,
                                    0.0 AS expense_cost,
                                    CASE
                                        WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
                                        ELSE 0.0
                                    END AS expense_amount_untaxed_to_invoice,
                                    CASE
                                        WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status = 'invoiced'
                                        THEN
                                            CASE
                                                WHEN T.expense_policy = 'sales_price'
                                                THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
                                                ELSE -AMOUNT_UNTAXED.expense_cost
                                            END
                                        ELSE 0.0
                                    END AS expense_amount_untaxed_invoiced,
                                    CASE
                                        WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
                                        ELSE 0.0
                                    END AS amount_untaxed_to_invoice,
                                    CASE
                                        WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
                                        ELSE 0.0
                                    END AS amount_untaxed_invoiced,
                                    S.date_order AS line_date
                                FROM project_project P
                                    JOIN res_company C ON C.id = P.company_id
                                    LEFT JOIN (
                                        -- Gets SOL linked to timesheets
                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            AAL.so_line AS sale_line_id,
                                            0.0 AS expense_cost
                                        FROM account_analytic_line AAL, project_project P
                                        WHERE AAL.project_id IS NOT NULL AND P.id = AAL.project_id AND P.active = 't'
                                        GROUP BY P.id, AAL.so_line
                                        UNION
                                        -- Service SOL linked to a project task AND not yet timesheeted
                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            SOL.id AS sale_line_id,
                                            0.0 AS expense_cost
                                        FROM sale_order_line SOL
                                        JOIN project_task T ON T.sale_line_id = SOL.id
                                        JOIN project_project P ON T.project_id = P.id
                                        LEFT JOIN account_analytic_line AAL ON AAL.task_id = T.id
                                        WHERE SOL.is_service = 't'
                                          AND AAL.id IS NULL -- not timesheeted
                                          AND P.active = 't' AND P.allow_timesheets = 't'
                                        GROUP BY P.id, SOL.id
                                        UNION
                                        -- Service SOL linked to project AND not yet timesheeted
                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            SOL.id AS sale_line_id,
                                            0.0 AS expense_cost
                                        FROM sale_order_line SOL
                                        JOIN project_project P ON P.sale_line_id = SOL.id
                                        LEFT JOIN account_analytic_line AAL ON AAL.project_id = P.id
                                        LEFT JOIN project_task T ON T.sale_line_id = SOL.id
                                        WHERE SOL.is_service = 't'
                                          AND AAL.id IS NULL -- not timesheeted
                                          AND (T.id IS NULL OR T.project_id != P.id) -- not linked to a task in this project
                                          AND P.active = 't' AND P.allow_timesheets = 't'
                                        GROUP BY P.id, SOL.id
                                        UNION
                                        -- Service SOL linked to analytic account AND not yet timesheeted
                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            SOL.id AS sale_line_id,
                                            0.0 AS expense_cost
                                        FROM sale_order_line SOL
                                        JOIN sale_order SO ON SO.id = SOL.order_id
                                        JOIN account_analytic_account AA ON AA.id = SO.analytic_account_id
                                        JOIN project_project P ON P.analytic_account_id = AA.id
                                        LEFT JOIN project_project PSOL ON PSOL.sale_line_id = SOL.id
                                        LEFT JOIN project_task TSOL ON TSOL.sale_line_id = SOL.id
                                        LEFT JOIN account_analytic_line AAL ON AAL.so_line = SOL.id
                                        WHERE SOL.is_service = 't'
                                          AND AAL.id IS NULL -- not timesheeted
                                          AND TSOL.id IS NULL -- not linked to a task
                                          AND PSOL.id IS NULL -- not linked to a project
                                          AND P.active = 't' AND P.allow_timesheets = 't'
                                        GROUP BY P.id, SOL.id
                                        UNION

                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            AAL.so_line AS sale_line_id,
                                            0.0 AS expense_cost
                                        FROM project_project P
                                            LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
                                            LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
                                        WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
                                        GROUP BY P.id, AA.id, AAL.so_line
                                        UNION
                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            AAL.so_line AS sale_line_id,
                                            SUM(AAL.amount) AS expense_cost
                                        FROM project_project P
                                            LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
                                            LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
                                        WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
                                        GROUP BY P.id, AA.id, AAL.so_line
                                        UNION
                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            SOLDOWN.id AS sale_line_id,
                                            0.0 AS expense_cost
                                        FROM project_project P
                                            LEFT JOIN sale_order_line SOL ON P.sale_line_id = SOL.id
                                            LEFT JOIN sale_order SO ON SO.id = SOL.order_id OR SO.analytic_account_id = P.analytic_account_id
                                            LEFT JOIN sale_order_line SOLDOWN ON SOLDOWN.order_id = SO.id AND SOLDOWN.is_downpayment = 't'
                                            LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.order_line_id = SOLDOWN.id
                                            LEFT JOIN account_move_line INVL ON SOINV.invoice_line_id = INVL.id
                                                                            AND INVL.parent_state = 'posted'
                                                                            AND INVL.exclude_from_invoice_tab = 'f'
                                            LEFT JOIN account_move RINV ON INVL.move_id = RINV.reversed_entry_id
                                            LEFT JOIN account_move_line RINVL ON RINV.id = RINVL.move_id
                                                                            AND RINVL.parent_state = 'posted'
                                                                            AND RINVL.exclude_from_invoice_tab = 'f'
                                                                            AND RINVL.product_id = SOLDOWN.product_id
                                            LEFT JOIN account_analytic_line ANLI ON ANLI.move_id = RINVL.id AND ANLI.amount < 0.0
                                        WHERE ANLI.id IS NULL -- there are no credit note for this downpayment
                                          AND P.active = 't' AND P.allow_timesheets = 't'
                                        GROUP BY P.id, SOLDOWN.id
                                        UNION
                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            SOL.id AS sale_line_id,
                                            0.0 AS expense_cost
                                        FROM sale_order_line SOL
                                            INNER JOIN project_project P ON SOL.project_id = P.id
                                        WHERE P.active = 't' AND P.allow_timesheets = 't'
                                        UNION
                                        SELECT
                                            P.id AS project_id,
                                            P.analytic_account_id AS analytic_account_id,
                                            SOL.id AS sale_line_id,
                                            0.0 AS expense_cost
                                        FROM sale_order_line SOL
                                            INNER JOIN project_task T ON SOL.task_id = T.id
                                            INNER JOIN project_project P ON P.id = T.project_id
                                        WHERE P.active = 't' AND P.allow_timesheets = 't'
                                    ) AMOUNT_UNTAXED ON AMOUNT_UNTAXED.project_id = P.id
                                    LEFT JOIN sale_order_line SOL ON AMOUNT_UNTAXED.sale_line_id = SOL.id
                                    LEFT JOIN sale_order S ON SOL.order_id = S.id
                                    LEFT JOIN product_product PP on (SOL.product_id = PP.id)
                                    LEFT JOIN product_template T on (PP.product_tmpl_id = T.id)
                                    WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL
                            ) SUB_COST_SUMMARY
                            GROUP BY project_id, analytic_account_id, sale_line_id, line_date
                        ) COST_SUMMARY ON COST_SUMMARY.project_id = P.id
                        LEFT JOIN sale_order_line SOL ON COST_SUMMARY.sale_line_id = SOL.id
                        LEFT JOIN sale_order S ON SOL.order_id = S.id
                        WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL
                    ) AS sub
            )
        """ % self._table
        self._cr.execute(query)
示例#16
0
class PayslipReport(models.Model):
    _name = "payslip.report"
    _description = "Payslip Analysis"
    _auto = False

    name = fields.Char(readonly=True)
    date_from = fields.Date(string='Date From', readonly=True)
    date_to = fields.Date(string='Date To', readonly=True)
    year = fields.Char(size=4, readonly=True)
    month = fields.Selection([('01', 'January'), ('02', 'February'),
                              ('03', 'March'), ('04', 'April'), ('05', 'May'),
                              ('06', 'June'), ('07', 'July'), ('08', 'August'),
                              ('09', 'September'), ('10', 'October'),
                              ('11', 'November'), ('12', 'December')],
                             readonly=True)
    day = fields.Char(size=128, readonly=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('done', 'Done'),
        ('cancel', 'Rejected'),
    ],
                             string='Status',
                             readonly=True)
    employee_id = fields.Many2one('hr.employee',
                                  string='Employee',
                                  readonly=True)
    nbr = fields.Integer(string='# Payslip lines', readonly=True)
    number = fields.Char(readonly=True)
    struct_id = fields.Many2one('hr.payroll.structure',
                                string='Structure',
                                readonly=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True)
    paid = fields.Boolean(string='Made Payment Order ? ', readonly=True)
    total = fields.Float(readonly=True)
    category_id = fields.Many2one('hr.salary.rule.category',
                                  string='Category',
                                  readonly=True)

    @api.model_cr
    def init(self):
        drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute("""
            create or replace view payslip_report as (
                select
                    min(l.id) as id,
                    l.name,
                    p.struct_id,
                    p.state,
                    p.date_from,
                    p.date_to,
                    p.number,
                    p.company_id,
                    p.paid,
                    l.category_id,
                    l.employee_id,
                    sum(l.total) as total,
                    to_char(p.date_from, 'YYYY') as year,
                    to_char(p.date_from, 'MM') as month,
                    to_char(p.date_from, 'YYYY-MM-DD') as day,
                    to_char(p.date_to, 'YYYY') as to_year,
                    to_char(p.date_to, 'MM') as to_month,
                    to_char(p.date_to, 'YYYY-MM-DD') as to_day,
                    1 AS nbr
                from
                    hr_payslip as p
                    left join hr_payslip_line as l on (p.id=l.slip_id)
                where
                    l.employee_id IS NOT NULL
                group by
                    p.number,l.name,p.date_from,p.date_to,p.state,p.company_id,p.paid,
                    l.employee_id,p.struct_id,l.category_id
            )
        """)
示例#17
0
class CrossoveredBudgetLines(models.Model):
    _name = "crossovered.budget.lines"
    _description = "Budget Line"

    crossovered_budget_id = fields.Many2one('crossovered.budget', 'Budget', ondelete='cascade', index=True, required=True)
    analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account')
    general_budget_id = fields.Many2one('account.budget.post', 'Budgetary Position', required=True)
    date_from = fields.Date('Start Date', required=True)
    date_to = fields.Date('End Date', required=True)
    paid_date = fields.Date('Paid Date')
    planned_amount = fields.Float('Planned Amount', required=True, digits=0)
    practical_amount = fields.Float(compute='_compute_practical_amount', string='Practical Amount', digits=0)
    theoritical_amount = fields.Float(compute='_compute_theoritical_amount', string='Theoretical Amount', digits=0)
    percentage = fields.Float(compute='_compute_percentage', string='Achievement')
    company_id = fields.Many2one(related='crossovered_budget_id.company_id', comodel_name='res.company',
        string='Company', store=True, readonly=True)

    @api.multi
    def _compute_practical_amount(self):
        for line in self:
            result = 0.0
            acc_ids = line.general_budget_id.account_ids.ids
            date_to = self.env.context.get('wizard_date_to') or line.date_to
            date_from = self.env.context.get('wizard_date_from') or line.date_from
            if line.analytic_account_id.id:
                self.env.cr.execute("""
                    SELECT SUM(amount)
                    FROM account_analytic_line
                    WHERE account_id=%s
                        AND (date between to_date(%s,'yyyy-mm-dd') AND to_date(%s,'yyyy-mm-dd'))
                        AND general_account_id=ANY(%s)""",
                (line.analytic_account_id.id, date_from, date_to, acc_ids,))
                result = self.env.cr.fetchone()[0] or 0.0
            line.practical_amount = result

    @api.multi
    def _compute_theoritical_amount(self):
        today = fields.Datetime.now()
        for line in self:
            # Used for the report

            if self.env.context.get('wizard_date_from') and self.env.context.get('wizard_date_to'):
                date_from = fields.Datetime.from_string(self.env.context.get('wizard_date_from'))
                date_to = fields.Datetime.from_string(self.env.context.get('wizard_date_to'))
                if date_from < fields.Datetime.from_string(line.date_from):
                    date_from = fields.Datetime.from_string(line.date_from)
                elif date_from > fields.Datetime.from_string(line.date_to):
                    date_from = False

                if date_to > fields.Datetime.from_string(line.date_to):
                    date_to = fields.Datetime.from_string(line.date_to)
                elif date_to < fields.Datetime.from_string(line.date_from):
                    date_to = False

                theo_amt = 0.00
                if date_from and date_to:
                    line_timedelta = fields.Datetime.from_string(line.date_to) - fields.Datetime.from_string(line.date_from)
                    elapsed_timedelta = date_to - date_from
                    if elapsed_timedelta.days > 0:
                        theo_amt = (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.planned_amount
            else:
                if line.paid_date:
                    if fields.Datetime.from_string(line.date_to) <= fields.Datetime.from_string(line.paid_date):
                        theo_amt = 0.00
                    else:
                        theo_amt = line.planned_amount
                else:
                    line_timedelta = fields.Datetime.from_string(line.date_to) - fields.Datetime.from_string(line.date_from)
                    elapsed_timedelta = fields.Datetime.from_string(today) - (fields.Datetime.from_string(line.date_from))

                    if elapsed_timedelta.days < 0:
                        # If the budget line has not started yet, theoretical amount should be zero
                        theo_amt = 0.00
                    elif line_timedelta.days > 0 and fields.Datetime.from_string(today) < fields.Datetime.from_string(line.date_to):
                        # If today is between the budget line date_from and date_to
                        theo_amt = (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.planned_amount
                    else:
                        theo_amt = line.planned_amount

            line.theoritical_amount = theo_amt

    @api.multi
    def _compute_percentage(self):
        for line in self:
            if line.theoritical_amount != 0.00:
                line.percentage = float((line.practical_amount or 0.0) / line.theoritical_amount) * 100
            else:
                line.percentage = 0.00
示例#18
0
class ProductProduct(models.Model):
    _inherit = "product.product"

    date_from = fields.Date(compute='_compute_product_margin_fields_values',
                            string='Margin Date From')
    date_to = fields.Date(compute='_compute_product_margin_fields_values',
                          string='Margin Date To')
    invoice_state = fields.Selection(
        compute='_compute_product_margin_fields_values',
        selection=[('paid', 'Paid'), ('open_paid', 'Open and Paid'),
                   ('draft_open_paid', 'Draft, Open and Paid')],
        string='Invoice State',
        readonly=True)
    sale_avg_price = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Avg. Sale Unit Price',
        help="Avg. Price in Customer Invoices.")
    purchase_avg_price = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Avg. Purchase Unit Price',
        help="Avg. Price in Vendor Bills ")
    sale_num_invoiced = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='# Invoiced in Sale',
        help="Sum of Quantity in Customer Invoices")
    purchase_num_invoiced = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='# Invoiced in Purchase',
        help="Sum of Quantity in Vendor Bills")
    sales_gap = fields.Float(compute='_compute_product_margin_fields_values',
                             string='Sales Gap',
                             help="Expected Sale - Turn Over")
    purchase_gap = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Purchase Gap',
        help="Normal Cost - Total Cost")
    turnover = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Turnover',
        help=
        "Sum of Multiplication of Invoice price and quantity of Customer Invoices"
    )
    total_cost = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Total Cost',
        help=
        "Sum of Multiplication of Invoice price and quantity of Vendor Bills ")
    sale_expected = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Expected Sale',
        help=
        "Sum of Multiplication of Sale Catalog price and quantity of Customer Invoices"
    )
    normal_cost = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Normal Cost',
        help="Sum of Multiplication of Cost price and quantity of Vendor Bills"
    )
    total_margin = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Total Margin',
        help="Turnover - Standard price")
    expected_margin = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Expected Margin',
        help="Expected Sale - Normal Cost")
    total_margin_rate = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Total Margin Rate(%)',
        help="Total margin * 100 / Turnover")
    expected_margin_rate = fields.Float(
        compute='_compute_product_margin_fields_values',
        string='Expected Margin (%)',
        help="Expected margin * 100 / Expected Sale")

    @api.model
    def read_group(self,
                   domain,
                   fields,
                   groupby,
                   offset=0,
                   limit=None,
                   orderby=False,
                   lazy=True):
        """
            Inherit read_group to calculate the sum of the non-stored fields, as it is not automatically done anymore through the XML.
        """
        res = super(ProductProduct, self).read_group(domain,
                                                     fields,
                                                     groupby,
                                                     offset=offset,
                                                     limit=limit,
                                                     orderby=orderby,
                                                     lazy=lazy)
        fields_list = [
            'turnover', 'sale_avg_price', 'sale_purchase_price',
            'sale_num_invoiced', 'purchase_num_invoiced', 'sales_gap',
            'purchase_gap', 'total_cost', 'sale_expected', 'normal_cost',
            'total_margin', 'expected_margin', 'total_margin_rate',
            'expected_margin_rate'
        ]
        if any(x in fields for x in fields_list):
            # Calculate first for every product in which line it needs to be applied
            re_ind = 0
            prod_re = {}
            tot_products = self.browse([])
            for re in res:
                if re.get('__domain'):
                    products = self.search(re['__domain'])
                    tot_products |= products
                    for prod in products:
                        prod_re[prod.id] = re_ind
                re_ind += 1
            res_val = tot_products._compute_product_margin_fields_values(
                field_names=[x for x in fields if fields in fields_list])
            for key in res_val:
                for l in res_val[key]:
                    re = res[prod_re[key]]
                    if re.get(l):
                        re[l] += res_val[key][l]
                    else:
                        re[l] = res_val[key][l]
        return res

    def _compute_product_margin_fields_values(self, field_names=None):
        res = {}
        if field_names is None:
            field_names = []
        for val in self:
            res[val.id] = {}
            date_from = self.env.context.get('date_from',
                                             time.strftime('%Y-01-01'))
            date_to = self.env.context.get('date_to',
                                           time.strftime('%Y-12-31'))
            invoice_state = self.env.context.get('invoice_state', 'open_paid')
            res[val.id]['date_from'] = date_from
            res[val.id]['date_to'] = date_to
            res[val.id]['invoice_state'] = invoice_state
            states = ()
            payment_states = ()
            if invoice_state == 'paid':
                states = ('posted', )
                payment_states = ('paid', )
            elif invoice_state == 'open_paid':
                states = ('posted', )
                payment_states = ('not_paid', 'paid')
            elif invoice_state == 'draft_open_paid':
                states = ('posted', 'draft')
                payment_states = ('not_paid', 'paid')
            company_id = self.env.company.id

            #Cost price is calculated afterwards as it is a property
            self.env['account.move.line'].flush([
                'price_unit', 'quantity', 'balance', 'product_id',
                'display_type'
            ])
            self.env['account.move'].flush([
                'state', 'payment_state', 'move_type', 'invoice_date',
                'company_id'
            ])
            self.env['product.template'].flush(['list_price'])
            sqlstr = """
                WITH currency_rate AS ({})
                SELECT
                    SUM(
                        l.price_unit / (CASE COALESCE(cr.rate, 0) WHEN 0 THEN 1.0 ELSE cr.rate END) *
                        l.quantity * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END) * ((100 - l.discount) * 0.01)
                    ) / NULLIF(SUM(l.quantity * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END)), 0) AS avg_unit_price,
                    SUM(l.quantity * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END)) AS num_qty,
                    SUM(ABS(l.balance) * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END)) AS total,
                    SUM(l.quantity * pt.list_price * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END)) AS sale_expected
                FROM account_move_line l
                LEFT JOIN account_move i ON (l.move_id = i.id)
                LEFT JOIN product_product product ON (product.id=l.product_id)
                LEFT JOIN product_template pt ON (pt.id = product.product_tmpl_id)
                left join currency_rate cr on
                (cr.currency_id = i.currency_id and
                 cr.company_id = i.company_id and
                 cr.date_start <= COALESCE(i.invoice_date, NOW()) and
                 (cr.date_end IS NULL OR cr.date_end > COALESCE(i.invoice_date, NOW())))
                WHERE l.product_id = %s
                AND i.state IN %s
                AND i.payment_state IN %s
                AND i.move_type IN %s
                AND i.invoice_date BETWEEN %s AND  %s
                AND i.company_id = %s
                AND l.display_type IS NULL
                AND l.exclude_from_invoice_tab = false
                """.format(self.env['res.currency']._select_companies_rates())
            invoice_types = ('out_invoice', 'out_refund')
            self.env.cr.execute(sqlstr,
                                (val.id, states, payment_states, invoice_types,
                                 date_from, date_to, company_id))
            result = self.env.cr.fetchall()[0]
            res[val.id]['sale_avg_price'] = result[0] and result[0] or 0.0
            res[val.id]['sale_num_invoiced'] = result[1] and result[1] or 0.0
            res[val.id]['turnover'] = result[2] and result[2] or 0.0
            res[val.id]['sale_expected'] = result[3] and result[3] or 0.0
            res[val.id]['sales_gap'] = res[val.id]['sale_expected'] - res[
                val.id]['turnover']
            invoice_types = ('in_invoice', 'in_refund')
            self.env.cr.execute(sqlstr,
                                (val.id, states, payment_states, invoice_types,
                                 date_from, date_to, company_id))
            result = self.env.cr.fetchall()[0]
            res[val.id]['purchase_avg_price'] = result[0] and result[0] or 0.0
            res[val.
                id]['purchase_num_invoiced'] = result[1] and result[1] or 0.0
            res[val.id]['total_cost'] = result[2] and result[2] or 0.0
            res[val.id]['normal_cost'] = val.standard_price * res[
                val.id]['purchase_num_invoiced']
            res[val.id]['purchase_gap'] = res[val.id]['normal_cost'] - res[
                val.id]['total_cost']

            res[val.id]['total_margin'] = res[val.id]['turnover'] - res[
                val.id]['total_cost']
            res[val.id]['expected_margin'] = res[
                val.id]['sale_expected'] - res[val.id]['normal_cost']
            res[val.id]['total_margin_rate'] = res[val.id]['turnover'] and res[
                val.id]['total_margin'] * 100 / res[val.id]['turnover'] or 0.0
            res[val.id]['expected_margin_rate'] = res[
                val.id]['sale_expected'] and res[
                    val.id]['expected_margin'] * 100 / res[
                        val.id]['sale_expected'] or 0.0
            for k, v in res[val.id].items():
                setattr(val, k, v)
        return res
示例#19
0
class Contract(models.Model):

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

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

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

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

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

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

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

        return True

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'state' in init_values and self.state == 'pending':
            return 'hr_contract.mt_contract_pending'
        elif 'state' in init_values and self.state == 'close':
            return 'hr_contract.mt_contract_close'
        return super(Contract, self)._track_subtype(init_values)
示例#20
0
class SaleOrder(models.Model):
    """Add several date fields to Sales Orders, computed or user-entered"""
    _inherit = 'sale.order'

    commitment_date = fields.Datetime(
        compute='_compute_commitment_date',
        string='Commitment Date',
        store=True,
        help="Date by which the products are sure to be delivered. This is "
        "a date that you can promise to the customer, based on the "
        "Product Lead Times.")
    requested_date = fields.Datetime(
        'Requested Date',
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'sent': [('readonly', False)]
        },
        copy=False,
        help="Date by which the customer has requested the items to be "
        "delivered.\n"
        "When this Order gets confirmed, the Delivery Order's "
        "expected date will be computed based on this date and the "
        "Company's Security Delay.\n"
        "Leave this field empty if you want the Delivery Order to be "
        "processed as soon as possible. In that case the expected "
        "date will be computed using the default method: based on "
        "the Product Lead Times and the Company's Security Delay.")
    effective_date = fields.Date(
        compute='_compute_picking_ids',
        string='Effective Date',
        store=True,
        help="Date on which the first Delivery Order was created.")

    @api.depends('date_order', 'order_line.customer_lead')
    def _compute_commitment_date(self):
        """Compute the commitment date"""
        for order in self:
            dates_list = []
            order_datetime = fields.Datetime.from_string(order.date_order)
            for line in order.order_line.filtered(
                    lambda x: x.state != 'cancel'):
                dt = order_datetime + timedelta(days=line.customer_lead or 0.0)
                dates_list.append(dt)
            if dates_list:
                commit_date = min(
                    dates_list) if order.picking_policy == 'direct' else max(
                        dates_list)
                order.commitment_date = fields.Datetime.to_string(commit_date)

    def _compute_picking_ids(self):
        super(SaleOrder, self)._compute_picking_ids()
        for order in self:
            dates_list = []
            for pick in order.picking_ids:
                dates_list.append(fields.Datetime.from_string(pick.date))
            if dates_list:
                order.effective_date = fields.Datetime.to_string(
                    min(dates_list))

    @api.onchange('requested_date')
    def onchange_requested_date(self):
        """Warn if the requested dates is sooner than the commitment date"""
        if (self.requested_date and self.commitment_date
                and self.requested_date < self.commitment_date):
            return {
                'warning': {
                    'title':
                    _('Requested date is too soon!'),
                    'message':
                    _("The date requested by the customer is "
                      "sooner than the commitment date. You may be "
                      "unable to honor the customer's request.")
                }
            }
示例#21
0
class PurchaseRequisition(models.Model):
    _name = "purchase.requisition"
    _description = "Purchase Requisition"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = "id desc"

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

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

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

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

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

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

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

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

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

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

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

    def unlink(self):
        if any(requisition.state not in ('draft', 'cancel') for requisition in self):
            raise UserError(_('You can only delete draft requisitions.'))
        # Draft requisitions could have some requisition lines.
        self.mapped('line_ids').unlink()
        return super(PurchaseRequisition, self).unlink()
示例#22
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.'
                )
示例#23
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='daily',
                                   required=True)
    next_run_date = fields.Date(string='Next Send Date')
    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.company.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_',
                     'x_studio_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.env.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):
        discussion_subtype_id = self.env.ref('mail.mt_comment').id
        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),
                ('subtype_id', '=', discussion_subtype_id),
                ('message_type', 'in', ['comment', 'email'])
            ])
            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):
        digest = super(Digest, self).create(vals)
        if not digest.next_run_date:
            digest.next_run_date = digest._get_next_run_date()
        return digest

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

    def action_subscribe(self):
        if self.env.user.has_group(
                'base.group_user') and self.env.user not in self.user_ids:
            self.sudo().user_ids |= self.env.user

    def action_unsubcribe(self):
        if self.env.user.has_group(
                'base.group_user') and self.env.user in self.user_ids:
            self.sudo().user_ids -= self.env.user

    def action_activate(self):
        self.state = 'activated'

    def action_deactivate(self):
        self.state = 'deactivated'

    def action_set_periodicity(self, periodicity):
        self.periodicity = periodicity

    def action_send(self):
        to_slowdown = self._check_daily_logs()
        for digest in self:
            for user in digest.user_ids:
                digest.with_context(digest_slowdown=digest in to_slowdown,
                                    lang=user.lang)._action_send_to_user(
                                        user, tips_count=1)
            if digest in to_slowdown:
                digest.write({'periodicity': 'weekly'})
            digest.next_run_date = digest._get_next_run_date()

    def _action_send_to_user(self, user, tips_count=1, consum_tips=True):
        web_base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')

        rendered_body = self.env['mail.render.mixin']._render_template(
            'digest.digest_mail_main',
            'digest.digest',
            self.ids,
            engine='qweb',
            add_context={
                'title':
                self.name,
                'top_button_label':
                _('Connect'),
                'top_button_url':
                url_join(web_base_url, '/web/login'),
                'company':
                user.company_id,
                'user':
                user,
                'tips_count':
                tips_count,
                'formatted_date':
                datetime.today().strftime('%B %d, %Y'),
                'display_mobile_banner':
                True,
                'kpi_data':
                self.compute_kpis(user.company_id, user),
                'tips':
                self.compute_tips(user.company_id,
                                  user,
                                  tips_count=tips_count,
                                  consumed=consum_tips),
                'preferences':
                self.compute_preferences(user.company_id, user),
            },
            post_process=True)[self.id]
        full_mail = self.env['mail.render.mixin']._render_encapsulate(
            'digest.digest_mail_layout',
            rendered_body,
            add_context={
                'company': user.company_id,
                'user': user,
            },
        )
        # create a mail_mail based on values, without attachments
        mail_values = {
            'auto_delete':
            True,
            'email_from': (self.company_id.partner_id.email_formatted
                           or self.env.user.email_formatted
                           or self.env.ref('base.user_root').email_formatted),
            'email_to':
            user.email_formatted,
            'body_html':
            full_mail,
            'state':
            'outgoing',
            'subject':
            '%s: %s' % (user.company_id.name, self.name),
        }
        mail = self.env['mail.mail'].sudo().create(mail_values)
        return True

    @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.',
                    digest.id)

    # ------------------------------------------------------------
    # KPIS
    # ------------------------------------------------------------

    def compute_kpis(self, company, user):
        """ Compute KPIs to display in the digest template. It is expected to be
        a list of KPIs, each containing values for 3 columns display.

        :return list: result [{
            'kpi_name': 'kpi_mail_message',
            'kpi_fullname': 'Messages',  # translated
            'kpi_action': 'crm.crm_lead_action_pipeline',  # xml id of an action to execute
            'kpi_col1': {
                'value': '12.0',
                'margin': 32.36,
                'col_subtitle': 'Yesterday',  # translated
            },
            'kpi_col2': { ... },
            'kpi_col3':  { ... },
        }, { ... }] """
        self.ensure_one()
        digest_fields = self._get_kpi_fields()
        invalid_fields = []
        kpis = [
            dict(
                kpi_name=field_name,
                kpi_fullname=self.env['ir.model.fields']._get(
                    self._name, field_name).field_description,
                kpi_action=False,
                kpi_col1=dict(),
                kpi_col2=dict(),
                kpi_col3=dict(),
            ) for field_name in digest_fields
        ]
        kpis_actions = self._compute_kpis_actions(company, user)

        for col_index, (tf_name,
                        tf) in enumerate(self._compute_timeframes(company)):
            digest = self.with_context(
                start_date=tf[0][0],
                end_date=tf[0][1]).with_user(user).with_company(company)
            previous_digest = self.with_context(
                start_date=tf[1][0],
                end_date=tf[1][1]).with_user(user).with_company(company)
            for index, field_name in enumerate(digest_fields):
                kpi_values = kpis[index]
                kpi_values['kpi_action'] = kpis_actions.get(field_name)
                try:
                    compute_value = digest[field_name + '_value']
                    # Context start and end date is different each time so invalidate to recompute.
                    digest.invalidate_cache([field_name + '_value'])
                    previous_value = previous_digest[field_name + '_value']
                    # Context start and end date is different each time so invalidate to recompute.
                    previous_digest.invalidate_cache([field_name + '_value'])
                except AccessError:  # no access rights -> just skip that digest details from that user's digest email
                    invalid_fields.append(field_name)
                    continue
                margin = self._get_margin_value(compute_value, previous_value)
                if self._fields['%s_value' % field_name].type == 'monetary':
                    converted_amount = tools.format_decimalized_amount(
                        compute_value)
                    compute_value = self._format_currency_amount(
                        converted_amount, company.currency_id)
                kpi_values['kpi_col%s' % (col_index + 1)].update({
                    'value':
                    compute_value,
                    'margin':
                    margin,
                    'col_subtitle':
                    tf_name,
                })

        # filter failed KPIs
        return [kpi for kpi in kpis if kpi['kpi_name'] not in invalid_fields]

    def compute_tips(self, company, user, tips_count=1, consumed=True):
        tips = self.env['digest.tip'].search(
            [('user_ids', '!=', user.id), '|',
             ('group_id', 'in', user.groups_id.ids), ('group_id', '=', False)],
            limit=tips_count)
        tip_descriptions = [
            self.env['mail.render.mixin']._render_template(
                tools.html_sanitize(tip.tip_description),
                'digest.tip',
                tip.ids,
                post_process=True)[tip.id] for tip in tips
        ]
        if consumed:
            tips.user_ids += user
        return tip_descriptions

    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 compute_preferences(self, company, user):
        """ Give an optional text for preferences, like a shortcut for configuration.

        :return string: html to put in template
        """
        preferences = []
        if self._context.get('digest_slowdown'):
            preferences.append(
                _("We have noticed you did not connect these last few days so we've automatically switched your preference to weekly Digests."
                  ))
        elif self.periodicity == 'daily' and user.has_group(
                'base.group_erp_manager'):
            preferences.append(
                '<p>%s<br /><a href="/digest/%s/set_periodicity?periodicity=weekly" target="_blank" style="color:#009EFB; font-weight: bold;">%s</a></p>'
                % (_('Prefer a broader overview ?'), self.id,
                   _('Switch to weekly Digests')))
        if user.has_group('base.group_erp_manager'):
            preferences.append(
                '<p>%s<br /><a href="/web#view_type=form&amp;model=%s&amp;id=%s" target="_blank" style="color:#009EFB; font-weight: bold;">%s</a></p>'
                % (_('Want to customize this email?'), self._name, self.id,
                   _('Choose the metrics you care about')))

        return preferences

    def _get_next_run_date(self):
        self.ensure_one()
        if self.periodicity == 'daily':
            delta = relativedelta(days=1)
        if 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)))),
                (_('Last 7 Days'), ((start_date + relativedelta(weeks=-1),
                                     start_date),
                                    (start_date + relativedelta(weeks=-2),
                                     start_date + relativedelta(weeks=-1)))),
                (_('Last 30 Days'), ((start_date + relativedelta(months=-1),
                                      start_date),
                                     (start_date + relativedelta(months=-2),
                                      start_date + relativedelta(months=-1))))]

    # ------------------------------------------------------------
    # FORMATTING / TOOLS
    # ------------------------------------------------------------

    def _get_kpi_fields(self):
        return [
            field_name for field_name, field in self._fields.items()
            if field.type == 'boolean' and field_name.startswith((
                'kpi_', 'x_kpi_', 'x_studio_kpi_')) and self[field_name]
        ]

    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 _check_daily_logs(self):
        three_days_ago = datetime.now().replace(
            hour=0, minute=0, second=0, microsecond=0) - relativedelta(days=3)
        to_slowdown = self.env['digest.digest']
        for digest in self.filtered(
                lambda digest: digest.periodicity == 'daily'):
            users_logs = self.env['res.users.log'].sudo().search_count([
                ('create_uid', 'in', digest.user_ids.ids),
                ('create_date', '>=', three_days_ago)
            ])
            if not users_logs:
                to_slowdown += digest
        return to_slowdown

    def _format_currency_amount(self, amount, currency_id):
        pre = currency_id.position == 'before'
        symbol = u'{symbol}'.format(symbol=currency_id.symbol or '')
        return u'{pre}{0}{post}'.format(amount,
                                        pre=symbol if pre else '',
                                        post=symbol if not pre else '')
class AccountInvoiceReport(models.Model):
    _name = "account.invoice.report"
    _inherit = ['ir.branch.company.mixin']
    _description = "Invoices Statistics"
    _auto = False
    _rec_name = 'date'

    @api.multi
    @api.depends('currency_id', 'date', 'price_total', 'price_average', 'residual')
    def _compute_amounts_in_user_currency(self):
        """Compute the amounts in the currency of the user
        """
        context = dict(self._context or {})
        user_currency_id = self.env.user.company_id.currency_id
        currency_rate_id = self.env['res.currency.rate'].search([
            ('rate', '=', 1),
            '|', ('company_id', '=', self.env.user.company_id.id), ('company_id', '=', False)], limit=1)
        base_currency_id = currency_rate_id.currency_id
        ctx = context.copy()
        for record in self:
            ctx['date'] = record.date
            record.user_currency_price_total = base_currency_id.with_context(ctx).compute(record.price_total, user_currency_id)
            record.user_currency_price_average = base_currency_id.with_context(ctx).compute(record.price_average, user_currency_id)
            record.user_currency_residual = base_currency_id.with_context(ctx).compute(record.residual, user_currency_id)

    date = fields.Date(readonly=True)
    product_id = fields.Many2one('product.product', string='Product', readonly=True)
    product_qty = fields.Float(string='Product Quantity', readonly=True)
    uom_name = fields.Char(string='Reference Unit of Measure', readonly=True)
    payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term', readonly=True)
    fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position', readonly=True)
    currency_id = fields.Many2one('res.currency', string='Currency', readonly=True)
    categ_id = fields.Many2one('product.category', string='Product Category', readonly=True)
    journal_id = fields.Many2one('account.journal', string='Journal', readonly=True)
    partner_id = fields.Many2one('res.partner', string='Partner', readonly=True)
    commercial_partner_id = fields.Many2one('res.partner', string='Partner Company', help="Commercial Entity")
    company_id = fields.Many2one('res.company', string='Company', readonly=True)
    user_id = fields.Many2one('res.users', string='Salesperson', readonly=True)
    price_total = fields.Float(string='Total Without Tax', readonly=True)
    user_currency_price_total = fields.Float(string="Total Without Tax", compute='_compute_amounts_in_user_currency', digits=0)
    price_average = fields.Float(string='Average Price', readonly=True, group_operator="avg")
    user_currency_price_average = fields.Float(string="Average Price", compute='_compute_amounts_in_user_currency', digits=0)
    currency_rate = fields.Float(string='Currency Rate', readonly=True, group_operator="avg", groups="base.group_multi_currency")
    nbr = fields.Integer(string='# of Lines', readonly=True)  # TDE FIXME master: rename into nbr_lines
    type = fields.Selection([
        ('out_invoice', 'Customer Invoice'),
        ('in_invoice', 'Vendor Bill'),
        ('out_refund', 'Customer Credit Note'),
        ('in_refund', 'Vendor Credit Note'),
        ], readonly=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('open', 'Open'),
        ('paid', 'Paid'),
        ('cancel', 'Cancelled')
        ], string='Invoice Status', readonly=True)
    date_due = fields.Date(string='Due Date', readonly=True)
    account_id = fields.Many2one('account.account', string='Account', readonly=True, domain=[('deprecated', '=', False)])
    account_line_id = fields.Many2one('account.account', string='Account Line', readonly=True, domain=[('deprecated', '=', False)])
    partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account', readonly=True)
    residual = fields.Float(string='Due Amount', readonly=True)
    user_currency_residual = fields.Float(string="Total Residual", compute='_compute_amounts_in_user_currency', digits=0)
    country_id = fields.Many2one('res.country', string='Country of the Partner Company')
    account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account', groups="analytic.group_analytic_accounting")

    _order = 'date desc'

    _depends = {
        'account.invoice': [
            'account_id', 'amount_total_company_signed', 'commercial_partner_id', 'company_id', 'branch_id',
            'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id',
            'journal_id', 'partner_bank_id', 'partner_id', 'payment_term_id',
            'residual', 'state', 'type', 'user_id',
        ],
        'account.invoice.line': [
            'account_id', 'invoice_id', 'price_subtotal', 'product_id',
            'quantity', 'uom_id', 'account_analytic_id',
        ],
        'product.product': ['product_tmpl_id'],
        'product.template': ['categ_id'],
        'product.uom': ['category_id', 'factor', 'name', 'uom_type'],
        'res.currency.rate': ['currency_id', 'name'],
        'res.partner': ['country_id'],
    }

    def _select(self):
        select_str = """
            SELECT sub.id, sub.date, sub.product_id, sub.partner_id, sub.country_id, sub.account_analytic_id,
                sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id,
                sub.fiscal_position_id, sub.user_id, sub.company_id, sub.branch_id, sub.nbr, sub.type, sub.state,
                sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id,
                sub.product_qty, sub.price_total as price_total, sub.price_average as price_average,
                COALESCE(cr.rate, 1) as currency_rate, sub.residual as residual, sub.commercial_partner_id as commercial_partner_id
        """
        return select_str

    def _sub_select(self):
        select_str = """
                SELECT ail.id AS id,
                    ai.date_invoice AS date,
                    ail.product_id, ai.partner_id, ai.payment_term_id, ail.account_analytic_id,
                    u2.name AS uom_name,
                    ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id,
                    1 AS nbr,
                    ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id,
                    ai.partner_bank_id,
                    SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty,
                    SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total,
                    SUM(ABS(ail.price_subtotal_signed)) / CASE
                            WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric
                               THEN SUM(ail.quantity / u.factor * u2.factor)
                               ELSE 1::numeric
                            END AS price_average,
                    ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
                    count(*) * invoice_type.sign AS residual,
                    ai.commercial_partner_id as commercial_partner_id,
                    coalesce(partner.country_id, partner_ai.country_id) AS country_id
        """
        return select_str

    def _from(self):
        from_str = """
                FROM account_invoice_line ail
                JOIN account_invoice ai ON ai.id = ail.invoice_id
                JOIN res_partner partner ON ai.commercial_partner_id = partner.id
                JOIN res_partner partner_ai ON ai.partner_id = partner_ai.id
                LEFT JOIN product_product pr ON pr.id = ail.product_id
                left JOIN product_template pt ON pt.id = pr.product_tmpl_id
                LEFT JOIN product_uom u ON u.id = ail.uom_id
                LEFT JOIN product_uom u2 ON u2.id = pt.uom_id
                JOIN (
                    -- Temporary table to decide if the qty should be added or retrieved (Invoice vs Credit Note)
                    SELECT id,(CASE
                         WHEN ai.type::text = ANY (ARRAY['in_refund'::character varying::text, 'in_invoice'::character varying::text])
                            THEN -1
                            ELSE 1
                        END) AS sign,(CASE
                         WHEN ai.type::text = ANY (ARRAY['out_refund'::character varying::text, 'in_invoice'::character varying::text])
                            THEN -1
                            ELSE 1
                        END) AS sign_qty
                    FROM account_invoice ai
                ) AS invoice_type ON invoice_type.id = ai.id
        """
        return from_str

    def _group_by(self):
        group_by_str = """
                GROUP BY ail.id, ail.product_id, ail.account_analytic_id, ai.date_invoice, ai.id,
                    ai.partner_id, ai.payment_term_id, u2.name, u2.id, ai.currency_id, ai.journal_id,
                    ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id, ai.type, invoice_type.sign, ai.state, pt.categ_id,
                    ai.date_due, ai.account_id, ail.account_id, ai.partner_bank_id, ai.residual_company_signed,
                    ai.amount_total_company_signed, ai.commercial_partner_id, coalesce(partner.country_id, partner_ai.country_id)
        """
        return group_by_str

    @api.model_cr
    def init(self):
        # self._table = account_invoice_report
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
            WITH currency_rate AS (%s)
            %s
            FROM (
                %s %s %s
            ) AS sub
            LEFT JOIN currency_rate cr ON
                (cr.currency_id = sub.currency_id AND
                 cr.company_id = sub.company_id AND
                 cr.date_start <= COALESCE(sub.date, NOW()) AND
                 (cr.date_end IS NULL OR cr.date_end > COALESCE(sub.date, NOW())))
        )""" % (
                    self._table, self.env['res.currency']._select_companies_rates(),
                    self._select(), self._sub_select(), self._from(), self._group_by()))
示例#25
0
class FleetReport(models.Model):
    _name = "fleet.vehicle.cost.report"
    _description = "Fleet Analysis Report"
    _auto = False
    _order = 'date_start desc'

    company_id = fields.Many2one('res.company', 'Company', readonly=True)
    vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', readonly=True)
    name = fields.Char('Vehicle Name', readonly=True)
    driver_id = fields.Many2one('res.partner', 'Driver', readonly=True)
    fuel_type = fields.Char('Fuel', readonly=True)
    date_start = fields.Date('Date', readonly=True)

    cost = fields.Float('Cost', readonly=True)
    cost_type = fields.Selection(string='Cost Type',
                                 selection=[('contract', 'Contract'),
                                            ('service', 'Service')],
                                 readonly=True)

    def init(self):
        query = """
WITH service_costs AS (
    SELECT
        ve.id AS vehicle_id,
        ve.company_id AS company_id,
        ve.name AS name,
        ve.driver_id AS driver_id,
        ve.fuel_type AS fuel_type,
        date(date_trunc('month', d)) AS date_start,
        COALESCE(sum(se.amount), 0) AS
        COST,
        'service' AS cost_type
    FROM
        fleet_vehicle ve
    CROSS JOIN generate_series((
            SELECT
                min(date)
                FROM fleet_vehicle_log_services), CURRENT_DATE + '1 month'::interval, '1 month') d
        LEFT JOIN fleet_vehicle_log_services se ON se.vehicle_id = ve.id
            AND date_trunc('month', se.date) = date_trunc('month', d)
    WHERE
        ve.active AND se.active AND se.state != 'cancelled'
    GROUP BY
        ve.id,
        ve.company_id,
        ve.name,
        date_start,
        d
    ORDER BY
        ve.id,
        date_start
),
contract_costs AS (
    SELECT
        ve.id AS vehicle_id,
        ve.company_id AS company_id,
        ve.name AS name,
        ve.driver_id AS driver_id,
        ve.fuel_type AS fuel_type,
        date(date_trunc('month', d)) AS date_start,
        (COALESCE(sum(co.amount), 0) + COALESCE(sum(cod.cost_generated * extract(day FROM least (date_trunc('month', d) + interval '1 month', cod.expiration_date) - greatest (date_trunc('month', d), cod.start_date))), 0) + COALESCE(sum(com.cost_generated), 0) + COALESCE(sum(coy.cost_generated), 0)) AS
        COST,
        'contract' AS cost_type
    FROM
        fleet_vehicle ve
    CROSS JOIN generate_series((
            SELECT
                min(acquisition_date)
                FROM fleet_vehicle), CURRENT_DATE + '1 month'::interval, '1 month') d
        LEFT JOIN fleet_vehicle_log_contract co ON co.vehicle_id = ve.id
            AND date_trunc('month', co.date) = date_trunc('month', d)
        LEFT JOIN fleet_vehicle_log_contract cod ON cod.vehicle_id = ve.id
            AND date_trunc('month', cod.start_date) <= date_trunc('month', d)
            AND date_trunc('month', cod.expiration_date) >= date_trunc('month', d)
            AND cod.cost_frequency = 'daily'
    LEFT JOIN fleet_vehicle_log_contract com ON com.vehicle_id = ve.id
        AND date_trunc('month', com.start_date) <= date_trunc('month', d)
        AND date_trunc('month', com.expiration_date) >= date_trunc('month', d)
        AND com.cost_frequency = 'monthly'
    LEFT JOIN fleet_vehicle_log_contract coy ON coy.vehicle_id = ve.id
        AND date_trunc('month', coy.date) = date_trunc('month', d)
        AND date_trunc('month', coy.start_date) <= date_trunc('month', d)
        AND date_trunc('month', coy.expiration_date) >= date_trunc('month', d)
        AND coy.cost_frequency = 'yearly'
WHERE
    ve.active
GROUP BY
    ve.id,
    ve.company_id,
    ve.name,
    date_start,
    d
ORDER BY
    ve.id,
    date_start
)
SELECT
    vehicle_id AS id,
    company_id,
    vehicle_id,
    name,
    driver_id,
    fuel_type,
    date_start,
    COST,
    'service' as cost_type
FROM
    service_costs sc
UNION ALL (
    SELECT
        vehicle_id AS id,
        company_id,
        vehicle_id,
        name,
        driver_id,
        fuel_type,
        date_start,
        COST,
        'contract' as cost_type
    FROM
        contract_costs cc)
"""
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute(
            sql.SQL("""CREATE or REPLACE VIEW {} as ({})""").format(
                sql.Identifier(self._table), sql.SQL(query)))
示例#26
0
class TaxAdjustments(models.TransientModel):
    _name = 'tax.adjustments.wizard'
    _description = 'Wizard for Tax Adjustments'

    @api.multi
    def _get_default_journal(self):
        return self.env['account.journal'].search([('type', '=', 'general')],
                                                  limit=1).id

    reason = fields.Char(string='Justification', required=True)
    journal_id = fields.Many2one('account.journal',
                                 string='Journal',
                                 required=True,
                                 default=_get_default_journal,
                                 domain=[('type', '=', 'general')])
    date = fields.Date(required=True, default=fields.Date.context_today)
    debit_account_id = fields.Many2one('account.account',
                                       string='Debit account',
                                       required=True,
                                       domain=[('deprecated', '=', False)])
    credit_account_id = fields.Many2one('account.account',
                                        string='Credit account',
                                        required=True,
                                        domain=[('deprecated', '=', False)])
    amount = fields.Monetary(currency_field='company_currency_id',
                             required=True)
    adjustment_type = fields.Selection(
        [('debit', 'Applied on debit journal item'),
         ('credit', 'Applied on credit journal item')],
        string="Adjustment Type",
        store=False,
        required=True)
    company_currency_id = fields.Many2one(
        'res.currency',
        readonly=True,
        default=lambda self: self.env.user.company_id.currency_id)
    tax_id = fields.Many2one('account.tax',
                             string='Adjustment Tax',
                             ondelete='restrict',
                             domain=[('type_tax_use', '=', 'none'),
                                     ('tax_adjustment', '=', True)],
                             required=True)

    @api.multi
    def _create_move(self):
        adjustment_type = self.env.context.get(
            'adjustment_type', (self.amount > 0.0 and 'debit' or 'credit'))
        debit_vals = {
            'name': self.reason,
            'debit': abs(self.amount),
            'credit': 0.0,
            'account_id': self.debit_account_id.id,
            'tax_line_id': adjustment_type == 'debit' and self.tax_id.id
            or False,
        }
        credit_vals = {
            'name': self.reason,
            'debit': 0.0,
            'credit': abs(self.amount),
            'account_id': self.credit_account_id.id,
            'tax_line_id': adjustment_type == 'credit' and self.tax_id.id
            or False,
        }
        vals = {
            'journal_id': self.journal_id.id,
            'date': self.date,
            'state': 'draft',
            'line_ids': [(0, 0, debit_vals), (0, 0, credit_vals)]
        }
        move = self.env['account.move'].create(vals)
        move.post()
        return move.id

    @api.multi
    def create_move_debit(self):
        return self.with_context(adjustment_type='debit').create_move()

    @api.multi
    def create_move_credit(self):
        return self.with_context(adjustment_type='credit').create_move()

    def create_move(self):
        #create the adjustment move
        move_id = self._create_move()
        #return an action showing the created move
        action = self.env.ref(
            self.env.context.get('action', 'account.action_move_line_form'))
        result = action.read()[0]
        result['views'] = [(False, 'form')]
        result['res_id'] = move_id
        return result
示例#27
0
class MailMail(models.Model):
    """ Model holding RFC2822 email messages to send. This model also provides
        facilities to queue and send new email messages.  """
    _name = 'mail.mail'
    _description = 'Outgoing Mails'
    _inherits = {'mail.message': 'mail_message_id'}
    _order = 'id desc'
    _rec_name = 'subject'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            if auto_commit is True:
                self._cr.commit()
        return True
示例#28
0
class GroupOnDate(models.Model):
    _name = 'test_read_group.on_date'
    _description = 'Group Test Read On Date'

    date = fields.Date("Date")
    value = fields.Integer("Value")
示例#29
0
class AccountInvoiceLine(models.Model):
    _inherit = 'account.move.line'

    asset_category_id = fields.Many2one('account.asset.category', string='Asset Category')
    asset_start_date = fields.Date(string='Asset Start Date', compute='_get_asset_date', readonly=True, store=True)
    asset_end_date = fields.Date(string='Asset End Date', compute='_get_asset_date', readonly=True, store=True)
    asset_mrr = fields.Float(string='Monthly Recurring Revenue', compute='_get_asset_date', readonly=True,
                             digits="Account", store=True)

    @api.depends('asset_category_id', 'move_id.invoice_date')
    def _get_asset_date(self):
        for rec in self:
            rec.asset_mrr = 0
            rec.asset_start_date = False
            rec.asset_end_date = False
            cat = rec.asset_category_id
            if cat:
                if cat.method_number == 0 or cat.method_period == 0:
                    raise UserError(_('The number of depreciations or the period length of '
                                      'your asset category cannot be 0.'))
                months = cat.method_number * cat.method_period
                if rec.move_id.move_type in ['out_invoice', 'out_refund']:
                    rec.asset_mrr = rec.price_subtotal / months
                if rec.move_id.invoice_date:
                    start_date = rec.move_id.invoice_date.replace(day=1)
                    end_date = (start_date + relativedelta(months=months, days=-1))
                    rec.asset_start_date = start_date
                    rec.asset_end_date = end_date

    def asset_create(self):
        if self.asset_category_id:
            vals = {
                'name': self.name,
                'code': self.name or False,
                'category_id': self.asset_category_id.id,
                'value': self.price_subtotal,
                'partner_id': self.move_id.partner_id.id,
                'company_id': self.move_id.company_id.id,
                'currency_id': self.move_id.company_currency_id.id,
                'date': self.move_id.invoice_date,
                'invoice_id': self.move_id.id,
            }
            changed_vals = self.env['account.asset.asset'].onchange_category_id_values(vals['category_id'])
            vals.update(changed_vals['value'])
            asset = self.env['account.asset.asset'].create(vals)
            if self.asset_category_id.open_asset:
                asset.validate()
        return True

    @api.onchange('asset_category_id')
    def onchange_asset_category_id(self):
        if self.move_id.move_type == 'out_invoice' and self.asset_category_id:
            self.account_id = self.asset_category_id.account_asset_id.id
        elif self.move_id.move_type == 'in_invoice' and self.asset_category_id:
            self.account_id = self.asset_category_id.account_asset_id.id

    # @api.onchange('uom_id')
    # def _onchange_uom_id(self):
    #     result = super(AccountInvoiceLine, self)._onchange_uom_id()
    #     self.onchange_asset_category_id()
    #     return result

    @api.onchange('product_id')
    def _onchange_product_id(self):
        vals = super(AccountInvoiceLine, self)._onchange_product_id()
        if self.product_id:
            for rec in self:
                if rec.move_id.move_type == 'out_invoice':
                    rec.asset_category_id = rec.product_id.product_tmpl_id.deferred_revenue_category_id
                elif rec.move_id.move_type == 'in_invoice':
                    rec.asset_category_id = rec.product_id.product_tmpl_id.asset_category_id
        return vals

    def _set_additional_fields(self, invoice):
        if not self.asset_category_id:
            if invoice.type == 'out_invoice':
                self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id
            elif invoice.type == 'in_invoice':
                self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id
            self.onchange_asset_category_id()
        super(AccountInvoiceLine, self)._set_additional_fields(invoice)

    def get_invoice_line_account(self, type, product, fpos, company):
        return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
示例#30
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',
        ]