Beispiel #1
0
class VendorDelayReport(models.Model):
    _name = "vendor.delay.report"
    _description = "Vendor Delay Report"
    _auto = False

    partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True)
    product_id = fields.Many2one('product.product', 'Product', readonly=True)
    category_id = fields.Many2one('product.category',
                                  'Product Category',
                                  readonly=True)
    date = fields.Datetime('Effective Date', readonly=True)
    qty_total = fields.Float('Total Quantity', readonly=True)
    qty_on_time = fields.Float('On-Time Quantity', readonly=True)
    on_time_rate = fields.Float('On-Time Delivery Rate', readonly=True)

    def init(self):
        tools.drop_view_if_exists(self.env.cr, 'vendor_delay_report')
        self.env.cr.execute("""
CREATE OR replace VIEW vendor_delay_report AS(
SELECT m.id                     AS id,
       m.date                   AS date,
       m.purchase_line_id       AS purchase_line_id,
       m.product_id             AS product_id,
       Min(pc.id)               AS category_id,
       Min(po.partner_id)       AS partner_id,
       Sum(pol.product_uom_qty) AS qty_total,
       Sum(CASE
             WHEN (pol.date_planned::date >= m.date::date) THEN ml.qty_done
             ELSE 0
           END)                 AS qty_on_time
FROM   stock_move m
       JOIN stock_move_line ml
         ON m.id = ml.move_id
       JOIN purchase_order_line pol
         ON pol.id = m.purchase_line_id
       JOIN purchase_order po
         ON po.id = pol.order_id
       JOIN product_product p
         ON p.id = m.product_id
       JOIN product_template pt
         ON pt.id = p.product_tmpl_id
       JOIN product_category pc
         ON pc.id = pt.categ_id
WHERE  m.state = 'done'
GROUP  BY m.id
)""")

    @api.model
    def read_group(self,
                   domain,
                   fields,
                   groupby,
                   offset=0,
                   limit=None,
                   orderby=False,
                   lazy=True):
        if 'on_time_rate' not in fields:
            res = super().read_group(domain,
                                     fields,
                                     groupby,
                                     offset=offset,
                                     limit=limit,
                                     orderby=orderby,
                                     lazy=lazy)
            return res

        fields.remove('on_time_rate')
        if 'qty_total' not in fields:
            fields.append('qty_total')
        if 'qty_on_time' not in fields:
            fields.append('qty_on_time')
        res = super().read_group(domain,
                                 fields,
                                 groupby,
                                 offset=offset,
                                 limit=limit,
                                 orderby=orderby,
                                 lazy=lazy)
        for group in res:
            if group['qty_total'] == 0:
                on_time_rate = 100
            else:
                on_time_rate = group['qty_on_time'] / group['qty_total'] * 100
            group.update({'on_time_rate': on_time_rate})

        return res
Beispiel #2
0
class FleetVehicleLogContract(models.Model):
    _inherit = 'fleet.vehicle.log.contract'

    recurring_cost_amount_depreciated = fields.Float(
        "Recurring Cost Amount (depreciated)")
Beispiel #3
0
class SiiTax(models.Model):
    _inherit = 'account.tax'

    sii_code = fields.Integer(string='SII Code', )
    sii_type = fields.Selection(
        [
            ('A', 'Anticipado'),
            ('R', 'Retención'),
        ],
        string="Tipo de impuesto para el SII",
    )
    retencion = fields.Float(
        string="Valor retención",
        default=0.00,
    )
    no_rec = fields.Boolean(string="Es No Recuperable", )
    activo_fijo = fields.Boolean(
        string="Activo Fijo",
        default=False,
    )

    @api.multi
    def compute_all(self,
                    price_unit,
                    currency=None,
                    quantity=1.0,
                    product=None,
                    partner=None,
                    discount=None):
        """ Returns all information required to apply taxes (in self + their children in case of a tax goup).
            We consider the sequence of the parent for group of taxes.
                Eg. considering letters as taxes and alphabetic order as sequence :
                [G, B([A, D, F]), E, C] will be computed as [A, D, F, C, E, G]
        RETURN: {
            'total_excluded': 0.0,    # Total without taxes
            'total_included': 0.0,    # Total with taxes
            'taxes': [{               # One dict for each tax in self and their children
                'id': int,
                'name': str,
                'amount': float,
                'sequence': int,
                'account_id': int,
                'refund_account_id': int,
                'analytic': boolean,
            }]
        } """
        if len(self) == 0:
            company_id = self.env.user.company_id
        else:
            company_id = self[0].company_id
        if not currency:
            currency = company_id.currency_id
        taxes = []
        # By default, for each tax, tax amount will first be computed
        # and rounded at the 'Account' decimal precision for each
        # PO/SO/invoice line and then these rounded amounts will be
        # summed, leading to the total amount for that tax. But, if the
        # company has tax_calculation_rounding_method = round_globally,
        # we still follow the same method, but we use a much larger
        # precision when we round the tax amount for each line (we use
        # the 'Account' decimal precision + 5), and that way it's like
        # rounding after the sum of the tax amounts of each line
        prec = currency.decimal_places
        base = round(price_unit * quantity, prec + 2)
        base = round(base, prec)
        tot_discount = round(base * ((discount or 0.0) / 100))
        base -= tot_discount
        total_excluded = base
        total_included = base

        if company_id.tax_calculation_rounding_method == 'round_globally' or not bool(
                self.env.context.get("round", True)):
            prec += 5

        # Sorting key is mandatory in this case. When no key is provided, sorted() will perform a
        # search. However, the search method is overridden in account.tax in order to add a domain
        # depending on the context. This domain might filter out some taxes from self, e.g. in the
        # case of group taxes.
        for tax in self.sorted(key=lambda r: r.sequence):
            if tax.amount_type == 'group':
                ret = tax.children_tax_ids.compute_all(price_unit, currency,
                                                       quantity, product,
                                                       partner)
                total_excluded = ret['total_excluded']
                base = ret['base']
                total_included = ret['total_included']
                tax_amount_retencion = ret['retencion']
                tax_amount = total_included - total_excluded + tax_amount_retencion
                taxes += ret['taxes']
                continue

            tax_amount = tax._compute_amount(base, price_unit, quantity,
                                             product, partner)
            if company_id.tax_calculation_rounding_method == 'round_globally' or not bool(
                    self.env.context.get("round", True)):
                tax_amount = round(tax_amount, prec)
            else:
                tax_amount = currency.round(tax_amount)
            tax_amount_retencion = 0
            if tax.sii_type in ['R']:
                tax_amount_retencion = tax._compute_amount_ret(
                    base, price_unit, quantity, product, partner)
                if company_id.tax_calculation_rounding_method == 'round_globally' or not bool(
                        self.env.context.get("round", True)):
                    tax_amount_retencion = round(tax_amount_retencion, prec)
                if tax.price_include:
                    total_excluded -= (tax_amount - tax_amount_retencion)
                    total_included -= (tax_amount_retencion)
                    base -= (tax_amount - tax_amount_retencion)
                else:
                    total_included += (tax_amount - tax_amount_retencion)
            else:
                if tax.price_include:
                    total_excluded -= tax_amount
                    base -= tax_amount
                else:
                    total_included += tax_amount
            # Keep base amount used for the current tax
            tax_base = base

            if tax.include_base_amount:
                base += tax_amount

            taxes.append({
                'id':
                tax.id,
                'name':
                tax.with_context(**{
                    'lang': partner.lang
                } if partner else {}).name,
                'amount':
                tax_amount,
                'retencion':
                tax_amount_retencion,
                'base':
                tax_base,
                'sequence':
                tax.sequence,
                'account_id':
                tax.account_id.id,
                'refund_account_id':
                tax.refund_account_id.id,
                'analytic':
                tax.analytic,
            })

        return {
            'taxes':
            sorted(taxes, key=lambda k: k['sequence']),
            'total_excluded':
            currency.round(total_excluded) if bool(
                self.env.context.get("round", True)) else total_excluded,
            'total_included':
            currency.round(total_included) if bool(
                self.env.context.get("round", True)) else total_included,
            'base':
            base,
        }

    def _compute_amount(self,
                        base_amount,
                        price_unit,
                        quantity=1.0,
                        product=None,
                        partner=None):
        if self.amount_type == 'percent' and self.price_include:
            neto = base_amount / (1 + self.amount / 100)
            tax = base_amount - neto
            return tax
        return super(SiiTax, self)._compute_amount(base_amount, price_unit,
                                                   quantity, product, partner)

    def _compute_amount_ret(self,
                            base_amount,
                            price_unit,
                            quantity=1.0,
                            product=None,
                            partner=None):
        if self.amount_type == 'percent' and self.price_include:
            neto = base_amount / (1 + self.retencion / 100)
            tax = base_amount - neto
            return tax
        if (self.amount_type == 'percent'
                and not self.price_include) or (self.amount_type == 'division'
                                                and self.price_include):
            return base_amount * self.retencion / 100
Beispiel #4
0
class AccountInvoice(models.Model):
    _inherit = "account.invoice"

    @api.multi
    @api.depends('discount_amount', 'discount_per', 'amount_untaxed',
                 'invoice_line_ids')
    def _get_discount(self):
        for record in self:
            total_discount = 0.0
            for invoice_line_id in record.invoice_line_ids:
                total_price = (invoice_line_id.quantity *
                               invoice_line_id.price_unit)
                total_discount += \
                    (total_price * invoice_line_id.discount) / 100
            record.discount = record.currency_id.round(total_discount)

    @api.multi
    @api.depends('invoice_line_ids', 'discount_per', 'discount_amount')
    def _get_total_amount(self):
        for invoice_id in self:
            invoice_id.gross_amount = sum([
                invoice_id.currency_id.round(line_id.quantity *
                                             line_id.price_unit)
                for line_id in invoice_id.invoice_line_ids
            ])

    discount_method = fields.Selection([('fixed', 'Fixed'),
                                        ('per', 'Percentage')],
                                       string="Discount Method")
    discount_amount = fields.Float(string="Discount Amount")
    discount_per = fields.Float(string="Discount (%)")
    discount = fields.Monetary(string='Discount',
                               readonly=True,
                               compute='_get_discount',
                               track_visibility='always')
    gross_amount = fields.Float(string="Gross Amount",
                                compute='_get_total_amount',
                                store=True)

    @api.multi
    def calculate_discount(self):
        for line in self.invoice_line_ids:
            line.write({'discount': 0.0})
        # amount_untaxed = self.amount_untaxed
        gross_amount = self.gross_amount
        if self.discount_method == 'per':
            for line in self.invoice_line_ids:
                line.write({'discount': self.discount_per})
                self._onchange_invoice_line_ids()
        else:
            for line in self.invoice_line_ids:
                discount_value_ratio = \
                    (self.discount_amount * line.price_subtotal) / \
                    gross_amount
                if discount_value_ratio:
                    discount_per_ratio = \
                        (discount_value_ratio * 100) / line.price_subtotal
                    line.write({'discount': discount_per_ratio})
                    self._onchange_invoice_line_ids()
        self._check_constrains()

    @api.multi
    @api.returns('self')
    def refund(self,
               date_invoice=None,
               date=None,
               description=None,
               journal_id=None):
        result = super(AccountInvoice, self).refund(date_invoice=date_invoice,
                                                    date=date,
                                                    description=description,
                                                    journal_id=journal_id)
        result.write({
            'discount_method': result.refund_invoice_id.discount_method,
            'discount_amount': result.refund_invoice_id.discount_amount,
            'discount_per': result.refund_invoice_id.discount_per
        })
        result.calculate_discount()
        return result

    @api.constrains('discount_per', 'discount_amount', 'invoice_line_ids')
    def _check_constrains(self):
        self.onchange_discount_per()
        self.onchange_discount_amount()

    @api.onchange('discount_method')
    def onchange_discount_method(self):
        if not self.refund_invoice_id:
            self.discount_amount = 0.0
            self.discount_per = 0.0
        if self.discount_method and not self.invoice_line_ids:
            raise Warning('No Invoice Line(s) were found!')

    @api.multi
    def get_maximum_per_amount(self):
        account_dis_config_obj = self.env['account.discount.config']
        max_percentage = 0
        max_amount = 0
        check_group = False
        for groups_id in self.env.user.groups_id:
            account_dis_config_id = account_dis_config_obj.search([
                ('group_id', '=', groups_id.id)
            ])
            if account_dis_config_id:
                check_group = True
                if account_dis_config_id.percentage > max_percentage:
                    max_percentage = account_dis_config_id.percentage
                if account_dis_config_id.fix_amount > max_amount:
                    max_amount = account_dis_config_id.fix_amount
        return {
            'max_percentage': max_percentage,
            'max_amount': max_amount,
            'check_group': check_group
        }

    @api.onchange('discount_per')
    def onchange_discount_per(self):
        values = self.get_maximum_per_amount()
        if self.discount_method == 'per' and (
                self.discount_per > 100 or self.discount_per < 0) \
                and values.get('check_group', False):
            raise Warning(_("Percentage should be between 0% to 100%"))
        if self.discount_per > values.get('max_percentage', False) \
                and values.get('check_group', False):
            raise Warning(
                _("You are not allowed to apply Discount Percentage "
                  "(%s) more than configured Discount Percentage "
                  "(%s) in configuration setting!") %
                (formatLang(self.env, self.discount_per, digits=2),
                 formatLang(self.env, values['max_percentage'], digits=2)))
        config_id = self.env['res.config.settings'].search([],
                                                           order='id desc',
                                                           limit=1)
        if config_id and config_id.global_discount_invoice_apply:
            global_percentage = config_id.global_discount_percentage_invoice
            if global_percentage < self.discount_per:
                raise Warning(
                    _("You are not allowed to apply Discount "
                      "Percentage(%s) more than configured Discount"
                      " Percentage (%s) in configuration setting!") %
                    (formatLang(self.env, self.discount_per, digits=2),
                     formatLang(self.env,
                                config_id.global_discount_percentage_invoice,
                                digits=2)))

    @api.onchange('discount_amount')
    def onchange_discount_amount(self):
        values = self.get_maximum_per_amount()
        if self.discount < 0:
            raise Warning(_("Discount should be less than Gross Amount"))
        discount = self.discount or self.discount_amount
        if self.gross_amount and discount > self.gross_amount:
            raise Warning(
                _("Discount (%s) should be less than "
                  "Gross Amount (%s).") %
                (formatLang(self.env, discount, digits=2),
                 formatLang(self.env, self.gross_amount, digits=2)))
        if self.discount > values.get('max_amount', False) \
                and values.get('check_group', False):
            raise Warning(
                _("You're not allowed to apply this amount of "
                  "discount as discount Amount (%s) is greater than"
                  " assign Fix Amount (%s).") %
                (formatLang(self.env, self.discount, digits=2),
                 formatLang(self.env, values['max_amount'], digits=2)))
        config_id = self.env['res.config.settings'].search([],
                                                           order='id desc',
                                                           limit=1)
        if config_id and config_id.global_discount_invoice_apply:
            fix_amount = config_id.global_discount_fix_invoice_amount
            if fix_amount < self.discount_amount:
                raise Warning(
                    _("You're not allowed to apply this amount of"
                      " discount as discount Amount (%s) is greater"
                      " than Configuration Amount (%s).") %
                    (formatLang(self.env, self.discount, digits=2),
                     formatLang(self.env,
                                config_id.global_discount_fix_invoice_amount,
                                digits=2)))
class PosSaleReport(models.Model):
    _name = "report.all.channels.sales"
    _description = "Sales by Channel (All in One)"
    _auto = False

    name = fields.Char('Order Reference', readonly=True)
    partner_id = fields.Many2one('res.partner', 'Partner', readonly=True)
    product_id = fields.Many2one('product.product', string='Product', readonly=True)
    product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True)
    date_order = fields.Datetime(string='Date Order', readonly=True)
    user_id = fields.Many2one('res.users', 'Salesperson', readonly=True)
    categ_id = fields.Many2one('product.category', 'Product Category', readonly=True)
    company_id = fields.Many2one('res.company', 'Company', readonly=True)
    price_total = fields.Float('Total', readonly=True)
    pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True)
    country_id = fields.Many2one('res.country', 'Partner Country', readonly=True)
    price_subtotal = fields.Float(string='Price Subtotal', readonly=True)
    product_qty = fields.Float('Product Quantity', readonly=True)
    analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True)
    team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True)

    def _so(self):
        so_str = """
                SELECT sol.id AS id,
                    so.name AS name,
                    so.partner_id AS partner_id,
                    sol.product_id AS product_id,
                    pro.product_tmpl_id AS product_tmpl_id,
                    so.date_order AS date_order,
                    so.user_id AS user_id,
                    pt.categ_id AS categ_id,
                    so.company_id AS company_id,
                    sol.price_total / CASE COALESCE(so.currency_rate, 0) WHEN 0 THEN 1.0 ELSE so.currency_rate END AS price_total,
                    so.pricelist_id AS pricelist_id,
                    rp.country_id AS country_id,
                    sol.price_subtotal / CASE COALESCE(so.currency_rate, 0) WHEN 0 THEN 1.0 ELSE so.currency_rate END AS price_subtotal,
                    (sol.product_uom_qty / u.factor * u2.factor) as product_qty,
                    so.analytic_account_id AS analytic_account_id,
                    so.team_id AS team_id

            FROM sale_order_line sol
                    JOIN sale_order so ON (sol.order_id = so.id)
                    LEFT JOIN product_product pro ON (sol.product_id = pro.id)
                    JOIN res_partner rp ON (so.partner_id = rp.id)
                    LEFT JOIN product_template pt ON (pro.product_tmpl_id = pt.id)
                    LEFT JOIN product_pricelist pp ON (so.pricelist_id = pp.id)
                    LEFT JOIN uom_uom u on (u.id=sol.product_uom)
                    LEFT JOIN uom_uom u2 on (u2.id=pt.uom_id)
            WHERE so.state in ('sale','done')
        """
        return so_str

    def _from(self):
        return """(%s)""" % (self._so())

    def _get_main_request(self):
        request = """
            CREATE or REPLACE VIEW %s AS
                SELECT id AS id,
                    name,
                    partner_id,
                    product_id,
                    product_tmpl_id,
                    date_order,
                    user_id,
                    categ_id,
                    company_id,
                    price_total,
                    pricelist_id,
                    analytic_account_id,
                    country_id,
                    team_id,
                    price_subtotal,
                    product_qty
                FROM %s
                AS foo""" % (self._table, self._from())
        return request

    def init(self):
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute(self._get_main_request())
Beispiel #6
0
class MrpByProduct(models.Model):
    _name = 'mrp.bom.byproduct'
    _description = 'Byproduct'
    _rec_name = "product_id"
    _check_company_auto = True

    product_id = fields.Many2one('product.product',
                                 'By-product',
                                 required=True,
                                 check_company=True)
    company_id = fields.Many2one(related='bom_id.company_id',
                                 store=True,
                                 index=True,
                                 readonly=True)
    product_qty = fields.Float('Quantity',
                               default=1.0,
                               digits='Product Unit of Measure',
                               required=True)
    product_uom_id = fields.Many2one('uom.uom',
                                     'Unit of Measure',
                                     required=True)
    bom_id = fields.Many2one('mrp.bom', 'BoM', ondelete='cascade')
    allowed_operation_ids = fields.Many2many(
        'mrp.routing.workcenter', compute='_compute_allowed_operation_ids')
    operation_id = fields.Many2one(
        'mrp.routing.workcenter',
        'Produced in Operation',
        check_company=True,
        domain="[('id', 'in', allowed_operation_ids)]")

    @api.depends('bom_id')
    def _compute_allowed_operation_ids(self):
        for byproduct in self:
            if not byproduct.bom_id.operation_ids:
                byproduct.allowed_operation_ids = self.env[
                    'mrp.routing.workcenter']
            else:
                operation_domain = [
                    ('id', 'in', byproduct.bom_id.operation_ids.ids), '|',
                    ('company_id', '=', byproduct.company_id.id),
                    ('company_id', '=', False)
                ]
                byproduct.allowed_operation_ids = self.env[
                    'mrp.routing.workcenter'].search(operation_domain)

    @api.onchange('product_id')
    def onchange_product_id(self):
        """ Changes UoM if product_id changes. """
        if self.product_id:
            self.product_uom_id = self.product_id.uom_id.id

    @api.onchange('product_uom_id')
    def onchange_uom(self):
        res = {}
        if self.product_uom_id and self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id:
            res['warning'] = {
                'title':
                _('Warning'),
                'message':
                _('The unit of measure you choose is in a different category than the product unit of measure.'
                  )
            }
            self.product_uom_id = self.product_id.uom_id.id
        return res
Beispiel #7
0
class OpMediaMovement(models.Model):
    _name = 'op.media.movement'
    _inherit = 'mail.thread'
    _description = 'Media Movement'
    _rec_name = 'media_id'

    media_id = fields.Many2one('op.media', 'Media', required=True)
    media_unit_id = fields.Many2one('op.media.unit',
                                    'Media Unit',
                                    required=True,
                                    track_visibility='onchange')
    type = fields.Selection([('student', 'Student'), ('faculty', 'Faculty')],
                            'Student/Faculty',
                            required=True)
    student_id = fields.Many2one('op.student', 'Student')
    faculty_id = fields.Many2one('op.faculty', 'Faculty')
    library_card_id = fields.Many2one('op.library.card',
                                      'Library Card',
                                      required=True,
                                      track_visibility='onchange')
    issued_date = fields.Date('Issued Date',
                              required=True,
                              default=fields.Date.today())
    return_date = fields.Date('Due Date', required=True)
    actual_return_date = fields.Date('Actual Return Date')
    penalty = fields.Float('Penalty')
    partner_id = fields.Many2one('res.partner',
                                 'Person',
                                 track_visibility='onchange')
    reserver_name = fields.Char('Person Name', size=256)
    state = fields.Selection([('available', 'Available'),
                              ('reserve', 'Reserved'), ('issue', 'Issued'),
                              ('lost', 'Lost'), ('return', 'Returned'),
                              ('return_done', 'Returned Done')],
                             'Status',
                             default='available',
                             track_visibility='onchange')
    media_type_id = fields.Many2one(related='media_id.media_type_id',
                                    store=True,
                                    string='Media Type')
    user_id = fields.Many2one('res.users',
                              related='student_id.user_id',
                              string='Users')
    invoice_id = fields.Many2one('account.invoice', 'Invoice', readonly=True)

    @api.multi
    def get_diff_day(self):
        for media_mov_id in self:
            today_date = datetime.strptime(fields.Date.today(), '%Y-%m-%d')
            return_date = datetime.strptime(media_mov_id.return_date,
                                            '%Y-%m-%d')
            diff = today_date - return_date
            return abs(diff.days)

    @api.constrains('issued_date', 'return_date')
    def _check_date(self):
        if self.issued_date > self.return_date:
            raise ValidationError(
                _('Return Date cannot be set before Issued Date.'))

    @api.constrains('issued_date', 'actual_return_date')
    def check_actual_return_date(self):
        if self.actual_return_date:
            if self.issued_date > self.actual_return_date:
                raise ValidationError(
                    _('Actual Return Date cannot be set before Issued Date'))

    @api.onchange('media_unit_id')
    def onchange_media_unit_id(self):
        self.state = self.media_unit_id.state
        self.media_id = self.media_unit_id.media_id

    @api.onchange('library_card_id')
    def onchange_library_card_id(self):
        self.type = self.library_card_id.type
        self.student_id = self.library_card_id.student_id.id
        self.faculty_id = self.library_card_id.faculty_id.id
        self.return_date = date.today() + \
            timedelta(days=self.library_card_id.library_card_type_id.duration)

    @api.multi
    def issue_media(self):
        ''' function to issue media '''
        for record in self:
            if record.media_unit_id.state and \
                    record.media_unit_id.state == 'available':
                record.media_unit_id.state = 'issue'
                record.state = 'issue'

    @api.multi
    def return_media(self, return_date):
        for record in self:
            if not return_date:
                return_date = fields.Date.today()
            record.actual_return_date = return_date
            record.calculate_penalty()
            if record.penalty > 0.0:
                record.state = 'return'
            else:
                record.state = 'return_done'
            record.media_unit_id.state = 'available'

    @api.multi
    def calculate_penalty(self):
        for record in self:
            penalty_amt = 0
            penalty_days = 0
            standard_diff = days_between(record.return_date,
                                         record.issued_date)
            actual_diff = days_between(record.actual_return_date,
                                       record.issued_date)
            if record.library_card_id and \
                    record.library_card_id.library_card_type_id:
                penalty_days = actual_diff > standard_diff and actual_diff - \
                    standard_diff or penalty_days
                penalty_amt = penalty_days * \
                    record.library_card_id.library_card_type_id.\
                    penalty_amt_per_day
            record.write({'penalty': penalty_amt})

    @api.multi
    def create_penalty_invoice(self):
        for rec in self:
            account_id = False
            product = self.env.ref('openeducat_library.op_product_7')
            if product.id:
                account_id = product.property_account_income_id.id
            if not account_id:
                account_id = \
                    product.categ_id.property_account_income_categ_id.id
            if not account_id:
                raise UserError(
                    _('There is no income account defined for this \
                    product: "%s". You may have to install a chart of \
                    account from Accounting app, settings \
                    menu.') % (product.name, ))

            invoice = self.env['account.invoice'].create({
                'partner_id':
                self.student_id.partner_id.id,
                'type':
                'out_invoice',
                'reference':
                False,
                'date_invoice':
                fields.Date.today(),
                'account_id':
                self.student_id.partner_id.property_account_receivable_id.id,
                'invoice_line_ids': [(0, 0, {
                    'name': product.name,
                    'account_id': account_id,
                    'price_unit': self.penalty,
                    'quantity': 1.0,
                    'discount': 0.0,
                    'uom_id': product.uom_id.id,
                    'product_id': product.id,
                })],
            })
            invoice.compute_taxes()
            invoice.action_invoice_open()
            self.invoice_id = invoice.id
Beispiel #8
0
class ProjectSprint(models.Model):
    _name = "project.sprint"
    _inherit = ['ir.branch.company.mixin', 'mail.thread']
    _description = "Sprint of the Project"
    _rec_name = 'sprint_seq'

    sprint_seq = fields.Char(
        string="Reference", readonly=True)
    name = fields.Char("Sprint Name", required=True,
                       track_visibility="onchange")
    goal_of_sprint = fields.Char("Goal of Sprint", track_visibility="onchange")

    meeting_date = fields.Datetime("Planning Meeting Date", required=True,
                                   track_visibility="onchange")
    hour = fields.Float(string="Hour", track_visibility="onchange")
    time_zone = fields.Selection([
        ('am', 'AM'),
        ('pm', 'PM'),
    ], track_visibility="onchange")
    estimated_velocity = fields.Integer(
        compute="calculate_estimated_velocity", string="Estimated Velocity",
        store=True, track_visibility="onchange")
    actual_velocity = fields.Integer(
        compute="calculate_actual_velocity", string="Actual Velocity",
        store=True, track_visibility="onchange")
    sprint_planning_line = fields.One2many(
        'sprint.planning.line', 'sprint_id', string="Sprint Planning Lines")
    project_id = fields.Many2one('project.project', string="Project",
                                 track_visibility="onchange")
    start_date = fields.Date(string="Start Date", track_visibility="onchange")
    end_date = fields.Date(string="End Date", track_visibility="onchange")
    working_days = fields.Integer(
        compute="calculate_business_days", string="Business Days",
        store=True, track_visibility="onchange")
    productivity_hours = fields.Float(string="Productivity Hours",
                                      track_visibility="onchange")
    productivity_per = fields.Float(
        compute="calculate_productivity_per", string="Productivity (%)",
        store=True, track_visibility="onchange")
    holiday_type = fields.Selection(
        [('hours', 'Hours'), ('days', 'Days')],
        string="Holiday (Hours / Days)", default='hours',
        track_visibility="onchange")
    holiday_count = fields.Float(string="Holiday Count",
                                 track_visibility="onchange")
    holiday_days = fields.Float(
        compute="calculate_holiday_days", string="Holiday Days", store=True,
        track_visibility="onchange")
    state = fields.Selection([
        ('draft', 'Draft'),
        ('in_progress', 'In Progress'),
        ('pending', 'Pending'),
        ('done', 'Done'),
        ('cancel', 'Cancel')], string="State", default='draft',
        track_visibility="onchange")
    team_id = fields.Many2one('project.team', string="Team",
                              track_visibility="onchange", required=True)
    task_line = fields.One2many('project.task', 'sprint_id', string='Tasks')
    color = fields.Integer('Color Index')

    @api.depends('start_date', 'end_date')
    def days_calculate(self):
        if self.start_date and self.end_date:
            diff = datetime.strptime(
                self.end_date, '%Y-%m-%d').date() - datetime.strptime(
                self.start_date, '%Y-%m-%d').date()
            self.duration = diff.days

    duration = fields.Integer(
        "Duration (In Days)", compute='days_calculate', store=True,
        track_visibility="onchange")

    @api.multi
    def _get_task_count(self):
        for record in self:
            record.number_of_tasks = record.env['project.task'].search_count([
                ('sprint_id', '=', record.id)])

    number_of_tasks = fields.Integer(
        string="# of tasks", compute="_get_task_count")

    @api.multi
    def _get_story_count(self):
        for record in self:
            count = record.env['project.story'].search_count([
                ('sprint_id', '=', record.id)])
            record.number_of_stories = count

    number_of_stories = fields.Integer(
        string="# of stories", compute="_get_story_count")

    @api.multi
    def _get_retrospective_count(self):
        for record in self:
            count = record.env['retrospective'].search_count([
                ('sprint_id', '=', record.id)])
            record.number_of_retrospectives = count

    number_of_retrospectives = fields.Integer(
        string="# of Retrospectives", compute="_get_retrospective_count")

    @api.multi
    @api.depends('task_line', 'task_line.stage_id', 'task_line.sprint_id',
                 'estimated_velocity', 'start_date', 'end_date', 'project_id',
                 'team_id')
    def calculate_tasks_json(self):
        data = []
        for record in self:
            task_ids = self.env['project.task'].search([
                ('sprint_id', '=', record.id)])
            velocity = record.estimated_velocity or 1.0
            for task in task_ids:
                data.append({
                    'task': task.task_seq or '/',
                    'velocity': task.velocity or 0,
                    'per': round(((float(task.velocity) * 100) / float(
                        velocity)), 2)
                })
            record.tasks_json = data

    tasks_json = fields.Char(
        string="Tasks", compute="calculate_tasks_json", store=True)

    @api.multi
    def get_data(self):
        task_dict_list = []
        for record in self:
            task_pool = self.env['project.task'].search([
                ('sprint_id', '=', record.id)])
            for task in task_pool:
                task_dict = {
                    'reference': task.task_seq,
                    'name': task.name,
                    'velocity': task.velocity,
                    'start_date': task.start_date,
                    'end_date': task.end_date,
                    'actual_end_date': task.actual_end_date,
                    'assigned_to': task.user_id.name,
                    'state': task.stage_id.name,
                }
                task_dict_list.append(task_dict)
        return task_dict_list

    @api.multi
    def set_state_open(self):
        self.state = 'in_progress'

    @api.multi
    def set_state_cancel(self):
        self.state = 'cancel'

    @api.multi
    def set_state_pending(self):
        self.state = 'pending'

    @api.multi
    def redirect_to_view(self, model, caption):
        return {
            'name': (_(caption)),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'tree,form',
            'res_model': model,
            'domain': [('sprint_id', '=', self.id)],
            'context': {
                'default_sprint_id': self.id,
                'default_project_id': self.project_id.id
            }
        }

    @api.multi
    def action_view_tasks(self):
        return self.redirect_to_view("project.task", "Tasks")

    @api.multi
    def action_view_stories(self):
        return self.redirect_to_view("project.story", "Stories")

    @api.multi
    def action_view_release_planning(self):
        return self.redirect_to_view("release.planning", "Release Planning")

    @api.multi
    def action_view_retrospective(self):
        return self.redirect_to_view("retrospective", "Retrospective")

    @api.constrains('start_date', 'end_date')
    def check_dates(self):
        if self.start_date and self.end_date and (
                self.start_date > self.end_date):
            raise ValidationError(
                "Start Date can not be greater than End date, Dude!")

    @api.onchange('holiday_type')
    def onchange_holiday_type(self):
        self.holiday_count = 0.0
        self.holiday_days = 0.0

    @api.multi
    @api.depends('project_id', 'project_id.no_of_days', 'start_date',
                 'end_date')
    def calculate_business_days(self):
        for record in self:
            if record.start_date and record.end_date:
                days_dict = {
                    0: (1, 2, 3, 4, 5, 6, 7),
                    1: (2, 3, 4, 5, 6, 7),
                    2: (3, 4, 5, 6, 7),
                    3: (4, 5, 6, 7),
                    4: (5, 6, 7),
                    5: (6, 7),
                    6: (7,),
                    7: (),
                }
                start = datetime.strptime(record.start_date, "%Y-%m-%d").date()
                end = datetime.strptime(record.end_date, "%Y-%m-%d").date()
                delta = timedelta(days=1)
                days = 0

                if record.project_id and end > start:
                    working_days = record.project_id.no_of_days
                    non_working_days = days_dict[working_days]

                    while end != start:
                        end -= delta
                        if end.isoweekday() not in non_working_days:
                            days += 1

                    record.working_days = days

    @api.multi
    @api.depends('project_id', 'project_id.no_of_hours', 'productivity_hours')
    def calculate_productivity_per(self):
        for record in self:
            project_id = record.project_id
            no_of_hours = project_id.no_of_hours if project_id else 0
            prod_hours = record.productivity_hours
            if project_id and no_of_hours > 0 and prod_hours:
                record.productivity_per = (prod_hours / no_of_hours) * 100

    @api.multi
    @api.depends('project_id', 'project_id.no_of_hours', 'holiday_count')
    def calculate_holiday_days(self):
        for record in self:
            if record.holiday_type == 'days' and record.project_id:
                hours = record.holiday_count * record.project_id.no_of_hours
                record.holiday_days = hours

    @api.multi
    @api.depends('project_id', 'task_line', 'task_line.velocity')
    def calculate_estimated_velocity(self):
        for record in self:
            task_ids = record.env['project.task'].search([
                ('sprint_id', '=', record.id)
            ])
            total_velocity = sum([
                task.velocity for task in task_ids if task.velocity])
            record.estimated_velocity = total_velocity

    @api.multi
    @api.depends('project_id', 'end_date', 'task_line', 'task_line.velocity',
                 'task_line.stage_id')
    def calculate_actual_velocity(self):
        for record in self:
            task_ids = record.env['project.task'].search([
                ('sprint_id', '=', record.id),
                ('actual_end_date', '<=', record.end_date),
            ])
            total_velocity = sum([
                task.velocity for task in task_ids if task.velocity])
            record.actual_velocity = total_velocity

    @api.onchange('duration')
    def onchange_start_date(self):
        if self.start_date:
            end_date = datetime.strptime(
                self.start_date, '%Y-%m-%d') + timedelta(days=self.duration)
            self.end_date = end_date

    @api.onchange('project_id')
    def onchange_project(self):
        if self.project_id and self.project_id.branch_id:
            self.branch_id = self.project_id.branch_id

    @api.multi
    def check_sprint_state(self):
        next_call = datetime.today()
        for record in self.search([('state', '!=', 'done')]):
            if record.end_date:
                end_date = datetime.strptime(
                    record.end_date, '%Y-%m-%d').date()
                if end_date < next_call:
                    record.state = 'done'

    @api.constrains('sprint_planning_line')
    def check_users_in_planning_line(self):
        user_list = []
        for user in self.sprint_planning_line:
            if user.user_id.id not in user_list:
                user_list.append(user.user_id.id)
            else:
                raise ValidationError(
                    "You can't add the same user twice in Sprint Planning!")

    @api.model
    def create(self, vals):
        vals['sprint_seq'] = self.env[
            'ir.sequence'].next_by_code('project.sprint')

        res = super(ProjectSprint, self).create(vals)
        partner_list = []

        mail_channel_id = self.env['mail.channel'].sudo().search([
            ('name', '=', 'Project Sprint')
        ])
        if mail_channel_id:
            mail_channel_ids = self.env['mail.followers'].sudo().search([
                ('channel_id', '=', mail_channel_id.id),
                ('res_model', '=', res._name),
                ('res_id', '=', res.id),
            ])
            if not mail_channel_ids:
                self.env['mail.followers'].sudo().create({
                    'channel_id': mail_channel_id.id,
                    'res_model': res._name,
                    'res_id': res.id,
                })

        if 'team_id' in vals:
            team_id = self.env['project.team'].browse(vals['team_id'])
            partner_list += [member.partner_id.id
                             for member in team_id.member_ids]

        for follower in partner_list:
            if follower:
                mail_follower_ids = self.env['mail.followers'].sudo().search([
                    ('res_id', '=', res.id),
                    ('partner_id', '=', follower),
                    ('res_model', '=', res._name),
                ])
                if not mail_follower_ids:
                    self.env['mail.followers'].sudo().create({
                        'res_id': res.id,
                        'res_model': res._name,
                        'partner_id': follower,
                        'team_id': team_id.id,
                    })
        return res

    @api.multi
    def write(self, vals):
        res = super(ProjectSprint, self).write(vals)

        if 'team_id' in vals:
            team_id = self.env['project.team'].browse(vals['team_id'])
        else:
            team_id = self.team_id

        delete_team_id = self.env['mail.followers'].sudo().search([
            ('team_id', '!=', team_id.id),
            ('res_id', '=', self.id),
        ])
        delete_team_id.unlink()

        partner_list = [member.partner_id.id for member in team_id.member_ids]
        for follower in partner_list:
            if follower:
                mail_follower_ids = self.env['mail.followers'].sudo().search([
                    ('res_id', '=', self.id),
                    ('partner_id', '=', follower),
                    ('res_model', '=', self._name),
                ])
                if not mail_follower_ids:
                    self.env['mail.followers'].sudo().create({
                        'res_id': self.id,
                        'res_model': self._name,
                        'partner_id': follower,
                        'team_id': team_id.id,
                    })
        return res

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'state' in init_values and self.state == 'draft':
            return 'project_scrum.state_sprint_draft'
        elif 'state' in init_values and self.state == 'in_progress':
            return 'project_scrum.state_sprint_in_progress'
        elif 'state' in init_values and self.state == 'pending':
            return 'project_scrum.state_sprint_pending'
        elif 'state' in init_values and self.state == 'done':
            return 'project_scrum.state_sprint_done'
        elif 'state' in init_values and self.state == 'cancel':
            return 'project_scrum.state_sprint_cancel'
        return super(ProjectSprint, self)._track_subtype(init_values)
Beispiel #9
0
class OpStudentFeesDetails(models.Model):
    _name = 'op.student.fees.details'
    _description = 'Student Fees Details'

    fees_line_id = fields.Many2one('op.fees.terms.line', 'Fees Line')
    invoice_id = fields.Many2one('account.invoice', 'Invoice')
    amount = fields.Float('Fees Amount')
    date = fields.Date('Submit Date')
    product_id = fields.Many2one('product.product', 'Product')
    student_id = fields.Many2one('op.student', 'Student')
    state = fields.Selection([
        ('draft', 'Draft'), ('invoice', 'Invoice Created')], 'Status')
    invoice_state = fields.Selection([
        ('draft', 'Draft'), ('proforma', 'Pro-forma'),
        ('proforma2', 'Pro-forma'), ('open', 'Open'),
        ('paid', 'Paid'), ('cancel', 'Cancelled')], 'State',
        related="invoice_id.state", readonly=True)

    @api.multi
    def get_invoice(self):
        """ Create invoice for fee payment process of student """

        inv_obj = self.env['account.invoice']
        partner_id = self.student_id.partner_id
        student = self.student_id

        account_id = False
        product = self.product_id
        if product.property_account_income_id:
            account_id = product.property_account_income_id.id
        if not account_id:
            account_id = product.categ_id.property_account_income_categ_id.id
        if not account_id:
            raise UserError(
                _('There is no income account defined for this product: "%s". \
                   You may have to install a chart of account from Accounting \
                   app, settings menu.') % (product.name,))

        if self.amount <= 0.00:
            raise UserError(_('The value of the deposit amount must be \
                             positive.'))
        else:
            amount = self.amount
            name = product.name

        invoice = inv_obj.create({
            'name': student.name,
            'origin': student.gr_no or False,
            'type': 'out_invoice',
            'reference': False,
            'account_id': partner_id.property_account_receivable_id.id,
            'partner_id': partner_id.id,
            'invoice_line_ids': [(0, 0, {
                'name': name,
                'origin': student.gr_no,
                'account_id': account_id,
                'price_unit': amount,
                'quantity': 1.0,
                'discount': 0.0,
                'uom_id': product.uom_id.id,
                'product_id': product.id,
            })],
        })
        invoice.compute_taxes()
        self.state = 'invoice'
        self.invoice_id = invoice.id
        return True

    def action_get_invoice(self):
        value = True
        if self.invoice_id:
            form_view = self.env.ref('account.invoice_form')
            tree_view = self.env.ref('account.invoice_tree')
            value = {
                'domain': str([('id', '=', self.invoice_id.id)]),
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'account.invoice',
                'view_id': False,
                'views': [(form_view and form_view.id or False, 'form'),
                          (tree_view and tree_view.id or False, 'tree')],
                'type': 'ir.actions.act_window',
                'res_id': self.invoice_id.id,
                'target': 'current',
                'nodestroy': True
            }
        return value
Beispiel #10
0
class HolidaysType(models.Model):
    _name = "hr.holidays.status"
    _description = "Leave Type"

    name = fields.Char('Leave Type', required=True, translate=True)
    categ_id = fields.Many2one(
        'calendar.event.type',
        string='Meeting Type',
        help=
        'Once a leave is validated, Flectra will create a corresponding meeting of this type in the calendar.'
    )
    color_name = fields.Selection(
        [('red', 'Red'), ('blue', 'Blue'), ('lightgreen', 'Light Green'),
         ('lightblue', 'Light Blue'), ('lightyellow', 'Light Yellow'),
         ('magenta', 'Magenta'), ('lightcyan', 'Light Cyan'),
         ('black', 'Black'), ('lightpink', 'Light Pink'), ('brown', 'Brown'),
         ('violet', 'Violet'), ('lightcoral', 'Light Coral'),
         ('lightsalmon', 'Light Salmon'), ('lavender', 'Lavender'),
         ('wheat', 'Wheat'), ('ivory', 'Ivory')],
        string='Color in Report',
        required=True,
        default='red',
        help=
        'This color will be used in the leaves summary located in Reporting > Leaves by Department.'
    )
    limit = fields.Boolean(
        'Allow to Override Limit',
        help=
        'If you select this check box, the system allows the employees to take more leaves '
        'than the available ones for this type and will not take them into account for the '
        '"Remaining Legal Leaves" defined on the employee form.')
    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "If the active field is set to false, it will allow you to hide the leave type without removing it."
    )

    max_leaves = fields.Float(
        compute='_compute_leaves',
        string='Maximum Allowed',
        help=
        'This value is given by the sum of all leaves requests with a positive value.'
    )
    leaves_taken = fields.Float(
        compute='_compute_leaves',
        string='Leaves Already Taken',
        help=
        'This value is given by the sum of all leaves requests with a negative value.'
    )
    remaining_leaves = fields.Float(
        compute='_compute_leaves',
        string='Remaining Leaves',
        help='Maximum Leaves Allowed - Leaves Already Taken')
    virtual_remaining_leaves = fields.Float(
        compute='_compute_leaves',
        string='Virtual Remaining Leaves',
        help=
        'Maximum Leaves Allowed - Leaves Already Taken - Leaves Waiting Approval'
    )

    double_validation = fields.Boolean(
        string='Apply Double Validation',
        help=
        "When selected, the Allocation/Leave Requests for this type require a second validation to be approved."
    )
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 default=lambda self: self.env.user.company_id)

    @api.multi
    def get_days(self, employee_id):
        # need to use `dict` constructor to create a dict per id
        result = dict((id,
                       dict(max_leaves=0,
                            leaves_taken=0,
                            remaining_leaves=0,
                            virtual_remaining_leaves=0)) for id in self.ids)

        holidays = self.env['hr.holidays'].search([
            ('employee_id', '=', employee_id),
            ('state', 'in', ['confirm', 'validate1', 'validate']),
            ('holiday_status_id', 'in', self.ids)
        ])

        for holiday in holidays:
            status_dict = result[holiday.holiday_status_id.id]
            if holiday.type == 'add':
                if holiday.state == 'validate':
                    # note: add only validated allocation even for the virtual
                    # count; otherwise pending then refused allocation allow
                    # the employee to create more leaves than possible
                    status_dict[
                        'virtual_remaining_leaves'] += holiday.number_of_days_temp
                    status_dict['max_leaves'] += holiday.number_of_days_temp
                    status_dict[
                        'remaining_leaves'] += holiday.number_of_days_temp
            elif holiday.type == 'remove':  # number of days is negative
                status_dict[
                    'virtual_remaining_leaves'] -= holiday.number_of_days_temp
                if holiday.state == 'validate':
                    status_dict['leaves_taken'] += holiday.number_of_days_temp
                    status_dict[
                        'remaining_leaves'] -= holiday.number_of_days_temp
        return result

    @api.multi
    def _compute_leaves(self):
        data_days = {}
        if 'employee_id' in self._context:
            employee_id = self._context['employee_id']
        else:
            employee_id = self.env['hr.employee'].search(
                [('user_id', '=', self.env.user.id)], limit=1).id

        if employee_id:
            data_days = self.get_days(employee_id)

        for holiday_status in self:
            result = data_days.get(holiday_status.id, {})
            holiday_status.max_leaves = result.get('max_leaves', 0)
            holiday_status.leaves_taken = result.get('leaves_taken', 0)
            holiday_status.remaining_leaves = result.get('remaining_leaves', 0)
            holiday_status.virtual_remaining_leaves = result.get(
                'virtual_remaining_leaves', 0)

    @api.multi
    def name_get(self):
        if not self._context.get('employee_id'):
            # leave counts is based on employee_id, would be inaccurate if not based on correct employee
            return super(HolidaysType, self).name_get()
        res = []
        for record in self:
            name = record.name
            if not record.limit:
                name = "%(name)s (%(count)s)" % {
                    'name':
                    name,
                    'count':
                    _('%g remaining out of %g') %
                    (float_round(record.virtual_remaining_leaves or 0.0,
                                 precision_digits=2) + 0.0, record.max_leaves
                     or 0.0)
                }
            res.append((record.id, name))
        return res

    @api.model
    def _search(self,
                args,
                offset=0,
                limit=None,
                order=None,
                count=False,
                access_rights_uid=None):
        """ Override _search to order the results, according to some employee.
        The order is the following

         - limit (limited leaves first, such as Legal Leaves)
         - virtual remaining leaves (higher the better, so using reverse on sorted)

        This override is necessary because those fields are not stored and depends
        on an employee_id given in context. This sort will be done when there
        is an employee_id in context and that no other order has been given
        to the method.
        """
        leave_ids = super(HolidaysType,
                          self)._search(args,
                                        offset=offset,
                                        limit=limit,
                                        order=order,
                                        count=count,
                                        access_rights_uid=access_rights_uid)
        if not count and not order and self._context.get('employee_id'):
            leaves = self.browse(leave_ids)
            sort_key = lambda l: (not l.limit, l.virtual_remaining_leaves)
            return leaves.sorted(key=sort_key, reverse=True).ids
        return leave_ids
Beispiel #11
0
class Holidays(models.Model):
    _name = "hr.holidays"
    _description = "Leave"
    _order = "type desc, date_from desc"
    _inherit = ['mail.thread']

    def _default_employee(self):
        return self.env.context.get(
            'default_employee_id') or self.env['hr.employee'].search(
                [('user_id', '=', self.env.uid)], limit=1)

    name = fields.Char('Description')
    state = fields.Selection(
        [('draft', 'To Submit'), ('cancel', 'Cancelled'),
         ('confirm', 'To Approve'), ('refuse', 'Refused'),
         ('validate1', 'Second Approval'), ('validate', 'Approved')],
        string='Status',
        readonly=True,
        track_visibility='onchange',
        copy=False,
        default='confirm',
        help=
        "The status is set to 'To Submit', when a leave request is created." +
        "\nThe status is 'To Approve', when leave request is confirmed by user."
        +
        "\nThe status is 'Refused', when leave request is refused by manager."
        +
        "\nThe status is 'Approved', when leave request is approved by manager."
    )
    payslip_status = fields.Boolean(
        'Reported in last payslips',
        help=
        'Green this button when the leave has been taken into account in the payslip.'
    )
    report_note = fields.Text('HR Comments')
    user_id = fields.Many2one('res.users',
                              string='User',
                              related='employee_id.user_id',
                              related_sudo=True,
                              compute_sudo=True,
                              store=True,
                              default=lambda self: self.env.uid,
                              readonly=True)
    date_from = fields.Datetime('Start Date',
                                readonly=True,
                                index=True,
                                copy=False,
                                states={
                                    'draft': [('readonly', False)],
                                    'confirm': [('readonly', False)]
                                },
                                track_visibility='onchange')
    date_to = fields.Datetime('End Date',
                              readonly=True,
                              copy=False,
                              states={
                                  'draft': [('readonly', False)],
                                  'confirm': [('readonly', False)]
                              },
                              track_visibility='onchange')
    holiday_status_id = fields.Many2one("hr.holidays.status",
                                        string="Leave Type",
                                        required=True,
                                        readonly=True,
                                        states={
                                            'draft': [('readonly', False)],
                                            'confirm': [('readonly', False)]
                                        })
    employee_id = fields.Many2one('hr.employee',
                                  string='Employee',
                                  index=True,
                                  readonly=True,
                                  states={
                                      'draft': [('readonly', False)],
                                      'confirm': [('readonly', False)]
                                  },
                                  default=_default_employee,
                                  track_visibility='onchange')
    manager_id = fields.Many2one('hr.employee',
                                 string='Manager',
                                 readonly=True)
    notes = fields.Text('Reasons',
                        readonly=True,
                        states={
                            'draft': [('readonly', False)],
                            'confirm': [('readonly', False)]
                        })
    number_of_days_temp = fields.Float(
        'Allocation',
        copy=False,
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'confirm': [('readonly', False)]
        },
        help=
        'Number of days of the leave request according to your working schedule.'
    )
    number_of_days = fields.Float('Number of Days',
                                  compute='_compute_number_of_days',
                                  store=True,
                                  track_visibility='onchange')
    meeting_id = fields.Many2one('calendar.event', string='Meeting')
    type = fields.Selection(
        [('remove', 'Leave Request'), ('add', 'Allocation Request')],
        string='Request Type',
        required=True,
        readonly=True,
        index=True,
        track_visibility='always',
        default='remove',
        states={
            'draft': [('readonly', False)],
            'confirm': [('readonly', False)]
        },
        help="Choose 'Leave Request' if someone wants to take an off-day. "
        "\nChoose 'Allocation Request' if you want to increase the number of leaves available for someone"
    )
    parent_id = fields.Many2one('hr.holidays', string='Parent', copy=False)
    linked_request_ids = fields.One2many('hr.holidays',
                                         'parent_id',
                                         string='Linked Requests')
    department_id = fields.Many2one('hr.department',
                                    string='Department',
                                    readonly=True)
    category_id = fields.Many2one('hr.employee.category',
                                  string='Employee Tag',
                                  readonly=True,
                                  states={
                                      'draft': [('readonly', False)],
                                      'confirm': [('readonly', False)]
                                  },
                                  help='Category of Employee')
    holiday_type = fields.Selection(
        [('employee', 'By Employee'), ('category', 'By Employee Tag')],
        string='Allocation Mode',
        readonly=True,
        required=True,
        default='employee',
        states={
            'draft': [('readonly', False)],
            'confirm': [('readonly', False)]
        },
        help=
        'By Employee: Allocation/Request for individual Employee, By Employee Tag: Allocation/Request for group of employees in category'
    )
    first_approver_id = fields.Many2one(
        'hr.employee',
        string='First Approval',
        readonly=True,
        copy=False,
        help=
        'This area is automatically filled by the user who validate the leave',
        oldname='manager_id')
    second_approver_id = fields.Many2one(
        'hr.employee',
        string='Second Approval',
        readonly=True,
        copy=False,
        oldname='manager_id2',
        help=
        'This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)'
    )
    double_validation = fields.Boolean(
        'Apply Double Validation',
        related='holiday_status_id.double_validation')
    can_reset = fields.Boolean('Can reset', compute='_compute_can_reset')

    @api.multi
    @api.depends('number_of_days_temp', 'type')
    def _compute_number_of_days(self):
        for holiday in self:
            if holiday.type == 'remove':
                holiday.number_of_days = -holiday.number_of_days_temp
            else:
                holiday.number_of_days = holiday.number_of_days_temp

    @api.multi
    def _compute_can_reset(self):
        """ User can reset a leave request if it is its own leave request
            or if he is an Hr Manager.
        """
        user = self.env.user
        group_hr_manager = self.env.ref(
            'hr_holidays.group_hr_holidays_manager')
        for holiday in self:
            if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user:
                holiday.can_reset = True

    @api.constrains('date_from', 'date_to')
    def _check_date(self):
        for holiday in self:
            domain = [
                ('date_from', '<=', holiday.date_to),
                ('date_to', '>=', holiday.date_from),
                ('employee_id', '=', holiday.employee_id.id),
                ('id', '!=', holiday.id),
                ('type', '=', holiday.type),
                ('state', 'not in', ['cancel', 'refuse']),
            ]
            nholidays = self.search_count(domain)
            if nholidays:
                raise ValidationError(
                    _('You can not have 2 leaves that overlaps on same day!'))

    @api.constrains('state', 'number_of_days_temp', 'holiday_status_id')
    def _check_holidays(self):
        for holiday in self:
            if holiday.holiday_type != 'employee' or holiday.type != 'remove' or not holiday.employee_id or holiday.holiday_status_id.limit:
                continue
            leave_days = holiday.holiday_status_id.get_days(
                holiday.employee_id.id)[holiday.holiday_status_id.id]
            if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \
              float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1:
                raise ValidationError(
                    _('The number of remaining leaves is not sufficient for this leave type.\n'
                      'Please verify also the leaves waiting for validation.'))

    _sql_constraints = [
        ('type_value',
         "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL))",
         "The employee or employee category of this request is missing. Please make sure that your user login is linked to an employee."
         ),
        ('date_check2', "CHECK ( (type='add') OR (date_from <= date_to))",
         "The start date must be anterior to the end date."),
        ('date_check', "CHECK ( number_of_days_temp >= 0 )",
         "The number of days must be greater than 0."),
    ]

    @api.onchange('holiday_type')
    def _onchange_type(self):
        if self.holiday_type == 'employee' and not self.employee_id:
            self.employee_id = self.env['hr.employee'].search(
                [('user_id', '=', self.env.uid)], limit=1)
        elif self.holiday_type != 'employee':
            self.employee_id = None

    @api.onchange('employee_id')
    def _onchange_employee_id(self):
        self.manager_id = self.employee_id and self.employee_id.parent_id
        self.department_id = self.employee_id.department_id

    def _get_number_of_days(self, date_from, date_to, employee_id):
        """ Returns a float equals to the timedelta between two dates given as string."""
        from_dt = fields.Datetime.from_string(date_from)
        to_dt = fields.Datetime.from_string(date_to)

        if employee_id:
            employee = self.env['hr.employee'].browse(employee_id)
            return employee.get_work_days_count(from_dt, to_dt)

        time_delta = to_dt - from_dt
        return math.ceil(time_delta.days + float(time_delta.seconds) / 86400)

    @api.onchange('date_from')
    def _onchange_date_from(self):
        """ If there are no date set for date_to, automatically set one 8 hours later than
            the date_from. Also update the number_of_days.
        """
        date_from = self.date_from
        date_to = self.date_to

        # No date_to set so far: automatically compute one 8 hours later
        if date_from and not date_to:
            date_to_with_delta = fields.Datetime.from_string(
                date_from) + timedelta(hours=HOURS_PER_DAY)
            self.date_to = str(date_to_with_delta)

        # Compute and update the number of days
        if (date_to and date_from) and (date_from <= date_to):
            self.number_of_days_temp = self._get_number_of_days(
                date_from, date_to, self.employee_id.id)
        else:
            self.number_of_days_temp = 0

    @api.onchange('date_to')
    def _onchange_date_to(self):
        """ Update the number_of_days. """
        date_from = self.date_from
        date_to = self.date_to

        # Compute and update the number of days
        if (date_to and date_from) and (date_from <= date_to):
            self.number_of_days_temp = self._get_number_of_days(
                date_from, date_to, self.employee_id.id)
        else:
            self.number_of_days_temp = 0

    ####################################################
    # ORM Overrides methods
    ####################################################

    @api.multi
    def name_get(self):
        res = []
        for leave in self:
            if leave.type == 'remove':
                if self.env.context.get('short_name'):
                    res.append((leave.id, _("%s : %.2f day(s)") %
                                (leave.name or leave.holiday_status_id.name,
                                 leave.number_of_days_temp)))
                else:
                    res.append(
                        (leave.id, _("%s on %s : %.2f day(s)") %
                         (leave.employee_id.name or leave.category_id.name,
                          leave.holiday_status_id.name,
                          leave.number_of_days_temp)))
            else:
                res.append(
                    (leave.id, _("Allocation of %s : %.2f day(s) To %s") %
                     (leave.holiday_status_id.name, leave.number_of_days_temp,
                      leave.employee_id.name)))
        return res

    def _check_state_access_right(self, vals):
        if vals.get('state') and vals['state'] not in [
                'draft', 'confirm', 'cancel'
        ] and not self.env['res.users'].has_group(
                'hr_holidays.group_hr_holidays_user'):
            return False
        return True

    @api.multi
    def add_follower(self, employee_id):
        employee = self.env['hr.employee'].browse(employee_id)
        if employee.user_id:
            self.message_subscribe_users(user_ids=employee.user_id.ids)

    @api.model
    def create(self, values):
        """ Override to avoid automatic logging of creation """
        employee_id = values.get('employee_id', False)
        if not self._check_state_access_right(values):
            raise AccessError(
                _('You cannot set a leave request as \'%s\'. Contact a human resource manager.'
                  ) % values.get('state'))
        if not values.get('department_id'):
            values.update({
                'department_id':
                self.env['hr.employee'].browse(employee_id).department_id.id
            })
        holiday = super(
            Holidays,
            self.with_context(mail_create_nolog=True,
                              mail_create_nosubscribe=True)).create(values)
        holiday.add_follower(employee_id)
        if 'employee_id' in values:
            holiday._onchange_employee_id()
        return holiday

    @api.multi
    def write(self, values):
        employee_id = values.get('employee_id', False)
        if not self._check_state_access_right(values):
            raise AccessError(
                _('You cannot set a leave request as \'%s\'. Contact a human resource manager.'
                  ) % values.get('state'))
        result = super(Holidays, self).write(values)
        self.add_follower(employee_id)
        if 'employee_id' in values:
            self._onchange_employee_id()
        return result

    @api.multi
    def unlink(self):
        for holiday in self.filtered(lambda holiday: holiday.state not in
                                     ['draft', 'cancel', 'confirm']):
            raise UserError(
                _('You cannot delete a leave which is in %s state.') %
                (holiday.state, ))
        return super(Holidays, self).unlink()

    ####################################################
    # Business methods
    ####################################################

    @api.multi
    def _create_resource_leave(self):
        """ This method will create entry in resource calendar leave object at the time of holidays validated """
        for leave in self:
            self.env['resource.calendar.leaves'].create({
                'name':
                leave.name,
                'date_from':
                leave.date_from,
                'holiday_id':
                leave.id,
                'date_to':
                leave.date_to,
                'resource_id':
                leave.employee_id.resource_id.id,
                'calendar_id':
                leave.employee_id.resource_calendar_id.id
            })
        return True

    @api.multi
    def _remove_resource_leave(self):
        """ This method will create entry in resource calendar leave object at the time of holidays cancel/removed """
        return self.env['resource.calendar.leaves'].search([
            ('holiday_id', 'in', self.ids)
        ]).unlink()

    @api.multi
    def action_draft(self):
        if any(not holiday.can_reset for holiday in self):
            raise UserError(
                _('Only an HR Manager or the concerned employee can reset to draft.'
                  ))
        if any(holiday.state not in ['confirm', 'refuse'] for holiday in self):
            raise UserError(
                _('Leave request state must be "Refused" or "To Approve" in order to reset to Draft.'
                  ))
        self.write({
            'state': 'draft',
            'first_approver_id': False,
            'second_approver_id': False,
        })
        linked_requests = self.mapped('linked_request_ids')
        if linked_requests:
            linked_requests.action_draft()
            linked_requests.unlink()
        return True

    @api.multi
    def action_confirm(self):
        if self.filtered(lambda holiday: holiday.state != 'draft'):
            raise UserError(
                _('Leave request must be in Draft state ("To Submit") in order to confirm it.'
                  ))
        return self.write({'state': 'confirm'})

    @api.multi
    def _check_security_action_approve(self):
        if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
            raise UserError(
                _('Only an HR Officer or Manager can approve leave requests.'))

    @api.multi
    def action_approve(self):
        # if double_validation: this method is the first approval approval
        # if not double_validation: this method calls action_validate() below
        self._check_security_action_approve()

        current_employee = self.env['hr.employee'].search(
            [('user_id', '=', self.env.uid)], limit=1)
        for holiday in self:
            if holiday.state != 'confirm':
                raise UserError(
                    _('Leave request must be confirmed ("To Approve") in order to approve it.'
                      ))

            if holiday.double_validation:
                return holiday.write({
                    'state': 'validate1',
                    'first_approver_id': current_employee.id
                })
            else:
                holiday.action_validate()

    @api.multi
    def _prepare_create_by_category(self, employee):
        self.ensure_one()
        values = {
            'name': self.name,
            'type': self.type,
            'holiday_type': 'employee',
            'holiday_status_id': self.holiday_status_id.id,
            'date_from': self.date_from,
            'date_to': self.date_to,
            'notes': self.notes,
            'number_of_days_temp': self.number_of_days_temp,
            'parent_id': self.id,
            'employee_id': employee.id
        }
        return values

    @api.multi
    def _check_security_action_validate(self):
        if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
            raise UserError(
                _('Only an HR Officer or Manager can approve leave requests.'))

    @api.multi
    def action_validate(self):
        self._check_security_action_validate()

        current_employee = self.env['hr.employee'].search(
            [('user_id', '=', self.env.uid)], limit=1)
        for holiday in self:
            if holiday.state not in ['confirm', 'validate1']:
                raise UserError(
                    _('Leave request must be confirmed in order to approve it.'
                      ))
            if holiday.state == 'validate1' and not holiday.env.user.has_group(
                    'hr_holidays.group_hr_holidays_manager'):
                raise UserError(
                    _('Only an HR Manager can apply the second approval on leave requests.'
                      ))

            holiday.write({'state': 'validate'})
            if holiday.double_validation:
                holiday.write({'second_approver_id': current_employee.id})
            else:
                holiday.write({'first_approver_id': current_employee.id})
            if holiday.holiday_type == 'employee' and holiday.type == 'remove':
                holiday._validate_leave_request()
            elif holiday.holiday_type == 'category':
                leaves = self.env['hr.holidays']
                for employee in holiday.category_id.employee_ids:
                    values = holiday._prepare_create_by_category(employee)
                    leaves += self.with_context(
                        mail_notify_force_send=False).create(values)
                # TODO is it necessary to interleave the calls?
                leaves.action_approve()
                if leaves and leaves[0].double_validation:
                    leaves.action_validate()
        return True

    def _validate_leave_request(self):
        """ Validate leave requests (holiday_type='employee' and holiday.type='remove')
        by creating a calendar event and a resource leaves. """
        for holiday in self.filtered(lambda request: request.type == 'remove'
                                     and request.holiday_type == 'employee'):
            meeting_values = holiday._prepare_holidays_meeting_values()
            meeting = self.env['calendar.event'].with_context(
                no_mail_to_attendees=True).create(meeting_values)
            holiday.write({'meeting_id': meeting.id})
            holiday._create_resource_leave()

    @api.multi
    def _prepare_holidays_meeting_values(self):
        self.ensure_one()
        meeting_values = {
            'name':
            _("%s on Time Off : %.2f day(s)") %
            (self.employee_id.name
             or self.category_id.name, self.number_of_days_temp),
            'categ_ids': [(6, 0, [self.holiday_status_id.categ_id.id])]
            if self.holiday_status_id.categ_id else [],
            'duration':
            self.number_of_days_temp * HOURS_PER_DAY,
            'description':
            self.notes,
            'user_id':
            self.user_id.id,
            'start':
            self.date_from,
            'stop':
            self.date_to,
            'allday':
            False,
            'state':
            'open',  # to block that meeting date in the calendar
            'privacy':
            'confidential'
        }
        # Add the partner_id (if exist) as an attendee
        if self.user_id and self.user_id.partner_id:
            meeting_values['partner_ids'] = [(4, self.user_id.partner_id.id)]
        return meeting_values

    @api.multi
    def action_refuse(self):
        self._check_security_action_refuse()

        current_employee = self.env['hr.employee'].search(
            [('user_id', '=', self.env.uid)], limit=1)
        if any(holiday.state not in ['confirm', 'validate', 'validate1']
               for holiday in self):
            raise UserError(
                _('Leave request must be confirmed or validated in order to refuse it.'
                  ))

        validated_holidays = self.filtered(
            lambda hol: hol.state == 'validate1')
        validated_holidays.write({
            'state': 'refuse',
            'first_approver_id': current_employee.id
        })
        (self - validated_holidays).write({
            'state':
            'refuse',
            'second_approver_id':
            current_employee.id
        })
        # Delete the meeting
        self.mapped('meeting_id').unlink()
        # If a category that created several holidays, cancel all related
        linked_requests = self.mapped('linked_request_ids')
        if linked_requests:
            linked_requests.action_refuse()
        self._remove_resource_leave()
        return True

    @api.multi
    def _check_security_action_refuse(self):
        if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
            raise UserError(
                _('Only an HR Officer or Manager can refuse leave requests.'))

    ####################################################
    # Messaging methods
    ####################################################

    @api.multi
    def _track_subtype(self, init_values):
        if 'state' in init_values and self.state == 'validate':
            return 'hr_holidays.mt_holidays_approved'
        elif 'state' in init_values and self.state == 'validate1':
            return 'hr_holidays.mt_holidays_first_validated'
        elif 'state' in init_values and self.state == 'confirm':
            return 'hr_holidays.mt_holidays_confirmed'
        elif 'state' in init_values and self.state == 'refuse':
            return 'hr_holidays.mt_holidays_refused'
        return super(Holidays, self)._track_subtype(init_values)

    @api.multi
    def _notification_recipients(self, message, groups):
        """ Handle HR users and officers recipients that can validate or refuse holidays
        directly from email. """
        groups = super(Holidays,
                       self)._notification_recipients(message, groups)

        self.ensure_one()
        hr_actions = []
        if self.state == 'confirm':
            app_action = self._notification_link_helper(
                'controller', controller='/hr_holidays/validate')
            hr_actions += [{'url': app_action, 'title': _('Approve')}]
        if self.state in ['confirm', 'validate', 'validate1']:
            ref_action = self._notification_link_helper(
                'controller', controller='/hr_holidays/refuse')
            hr_actions += [{'url': ref_action, 'title': _('Refuse')}]

        new_group = ('group_hr_holidays_user',
                     lambda partner: bool(partner.user_ids) and any(
                         user.has_group('hr_holidays.group_hr_holidays_user')
                         for user in partner.user_ids), {
                             'actions': hr_actions,
                         })

        return [new_group] + groups

    @api.multi
    def _message_notification_recipients(self, message, recipients):
        result = super(Holidays, self)._message_notification_recipients(
            message, recipients)
        leave_type = self.env[message.model].browse(message.res_id).type
        title = _("See Leave") if leave_type == 'remove' else _(
            "See Allocation")
        for res in result:
            if result[res].get('button_access'):
                result[res]['button_access']['title'] = title
        return result
Beispiel #12
0
class PricelistItem(models.Model):
    _name = "product.pricelist.item"
    _description = "Pricelist Rule"
    _order = "applied_on, min_quantity desc, categ_id desc, id desc"
    _check_company_auto = True

    # NOTE: if you change _order on this model, make sure it matches the SQL
    # query built in _compute_price_rule() above in this file to avoid
    # inconstencies and undeterministic issues.

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

    product_tmpl_id = fields.Many2one(
        'product.template',
        'Product',
        ondelete='cascade',
        check_company=True,
        help=
        "Specify a template if this rule only applies to one product template. Keep empty otherwise."
    )
    product_id = fields.Many2one(
        'product.product',
        'Product Variant',
        ondelete='cascade',
        check_company=True,
        help=
        "Specify a product if this rule only applies to one product. Keep empty otherwise."
    )
    categ_id = fields.Many2one(
        'product.category',
        'Product Category',
        ondelete='cascade',
        help=
        "Specify a product category if this rule only applies to products belonging to this category or its children categories. Keep empty otherwise."
    )
    min_quantity = fields.Float(
        'Min. Quantity',
        default=0,
        digits="Product Unit Of Measure",
        help="For the rule to apply, bought/sold quantity must be greater "
        "than or equal to the minimum quantity specified in this field.\n"
        "Expressed in the default unit of measure of the product.")
    applied_on = fields.Selection(
        [('3_global', 'All Products'),
         ('2_product_category', 'Product Category'), ('1_product', 'Product'),
         ('0_product_variant', 'Product Variant')],
        "Apply On",
        default='3_global',
        required=True,
        help='Pricelist Item applicable on selected option')
    base = fields.Selection(
        [('list_price', 'Sales Price'), ('standard_price', 'Cost'),
         ('pricelist', 'Other Pricelist')],
        "Based on",
        default='list_price',
        required=True,
        help='Base price for computation.\n'
        'Sales Price: The base price will be the Sales Price.\n'
        'Cost Price : The base price will be the cost price.\n'
        'Other Pricelist : Computation of the base price based on another Pricelist.'
    )
    base_pricelist_id = fields.Many2one('product.pricelist',
                                        'Other Pricelist',
                                        check_company=True)
    pricelist_id = fields.Many2one('product.pricelist',
                                   'Pricelist',
                                   index=True,
                                   ondelete='cascade',
                                   required=True,
                                   default=_default_pricelist_id)
    price_surcharge = fields.Float(
        'Price Surcharge',
        digits='Product Price',
        help=
        'Specify the fixed amount to add or substract(if negative) to the amount calculated with the discount.'
    )
    price_discount = fields.Float('Price Discount', default=0, digits=(16, 2))
    price_round = fields.Float(
        'Price Rounding',
        digits='Product Price',
        help="Sets the price so that it is a multiple of this value.\n"
        "Rounding is applied after the discount and before the surcharge.\n"
        "To have prices that end in 9.99, set rounding 10, surcharge -0.01")
    price_min_margin = fields.Float(
        'Min. Price Margin',
        digits='Product Price',
        help='Specify the minimum amount of margin over the base price.')
    price_max_margin = fields.Float(
        'Max. Price Margin',
        digits='Product Price',
        help='Specify the maximum amount of margin over the base price.')
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 readonly=True,
                                 related='pricelist_id.company_id',
                                 store=True)
    currency_id = fields.Many2one('res.currency',
                                  'Currency',
                                  readonly=True,
                                  related='pricelist_id.currency_id',
                                  store=True)
    active = fields.Boolean(readonly=True,
                            related="pricelist_id.active",
                            store=True)
    date_start = fields.Datetime(
        'Start Date',
        help="Starting datetime for the pricelist item validation\n"
        "The displayed value depends on the timezone set in your preferences.")
    date_end = fields.Datetime(
        'End Date',
        help="Ending datetime for the pricelist item validation\n"
        "The displayed value depends on the timezone set in your preferences.")
    compute_price = fields.Selection([('fixed', 'Fixed Price'),
                                      ('percentage', 'Percentage (discount)'),
                                      ('formula', 'Formula')],
                                     index=True,
                                     default='fixed',
                                     required=True)
    fixed_price = fields.Float('Fixed Price', digits='Product Price')
    percent_price = fields.Float('Percentage Price')
    # functional fields used for usability purposes
    name = fields.Char('Name',
                       compute='_get_pricelist_item_name_price',
                       help="Explicit rule name for this pricelist line.")
    price = fields.Char('Price',
                        compute='_get_pricelist_item_name_price',
                        help="Explicit rule name for this pricelist line.")

    @api.constrains('base_pricelist_id', 'pricelist_id', 'base')
    def _check_recursion(self):
        if any(item.base == 'pricelist' and item.pricelist_id
               and item.pricelist_id == item.base_pricelist_id
               for item in self):
            raise ValidationError(
                _('You cannot assign the Main Pricelist as Other Pricelist in PriceList Item'
                  ))
        return True

    @api.constrains('price_min_margin', 'price_max_margin')
    def _check_margin(self):
        if any(item.price_min_margin > item.price_max_margin for item in self):
            raise ValidationError(
                _('The minimum margin should be lower than the maximum margin.'
                  ))
        return True

    @api.constrains('product_id', 'product_tmpl_id', 'categ_id')
    def _check_product_consistency(self):
        for item in self:
            if item.applied_on == "2_product_category" and not item.categ_id:
                raise ValidationError(
                    _("Please specify the category for which this rule should be applied"
                      ))
            elif item.applied_on == "1_product" and not item.product_tmpl_id:
                raise ValidationError(
                    _("Please specify the product for which this rule should be applied"
                      ))
            elif item.applied_on == "0_product_variant" and not item.product_id:
                raise ValidationError(
                    _("Please specify the product variant for which this rule should be applied"
                      ))

    @api.depends('applied_on', 'categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \
        'pricelist_id', 'percent_price', 'price_discount', 'price_surcharge')
    def _get_pricelist_item_name_price(self):
        for item in self:
            if item.categ_id and item.applied_on == '2_product_category':
                item.name = _("Category: %s") % (item.categ_id.display_name)
            elif item.product_tmpl_id and item.applied_on == '1_product':
                item.name = _("Product: %s") % (
                    item.product_tmpl_id.display_name)
            elif item.product_id and item.applied_on == '0_product_variant':
                item.name = _("Variant: %s") % (item.product_id.with_context(
                    display_default_code=False).display_name)
            else:
                item.name = _("All Products")

            if item.compute_price == 'fixed':
                item.price = formatLang(item.env,
                                        item.fixed_price,
                                        monetary=True,
                                        dp="Product Price",
                                        currency_obj=item.currency_id)
            elif item.compute_price == 'percentage':
                item.price = _("%s %% discount", item.percent_price)
            else:
                item.price = _(
                    "%(percentage)s %% discount and %(price)s surcharge",
                    percentage=item.price_discount,
                    price=item.price_surcharge)

    @api.onchange('compute_price')
    def _onchange_compute_price(self):
        if self.compute_price != 'fixed':
            self.fixed_price = 0.0
        if self.compute_price != 'percentage':
            self.percent_price = 0.0
        if self.compute_price != 'formula':
            self.update({
                'base': 'list_price',
                'price_discount': 0.0,
                'price_surcharge': 0.0,
                'price_round': 0.0,
                'price_min_margin': 0.0,
                'price_max_margin': 0.0,
            })

    @api.onchange('product_id')
    def _onchange_product_id(self):
        has_product_id = self.filtered('product_id')
        for item in has_product_id:
            item.product_tmpl_id = item.product_id.product_tmpl_id
        if self.env.context.get('default_applied_on', False) == '1_product':
            # If a product variant is specified, apply on variants instead
            # Reset if product variant is removed
            has_product_id.update({'applied_on': '0_product_variant'})
            (self - has_product_id).update({'applied_on': '1_product'})

    @api.onchange('product_tmpl_id')
    def _onchange_product_tmpl_id(self):
        has_tmpl_id = self.filtered('product_tmpl_id')
        for item in has_tmpl_id:
            if item.product_id and item.product_id.product_tmpl_id != item.product_tmpl_id:
                item.product_id = None

    @api.onchange('product_id', 'product_tmpl_id', 'categ_id')
    def _onchane_rule_content(self):
        if not self.user_has_groups(
                'product.group_sale_pricelist') and not self.env.context.get(
                    'default_applied_on', False):
            # If advanced pricelists are disabled (applied_on field is not visible)
            # AND we aren't coming from a specific product template/variant.
            variants_rules = self.filtered('product_id')
            template_rules = (self -
                              variants_rules).filtered('product_tmpl_id')
            variants_rules.update({'applied_on': '0_product_variant'})
            template_rules.update({'applied_on': '1_product'})
            (self - variants_rules - template_rules).update(
                {'applied_on': '3_global'})

    @api.model_create_multi
    def create(self, vals_list):
        for values in vals_list:
            if values.get('applied_on', False):
                # Ensure item consistency for later searches.
                applied_on = values['applied_on']
                if applied_on == '3_global':
                    values.update(
                        dict(product_id=None,
                             product_tmpl_id=None,
                             categ_id=None))
                elif applied_on == '2_product_category':
                    values.update(dict(product_id=None, product_tmpl_id=None))
                elif applied_on == '1_product':
                    values.update(dict(product_id=None, categ_id=None))
                elif applied_on == '0_product_variant':
                    values.update(dict(categ_id=None))
        return super(PricelistItem, self).create(vals_list)

    def write(self, values):
        if values.get('applied_on', False):
            # Ensure item consistency for later searches.
            applied_on = values['applied_on']
            if applied_on == '3_global':
                values.update(
                    dict(product_id=None, product_tmpl_id=None, categ_id=None))
            elif applied_on == '2_product_category':
                values.update(dict(product_id=None, product_tmpl_id=None))
            elif applied_on == '1_product':
                values.update(dict(product_id=None, categ_id=None))
            elif applied_on == '0_product_variant':
                values.update(dict(categ_id=None))
        res = super(PricelistItem, self).write(values)
        # When the pricelist changes we need the product.template price
        # to be invalided and recomputed.
        self.env['product.template'].invalidate_cache(['price'])
        self.env['product.product'].invalidate_cache(['price'])
        return res

    def _compute_price(self,
                       price,
                       price_uom,
                       product,
                       quantity=1.0,
                       partner=False):
        """Compute the unit price of a product in the context of a pricelist application.
           The unused parameters are there to make the full context available for overrides.
        """
        self.ensure_one()
        convert_to_price_uom = (
            lambda price: product.uom_id._compute_price(price, price_uom))
        if self.compute_price == 'fixed':
            price = convert_to_price_uom(self.fixed_price)
        elif self.compute_price == 'percentage':
            price = (price - (price * (self.percent_price / 100))) or 0.0
        else:
            # complete formula
            price_limit = price
            price = (price - (price * (self.price_discount / 100))) or 0.0
            if self.price_round:
                price = tools.float_round(price,
                                          precision_rounding=self.price_round)

            if self.price_surcharge:
                price_surcharge = convert_to_price_uom(self.price_surcharge)
                price += price_surcharge

            if self.price_min_margin:
                price_min_margin = convert_to_price_uom(self.price_min_margin)
                price = max(price, price_limit + price_min_margin)

            if self.price_max_margin:
                price_max_margin = convert_to_price_uom(self.price_max_margin)
                price = min(price, price_limit + price_max_margin)
        return price
Beispiel #13
0
class UoM(models.Model):
    _name = 'uom.uom'
    _description = 'Product Unit of Measure'
    _order = "name"

    name = fields.Char('Unit of Measure', required=True, translate=True)
    category_id = fields.Many2one(
        'uom.category',
        'Category',
        required=True,
        ondelete='cascade',
        help=
        "Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."
    )
    factor = fields.Float(
        'Ratio',
        default=1.0,
        digits=0,
        required=True,  # force NUMERIC with unlimited precision
        help=
        'How much bigger or smaller this unit is compared to the reference Unit of Measure for this category: 1 * (reference unit) = ratio * (this unit)'
    )
    factor_inv = fields.Float(
        'Bigger Ratio',
        compute='_compute_factor_inv',
        digits=0,  # force NUMERIC with unlimited precision
        readonly=True,
        required=True,
        help=
        'How many times this Unit of Measure is bigger than the reference Unit of Measure in this category: 1 * (this unit) = ratio * (reference unit)'
    )
    rounding = fields.Float(
        'Rounding Precision',
        default=0.01,
        digits=0,
        required=True,
        help="The computed quantity will be a multiple of this value. "
        "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."
    )
    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "Uncheck the active field to disable a unit of measure without deleting it."
    )
    uom_type = fields.Selection(
        [('bigger', 'Bigger than the reference Unit of Measure'),
         ('reference', 'Reference Unit of Measure for this category'),
         ('smaller', 'Smaller than the reference Unit of Measure')],
        'Type',
        default='reference',
        required=1)

    _sql_constraints = [
        ('factor_gt_zero', 'CHECK (factor!=0)',
         'The conversion ratio for a unit of measure cannot be 0!'),
        ('rounding_gt_zero', 'CHECK (rounding>0)',
         'The rounding precision must be strictly positive.'),
        ('factor_reference_is_one',
         "CHECK((uom_type = 'reference' AND factor = 1.0) OR (uom_type != 'reference'))",
         "The reference unit must have a conversion factor equal to 1.")
    ]

    @api.depends('factor')
    def _compute_factor_inv(self):
        for uom in self:
            uom.factor_inv = uom.factor and (1.0 / uom.factor) or 0.0

    @api.onchange('uom_type')
    def _onchange_uom_type(self):
        if self.uom_type == 'reference':
            self.factor = 1

    @api.constrains('category_id', 'uom_type', 'active')
    def _check_category_reference_uniqueness(self):
        """ Force the existence of only one UoM reference per category
            NOTE: this is a constraint on the all table. This might not be a good practice, but this is
            not possible to do it in SQL directly.
        """
        category_ids = self.mapped('category_id').ids
        self.env['uom.uom'].flush(['category_id', 'uom_type', 'active'])
        self._cr.execute(
            """
            SELECT C.id AS category_id, count(U.id) AS uom_count
            FROM uom_category C
            LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference' AND U.active = 't'
            WHERE C.id IN %s
            GROUP BY C.id
        """, (tuple(category_ids), ))
        for uom_data in self._cr.dictfetchall():
            if uom_data['uom_count'] == 0:
                raise ValidationError(
                    _("UoM category %s should have a reference unit of measure. If you just created a new category, please record the 'reference' unit first."
                      ) % (self.env['uom.category'].browse(
                          uom_data['category_id']).name, ))
            if uom_data['uom_count'] > 1:
                raise ValidationError(
                    _("UoM category %s should only have one reference unit of measure."
                      ) % (self.env['uom.category'].browse(
                          uom_data['category_id']).name, ))

    @api.constrains('category_id')
    def _validate_uom_category(self):
        for uom in self:
            reference_uoms = self.env['uom.uom'].search([
                ('category_id', '=', uom.category_id.id),
                ('uom_type', '=', 'reference')
            ])
            if len(reference_uoms) > 1:
                raise ValidationError(
                    _("UoM category %s should only have one reference unit of measure."
                      ) % (self.category_id.name))

    @api.model_create_multi
    def create(self, vals_list):
        for values in vals_list:
            if 'factor_inv' in values:
                factor_inv = values.pop('factor_inv')
                values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0
        return super(UoM, self).create(vals_list)

    def write(self, values):
        if 'factor_inv' in values:
            factor_inv = values.pop('factor_inv')
            values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0
        return super(UoM, self).write(values)

    def unlink(self):
        uom_categ_unit = self.env.ref('uom.product_uom_categ_unit')
        uom_categ_wtime = self.env.ref('uom.uom_categ_wtime')
        uom_categ_kg = self.env.ref('uom.product_uom_categ_kgm')
        if any(uom.category_id.id in (
                uom_categ_unit + uom_categ_wtime +
                uom_categ_kg).ids and uom.uom_type == 'reference'
               for uom in self):
            raise UserError(
                _("You cannot delete this UoM as it is used by the system. You should rather archive it."
                  ))
        # UoM with external IDs shouldn't be deleted since they will most probably break the app somewhere else.
        # For example, in addons/product/models/product_template.py, cubic meters are used in `_get_volume_uom_id_from_ir_config_parameter()`,
        # meters in `_get_length_uom_id_from_ir_config_parameter()`, and so on.
        if self.env['ir.model.data'].search_count([('model', '=', self._name),
                                                   ('res_id', 'in', self.ids)
                                                   ]):
            raise UserError(
                _("You cannot delete this UoM as it is used by the system. You should rather archive it."
                  ))
        return super(UoM, self).unlink()

    @api.model
    def name_create(self, name):
        """ The UoM category and factor are required, so we'll have to add temporary values
        for imported UoMs """
        values = {self._rec_name: name, 'factor': 1}
        # look for the category based on the english name, i.e. no context on purpose!
        # TODO: should find a way to have it translated but not created until actually used
        if not self._context.get('default_category_id'):
            EnglishUoMCateg = self.env['uom.category'].with_context({})
            misc_category = EnglishUoMCateg.search([
                ('name', '=', 'Unsorted/Imported Units')
            ])
            if misc_category:
                values['category_id'] = misc_category.id
            else:
                values['category_id'] = EnglishUoMCateg.name_create(
                    'Unsorted/Imported Units')[0]
        new_uom = self.create(values)
        return new_uom.name_get()[0]

    def _compute_quantity(self,
                          qty,
                          to_unit,
                          round=True,
                          rounding_method='UP',
                          raise_if_failure=True):
        """ Convert the given quantity from the current UoM `self` into a given one
            :param qty: the quantity to convert
            :param to_unit: the destination UoM record (uom.uom)
            :param raise_if_failure: only if the conversion is not possible
                - if true, raise an exception if the conversion is not possible (different UoM category),
                - otherwise, return the initial quantity
        """
        if not self or not qty:
            return qty
        self.ensure_one()

        if self != to_unit and self.category_id.id != to_unit.category_id.id:
            if raise_if_failure:
                raise UserError(
                    _('The unit of measure %s defined on the order line doesn\'t belong to the same category as the unit of measure %s defined on the product. Please correct the unit of measure defined on the order line or on the product, they should belong to the same category.'
                      ) % (self.name, to_unit.name))
            else:
                return qty

        if self == to_unit:
            amount = qty
        else:
            amount = qty / self.factor
            if to_unit:
                amount = amount * to_unit.factor

        if to_unit and round:
            amount = tools.float_round(amount,
                                       precision_rounding=to_unit.rounding,
                                       rounding_method=rounding_method)

        return amount

    def _compute_price(self, price, to_unit):
        self.ensure_one()
        if not self or not price or not to_unit or self == to_unit:
            return price
        if self.category_id.id != to_unit.category_id.id:
            return price
        amount = price * self.factor
        if to_unit:
            amount = amount / to_unit.factor
        return amount
Beispiel #14
0
class OpAdmission(models.Model):
    _name = 'op.admission'
    _inherit = 'mail.thread'
    _rec_name = 'application_number'
    _order = "application_number desc"
    _description = "Admission"

    name = fields.Char(
        'First Name', size=128, required=True,
        states={'done': [('readonly', True)]})
    middle_name = fields.Char(
        'Middle Name', size=128,
        states={'done': [('readonly', True)]})
    last_name = fields.Char(
        'Last Name', size=128, required=True,
        states={'done': [('readonly', True)]})
    title = fields.Many2one(
        'res.partner.title', 'Title', states={'done': [('readonly', True)]})
    application_number = fields.Char(
        'Application Number', size=16, required=True, copy=False,
        states={'done': [('readonly', True)]},
        default=lambda self:
        self.env['ir.sequence'].next_by_code('op.admission'))
    admission_date = fields.Date(
        'Admission Date', copy=False,
        states={'done': [('readonly', True)]})
    application_date = fields.Datetime(
        'Application Date', required=True, copy=False,
        states={'done': [('readonly', True)]},
        default=lambda self: fields.Datetime.now())
    birth_date = fields.Date(
        'Birth Date', required=True, states={'done': [('readonly', True)]})
    course_id = fields.Many2one(
        'op.course', 'Course', required=True,
        states={'done': [('readonly', True)]})
    batch_id = fields.Many2one(
        'op.batch', 'Batch', required=False,
        states={'done': [('readonly', True)],
                'fees_paid': [('required', True)]})
    street = fields.Char(
        'Street', size=256, states={'done': [('readonly', True)]})
    street2 = fields.Char(
        'Street2', size=256, states={'done': [('readonly', True)]})
    phone = fields.Char(
        'Phone', size=16, states={'done': [('readonly', True)]})
    mobile = fields.Char(
        'Mobile', size=16, states={'done': [('readonly', True)]})
    email = fields.Char(
        'Email', size=256, required=True,
        states={'done': [('readonly', True)]})
    city = fields.Char('City', size=64, states={'done': [('readonly', True)]})
    zip = fields.Char('Zip', size=8, states={'done': [('readonly', True)]})
    state_id = fields.Many2one(
        'res.country.state', 'States', states={'done': [('readonly', True)]})
    country_id = fields.Many2one(
        'res.country', 'Country', states={'done': [('readonly', True)]})
    fees = fields.Float('Fees', states={'done': [('readonly', True)]})
    image = fields.Binary('image', states={'done': [('readonly', True)]})
    state = fields.Selection(
        [('draft', 'Draft'), ('submit', 'Submitted'),
         ('confirm', 'Confirmed'), ('admission', 'Admission Confirm'),
         ('reject', 'Rejected'), ('pending', 'Pending'),
         ('cancel', 'Cancelled'), ('done', 'Done')],
        'State', default='draft', track_visibility='onchange')
    due_date = fields.Date('Due Date', states={'done': [('readonly', True)]})
    prev_institute_id = fields.Many2one(
        'res.partner', 'Previous Institute',
        states={'done': [('readonly', True)]})
    prev_course_id = fields.Many2one(
        'op.course', 'Previous Course', states={'done': [('readonly', True)]})
    prev_result = fields.Char(
        'Previous Result', size=256, states={'done': [('readonly', True)]})
    family_business = fields.Char(
        'Family Business', size=256, states={'done': [('readonly', True)]})
    family_income = fields.Float(
        'Family Income', states={'done': [('readonly', True)]})
    gender = fields.Selection(
        [('m', 'Male'), ('f', 'Female'), ('o', 'Other')], 'Gender',
        required=True, states={'done': [('readonly', True)]})
    student_id = fields.Many2one(
        'op.student', 'Student', states={'done': [('readonly', True)]})
    nbr = fields.Integer('No of Admission', readonly=True)
    register_id = fields.Many2one(
        'op.admission.register', 'Admission Register', required=True,
        states={'done': [('readonly', True)]})
    partner_id = fields.Many2one('res.partner', 'Partner')
    is_student = fields.Boolean('Is Already Student')
    fees_term_id = fields.Many2one('op.fees.terms', 'Fees Term')

    @api.onchange('student_id', 'is_student')
    def onchange_student(self):
        if self.is_student and self.student_id:
            student = self.student_id
            self.title = student.title and student.title.id or False
            self.name = student.name
            self.middle_name = student.middle_name
            self.last_name = student.last_name
            self.birth_date = student.birth_date
            self.gender = student.gender
            self.image = student.image or False
            self.street = student.street or False
            self.street2 = student.street2 or False
            self.phone = student.phone or False
            self.mobile = student.mobile or False
            self.email = student.email or False
            self.zip = student.zip or False
            self.city = student.city or False
            self.country_id = student.country_id and \
                student.country_id.id or False
            self.state_id = student.state_id and \
                student.state_id.id or False
            self.partner_id = student.partner_id and \
                student.partner_id.id or False
        else:
            self.title = ''
            self.name = ''
            self.middle_name = ''
            self.last_name = ''
            self.birth_date = ''
            self.gender = ''
            self.image = False
            self.street = ''
            self.street2 = ''
            self.phone = ''
            self.mobile = ''
            self.zip = ''
            self.city = ''
            self.country_id = False
            self.state_id = False
            self.partner_id = False

    @api.onchange('register_id')
    def onchange_register(self):
        self.course_id = self.register_id.course_id
        self.fees = self.register_id.product_id.lst_price

    @api.onchange('course_id')
    def onchange_course(self):
        self.batch_id = False
        term_id = False
        if self.course_id and self.course_id.fees_term_id:
            term_id = self.course_id.fees_term_id.id
        self.fees_term_id = term_id

    @api.multi
    @api.constrains('register_id', 'application_date')
    def _check_admission_register(self):
        for record in self:
            start_date = fields.Date.from_string(record.register_id.start_date)
            end_date = fields.Date.from_string(record.register_id.end_date)
            application_date = fields.Date.from_string(record.application_date)
            if application_date < start_date or application_date > end_date:
                raise ValidationError(_(
                    "Application Date should be between Start Date & \
                    End Date of Admission Register."))

    @api.multi
    @api.constrains('birth_date')
    def _check_birthdate(self):
        for record in self:
            if record.birth_date > fields.Date.today():
                raise ValidationError(_(
                    "Birth Date can't be greater than current date!"))

    @api.multi
    def submit_form(self):
        self.state = 'submit'

    @api.multi
    def admission_confirm(self):
        self.state = 'admission'

    @api.multi
    def confirm_in_progress(self):
        for record in self:
            if not record.batch_id:
                raise ValidationError(_('Please assign batch.'))
            if not record.partner_id:
                partner_id = self.env['res.partner'].create({
                    'name': record.name
                })
                record.partner_id = partner_id.id
            record.state = 'confirm'

    @api.multi
    def get_student_vals(self):
        for student in self:
            return {
                'title': student.title and student.title.id or False,
                'name': student.name,
                'middle_name': student.middle_name,
                'last_name': student.last_name,
                'birth_date': student.birth_date,
                'gender': student.gender,
                'course_id':
                student.course_id and student.course_id.id or False,
                'batch_id':
                student.batch_id and student.batch_id.id or False,
                'image': student.image or False,
                'street': student.street or False,
                'street2': student.street2 or False,
                'phone': student.phone or False,
                'email': student.email or False,
                'mobile': student.mobile or False,
                'zip': student.zip or False,
                'city': student.city or False,
                'country_id':
                student.country_id and student.country_id.id or False,
                'state_id': student.state_id and student.state_id.id or False,
                'course_detail_ids': [[0, False, {
                    'date': fields.Date.today(),
                    'course_id':
                    student.course_id and student.course_id.id or False,
                    'batch_id':
                    student.batch_id and student.batch_id.id or False,
                }]],
            }

    @api.multi
    def enroll_student(self):
        for record in self:
            total_admission = self.env['op.admission'].search_count(
                [('register_id', '=', record.register_id.id),
                 ('state', '=', 'done')])
            if record.register_id.max_count:
                if not total_admission < record.register_id.max_count:
                    msg = 'Max Admission In Admission Register :- (%s)' % (
                        record.register_id.max_count)
                    raise ValidationError(_(msg))
            if not record.student_id:
                vals = record.get_student_vals()
                vals.update({'partner_id': record.partner_id.id})
                student_id = self.env['op.student'].create(vals).id
            else:
                student_id = record.student_id.id
                record.student_id.write({
                    'course_detail_ids': [[0, False, {
                        'date': fields.Date.today(),
                        'course_id':
                        record.course_id and record.course_id.id or False,
                        'batch_id':
                        record.batch_id and record.batch_id.id or False,
                    }]],
                })
            if record.fees_term_id:
                val = []
                product_id = record.register_id.product_id.id
                for line in record.fees_term_id.line_ids:
                    no_days = line.due_days
                    per_amount = line.value
                    amount = (per_amount * record.fees) / 100
                    date = (
                        datetime.today() + relativedelta(days=no_days)).date()
                    dict_val = {
                        'fees_line_id': line.id,
                        'amount': amount,
                        'date': date,
                        'product_id': product_id,
                        'state': 'draft',
                    }
                    val.append([0, False, dict_val])
                self.env['op.student'].browse(student_id).write({
                    'fees_detail_ids': val
                })
            record.write({
                'nbr': 1,
                'state': 'done',
                'admission_date': fields.Date.today(),
                'student_id': student_id,
            })
            reg_id = self.env['op.subject.registration'].create({
                'student_id': student_id,
                'batch_id': record.batch_id.id,
                'course_id': record.course_id.id,
                'min_unit_load': record.course_id.min_unit_load or 0.0,
                'max_unit_load': record.course_id.max_unit_load or 0.0,
                'state': 'draft',
            })
            reg_id.get_subjects()

    @api.multi
    def confirm_rejected(self):
        self.state = 'reject'

    @api.multi
    def confirm_pending(self):
        self.state = 'pending'

    @api.multi
    def confirm_to_draft(self):
        self.state = 'draft'

    @api.multi
    def confirm_cancel(self):
        self.state = 'cancel'

    @api.multi
    def payment_process(self):
        self.state = 'fees_paid'

    @api.multi
    def open_student(self):
        form_view = self.env.ref('openeducat_core.view_op_student_form')
        tree_view = self.env.ref('openeducat_core.view_op_student_tree')
        value = {
            'domain': str([('id', '=', self.student_id.id)]),
            'view_type': 'form',
            'view_mode': 'tree, form',
            'res_model': 'op.student',
            'view_id': False,
            'views': [(form_view and form_view.id or False, 'form'),
                      (tree_view and tree_view.id or False, 'tree')],
            'type': 'ir.actions.act_window',
            'res_id': self.student_id.id,
            'target': 'current',
            'nodestroy': True
        }
        self.state = 'done'
        return value

    @api.multi
    def create_invoice(self):
        """ Create invoice for fee payment process of student """

        inv_obj = self.env['account.invoice']
        partner_id = self.env['res.partner'].create({'name': self.name})

        account_id = False
        product = self.register_id.product_id
        if product.id:
            account_id = product.property_account_income_id.id
        if not account_id:
            account_id = product.categ_id.property_account_income_categ_id.id
        if not account_id:
            raise UserError(
                _('There is no income account defined for this product: "%s". \
                   You may have to install a chart of account from Accounting \
                   app, settings menu.') % (product.name,))

        if self.fees <= 0.00:
            raise UserError(_('The value of the deposit amount must be \
                             positive.'))
        else:
            amount = self.fees
            name = product.name

        invoice = inv_obj.create({
            'name': self.name,
            'origin': self.application_number,
            'type': 'out_invoice',
            'reference': False,
            'account_id': partner_id.property_account_receivable_id.id,
            'partner_id': partner_id.id,
            'invoice_line_ids': [(0, 0, {
                'name': name,
                'origin': self.application_number,
                'account_id': account_id,
                'price_unit': amount,
                'quantity': 1.0,
                'discount': 0.0,
                'uom_id': self.register_id.product_id.uom_id.id,
                'product_id': product.id,
            })],
        })
        invoice.compute_taxes()

        form_view = self.env.ref('account.invoice_form')
        tree_view = self.env.ref('account.invoice_tree')
        value = {
            'domain': str([('id', '=', invoice.id)]),
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'account.invoice',
            'view_id': False,
            'views': [(form_view and form_view.id or False, 'form'),
                      (tree_view and tree_view.id or False, 'tree')],
            'type': 'ir.actions.act_window',
            'res_id': invoice.id,
            'target': 'current',
            'nodestroy': True
        }
        self.partner_id = partner_id
        self.state = 'payment_process'
        return value
Beispiel #15
0
class MrpBom(models.Model):
    """ Defines bills of material for a product or a product template """
    _name = 'mrp.bom'
    _description = 'Bill of Material'
    _inherit = ['mail.thread']
    _rec_name = 'product_tmpl_id'
    _order = "sequence"
    _check_company_auto = True

    def _get_default_product_uom_id(self):
        return self.env['uom.uom'].search([], limit=1, order='id').id

    code = fields.Char('Reference')
    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "If the active field is set to False, it will allow you to hide the bills of material without removing it."
    )
    type = fields.Selection([('normal', 'Manufacture this product'),
                             ('phantom', 'Kit')],
                            'BoM Type',
                            default='normal',
                            required=True)
    product_tmpl_id = fields.Many2one(
        'product.template',
        'Product',
        check_company=True,
        index=True,
        domain=
        "[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        required=True)
    product_id = fields.Many2one(
        'product.product',
        'Product Variant',
        check_company=True,
        index=True,
        domain=
        "['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu']),  '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        help=
        "If a product variant is defined the BOM is available only for this product."
    )
    bom_line_ids = fields.One2many('mrp.bom.line',
                                   'bom_id',
                                   'BoM Lines',
                                   copy=True)
    byproduct_ids = fields.One2many('mrp.bom.byproduct',
                                    'bom_id',
                                    'By-products',
                                    copy=True)
    product_qty = fields.Float('Quantity',
                               default=1.0,
                               digits='Unit of Measure',
                               required=True)
    product_uom_id = fields.Many2one(
        'uom.uom',
        'Unit of Measure',
        default=_get_default_product_uom_id,
        required=True,
        help=
        "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control",
        domain="[('category_id', '=', product_uom_category_id)]")
    product_uom_category_id = fields.Many2one(
        related='product_tmpl_id.uom_id.category_id')
    sequence = fields.Integer(
        'Sequence',
        help=
        "Gives the sequence order when displaying a list of bills of material."
    )
    operation_ids = fields.One2many('mrp.routing.workcenter',
                                    'bom_id',
                                    'Operations',
                                    copy=True)
    ready_to_produce = fields.Selection(
        [('all_available', ' When all components are available'),
         ('asap', 'When components for 1st operation are available')],
        string='Manufacturing Readiness',
        default='asap',
        help=
        "Defines when a Manufacturing Order is considered as ready to be started",
        required=True)
    picking_type_id = fields.Many2one(
        'stock.picking.type',
        'Operation Type',
        domain=
        "[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]",
        check_company=True,
        help=
        u"When a procurement has a ‘produce’ route with a operation type set, it will try to create "
        "a Manufacturing Order for that product using a BoM of the same operation type. That allows "
        "to define stock rules which trigger different manufacturing orders with different BoMs."
    )
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 index=True,
                                 default=lambda self: self.env.company)
    consumption = fields.Selection(
        [('flexible', 'Allowed'), ('warning', 'Allowed with warning'),
         ('strict', 'Blocked')],
        help=
        "Defines if you can consume more or less components than the quantity defined on the BoM:\n"
        "  * Allowed: allowed for all manufacturing users.\n"
        "  * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n"
        "  * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.",
        default='warning',
        string='Flexible Consumption',
        required=True)

    _sql_constraints = [
        ('qty_positive', 'check (product_qty > 0)',
         'The quantity to produce must be positive!'),
    ]

    @api.onchange('product_id')
    def onchange_product_id(self):
        if self.product_id:
            for line in self.bom_line_ids:
                line.bom_product_template_attribute_value_ids = False

    @api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids')
    def _check_bom_lines(self):
        for bom in self:
            for bom_line in bom.bom_line_ids:
                if bom.product_id:
                    same_product = bom.product_id == bom_line.product_id
                else:
                    same_product = bom.product_tmpl_id == bom_line.product_id.product_tmpl_id
                if same_product:
                    raise ValidationError(
                        _("BoM line product %s should not be the same as BoM product."
                          ) % bom.display_name)
                if bom.product_id and bom_line.bom_product_template_attribute_value_ids:
                    raise ValidationError(
                        _("BoM cannot concern product %s and have a line with attributes (%s) at the same time."
                          ) % (bom.product_id.display_name, ", ".join([
                              ptav.display_name for ptav in
                              bom_line.bom_product_template_attribute_value_ids
                          ])))
                for ptav in bom_line.bom_product_template_attribute_value_ids:
                    if ptav.product_tmpl_id != bom.product_tmpl_id:
                        raise ValidationError(
                            _("The attribute value %(attribute)s set on product %(product)s does not match the BoM product %(bom_product)s.",
                              attribute=ptav.display_name,
                              product=ptav.product_tmpl_id.display_name,
                              bom_product=bom_line.parent_product_tmpl_id.
                              display_name))

    @api.onchange('product_uom_id')
    def onchange_product_uom_id(self):
        res = {}
        if not self.product_uom_id or not self.product_tmpl_id:
            return
        if self.product_uom_id.category_id.id != self.product_tmpl_id.uom_id.category_id.id:
            self.product_uom_id = self.product_tmpl_id.uom_id.id
            res['warning'] = {
                'title':
                _('Warning'),
                'message':
                _('The Product Unit of Measure you chose has a different category than in the product form.'
                  )
            }
        return res

    @api.onchange('product_tmpl_id')
    def onchange_product_tmpl_id(self):
        if self.product_tmpl_id:
            self.product_uom_id = self.product_tmpl_id.uom_id.id
            if self.product_id.product_tmpl_id != self.product_tmpl_id:
                self.product_id = False
            for line in self.bom_line_ids:
                line.bom_product_template_attribute_value_ids = False

    def copy(self, default=None):
        res = super().copy(default)
        for bom_line in res.bom_line_ids:
            if bom_line.operation_id:
                operation = res.operation_ids.filtered(
                    lambda op: op.name == bom_line.operation_id.name and op.
                    workcenter_id == bom_line.operation_id.workcenter_id)
                bom_line.operation_id = operation
        return res

    @api.model
    def name_create(self, name):
        # prevent to use string as product_tmpl_id
        if isinstance(name, str):
            raise UserError(
                _("You cannot create a new Bill of Material from here."))
        return super(MrpBom, self).name_create(name)

    def name_get(self):
        return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code
                                   or '', bom.product_tmpl_id.display_name))
                for bom in self]

    @api.constrains('product_tmpl_id', 'product_id', 'type')
    def check_kit_has_not_orderpoint(self):
        product_ids = [
            pid for bom in self.filtered(lambda bom: bom.type == "phantom")
            for pid in (bom.product_id.ids
                        or bom.product_tmpl_id.product_variant_ids.ids)
        ]
        if self.env['stock.warehouse.orderpoint'].search(
            [('product_id', 'in', product_ids)], count=True):
            raise ValidationError(
                _("You can not create a kit-type bill of materials for products that have at least one reordering rule."
                  ))

    def unlink(self):
        if self.env['mrp.production'].search(
            [('bom_id', 'in', self.ids),
             ('state', 'not in', ['done', 'cancel'])],
                limit=1):
            raise UserError(
                _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.'
                  ))
        return super(MrpBom, self).unlink()

    @api.model
    def _bom_find_domain(self,
                         product_tmpl=None,
                         product=None,
                         picking_type=None,
                         company_id=False,
                         bom_type=False):
        if product:
            if not product_tmpl:
                product_tmpl = product.product_tmpl_id
            domain = [
                '|', ('product_id', '=', product.id), '&',
                ('product_id', '=', False),
                ('product_tmpl_id', '=', product_tmpl.id)
            ]
        elif product_tmpl:
            domain = [('product_tmpl_id', '=', product_tmpl.id)]
        else:
            # neither product nor template, makes no sense to search
            raise UserError(
                _('You should provide either a product or a product template to search a BoM'
                  ))
        if picking_type:
            domain += [
                '|', ('picking_type_id', '=', picking_type.id),
                ('picking_type_id', '=', False)
            ]
        if company_id or self.env.context.get('company_id'):
            domain = domain + [
                '|', ('company_id', '=', False),
                ('company_id', '=', company_id
                 or self.env.context.get('company_id'))
            ]
        if bom_type:
            domain += [('type', '=', bom_type)]
        # order to prioritize bom with product_id over the one without
        return domain

    @api.model
    def _bom_find(self,
                  product_tmpl=None,
                  product=None,
                  picking_type=None,
                  company_id=False,
                  bom_type=False):
        """ Finds BoM for particular product, picking and company """
        if product and product.type == 'service' or product_tmpl and product_tmpl.type == 'service':
            return self.env['mrp.bom']
        domain = self._bom_find_domain(product_tmpl=product_tmpl,
                                       product=product,
                                       picking_type=picking_type,
                                       company_id=company_id,
                                       bom_type=bom_type)
        if domain is False:
            return self.env['mrp.bom']
        return self.search(domain, order='sequence, product_id', limit=1)

    @api.model
    def _get_product2bom(self, products, bom_type=False):
        """Optimized variant of _bom_find to work with recordset"""
        products = products.filtered(lambda product: product.type != 'service')
        if not products:
            return {}
        product_templates = products.mapped('product_tmpl_id')
        domain = [
            '|', ('product_id', 'in', products.ids), '&',
            ('product_id', '=', False),
            ('product_tmpl_id', 'in', product_templates.ids)
        ]
        if self.env.context.get('company_id'):
            domain = domain + [
                '|', ('company_id', '=', False),
                ('company_id', '=', self.env.context.get('company_id'))
            ]
        if bom_type:
            domain += [('type', '=', bom_type)]

        boms = self.search(domain, order='sequence, product_id')
        template2bom = {}
        variant2bom = {}
        for bom in boms:
            # Use "setdefault" to take only first bom if we have few ones for
            # the same product
            if bom.product_id:
                variant2bom.setdefault(bom.product_id, bom)
            else:
                template2bom.setdefault(bom.product_tmpl_id, bom)

        result = {}
        for p in products:
            bom = variant2bom.get(p) or template2bom.get(p.product_tmpl_id)
            if bom:
                result[p] = bom
        return result

    def explode(self, product, quantity, picking_type=False):
        """
            Explodes the BoM and creates two lists with all the information you need: bom_done and line_done
            Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM
            and converted into its UoM
        """
        from collections import defaultdict

        graph = defaultdict(list)
        V = set()

        def check_cycle(v, visited, recStack, graph):
            visited[v] = True
            recStack[v] = True
            for neighbour in graph[v]:
                if visited[neighbour] == False:
                    if check_cycle(neighbour, visited, recStack,
                                   graph) == True:
                        return True
                elif recStack[neighbour] == True:
                    return True
            recStack[v] = False
            return False

        boms_done = [(self, {
            'qty': quantity,
            'product': product,
            'original_qty': quantity,
            'parent_line': False
        })]
        lines_done = []
        V |= set([product.product_tmpl_id.id])

        bom_lines = [(bom_line, product, quantity, False)
                     for bom_line in self.bom_line_ids]
        for bom_line in self.bom_line_ids:
            V |= set([bom_line.product_id.product_tmpl_id.id])
            graph[product.product_tmpl_id.id].append(
                bom_line.product_id.product_tmpl_id.id)
        while bom_lines:
            current_line, current_product, current_qty, parent_line = bom_lines[
                0]
            bom_lines = bom_lines[1:]

            if current_line._skip_bom_line(current_product):
                continue

            line_quantity = current_qty * current_line.product_qty
            bom = self._bom_find(product=current_line.product_id,
                                 picking_type=picking_type
                                 or self.picking_type_id,
                                 company_id=self.company_id.id,
                                 bom_type='phantom')
            if bom:
                converted_line_quantity = current_line.product_uom_id._compute_quantity(
                    line_quantity / bom.product_qty, bom.product_uom_id)
                bom_lines = [(line, current_line.product_id,
                              converted_line_quantity, current_line)
                             for line in bom.bom_line_ids] + bom_lines
                for bom_line in bom.bom_line_ids:
                    graph[current_line.product_id.product_tmpl_id.id].append(
                        bom_line.product_id.product_tmpl_id.id)
                    if bom_line.product_id.product_tmpl_id.id in V and check_cycle(
                            bom_line.product_id.product_tmpl_id.id,
                        {key: False
                         for key in V}, {key: False
                                         for key in V}, graph):
                        raise UserError(
                            _('Recursion error!  A product with a Bill of Material should not have itself in its BoM or child BoMs!'
                              ))
                    V |= set([bom_line.product_id.product_tmpl_id.id])
                boms_done.append((bom, {
                    'qty': converted_line_quantity,
                    'product': current_product,
                    'original_qty': quantity,
                    'parent_line': current_line
                }))
            else:
                # We round up here because the user expects that if he has to consume a little more, the whole UOM unit
                # should be consumed.
                rounding = current_line.product_uom_id.rounding
                line_quantity = float_round(line_quantity,
                                            precision_rounding=rounding,
                                            rounding_method='UP')
                lines_done.append((current_line, {
                    'qty': line_quantity,
                    'product': current_product,
                    'original_qty': quantity,
                    'parent_line': parent_line
                }))

        return boms_done, lines_done

    @api.model
    def get_import_templates(self):
        return [{
            'label': _('Import Template for Bills of Materials'),
            'template': '/mrp/static/xls/mrp_bom.xls'
        }]
Beispiel #16
0
class ReportStockForecat(models.Model):
    _name = 'report.stock.forecast'
    _auto = False

    date = fields.Date(string='Date')
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 readonly=True)
    product_tmpl_id = fields.Many2one('product.template',
                                      string='Product Template',
                                      related='product_id.product_tmpl_id',
                                      readonly=True)
    cumulative_quantity = fields.Float(string='Cumulative Quantity',
                                       readonly=True)
    quantity = fields.Float(readonly=True)

    @api.model_cr
    def init(self):
        tools.drop_view_if_exists(self._cr, 'report_stock_forecast')
        self._cr.execute(
            """CREATE or REPLACE VIEW report_stock_forecast AS (SELECT
        MIN(id) as id,
        product_id as product_id,
        date as date,
        sum(product_qty) AS quantity,
        sum(sum(product_qty)) OVER (PARTITION BY product_id ORDER BY date) AS cumulative_quantity
        FROM
        (SELECT
        MIN(id) as id,
        MAIN.product_id as product_id,
        SUB.date as date,
        CASE WHEN MAIN.date = SUB.date THEN sum(MAIN.product_qty) ELSE 0 END as product_qty
        FROM
        (SELECT
            MIN(sq.id) as id,
            sq.product_id,
            date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) as date,
            SUM(sq.quantity) AS product_qty
            FROM
            stock_quant as sq
            LEFT JOIN
            product_product ON product_product.id = sq.product_id
            LEFT JOIN
            stock_location location_id ON sq.location_id = location_id.id
            WHERE
            location_id.usage = 'internal'
            GROUP BY date, sq.product_id
            UNION ALL
            SELECT
            MIN(-sm.id) as id,
            sm.product_id,
            CASE WHEN sm.date_expected > CURRENT_DATE
            THEN date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD'))
            ELSE date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) END
            AS date,
            SUM(sm.product_qty) AS product_qty
            FROM
               stock_move as sm
            LEFT JOIN
               product_product ON product_product.id = sm.product_id
            LEFT JOIN
            stock_location dest_location ON sm.location_dest_id = dest_location.id
            LEFT JOIN
            stock_location source_location ON sm.location_id = source_location.id
            WHERE
            sm.state IN ('confirmed','assigned','waiting') and
            source_location.usage != 'internal' and dest_location.usage = 'internal'
            GROUP BY sm.date_expected,sm.product_id
            UNION ALL
            SELECT
                MIN(-sm.id) as id,
                sm.product_id,
                CASE WHEN sm.date_expected > CURRENT_DATE
                    THEN date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD'))
                    ELSE date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) END
                AS date,
                SUM(-(sm.product_qty)) AS product_qty
            FROM
               stock_move as sm
            LEFT JOIN
               product_product ON product_product.id = sm.product_id
            LEFT JOIN
               stock_location source_location ON sm.location_id = source_location.id
            LEFT JOIN
               stock_location dest_location ON sm.location_dest_id = dest_location.id
            WHERE
                sm.state IN ('confirmed','assigned','waiting') and
            source_location.usage = 'internal' and dest_location.usage != 'internal'
            GROUP BY sm.date_expected,sm.product_id)
         as MAIN
     LEFT JOIN
     (SELECT DISTINCT date
      FROM
      (
             SELECT date_trunc('week', CURRENT_DATE) AS DATE
             UNION ALL
             SELECT date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD')) AS date
             FROM stock_move sm
             LEFT JOIN
             stock_location source_location ON sm.location_id = source_location.id
             LEFT JOIN
             stock_location dest_location ON sm.location_dest_id = dest_location.id
             WHERE
             sm.state IN ('confirmed','assigned','waiting') and sm.date_expected > CURRENT_DATE and
             ((dest_location.usage = 'internal' AND source_location.usage != 'internal')
              or (source_location.usage = 'internal' AND dest_location.usage != 'internal'))) AS DATE_SEARCH)
             SUB ON (SUB.date IS NOT NULL)
    GROUP BY MAIN.product_id,SUB.date, MAIN.date
    ) AS FINAL
    GROUP BY product_id,date)""")
Beispiel #17
0
class MrpBomLine(models.Model):
    _name = 'mrp.bom.line'
    _order = "sequence, id"
    _rec_name = "product_id"
    _description = 'Bill of Material Line'
    _check_company_auto = True

    def _get_default_product_uom_id(self):
        return self.env['uom.uom'].search([], limit=1, order='id').id

    product_id = fields.Many2one('product.product',
                                 'Component',
                                 required=True,
                                 check_company=True)
    product_tmpl_id = fields.Many2one('product.template',
                                      'Product Template',
                                      related='product_id.product_tmpl_id')
    company_id = fields.Many2one(related='bom_id.company_id',
                                 store=True,
                                 index=True,
                                 readonly=True)
    product_qty = fields.Float('Quantity',
                               default=1.0,
                               digits='Product Unit of Measure',
                               required=True)
    product_uom_id = fields.Many2one(
        'uom.uom',
        'Product Unit of Measure',
        default=_get_default_product_uom_id,
        required=True,
        help=
        "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control",
        domain="[('category_id', '=', product_uom_category_id)]")
    product_uom_category_id = fields.Many2one(
        related='product_id.uom_id.category_id')
    sequence = fields.Integer('Sequence',
                              default=1,
                              help="Gives the sequence order when displaying.")
    bom_id = fields.Many2one('mrp.bom',
                             'Parent BoM',
                             index=True,
                             ondelete='cascade',
                             required=True)
    parent_product_tmpl_id = fields.Many2one('product.template',
                                             'Parent Product Template',
                                             related='bom_id.product_tmpl_id')
    possible_bom_product_template_attribute_value_ids = fields.Many2many(
        'product.template.attribute.value',
        compute='_compute_possible_bom_product_template_attribute_value_ids')
    bom_product_template_attribute_value_ids = fields.Many2many(
        'product.template.attribute.value',
        string="Apply on Variants",
        ondelete='restrict',
        domain=
        "[('id', 'in', possible_bom_product_template_attribute_value_ids)]",
        help="BOM Product Variants needed to apply this line.")
    allowed_operation_ids = fields.Many2many(
        'mrp.routing.workcenter', compute='_compute_allowed_operation_ids')
    operation_id = fields.Many2one(
        'mrp.routing.workcenter',
        'Consumed in Operation',
        check_company=True,
        domain="[('id', 'in', allowed_operation_ids)]",
        help=
        "The operation where the components are consumed, or the finished products created."
    )
    child_bom_id = fields.Many2one('mrp.bom',
                                   'Sub BoM',
                                   compute='_compute_child_bom_id')
    child_line_ids = fields.One2many('mrp.bom.line',
                                     string="BOM lines of the referred bom",
                                     compute='_compute_child_line_ids')
    attachments_count = fields.Integer('Attachments Count',
                                       compute='_compute_attachments_count')

    _sql_constraints = [
        ('bom_qty_zero', 'CHECK (product_qty>=0)',
         'All product quantities must be greater or equal to 0.\n'
         'Lines with 0 quantities can be used as optional lines. \n'
         'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'
         ),
    ]

    @api.depends(
        'parent_product_tmpl_id.attribute_line_ids.value_ids',
        'parent_product_tmpl_id.attribute_line_ids.attribute_id.create_variant',
        'parent_product_tmpl_id.attribute_line_ids.product_template_value_ids.ptav_active',
    )
    def _compute_possible_bom_product_template_attribute_value_ids(self):
        for line in self:
            line.possible_bom_product_template_attribute_value_ids = line.parent_product_tmpl_id.valid_product_template_attribute_line_ids._without_no_variant_attributes(
            ).product_template_value_ids._only_active()

    @api.depends('product_id', 'bom_id')
    def _compute_child_bom_id(self):
        for line in self:
            if not line.product_id:
                line.child_bom_id = False
            else:
                line.child_bom_id = self.env['mrp.bom']._bom_find(
                    product_tmpl=line.product_id.product_tmpl_id,
                    product=line.product_id)

    @api.depends('product_id')
    def _compute_attachments_count(self):
        for line in self:
            nbr_attach = self.env['mrp.document'].search_count([
                '|', '&', ('res_model', '=', 'product.product'),
                ('res_id', '=', line.product_id.id), '&',
                ('res_model', '=', 'product.template'),
                ('res_id', '=', line.product_id.product_tmpl_id.id)
            ])
            line.attachments_count = nbr_attach

    @api.depends('child_bom_id')
    def _compute_child_line_ids(self):
        """ If the BOM line refers to a BOM, return the ids of the child BOM lines """
        for line in self:
            line.child_line_ids = line.child_bom_id.bom_line_ids.ids or False

    @api.depends('bom_id')
    def _compute_allowed_operation_ids(self):
        for bom_line in self:
            if not bom_line.bom_id.operation_ids:
                bom_line.allowed_operation_ids = self.env[
                    'mrp.routing.workcenter']
            else:
                operation_domain = [
                    ('id', 'in', bom_line.bom_id.operation_ids.ids), '|',
                    ('company_id', '=', bom_line.company_id.id),
                    ('company_id', '=', False)
                ]
                bom_line.allowed_operation_ids = self.env[
                    'mrp.routing.workcenter'].search(operation_domain)

    @api.onchange('product_uom_id')
    def onchange_product_uom_id(self):
        res = {}
        if not self.product_uom_id or not self.product_id:
            return res
        if self.product_uom_id.category_id != self.product_id.uom_id.category_id:
            self.product_uom_id = self.product_id.uom_id.id
            res['warning'] = {
                'title':
                _('Warning'),
                'message':
                _('The Product Unit of Measure you chose has a different category than in the product form.'
                  )
            }
        return res

    @api.onchange('product_id')
    def onchange_product_id(self):
        if self.product_id:
            self.product_uom_id = self.product_id.uom_id.id

    @api.model_create_multi
    def create(self, vals_list):
        for values in vals_list:
            if 'product_id' in values and 'product_uom_id' not in values:
                values['product_uom_id'] = self.env['product.product'].browse(
                    values['product_id']).uom_id.id
        return super(MrpBomLine, self).create(vals_list)

    def _skip_bom_line(self, product):
        """ Control if a BoM line should be produced, can be inherited to add
        custom control. It currently checks that all variant values are in the
        product.

        If multiple values are encoded for the same attribute line, only one of
        them has to be found on the variant.
        """
        self.ensure_one()
        if product._name == 'product.template':
            return False
        if self.bom_product_template_attribute_value_ids:
            for ptal, iter_ptav in groupby(
                    self.bom_product_template_attribute_value_ids.sorted(
                        'attribute_line_id'),
                    lambda ptav: ptav.attribute_line_id):
                if not any(ptav in product.product_template_attribute_value_ids
                           for ptav in iter_ptav):
                    return True
        return False

    def action_see_attachments(self):
        domain = [
            '|', '&', ('res_model', '=', 'product.product'),
            ('res_id', '=', self.product_id.id), '&',
            ('res_model', '=', 'product.template'),
            ('res_id', '=', self.product_id.product_tmpl_id.id)
        ]
        attachment_view = self.env.ref('mrp.view_document_file_kanban_mrp')
        return {
            'name':
            _('Attachments'),
            'domain':
            domain,
            'res_model':
            'mrp.document',
            'type':
            'ir.actions.act_window',
            'view_id':
            attachment_view.id,
            'views': [(attachment_view.id, 'kanban'), (False, 'form')],
            'view_mode':
            'kanban,tree,form',
            'help':
            _('''<p class="o_view_nocontent_smiling_face">
                        Upload files to your product
                    </p><p>
                        Use this feature to store any files, like drawings or specifications.
                    </p>'''),
            'limit':
            80,
            'context':
            "{'default_res_model': '%s','default_res_id': %d, 'default_company_id': %s}"
            % ('product.product', self.product_id.id, self.company_id.id)
        }
Beispiel #18
0
class AdjustmentLines(models.Model):
    _name = 'stock.valuation.adjustment.lines'
    _description = 'Valuation Adjustment Lines'

    name = fields.Char('Description', compute='_compute_name', store=True)
    cost_id = fields.Many2one('stock.landed.cost',
                              'Landed Cost',
                              ondelete='cascade',
                              required=True)
    cost_line_id = fields.Many2one('stock.landed.cost.lines',
                                   'Cost Line',
                                   readonly=True)
    move_id = fields.Many2one('stock.move', 'Stock Move', readonly=True)
    product_id = fields.Many2one('product.product', 'Product', required=True)
    quantity = fields.Float('Quantity', default=1.0, digits=0, required=True)
    weight = fields.Float('Weight', default=1.0, digits='Stock Weight')
    volume = fields.Float('Volume', default=1.0, digits='Volume')
    former_cost = fields.Monetary('Original Value')
    additional_landed_cost = fields.Monetary('Additional Landed Cost')
    final_cost = fields.Monetary('New Value',
                                 compute='_compute_final_cost',
                                 store=True)
    currency_id = fields.Many2one('res.currency',
                                  related='cost_id.company_id.currency_id')

    @api.depends('cost_line_id.name', 'product_id.code', 'product_id.name')
    def _compute_name(self):
        for line in self:
            name = '%s - ' % (line.cost_line_id.name
                              if line.cost_line_id else '')
            line.name = name + (line.product_id.code or line.product_id.name
                                or '')

    @api.depends('former_cost', 'additional_landed_cost')
    def _compute_final_cost(self):
        for line in self:
            line.final_cost = line.former_cost + line.additional_landed_cost

    def _create_accounting_entries(self, move, qty_out):
        # TDE CLEANME: product chosen for computation ?
        cost_product = self.cost_line_id.product_id
        if not cost_product:
            return False
        accounts = self.product_id.product_tmpl_id.get_product_accounts()
        debit_account_id = accounts.get(
            'stock_valuation') and accounts['stock_valuation'].id or False
        # If the stock move is dropshipped move we need to get the cost account instead the stock valuation account
        if self.move_id._is_dropshipped():
            debit_account_id = accounts.get(
                'expense') and accounts['expense'].id or False
        already_out_account_id = accounts['stock_output'].id
        credit_account_id = self.cost_line_id.account_id.id or cost_product.categ_id.property_stock_account_input_categ_id.id

        if not credit_account_id:
            raise UserError(
                _('Please configure Stock Expense Account for product: %s.') %
                (cost_product.name))

        return self._create_account_move_line(move, credit_account_id,
                                              debit_account_id, qty_out,
                                              already_out_account_id)

    def _create_account_move_line(self, move, credit_account_id,
                                  debit_account_id, qty_out,
                                  already_out_account_id):
        """
        Generate the account.move.line values to track the landed cost.
        Afterwards, for the goods that are already out of stock, we should create the out moves
        """
        AccountMoveLine = []

        base_line = {
            'name': self.name,
            'product_id': self.product_id.id,
            'quantity': 0,
        }
        debit_line = dict(base_line, account_id=debit_account_id)
        credit_line = dict(base_line, account_id=credit_account_id)
        diff = self.additional_landed_cost
        if diff > 0:
            debit_line['debit'] = diff
            credit_line['credit'] = diff
        else:
            # negative cost, reverse the entry
            debit_line['credit'] = -diff
            credit_line['debit'] = -diff
        AccountMoveLine.append([0, 0, debit_line])
        AccountMoveLine.append([0, 0, credit_line])

        # Create account move lines for quants already out of stock
        if qty_out > 0:
            debit_line = dict(base_line,
                              name=(self.name + ": " + str(qty_out) +
                                    _(' already out')),
                              quantity=0,
                              account_id=already_out_account_id)
            credit_line = dict(base_line,
                               name=(self.name + ": " + str(qty_out) +
                                     _(' already out')),
                               quantity=0,
                               account_id=debit_account_id)
            diff = diff * qty_out / self.quantity
            if diff > 0:
                debit_line['debit'] = diff
                credit_line['credit'] = diff
            else:
                # negative cost, reverse the entry
                debit_line['credit'] = -diff
                credit_line['debit'] = -diff
            AccountMoveLine.append([0, 0, debit_line])
            AccountMoveLine.append([0, 0, credit_line])

            if self.env.company.anglo_saxon_accounting:
                expense_account_id = self.product_id.product_tmpl_id.get_product_accounts(
                )['expense'].id
                debit_line = dict(base_line,
                                  name=(self.name + ": " + str(qty_out) +
                                        _(' already out')),
                                  quantity=0,
                                  account_id=expense_account_id)
                credit_line = dict(base_line,
                                   name=(self.name + ": " + str(qty_out) +
                                         _(' already out')),
                                   quantity=0,
                                   account_id=already_out_account_id)

                if diff > 0:
                    debit_line['debit'] = diff
                    credit_line['credit'] = diff
                else:
                    # negative cost, reverse the entry
                    debit_line['credit'] = -diff
                    credit_line['debit'] = -diff
                AccountMoveLine.append([0, 0, debit_line])
                AccountMoveLine.append([0, 0, credit_line])

        return AccountMoveLine
Beispiel #19
0
class TrialBalanceReportAccount(models.TransientModel):
    _name = 'report_trial_balance_account'
    _inherit = 'account_financial_report_abstract'
    _order = 'sequence, code ASC, name'

    report_id = fields.Many2one(comodel_name='report_trial_balance',
                                ondelete='cascade',
                                index=True)
    hide_line = fields.Boolean(compute='_compute_hide_line')
    # Data fields, used to keep link with real object
    sequence = fields.Integer(index=True, default=1)
    level = fields.Integer(index=True, default=1)

    # Data fields, used to keep link with real object
    account_id = fields.Many2one('account.account', index=True)

    account_group_id = fields.Many2one('account.group', index=True)
    parent_id = fields.Many2one('account.group', index=True)
    child_account_ids = fields.Char(string="Accounts")
    compute_account_ids = fields.Many2many('account.account',
                                           string="Accounts",
                                           store=True)

    # Data fields, used for report display
    code = fields.Char()
    name = fields.Char()

    currency_id = fields.Many2one('res.currency')
    initial_balance = fields.Float(digits=(16, 2))
    initial_balance_foreign_currency = fields.Float(digits=(16, 2))
    debit = fields.Float(digits=(16, 2))
    credit = fields.Float(digits=(16, 2))
    period_balance = fields.Float(digits=(16, 2))
    final_balance = fields.Float(digits=(16, 2))
    final_balance_foreign_currency = fields.Float(digits=(16, 2))

    # Data fields, used to browse report data
    partner_ids = fields.One2many(comodel_name='report_trial_balance_partner',
                                  inverse_name='report_account_id')

    @api.depends(
        'currency_id',
        'report_id',
        'report_id.hide_account_at_0',
        'report_id.limit_hierarchy_level',
        'report_id.show_hierarchy_level',
        'initial_balance',
        'final_balance',
        'debit',
        'credit',
    )
    def _compute_hide_line(self):
        for rec in self:
            report = rec.report_id
            r = (rec.currency_id or report.company_id.currency_id).rounding
            if report.hide_account_at_0 and (
                    float_is_zero(rec.initial_balance, precision_rounding=r)
                    and float_is_zero(rec.final_balance, precision_rounding=r)
                    and float_is_zero(rec.debit, precision_rounding=r)
                    and float_is_zero(rec.credit, precision_rounding=r)):
                rec.hide_line = True
            elif report.limit_hierarchy_level and report.show_hierarchy_level:
                if report.hide_parent_hierarchy_level:
                    distinct_level = rec.level != report.show_hierarchy_level
                    if rec.account_group_id and distinct_level:
                        rec.hide_line = True
                    elif rec.level and distinct_level:
                        rec.hide_line = True
                elif not report.hide_parent_hierarchy_level and \
                        rec.level > report.show_hierarchy_level:
                    rec.hide_line = True
Beispiel #20
0
class Slide(models.Model):
    _name = 'slide.slide'
    _inherit = [
        'mail.thread', 'image.mixin', 'website.seo.metadata',
        'website.published.mixin'
    ]
    _description = 'Slides'
    _mail_post_access = 'read'
    _order_by_strategy = {
        'sequence': 'sequence asc, id asc',
        'most_viewed': 'total_views desc',
        'most_voted': 'likes desc',
        'latest': 'date_published desc',
    }
    _order = 'sequence asc, is_category asc, id asc'

    # description
    name = fields.Char('Title', required=True, translate=True)
    active = fields.Boolean(default=True, tracking=100)
    sequence = fields.Integer('Sequence', default=0)
    user_id = fields.Many2one('res.users',
                              string='Uploaded by',
                              default=lambda self: self.env.uid)
    description = fields.Text('Description', translate=True)
    channel_id = fields.Many2one('slide.channel',
                                 string="Course",
                                 required=True)
    tag_ids = fields.Many2many('slide.tag',
                               'rel_slide_tag',
                               'slide_id',
                               'tag_id',
                               string='Tags')
    is_preview = fields.Boolean(
        'Allow Preview',
        default=False,
        help=
        "The course is accessible by anyone : the users don't need to join the channel to access the content of the course."
    )
    is_new_slide = fields.Boolean('Is New Slide',
                                  compute='_compute_is_new_slide')
    completion_time = fields.Float(
        'Duration',
        digits=(10, 4),
        help="The estimated completion time for this slide")
    # Categories
    is_category = fields.Boolean('Is a category', default=False)
    category_id = fields.Many2one('slide.slide',
                                  string="Section",
                                  compute="_compute_category_id",
                                  store=True)
    slide_ids = fields.One2many('slide.slide', "category_id", string="Slides")
    # subscribers
    partner_ids = fields.Many2many(
        'res.partner',
        'slide_slide_partner',
        'slide_id',
        'partner_id',
        string='Subscribers',
        groups='website_slides.group_website_slides_officer',
        copy=False)
    slide_partner_ids = fields.One2many(
        'slide.slide.partner',
        'slide_id',
        string='Subscribers information',
        groups='website_slides.group_website_slides_officer',
        copy=False)
    user_membership_id = fields.Many2one(
        'slide.slide.partner',
        string="Subscriber information",
        compute='_compute_user_membership_id',
        compute_sudo=False,
        help="Subscriber information for the current logged in user")
    # Quiz related fields
    question_ids = fields.One2many("slide.question",
                                   "slide_id",
                                   string="Questions")
    questions_count = fields.Integer(string="Numbers of Questions",
                                     compute='_compute_questions_count')
    quiz_first_attempt_reward = fields.Integer("Reward: first attempt",
                                               default=10)
    quiz_second_attempt_reward = fields.Integer("Reward: second attempt",
                                                default=7)
    quiz_third_attempt_reward = fields.Integer(
        "Reward: third attempt",
        default=5,
    )
    quiz_fourth_attempt_reward = fields.Integer(
        "Reward: every attempt after the third try", default=2)
    # content
    slide_type = fields.Selection(
        [('infographic', 'Infographic'), ('webpage', 'Web Page'),
         ('presentation', 'Presentation'), ('document', 'Document'),
         ('video', 'Video'), ('quiz', "Quiz")],
        string='Type',
        required=True,
        default='document',
        help=
        "The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document)."
    )
    datas = fields.Binary('Content', attachment=True)
    url = fields.Char('Document URL', help="Youtube or Google Document URL")
    document_id = fields.Char('Document ID',
                              help="Youtube or Google Document ID")
    link_ids = fields.One2many('slide.slide.link',
                               'slide_id',
                               string="External URL for this slide")
    slide_resource_ids = fields.One2many(
        'slide.slide.resource',
        'slide_id',
        string="Additional Resource for this slide")
    slide_resource_downloadable = fields.Boolean(
        'Allow Download',
        default=True,
        help="Allow the user to download the content of the slide.")
    mime_type = fields.Char('Mime-type')
    html_content = fields.Html(
        "HTML Content",
        help="Custom HTML content for slides of type 'Web Page'.",
        translate=True,
        sanitize_form=False)
    # website
    website_id = fields.Many2one(related='channel_id.website_id',
                                 readonly=True)
    date_published = fields.Datetime('Publish Date', readonly=True, tracking=1)
    likes = fields.Integer('Likes',
                           compute='_compute_user_info',
                           store=True,
                           compute_sudo=False)
    dislikes = fields.Integer('Dislikes',
                              compute='_compute_user_info',
                              store=True,
                              compute_sudo=False)
    user_vote = fields.Integer('User vote',
                               compute='_compute_user_info',
                               compute_sudo=False)
    embed_code = fields.Text('Embed Code',
                             readonly=True,
                             compute='_compute_embed_code')
    # views
    embedcount_ids = fields.One2many('slide.embed',
                                     'slide_id',
                                     string="Embed Count")
    slide_views = fields.Integer('# of Website Views',
                                 store=True,
                                 compute="_compute_slide_views")
    public_views = fields.Integer('# of Public Views', copy=False)
    total_views = fields.Integer("Views",
                                 default="0",
                                 compute='_compute_total',
                                 store=True)
    # comments
    comments_count = fields.Integer('Number of comments',
                                    compute="_compute_comments_count")
    # channel
    channel_type = fields.Selection(related="channel_id.channel_type",
                                    string="Channel type")
    channel_allow_comment = fields.Boolean(related="channel_id.allow_comment",
                                           string="Allows comment")
    # Statistics in case the slide is a category
    nbr_presentation = fields.Integer("Number of Presentations",
                                      compute='_compute_slides_statistics',
                                      store=True)
    nbr_document = fields.Integer("Number of Documents",
                                  compute='_compute_slides_statistics',
                                  store=True)
    nbr_video = fields.Integer("Number of Videos",
                               compute='_compute_slides_statistics',
                               store=True)
    nbr_infographic = fields.Integer("Number of Infographics",
                                     compute='_compute_slides_statistics',
                                     store=True)
    nbr_webpage = fields.Integer("Number of Webpages",
                                 compute='_compute_slides_statistics',
                                 store=True)
    nbr_quiz = fields.Integer("Number of Quizs",
                              compute="_compute_slides_statistics",
                              store=True)
    total_slides = fields.Integer(compute='_compute_slides_statistics',
                                  store=True)

    _sql_constraints = [(
        'exclusion_html_content_and_url',
        "CHECK(html_content IS NULL OR url IS NULL)",
        "A slide is either filled with a document url or HTML content. Not both."
    )]

    @api.depends('date_published', 'is_published')
    def _compute_is_new_slide(self):
        for slide in self:
            slide.is_new_slide = slide.date_published > fields.Datetime.now(
            ) - relativedelta(days=7) if slide.is_published else False

    @api.depends('channel_id.slide_ids.is_category',
                 'channel_id.slide_ids.sequence')
    def _compute_category_id(self):
        """ Will take all the slides of the channel for which the index is higher
        than the index of this category and lower than the index of the next category.

        Lists are manually sorted because when adding a new browse record order
        will not be correct as the added slide would actually end up at the
        first place no matter its sequence."""
        self.category_id = False  # initialize whatever the state

        channel_slides = {}
        for slide in self:
            if slide.channel_id.id not in channel_slides:
                channel_slides[
                    slide.channel_id.id] = slide.channel_id.slide_ids

        for cid, slides in channel_slides.items():
            current_category = self.env['slide.slide']
            slide_list = list(slides)
            slide_list.sort(key=lambda s: (s.sequence, not s.is_category))
            for slide in slide_list:
                if slide.is_category:
                    current_category = slide
                elif slide.category_id != current_category:
                    slide.category_id = current_category.id

    @api.depends('question_ids')
    def _compute_questions_count(self):
        for slide in self:
            slide.questions_count = len(slide.question_ids)

    @api.depends('website_message_ids.res_id', 'website_message_ids.model',
                 'website_message_ids.message_type')
    def _compute_comments_count(self):
        for slide in self:
            slide.comments_count = len(slide.website_message_ids)

    @api.depends('slide_views', 'public_views')
    def _compute_total(self):
        for record in self:
            record.total_views = record.slide_views + record.public_views

    @api.depends('slide_partner_ids.vote')
    @api.depends_context('uid')
    def _compute_user_info(self):
        default_stats = {'likes': 0, 'dislikes': 0, 'user_vote': False}

        if not self.ids:
            self.update(default_stats)
            return

        slide_data = dict.fromkeys(self.ids, default_stats)
        slide_partners = self.env['slide.slide.partner'].sudo().search([
            ('slide_id', 'in', self.ids)
        ])
        for slide_partner in slide_partners:
            if slide_partner.vote == 1:
                slide_data[slide_partner.slide_id.id]['likes'] += 1
                if slide_partner.partner_id == self.env.user.partner_id:
                    slide_data[slide_partner.slide_id.id]['user_vote'] = 1
            elif slide_partner.vote == -1:
                slide_data[slide_partner.slide_id.id]['dislikes'] += 1
                if slide_partner.partner_id == self.env.user.partner_id:
                    slide_data[slide_partner.slide_id.id]['user_vote'] = -1
        for slide in self:
            slide.update(slide_data[slide.id])

    @api.depends('slide_partner_ids.slide_id')
    def _compute_slide_views(self):
        # TODO awa: tried compute_sudo, for some reason it doesn't work in here...
        read_group_res = self.env['slide.slide.partner'].sudo().read_group(
            [('slide_id', 'in', self.ids)], ['slide_id'], groupby=['slide_id'])
        mapped_data = dict((res['slide_id'][0], res['slide_id_count'])
                           for res in read_group_res)
        for slide in self:
            slide.slide_views = mapped_data.get(slide.id, 0)

    @api.depends('slide_ids.sequence', 'slide_ids.slide_type',
                 'slide_ids.is_published', 'slide_ids.is_category')
    def _compute_slides_statistics(self):
        # Do not use dict.fromkeys(self.ids, dict()) otherwise it will use the same dictionnary for all keys.
        # Therefore, when updating the dict of one key, it updates the dict of all keys.
        keys = [
            'nbr_%s' % slide_type for slide_type in
            self.env['slide.slide']._fields['slide_type'].get_values(self.env)
        ]
        default_vals = dict((key, 0) for key in keys + ['total_slides'])

        res = self.env['slide.slide'].read_group(
            [('is_published', '=', True), ('category_id', 'in', self.ids),
             ('is_category', '=', False)], ['category_id', 'slide_type'],
            ['category_id', 'slide_type'],
            lazy=False)

        type_stats = self._compute_slides_statistics_type(res)

        for record in self:
            record.update(type_stats.get(record._origin.id, default_vals))

    def _compute_slides_statistics_type(self, read_group_res):
        """ Compute statistics based on all existing slide types """
        slide_types = self.env['slide.slide']._fields['slide_type'].get_values(
            self.env)
        keys = ['nbr_%s' % slide_type for slide_type in slide_types]
        result = dict((cid, dict((key, 0) for key in keys + ['total_slides']))
                      for cid in self.ids)
        for res_group in read_group_res:
            cid = res_group['category_id'][0]
            slide_type = res_group.get('slide_type')
            if slide_type:
                slide_type_count = res_group.get('__count', 0)
                result[cid]['nbr_%s' % slide_type] = slide_type_count
                result[cid]['total_slides'] += slide_type_count
        return result

    @api.depends('slide_partner_ids.partner_id')
    @api.depends('uid')
    def _compute_user_membership_id(self):
        slide_partners = self.env['slide.slide.partner'].sudo().search([
            ('slide_id', 'in', self.ids),
            ('partner_id', '=', self.env.user.partner_id.id),
        ])

        for record in self:
            record.user_membership_id = next(
                (slide_partner for slide_partner in slide_partners
                 if slide_partner.slide_id == record),
                self.env['slide.slide.partner'])

    @api.depends('document_id', 'slide_type', 'mime_type')
    def _compute_embed_code(self):
        base_url = request and request.httprequest.url_root or self.env[
            'ir.config_parameter'].sudo().get_param('web.base.url')
        if base_url[-1] == '/':
            base_url = base_url[:-1]
        for record in self:
            if record.datas and (not record.document_id or record.slide_type
                                 in ['document', 'presentation']):
                slide_url = base_url + url_for(
                    '/slides/embed/%s?page=1' % record.id)
                record.embed_code = '<iframe src="%s" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (
                    slide_url, 315, 420)
            elif record.slide_type == 'video' and record.document_id:
                if not record.mime_type:
                    # embed youtube video
                    query = urls.url_parse(record.url).query
                    query = query + '&theme=light' if query else 'theme=light'
                    record.embed_code = '<iframe src="//www.youtube-nocookie.com/embed/%s?%s" allowFullScreen="true" frameborder="0"></iframe>' % (
                        record.document_id, query)
                else:
                    # embed google doc video
                    record.embed_code = '<iframe src="//drive.google.com/file/d/%s/preview" allowFullScreen="true" frameborder="0"></iframe>' % (
                        record.document_id)
            else:
                record.embed_code = False

    @api.onchange('url')
    def _on_change_url(self):
        self.ensure_one()
        if self.url:
            res = self._parse_document_url(self.url)
            if res.get('error'):
                raise Warning(res.get('error'))
            values = res['values']
            if not values.get('document_id'):
                raise Warning(
                    _('Please enter valid Youtube or Google Doc URL'))
            for key, value in values.items():
                self[key] = value

    @api.onchange('datas')
    def _on_change_datas(self):
        """ For PDFs, we assume that it takes 5 minutes to read a page.
            If the selected file is not a PDF, it is an image (You can
            only upload PDF or Image file) then the slide_type is changed
            into infographic and the uploaded dataS is transfered to the
            image field. (It avoids the infinite loading in PDF viewer)"""
        if self.datas:
            data = base64.b64decode(self.datas)
            if data.startswith(b'%PDF-'):
                pdf = PyPDF2.PdfFileReader(io.BytesIO(data),
                                           overwriteWarnings=False)
                self.completion_time = (5 * len(pdf.pages)) / 60
            else:
                self.slide_type = 'infographic'
                self.image_1920 = self.datas
                self.datas = None

    @api.depends('name', 'channel_id.website_id.domain')
    def _compute_website_url(self):
        # TDE FIXME: clena this link.tracker strange stuff
        super(Slide, self)._compute_website_url()
        for slide in self:
            if slide.id:  # avoid to perform a slug on a not yet saved record in case of an onchange.
                base_url = slide.channel_id.get_base_url()
                # link_tracker is not in dependencies, so use it to shorten url only if installed.
                if self.env.registry.get('link.tracker'):
                    url = self.env['link.tracker'].sudo().create({
                        'url':
                        '%s/slides/slide/%s' % (base_url, slug(slide)),
                        'title':
                        slide.name,
                    }).short_url
                else:
                    url = '%s/slides/slide/%s' % (base_url, slug(slide))
                slide.website_url = url

    @api.depends('channel_id.can_publish')
    def _compute_can_publish(self):
        for record in self:
            record.can_publish = record.channel_id.can_publish

    @api.model
    def _get_can_publish_error_message(self):
        return _(
            "Publishing is restricted to the responsible of training courses or members of the publisher group for documentation courses"
        )

    # ---------------------------------------------------------
    # ORM Overrides
    # ---------------------------------------------------------

    @api.model
    def create(self, values):
        # Do not publish slide if user has not publisher rights
        channel = self.env['slide.channel'].browse(values['channel_id'])
        if not channel.can_publish:
            # 'website_published' is handled by mixin
            values['date_published'] = False

        if values.get('slide_type'
                      ) == 'infographic' and not values.get('image_1920'):
            values['image_1920'] = values['datas']
        if values.get('is_category'):
            values['is_preview'] = True
            values['is_published'] = True
        if values.get('is_published') and not values.get('date_published'):
            values['date_published'] = datetime.datetime.now()
        if values.get('url') and not values.get('document_id'):
            doc_data = self._parse_document_url(values['url']).get(
                'values', dict())
            for key, value in doc_data.items():
                values.setdefault(key, value)

        slide = super(Slide, self).create(values)

        if slide.is_published and not slide.is_category:
            slide._post_publication()
        return slide

    def write(self, values):
        if values.get('url') and values['url'] != self.url:
            doc_data = self._parse_document_url(values['url']).get(
                'values', dict())
            for key, value in doc_data.items():
                values.setdefault(key, value)
        if values.get('is_category'):
            values['is_preview'] = True
            values['is_published'] = True

        res = super(Slide, self).write(values)
        if values.get('is_published'):
            self.date_published = datetime.datetime.now()
            self._post_publication()

        if 'is_published' in values or 'active' in values:
            # if the slide is published/unpublished, recompute the completion for the partners
            self.slide_partner_ids._set_completed_callback()

        return res

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        """Sets the sequence to zero so that it always lands at the beginning
        of the newly selected course as an uncategorized slide"""
        rec = super(Slide, self).copy(default)
        rec.sequence = 0
        return rec

    def unlink(self):
        if self.question_ids and self.channel_id.channel_partner_ids:
            raise UserError(
                _("People already took this quiz. To keep course progression it should not be deleted."
                  ))
        for category in self.filtered(lambda slide: slide.is_category):
            category.channel_id._move_category_slides(category, False)
        super(Slide, self).unlink()

    def toggle_active(self):
        # archiving/unarchiving a channel does it on its slides, too
        to_archive = self.filtered(lambda slide: slide.active)
        res = super(Slide, self).toggle_active()
        if to_archive:
            to_archive.filtered(
                lambda slide: not slide.is_category).is_published = False
        return res

    # ---------------------------------------------------------
    # Mail/Rating
    # ---------------------------------------------------------

    @api.returns('mail.message', lambda value: value.id)
    def message_post(self, *, message_type='notification', **kwargs):
        self.ensure_one()
        if message_type == 'comment' and not self.channel_id.can_comment:  # user comments have a restriction on karma
            raise AccessError(_('Not enough karma to comment'))
        return super(Slide, self).message_post(message_type=message_type,
                                               **kwargs)

    def get_access_action(self, access_uid=None):
        """ Instead of the classic form view, redirect to website if it is published. """
        self.ensure_one()
        if self.website_published:
            return {
                'type': 'ir.actions.act_url',
                'url': '%s' % self.website_url,
                'target': 'self',
                'target_type': 'public',
                'res_id': self.id,
            }
        return super(Slide, self).get_access_action(access_uid)

    def _notify_get_groups(self, msg_vals=None):
        """ Add access button to everyone if the document is active. """
        groups = super(Slide, self)._notify_get_groups(msg_vals=msg_vals)

        if self.website_published:
            for group_name, group_method, group_data in groups:
                group_data['has_button_access'] = True

        return groups

    # ---------------------------------------------------------
    # Business Methods
    # ---------------------------------------------------------

    def _post_publication(self):
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        for slide in self.filtered(lambda slide: slide.website_published and
                                   slide.channel_id.publish_template_id):
            publish_template = slide.channel_id.publish_template_id
            html_body = publish_template.with_context(
                base_url=base_url)._render_field('body_html',
                                                 slide.ids)[slide.id]
            subject = publish_template._render_field('subject',
                                                     slide.ids)[slide.id]
            # We want to use the 'reply_to' of the template if set. However, `mail.message` will check
            # if the key 'reply_to' is in the kwargs before calling _get_reply_to. If the value is
            # falsy, we don't include it in the 'message_post' call.
            kwargs = {}
            reply_to = publish_template._render_field('reply_to',
                                                      slide.ids)[slide.id]
            if reply_to:
                kwargs['reply_to'] = reply_to
            slide.channel_id.with_context(
                mail_create_nosubscribe=True).message_post(
                    subject=subject,
                    body=html_body,
                    subtype_xmlid='website_slides.mt_channel_slide_published',
                    email_layout_xmlid='mail.mail_notification_light',
                    **kwargs,
                )
        return True

    def _generate_signed_token(self, partner_id):
        """ Lazy generate the acces_token and return it signed by the given partner_id
            :rtype tuple (string, int)
            :return (signed_token, partner_id)
        """
        if not self.access_token:
            self.write({'access_token': self._default_access_token()})
        return self._sign_token(partner_id)

    def _send_share_email(self, email, fullscreen):
        # TDE FIXME: template to check
        mail_ids = []
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        for record in self:
            template = record.channel_id.share_template_id.with_context(
                user=self.env.user,
                email=email,
                base_url=base_url,
                fullscreen=fullscreen)
            email_values = {'email_to': email}
            if self.env.user.has_group('base.group_portal'):
                template = template.sudo()
                email_values[
                    'email_from'] = self.env.company.catchall_formatted or self.env.company.email_formatted

            mail_ids.append(
                template.send_mail(record.id,
                                   notif_layout='mail.mail_notification_light',
                                   email_values=email_values))
        return mail_ids

    def action_like(self):
        self.check_access_rights('read')
        self.check_access_rule('read')
        return self._action_vote(upvote=True)

    def action_dislike(self):
        self.check_access_rights('read')
        self.check_access_rule('read')
        return self._action_vote(upvote=False)

    def _action_vote(self, upvote=True):
        """ Private implementation of voting. It does not check for any real access
        rights; public methods should grant access before calling this method.

          :param upvote: if True, is a like; if False, is a dislike
        """
        self_sudo = self.sudo()
        SlidePartnerSudo = self.env['slide.slide.partner'].sudo()
        slide_partners = SlidePartnerSudo.search([
            ('slide_id', 'in', self.ids),
            ('partner_id', '=', self.env.user.partner_id.id)
        ])
        slide_id = slide_partners.mapped('slide_id')
        new_slides = self_sudo - slide_id
        channel = slide_id.channel_id
        karma_to_add = 0

        for slide_partner in slide_partners:
            if upvote:
                new_vote = 0 if slide_partner.vote == -1 else 1
                if slide_partner.vote != 1:
                    karma_to_add += channel.karma_gen_slide_vote
            else:
                new_vote = 0 if slide_partner.vote == 1 else -1
                if slide_partner.vote != -1:
                    karma_to_add -= channel.karma_gen_slide_vote
            slide_partner.vote = new_vote

        for new_slide in new_slides:
            new_vote = 1 if upvote else -1
            new_slide.write({
                'slide_partner_ids': [(0, 0, {
                    'vote':
                    new_vote,
                    'partner_id':
                    self.env.user.partner_id.id
                })]
            })
            karma_to_add += new_slide.channel_id.karma_gen_slide_vote * (
                1 if upvote else -1)

        if karma_to_add:
            self.env.user.add_karma(karma_to_add)

    def action_set_viewed(self, quiz_attempts_inc=False):
        if any(not slide.channel_id.is_member for slide in self):
            raise UserError(
                _('You cannot mark a slide as viewed if you are not among its members.'
                  ))

        return bool(
            self._action_set_viewed(self.env.user.partner_id,
                                    quiz_attempts_inc=quiz_attempts_inc))

    def _action_set_viewed(self, target_partner, quiz_attempts_inc=False):
        self_sudo = self.sudo()
        SlidePartnerSudo = self.env['slide.slide.partner'].sudo()
        existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids),
                                                 ('partner_id', '=',
                                                  target_partner.id)])
        if quiz_attempts_inc and existing_sudo:
            sql.increment_field_skiplock(existing_sudo, 'quiz_attempts_count')
            SlidePartnerSudo.invalidate_cache(fnames=['quiz_attempts_count'],
                                              ids=existing_sudo.ids)

        new_slides = self_sudo - existing_sudo.mapped('slide_id')
        return SlidePartnerSudo.create([{
            'slide_id':
            new_slide.id,
            'channel_id':
            new_slide.channel_id.id,
            'partner_id':
            target_partner.id,
            'quiz_attempts_count':
            1 if quiz_attempts_inc else 0,
            'vote':
            0
        } for new_slide in new_slides])

    def action_set_completed(self):
        if any(not slide.channel_id.is_member for slide in self):
            raise UserError(
                _('You cannot mark a slide as completed if you are not among its members.'
                  ))

        return self._action_set_completed(self.env.user.partner_id)

    def _action_set_completed(self, target_partner):
        self_sudo = self.sudo()
        SlidePartnerSudo = self.env['slide.slide.partner'].sudo()
        existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids),
                                                 ('partner_id', '=',
                                                  target_partner.id)])
        existing_sudo.write({'completed': True})

        new_slides = self_sudo - existing_sudo.mapped('slide_id')
        SlidePartnerSudo.create([{
            'slide_id': new_slide.id,
            'channel_id': new_slide.channel_id.id,
            'partner_id': target_partner.id,
            'vote': 0,
            'completed': True
        } for new_slide in new_slides])

        return True

    def _action_set_quiz_done(self):
        if any(not slide.channel_id.is_member for slide in self):
            raise UserError(
                _('You cannot mark a slide quiz as completed if you are not among its members.'
                  ))

        points = 0
        for slide in self:
            user_membership_sudo = slide.user_membership_id.sudo()
            if not user_membership_sudo or user_membership_sudo.completed or not user_membership_sudo.quiz_attempts_count:
                continue

            gains = [
                slide.quiz_first_attempt_reward,
                slide.quiz_second_attempt_reward,
                slide.quiz_third_attempt_reward,
                slide.quiz_fourth_attempt_reward
            ]
            points += gains[
                user_membership_sudo.quiz_attempts_count -
                1] if user_membership_sudo.quiz_attempts_count <= len(
                    gains) else gains[-1]

        return self.env.user.sudo().add_karma(points)

    def _compute_quiz_info(self, target_partner, quiz_done=False):
        result = dict.fromkeys(self.ids, False)
        slide_partners = self.env['slide.slide.partner'].sudo().search([
            ('slide_id', 'in', self.ids),
            ('partner_id', '=', target_partner.id)
        ])
        slide_partners_map = dict(
            (sp.slide_id.id, sp) for sp in slide_partners)
        for slide in self:
            if not slide.question_ids:
                gains = [0]
            else:
                gains = [
                    slide.quiz_first_attempt_reward,
                    slide.quiz_second_attempt_reward,
                    slide.quiz_third_attempt_reward,
                    slide.quiz_fourth_attempt_reward
                ]
            result[slide.id] = {
                'quiz_karma_max':
                gains[0],  # what could be gained if succeed at first try
                'quiz_karma_gain':
                gains[0],  # what would be gained at next test
                'quiz_karma_won': 0,  # what has been gained
                'quiz_attempts_count': 0,  # number of attempts
            }
            slide_partner = slide_partners_map.get(slide.id)
            if slide.question_ids and slide_partner:
                if slide_partner.quiz_attempts_count:
                    result[slide.id]['quiz_karma_gain'] = gains[
                        slide_partner.
                        quiz_attempts_count] if slide_partner.quiz_attempts_count < len(
                            gains) else gains[-1]
                    result[slide.id][
                        'quiz_attempts_count'] = slide_partner.quiz_attempts_count
                if quiz_done or slide_partner.completed:
                    result[slide.id]['quiz_karma_won'] = gains[
                        slide_partner.quiz_attempts_count -
                        1] if slide_partner.quiz_attempts_count < len(
                            gains) else gains[-1]
        return result

    # --------------------------------------------------
    # Parsing methods
    # --------------------------------------------------

    @api.model
    def _fetch_data(self, base_url, params, content_type=False):
        result = {'values': dict()}
        try:
            response = requests.get(base_url, timeout=3, params=params)
            response.raise_for_status()
            if content_type == 'json':
                result['values'] = response.json()
            elif content_type in ('image', 'pdf'):
                result['values'] = base64.b64encode(response.content)
            else:
                result['values'] = response.content
        except requests.exceptions.HTTPError as e:
            result['error'] = e.response.content
        except requests.exceptions.ConnectionError as e:
            result['error'] = str(e)
        return result

    def _find_document_data_from_url(self, url):
        url_obj = urls.url_parse(url)
        if url_obj.ascii_host == 'youtu.be':
            return ('youtube', url_obj.path[1:] if url_obj.path else False)
        elif url_obj.ascii_host in ('youtube.com', 'www.youtube.com',
                                    'm.youtube.com',
                                    'www.youtube-nocookie.com'):
            v_query_value = url_obj.decode_query().get('v')
            if v_query_value:
                return ('youtube', v_query_value)
            split_path = url_obj.path.split('/')
            if len(split_path) >= 3 and split_path[1] in ('v', 'embed'):
                return ('youtube', split_path[2])

        expr = re.compile(
            r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)'
        )
        arg = expr.match(url)
        document_id = arg and arg.group(2) or False
        if document_id:
            return ('google', document_id)

        return (None, False)

    def _parse_document_url(self, url, only_preview_fields=False):
        document_source, document_id = self._find_document_data_from_url(url)
        if document_source and hasattr(self,
                                       '_parse_%s_document' % document_source):
            return getattr(self, '_parse_%s_document' % document_source)(
                document_id, only_preview_fields)
        return {'error': _('Unknown document')}

    def _parse_youtube_document(self, document_id, only_preview_fields):
        """ If we receive a duration (YT video), we use it to determine the slide duration.
        The received duration is under a special format (e.g: PT1M21S15, meaning 1h 21m 15s). """

        key = self.env['website'].get_current_website(
        ).website_slide_google_app_key
        fetch_res = self._fetch_data(
            'https://www.googleapis.com/youtube/v3/videos', {
                'id': document_id,
                'key': key,
                'part': 'snippet,contentDetails',
                'fields': 'items(id,snippet,contentDetails)'
            }, 'json')
        if fetch_res.get('error'):
            return {
                'error':
                self._extract_google_error_message(fetch_res.get('error'))
            }

        values = {'slide_type': 'video', 'document_id': document_id}
        items = fetch_res['values'].get('items')
        if not items:
            return {'error': _('Please enter valid Youtube or Google Doc URL')}
        youtube_values = items[0]

        youtube_duration = youtube_values.get('contentDetails',
                                              {}).get('duration')
        if youtube_duration:
            parsed_duration = re.search(
                r'^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$', youtube_duration)
            values['completion_time'] = (int(parsed_duration.group(1) or 0)) + \
                                        (int(parsed_duration.group(2) or 0) / 60) + \
                                        (int(parsed_duration.group(3) or 0) / 3600)

        if youtube_values.get('snippet'):
            snippet = youtube_values['snippet']
            if only_preview_fields:
                values.update({
                    'url_src': snippet['thumbnails']['high']['url'],
                    'title': snippet['title'],
                    'description': snippet['description']
                })

                return values

            values.update({
                'name':
                snippet['title'],
                'image_1920':
                self._fetch_data(snippet['thumbnails']['high']['url'], {},
                                 'image')['values'],
                'description':
                snippet['description'],
                'mime_type':
                False,
            })
        return {'values': values}

    def _extract_google_error_message(self, error):
        """
        See here for Google error format
        https://developers.google.com/drive/api/v3/handle-errors
        """
        try:
            error = json.loads(error)
            error = (error.get('error', {}).get('errors', [])
                     or [{}])[0].get('reason')
        except json.decoder.JSONDecodeError:
            error = str(error)

        if error == 'keyInvalid':
            return _(
                'Your Google API key is invalid, please update it in your settings.\nSettings > Website > Features > API Key'
            )

        return _(
            'Could not fetch data from url. Document or access right not available:\n%s',
            error)

    @api.model
    def _parse_google_document(self, document_id, only_preview_fields):
        def get_slide_type(vals):
            # TDE FIXME: WTF ??
            slide_type = 'presentation'
            if vals.get('image_1920'):
                image = Image.open(
                    io.BytesIO(base64.b64decode(vals['image_1920'])))
                width, height = image.size
                if height > width:
                    return 'document'
            return slide_type

        # Google drive doesn't use a simple API key to access the data, but requires an access
        # token. However, this token is generated in module google_drive, which is not in the
        # dependencies of website_slides. We still keep the 'key' parameter just in case, but that
        # is probably useless.
        params = {}
        params['projection'] = 'BASIC'
        if 'google.drive.config' in self.env:
            access_token = self.env['google.drive.config'].get_access_token()
            if access_token:
                params['access_token'] = access_token
        if not params.get('access_token'):
            params['key'] = self.env['website'].get_current_website(
            ).website_slide_google_app_key

        fetch_res = self._fetch_data(
            'https://www.googleapis.com/drive/v2/files/%s' % document_id,
            params, "json")
        if fetch_res.get('error'):
            return {
                'error':
                self._extract_google_error_message(fetch_res.get('error'))
            }

        google_values = fetch_res['values']
        if only_preview_fields:
            return {
                'url_src': google_values['thumbnailLink'],
                'title': google_values['title'],
            }

        values = {
            'name':
            google_values['title'],
            'image_1920':
            self._fetch_data(
                google_values['thumbnailLink'].replace('=s220', ''), {},
                'image')['values'],
            'mime_type':
            google_values['mimeType'],
            'document_id':
            document_id,
        }
        if google_values['mimeType'].startswith('video/'):
            values['slide_type'] = 'video'
        elif google_values['mimeType'].startswith('image/'):
            values['datas'] = values['image_1920']
            values['slide_type'] = 'infographic'
        elif google_values['mimeType'].startswith(
                'application/vnd.google-apps'):
            values['slide_type'] = get_slide_type(values)
            if 'exportLinks' in google_values:
                values['datas'] = self._fetch_data(
                    google_values['exportLinks']['application/pdf'], params,
                    'pdf')['values']
        elif google_values['mimeType'] == 'application/pdf':
            # TODO: Google Drive PDF document doesn't provide plain text transcript
            values['datas'] = self._fetch_data(google_values['webContentLink'],
                                               {}, 'pdf')['values']
            values['slide_type'] = get_slide_type(values)

        return {'values': values}

    def _default_website_meta(self):
        res = super(Slide, self)._default_website_meta()
        res['default_opengraph']['og:title'] = res['default_twitter'][
            'twitter:title'] = self.name
        res['default_opengraph']['og:description'] = res['default_twitter'][
            'twitter:description'] = self.description
        res['default_opengraph']['og:image'] = res['default_twitter'][
            'twitter:image'] = self.env['website'].image_url(
                self, 'image_1024')
        res['default_meta_description'] = self.description
        return res

    # ---------------------------------------------------------
    # Data / Misc
    # ---------------------------------------------------------

    def get_backend_menu_id(self):
        return self.env.ref('website_slides.website_slides_menu_root').id
Beispiel #21
0
class StockMoveLine(models.Model):
    _name = "stock.move.line"
    _description = "Packing Operation"
    _order = "result_package_id desc, id"

    picking_id = fields.Many2one(
        'stock.picking',
        'Stock Picking',
        help='The stock operation where the packing has been made')
    move_id = fields.Many2one('stock.move',
                              'Stock Move',
                              help="Change to a better name")
    product_id = fields.Many2one('product.product',
                                 'Product',
                                 ondelete="cascade")
    product_uom_id = fields.Many2one('product.uom',
                                     'Unit of Measure',
                                     required=True)
    product_qty = fields.Float('Real Reserved Quantity',
                               digits=0,
                               compute='_compute_product_qty',
                               inverse='_set_product_qty',
                               store=True)
    product_uom_qty = fields.Float(
        'Reserved',
        default=0.0,
        digits=dp.get_precision('Product Unit of Measure'),
        required=True)
    ordered_qty = fields.Float(
        'Ordered Quantity', digits=dp.get_precision('Product Unit of Measure'))
    qty_done = fields.Float('Done',
                            default=0.0,
                            digits=dp.get_precision('Product Unit of Measure'),
                            copy=False)
    package_id = fields.Many2one('stock.quant.package',
                                 'Source Package',
                                 ondelete='restrict')
    lot_id = fields.Many2one('stock.production.lot', 'Lot')
    lot_name = fields.Char('Lot/Serial Number')
    result_package_id = fields.Many2one(
        'stock.quant.package',
        'Destination Package',
        ondelete='restrict',
        required=False,
        help="If set, the operations are packed into this package")
    date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
    owner_id = fields.Many2one('res.partner',
                               'Owner',
                               help="Owner of the quants")
    location_id = fields.Many2one('stock.location', 'From', required=True)
    location_dest_id = fields.Many2one('stock.location', 'To', required=True)
    from_loc = fields.Char(compute='_compute_location_description')
    to_loc = fields.Char(compute='_compute_location_description')
    lots_visible = fields.Boolean(compute='_compute_lots_visible')
    state = fields.Selection(related='move_id.state', store=True)
    is_initial_demand_editable = fields.Boolean(
        related='move_id.is_initial_demand_editable')
    is_locked = fields.Boolean(related='move_id.is_locked',
                               default=True,
                               readonly=True)
    consume_line_ids = fields.Many2many(
        'stock.move.line',
        'stock_move_line_consume_rel',
        'consume_line_id',
        'produce_line_id',
        help="Technical link to see who consumed what. ")
    produce_line_ids = fields.Many2many(
        'stock.move.line',
        'stock_move_line_consume_rel',
        'produce_line_id',
        'consume_line_id',
        help="Technical link to see which line was produced with this. ")
    reference = fields.Char(related='move_id.reference', store=True)

    @api.one
    def _compute_location_description(self):
        for operation, operation_sudo in izip(self, self.sudo()):
            operation.from_loc = '%s%s' % (
                operation_sudo.location_id.name,
                operation.product_id and operation_sudo.package_id.name or '')
            operation.to_loc = '%s%s' % (operation_sudo.location_dest_id.name,
                                         operation_sudo.result_package_id.name
                                         or '')

    @api.one
    @api.depends('picking_id.picking_type_id', 'product_id.tracking')
    def _compute_lots_visible(self):
        picking = self.picking_id
        if picking.picking_type_id and self.product_id.tracking != 'none':  # TDE FIXME: not sure correctly migrated
            self.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots
        else:
            self.lots_visible = self.product_id.tracking != 'none'

    @api.one
    @api.depends('product_id', 'product_uom_id', 'product_uom_qty')
    def _compute_product_qty(self):
        self.product_qty = self.product_uom_id._compute_quantity(
            self.product_uom_qty,
            self.product_id.uom_id,
            rounding_method='HALF-UP')

    @api.one
    def _set_product_qty(self):
        """ The meaning of product_qty field changed lately and is now a functional field computing the quantity
        in the default product UoM. This code has been added to raise an error if a write is made given a value
        for `product_qty`, where the same write should set the `product_uom_qty` field instead, in order to
        detect errors. """
        raise UserError(
            _('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.'
              ))

    @api.constrains('product_uom_qty')
    def check_reserved_done_quantity(self):
        for move_line in self:
            if move_line.state == 'done' and not float_is_zero(
                    move_line.product_uom_qty,
                    precision_rounding=self.env['decimal.precision'].
                    precision_get('Product Unit of Measure')):
                raise ValidationError(
                    _('A done move line should never have a reserved quantity.'
                      ))

    @api.onchange('product_id', 'product_uom_id')
    def onchange_product_id(self):
        if self.product_id:
            self.lots_visible = self.product_id.tracking != 'none'
            if not self.product_uom_id or self.product_uom_id.category_id != self.product_id.uom_id.category_id:
                if self.move_id.product_uom:
                    self.product_uom_id = self.move_id.product_uom.id
                else:
                    self.product_uom_id = self.product_id.uom_id.id
            res = {
                'domain': {
                    'product_uom_id':
                    [('category_id', '=', self.product_uom_id.category_id.id)]
                }
            }
        else:
            res = {'domain': {'product_uom_id': []}}
        return res

    @api.onchange('lot_name', 'lot_id')
    def onchange_serial_number(self):
        res = {}
        if self.product_id.tracking == 'serial':
            self.qty_done = 1
            move_lines_to_check = self._get_similar_move_lines() - self
            message = move_lines_to_check._check_for_duplicated_serial_numbers(
            )
            if message:
                res['warning'] = {'title': _('Warning'), 'message': message}
        return res

    @api.constrains('qty_done')
    def _check_positive_qty_done(self):
        if any([ml.qty_done < 0 for ml in self]):
            raise ValidationError(_('You can not enter negative quantities!'))

    @api.constrains('lot_id', 'lot_name', 'qty_done')
    def _check_unique_serial_number(self):
        for ml in self.filtered(lambda ml: ml.move_id.product_id.tracking ==
                                'serial' and (ml.lot_id or ml.lot_name)):
            move_lines_to_check = ml._get_similar_move_lines()
            message = move_lines_to_check._check_for_duplicated_serial_numbers(
            )
            if message:
                raise ValidationError(message)
            if float_compare(ml.qty_done,
                             1.0,
                             precision_rounding=ml.move_id.product_id.uom_id.
                             rounding) == 1:
                raise UserError(
                    _('You can only process 1.0 %s for products with unique serial number.'
                      ) % ml.product_id.uom_id.name)
            if ml.lot_name:
                already_exist = self.env['stock.production.lot'].search([
                    ('name', '=', ml.lot_name),
                    ('product_id', '=', ml.product_id.id)
                ])
                if already_exist:
                    return _(
                        'You have already assigned this serial number to this product. Please correct the serial numbers encoded.'
                    )

    def _get_similar_move_lines(self):
        self.ensure_one()
        lines = self.env['stock.move.line']
        picking_id = self.move_id.picking_id if self.move_id else self.picking_id
        if picking_id:
            lines |= picking_id.move_line_ids.filtered(
                lambda ml: ml.product_id == self.product_id and
                (ml.lot_id or ml.lot_name))
        return lines

    def _check_for_duplicated_serial_numbers(self):
        """ This method is used in _check_unique_serial_number and in onchange_serial_number to check that a same serial number is not used twice amongst the recordset passed.

        :return: an error message directed to the user if needed else False
        """
        if self.mapped('lot_id'):
            lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self]
            recorded_serials_counter = Counter(lots_map)
            for (product_id,
                 lot_id), occurrences in recorded_serials_counter.items():
                if occurrences > 1 and lot_id is not False:
                    return _(
                        'You cannot use the same serial number twice. Please correct the serial numbers encoded.'
                    )
        elif self.mapped('lot_name'):
            lots_map = [(ml.product_id.id, ml.lot_name) for ml in self]
            recorded_serials_counter = Counter(lots_map)
            for (product_id,
                 lot_id), occurrences in recorded_serials_counter.items():
                if occurrences > 1 and lot_id is not False:
                    return _(
                        'You cannot use the same serial number twice. Please correct the serial numbers encoded.'
                    )
        return False

    @api.model
    def create(self, vals):
        vals['ordered_qty'] = vals.get('product_uom_qty')

        # If the move line is directly create on the picking view.
        # If this picking is already done we should generate an
        # associated done move.
        if 'picking_id' in vals and 'move_id' not in vals:
            picking = self.env['stock.picking'].browse(vals['picking_id'])
            if picking.state == 'done':
                product = self.env['product.product'].browse(
                    vals['product_id'])
                new_move = self.env['stock.move'].create({
                    'name':
                    _('New Move:') + product.display_name,
                    'product_id':
                    product.id,
                    'product_uom_qty':
                    'qty_done' in vals and vals['qty_done'] or 0,
                    'product_uom':
                    vals['product_uom_id'],
                    'location_id':
                    'location_id' in vals and vals['location_id']
                    or picking.location_id.id,
                    'location_dest_id':
                    'location_dest_id' in vals and vals['location_dest_id']
                    or picking.location_dest_id.id,
                    'state':
                    'done',
                    'additional':
                    True,
                    'picking_id':
                    picking.id,
                })
                vals['move_id'] = new_move.id

        ml = super(StockMoveLine, self).create(vals)
        if ml.state == 'done':
            if ml.product_id.type == 'product':
                Quant = self.env['stock.quant']
                quantity = ml.product_uom_id._compute_quantity(
                    ml.qty_done,
                    ml.move_id.product_id.uom_id,
                    rounding_method='HALF-UP')
                in_date = None
                available_qty, in_date = Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_id,
                    -quantity,
                    lot_id=ml.lot_id,
                    package_id=ml.package_id,
                    owner_id=ml.owner_id)
                if available_qty < 0 and ml.lot_id:
                    # see if we can compensate the negative quants with some untracked quants
                    untracked_qty = Quant._get_available_quantity(
                        ml.product_id,
                        ml.location_id,
                        lot_id=False,
                        package_id=ml.package_id,
                        owner_id=ml.owner_id,
                        strict=True)
                    if untracked_qty:
                        taken_from_untracked_qty = min(untracked_qty,
                                                       abs(quantity))
                        Quant._update_available_quantity(
                            ml.product_id,
                            ml.location_id,
                            -taken_from_untracked_qty,
                            lot_id=False,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id)
                        Quant._update_available_quantity(
                            ml.product_id,
                            ml.location_id,
                            taken_from_untracked_qty,
                            lot_id=ml.lot_id,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id)
                Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_dest_id,
                    quantity,
                    lot_id=ml.lot_id,
                    package_id=ml.result_package_id,
                    owner_id=ml.owner_id,
                    in_date=in_date)
            next_moves = ml.move_id.move_dest_ids.filtered(
                lambda move: move.state not in ('done', 'cancel'))
            next_moves._do_unreserve()
            next_moves._action_assign()
        return ml

    def write(self, vals):
        """ Through the interface, we allow users to change the charateristics of a move line. If a
        quantity has been reserved for this move line, we impact the reservation directly to free
        the old quants and allocate the new ones.
        """
        if self.env.context.get('bypass_reservation_update'):
            return super(StockMoveLine, self).write(vals)

        Quant = self.env['stock.quant']
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        # We forbid to change the reserved quantity in the interace, but it is needed in the
        # case of stock.move's split.
        # TODO Move me in the update
        if 'product_uom_qty' in vals:
            for ml in self.filtered(lambda m: m.state in
                                    ('partially_available', 'assigned'
                                     ) and m.product_id.type == 'product'):
                if not ml.location_id.should_bypass_reservation():
                    qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity(
                        vals['product_uom_qty'],
                        ml.product_id.uom_id,
                        rounding_method='HALF-UP')
                    try:
                        Quant._update_reserved_quantity(
                            ml.product_id,
                            ml.location_id,
                            -qty_to_decrease,
                            lot_id=ml.lot_id,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id,
                            strict=True)
                    except UserError:
                        if ml.lot_id:
                            Quant._update_reserved_quantity(
                                ml.product_id,
                                ml.location_id,
                                -qty_to_decrease,
                                lot_id=False,
                                package_id=ml.package_id,
                                owner_id=ml.owner_id,
                                strict=True)
                        else:
                            raise

        triggers = [('location_id', 'stock.location'),
                    ('location_dest_id', 'stock.location'),
                    ('lot_id', 'stock.production.lot'),
                    ('package_id', 'stock.quant.package'),
                    ('result_package_id', 'stock.quant.package'),
                    ('owner_id', 'res.partner')]
        updates = {}
        for key, model in triggers:
            if key in vals:
                updates[key] = self.env[model].browse(vals[key])

        if updates:
            for ml in self.filtered(
                    lambda ml: ml.state in ['partially_available', 'assigned']
                    and ml.product_id.type == 'product'):
                if not ml.location_id.should_bypass_reservation():
                    try:
                        Quant._update_reserved_quantity(
                            ml.product_id,
                            ml.location_id,
                            -ml.product_qty,
                            lot_id=ml.lot_id,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id,
                            strict=True)
                    except UserError:
                        if ml.lot_id:
                            Quant._update_reserved_quantity(
                                ml.product_id,
                                ml.location_id,
                                -ml.product_qty,
                                lot_id=False,
                                package_id=ml.package_id,
                                owner_id=ml.owner_id,
                                strict=True)
                        else:
                            raise

                if not updates.get('location_id',
                                   ml.location_id).should_bypass_reservation():
                    new_product_qty = 0
                    try:
                        q = Quant._update_reserved_quantity(
                            ml.product_id,
                            updates.get('location_id', ml.location_id),
                            ml.product_qty,
                            lot_id=updates.get('lot_id', ml.lot_id),
                            package_id=updates.get('package_id',
                                                   ml.package_id),
                            owner_id=updates.get('owner_id', ml.owner_id),
                            strict=True)
                        new_product_qty = sum([x[1] for x in q])
                    except UserError:
                        if updates.get('lot_id'):
                            # If we were not able to reserve on tracked quants, we can use untracked ones.
                            try:
                                q = Quant._update_reserved_quantity(
                                    ml.product_id,
                                    updates.get('location_id', ml.location_id),
                                    ml.product_qty,
                                    lot_id=False,
                                    package_id=updates.get(
                                        'package_id', ml.package_id),
                                    owner_id=updates.get(
                                        'owner_id', ml.owner_id),
                                    strict=True)
                                new_product_qty = sum([x[1] for x in q])
                            except UserError:
                                pass
                    if new_product_qty != ml.product_qty:
                        new_product_uom_qty = self.product_id.uom_id._compute_quantity(
                            new_product_qty,
                            self.product_uom_id,
                            rounding_method='HALF-UP')
                        ml.with_context(bypass_reservation_update=True
                                        ).product_uom_qty = new_product_uom_qty

        # When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
        next_moves = self.env['stock.move']
        if updates or 'qty_done' in vals:
            for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and
                                    ml.product_id.type == 'product'):
                # undo the original move line
                qty_done_orig = ml.move_id.product_uom._compute_quantity(
                    ml.qty_done,
                    ml.move_id.product_id.uom_id,
                    rounding_method='HALF-UP')
                in_date = Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_dest_id,
                    -qty_done_orig,
                    lot_id=ml.lot_id,
                    package_id=ml.result_package_id,
                    owner_id=ml.owner_id)[1]
                Quant._update_available_quantity(ml.product_id,
                                                 ml.location_id,
                                                 qty_done_orig,
                                                 lot_id=ml.lot_id,
                                                 package_id=ml.package_id,
                                                 owner_id=ml.owner_id,
                                                 in_date=in_date)

                # move what's been actually done
                product_id = ml.product_id
                location_id = updates.get('location_id', ml.location_id)
                location_dest_id = updates.get('location_dest_id',
                                               ml.location_dest_id)
                qty_done = vals.get('qty_done', ml.qty_done)
                lot_id = updates.get('lot_id', ml.lot_id)
                package_id = updates.get('package_id', ml.package_id)
                result_package_id = updates.get('result_package_id',
                                                ml.result_package_id)
                owner_id = updates.get('owner_id', ml.owner_id)
                quantity = ml.move_id.product_uom._compute_quantity(
                    qty_done,
                    ml.move_id.product_id.uom_id,
                    rounding_method='HALF-UP')
                if not location_id.should_bypass_reservation():
                    ml._free_reservation(product_id,
                                         location_id,
                                         quantity,
                                         lot_id=lot_id,
                                         package_id=package_id,
                                         owner_id=owner_id)
                if not float_is_zero(quantity, precision_digits=precision):
                    available_qty, in_date = Quant._update_available_quantity(
                        product_id,
                        location_id,
                        -quantity,
                        lot_id=lot_id,
                        package_id=package_id,
                        owner_id=owner_id)
                    if available_qty < 0 and lot_id:
                        # see if we can compensate the negative quants with some untracked quants
                        untracked_qty = Quant._get_available_quantity(
                            product_id,
                            location_id,
                            lot_id=False,
                            package_id=package_id,
                            owner_id=owner_id,
                            strict=True)
                        if untracked_qty:
                            taken_from_untracked_qty = min(
                                untracked_qty, abs(available_qty))
                            Quant._update_available_quantity(
                                product_id,
                                location_id,
                                -taken_from_untracked_qty,
                                lot_id=False,
                                package_id=package_id,
                                owner_id=owner_id)
                            Quant._update_available_quantity(
                                product_id,
                                location_id,
                                taken_from_untracked_qty,
                                lot_id=lot_id,
                                package_id=package_id,
                                owner_id=owner_id)
                            if not location_id.should_bypass_reservation():
                                ml._free_reservation(ml.product_id,
                                                     location_id,
                                                     untracked_qty,
                                                     lot_id=False,
                                                     package_id=package_id,
                                                     owner_id=owner_id)
                    Quant._update_available_quantity(
                        product_id,
                        location_dest_id,
                        quantity,
                        lot_id=lot_id,
                        package_id=result_package_id,
                        owner_id=owner_id,
                        in_date=in_date)
                # Unreserve and reserve following move in order to have the real reserved quantity on move_line.
                next_moves |= ml.move_id.move_dest_ids.filtered(
                    lambda move: move.state not in ('done', 'cancel'))

                # Log a note
                if ml.picking_id:
                    ml._log_message(ml.picking_id, ml,
                                    'stock.track_move_template', vals)

        res = super(StockMoveLine, self).write(vals)

        # Update scrap object linked to move_lines to the new quantity.
        if 'qty_done' in vals:
            for move in self.mapped('move_id'):
                if move.scrapped:
                    move.scrap_ids.write({'scrap_qty': move.quantity_done})

        # As stock_account values according to a move's `product_uom_qty`, we consider that any
        # done stock move should have its `quantity_done` equals to its `product_uom_qty`, and
        # this is what move's `action_done` will do. So, we replicate the behavior here.
        if updates or 'qty_done' in vals:
            moves = self.filtered(
                lambda ml: ml.move_id.state == 'done').mapped('move_id')
            for move in moves:
                move.product_uom_qty = move.quantity_done
        next_moves._do_unreserve()
        next_moves._action_assign()
        return res

    def unlink(self):
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        for ml in self:
            if ml.state in ('done', 'cancel'):
                raise UserError(
                    _('You can not delete product moves if the picking is done. You can only correct the done quantities.'
                      ))
            # Unlinking a move line should unreserve.
            if ml.product_id.type == 'product' and not ml.location_id.should_bypass_reservation(
            ) and not float_is_zero(ml.product_qty,
                                    precision_digits=precision):
                self.env['stock.quant']._update_reserved_quantity(
                    ml.product_id,
                    ml.location_id,
                    -ml.product_qty,
                    lot_id=ml.lot_id,
                    package_id=ml.package_id,
                    owner_id=ml.owner_id,
                    strict=True)
        return super(StockMoveLine, self).unlink()

    def _action_done(self):
        """ This method is called during a move's `action_done`. It'll actually move a quant from
        the source location to the destination location, and unreserve if needed in the source
        location.

        This method is intended to be called on all the move lines of a move. This method is not
        intended to be called when editing a `done` move (that's what the override of `write` here
        is done.
        """

        # First, we loop over all the move lines to do a preliminary check: `qty_done` should not
        # be negative and, according to the presence of a picking type or a linked inventory
        # adjustment, enforce some rules on the `lot_id` field. If `qty_done` is null, we unlink
        # the line. It is mandatory in order to free the reservation and correctly apply
        # `action_done` on the next move lines.
        ml_to_delete = self.env['stock.move.line']
        for ml in self:
            qty_done_float_compared = float_compare(
                ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding)
            if qty_done_float_compared > 0:
                if ml.product_id.tracking != 'none':
                    picking_type_id = ml.move_id.picking_type_id
                    if picking_type_id:
                        if picking_type_id.use_create_lots:
                            # If a picking type is linked, we may have to create a production lot on
                            # the fly before assigning it to the move line if the user checked both
                            # `use_create_lots` and `use_existing_lots`.
                            if ml.lot_name and not ml.lot_id:
                                lot = self.env['stock.production.lot'].create({
                                    'name':
                                    ml.lot_name,
                                    'product_id':
                                    ml.product_id.id
                                })
                                ml.write({'lot_id': lot.id})
                        elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots:
                            # If the user disabled both `use_create_lots` and `use_existing_lots`
                            # checkboxes on the picking type, he's allowed to enter tracked
                            # products without a `lot_id`.
                            continue
                    elif ml.move_id.inventory_id:
                        # If an inventory adjustment is linked, the user is allowed to enter
                        # tracked products without a `lot_id`.
                        continue

                    if not ml.lot_id:
                        raise UserError(
                            _('You need to supply a lot/serial number for %s.')
                            % ml.product_id.name)
            elif qty_done_float_compared < 0:
                raise UserError(_('No negative quantities allowed'))
            else:
                ml_to_delete |= ml
        ml_to_delete.unlink()

        # Now, we can actually move the quant.
        for ml in self - ml_to_delete:
            if ml.product_id.type == 'product':
                Quant = self.env['stock.quant']
                rounding = ml.product_uom_id.rounding

                # if this move line is force assigned, unreserve elsewhere if needed
                if not ml.location_id.should_bypass_reservation(
                ) and float_compare(ml.qty_done,
                                    ml.product_qty,
                                    precision_rounding=rounding) > 0:
                    extra_qty = ml.qty_done - ml.product_qty
                    ml._free_reservation(ml.product_id,
                                         ml.location_id,
                                         extra_qty,
                                         lot_id=ml.lot_id,
                                         package_id=ml.package_id,
                                         owner_id=ml.owner_id)
                # unreserve what's been reserved
                if not ml.location_id.should_bypass_reservation(
                ) and ml.product_id.type == 'product' and ml.product_qty:
                    try:
                        Quant._update_reserved_quantity(
                            ml.product_id,
                            ml.location_id,
                            -ml.product_qty,
                            lot_id=ml.lot_id,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id,
                            strict=True)
                    except UserError:
                        Quant._update_reserved_quantity(
                            ml.product_id,
                            ml.location_id,
                            -ml.product_qty,
                            lot_id=False,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id,
                            strict=True)

                # move what's been actually done
                quantity = ml.product_uom_id._compute_quantity(
                    ml.qty_done,
                    ml.move_id.product_id.uom_id,
                    rounding_method='HALF-UP')
                available_qty, in_date = Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_id,
                    -quantity,
                    lot_id=ml.lot_id,
                    package_id=ml.package_id,
                    owner_id=ml.owner_id)
                if available_qty < 0 and ml.lot_id:
                    # see if we can compensate the negative quants with some untracked quants
                    untracked_qty = Quant._get_available_quantity(
                        ml.product_id,
                        ml.location_id,
                        lot_id=False,
                        package_id=ml.package_id,
                        owner_id=ml.owner_id,
                        strict=True)
                    if untracked_qty:
                        taken_from_untracked_qty = min(untracked_qty,
                                                       abs(quantity))
                        Quant._update_available_quantity(
                            ml.product_id,
                            ml.location_id,
                            -taken_from_untracked_qty,
                            lot_id=False,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id)
                        Quant._update_available_quantity(
                            ml.product_id,
                            ml.location_id,
                            taken_from_untracked_qty,
                            lot_id=ml.lot_id,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id)
                Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_dest_id,
                    quantity,
                    lot_id=ml.lot_id,
                    package_id=ml.result_package_id,
                    owner_id=ml.owner_id,
                    in_date=in_date)
        # Reset the reserved quantity as we just moved it to the destination location.
        (self - ml_to_delete).with_context(
            bypass_reservation_update=True).write({'product_uom_qty': 0.00})

    def _log_message(self, record, move, template, vals):
        data = vals.copy()
        if 'lot_id' in vals and vals['lot_id'] != move.lot_id.id:
            data['lot_name'] = self.env['stock.production.lot'].browse(
                vals.get('lot_id')).name
        if 'location_id' in vals:
            data['location_name'] = self.env['stock.location_id'].browse(
                vals.get('location_id')).name
        if 'location_dest_id' in vals:
            data['location_dest_name'] = self.env['stock.location_id'].browse(
                vals.get('location_dest_id')).name
        if 'package_id' in vals and vals['package_id'] != move.package_id.id:
            data['package_name'] = self.env['stock.quant.package'].browse(
                vals.get('package_id')).name
        if 'package_result_id' in vals and vals[
                'package_result_id'] != move.package_result_id.id:
            data['result_package_name'] = self.env[
                'stock.quant.package'].browse(
                    vals.get('result_package_id')).name
        if 'owner_id' in vals and vals['owner_id'] != move.owner_id.id:
            data['owner_name'] = self.env['res.partner'].browse(
                vals.get('owner_id')).name
        record.message_post_with_view(
            template,
            values={
                'move': move,
                'vals': dict(vals, **data)
            },
            subtype_id=self.env.ref('mail.mt_note').id)

    def _free_reservation(self,
                          product_id,
                          location_id,
                          quantity,
                          lot_id=None,
                          package_id=None,
                          owner_id=None):
        """ When editing a done move line or validating one with some forced quantities, it is
        possible to impact quants that were not reserved. It is therefore necessary to edit or
        unlink the move lines that reserved a quantity now unavailable.
        """
        self.ensure_one()

        # Check the available quantity, with the `strict` kw set to `True`. If the available
        # quantity is greather than the quantity now unavailable, there is nothing to do.
        available_quantity = self.env['stock.quant']._get_available_quantity(
            product_id,
            location_id,
            lot_id=lot_id,
            package_id=package_id,
            owner_id=owner_id,
            strict=True)
        if quantity > available_quantity:
            # We now have to find the move lines that reserved our now unavailable quantity. We
            # take care to exclude ourselves and the move lines were work had already been done.
            oudated_move_lines_domain = [
                ('move_id.state', 'not in', ['done', 'cancel']),
                ('product_id', '=', product_id.id),
                ('lot_id', '=', lot_id.id if lot_id else False),
                ('location_id', '=', location_id.id),
                ('owner_id', '=', owner_id.id if owner_id else False),
                ('package_id', '=', package_id.id if package_id else False),
                ('product_qty', '>', 0.0),
                ('qty_done', '=', 0.0),
                ('id', '!=', self.id),
            ]
            oudated_candidates = self.env['stock.move.line'].search(
                oudated_move_lines_domain)

            # As the move's state is not computed over the move lines, we'll have to manually
            # recompute the moves which we adapted their lines.
            move_to_recompute_state = self.env['stock.move']

            rounding = self.product_uom_id.rounding
            for candidate in oudated_candidates:
                if float_compare(candidate.product_qty,
                                 quantity,
                                 precision_rounding=rounding) <= 0:
                    quantity -= candidate.product_qty
                    move_to_recompute_state |= candidate.move_id
                    candidate.unlink()
                else:
                    # split this move line and assign the new part to our extra move
                    quantity_split = float_round(
                        candidate.product_qty - quantity,
                        precision_rounding=self.product_uom_id.rounding,
                        rounding_method='UP')
                    candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(
                        quantity_split,
                        self.product_uom_id,
                        rounding_method='HALF-UP')
                    quantity -= quantity_split
                    move_to_recompute_state |= candidate.move_id
                if quantity == 0.0:
                    break
            move_to_recompute_state._recompute_state()
Beispiel #22
0
class ProductAttributevalue(models.Model):
    _name = "product.attribute.value"
    _order = 'sequence, attribute_id, id'

    name = fields.Char('Value', required=True, translate=True)
    sequence = fields.Integer('Sequence', help="Determine the display order")
    attribute_id = fields.Many2one('product.attribute',
                                   'Attribute',
                                   ondelete='cascade',
                                   required=True)
    product_ids = fields.Many2many('product.product',
                                   string='Variants',
                                   readonly=True)
    price_extra = fields.Float(
        'Attribute Price Extra',
        compute='_compute_price_extra',
        inverse='_set_price_extra',
        default=0.0,
        digits=dp.get_precision('Product Price'),
        help=
        "Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200."
    )
    price_ids = fields.One2many('product.attribute.price',
                                'value_id',
                                'Attribute Prices',
                                readonly=True)

    _sql_constraints = [('value_company_uniq', 'unique (name,attribute_id)',
                         'This attribute value already exists !')]

    @api.one
    def _compute_price_extra(self):
        if self._context.get('active_id'):
            price = self.price_ids.filtered(lambda price: price.product_tmpl_id
                                            .id == self._context['active_id'])
            self.price_extra = price.price_extra
        else:
            self.price_extra = 0.0

    def _set_price_extra(self):
        if not self._context.get('active_id'):
            return

        AttributePrice = self.env['product.attribute.price']
        prices = AttributePrice.search([('value_id', 'in', self.ids),
                                        ('product_tmpl_id', '=',
                                         self._context['active_id'])])
        updated = prices.mapped('value_id')
        if prices:
            prices.write({'price_extra': self.price_extra})
        else:
            for value in self - updated:
                AttributePrice.create({
                    'product_tmpl_id':
                    self._context['active_id'],
                    'value_id':
                    value.id,
                    'price_extra':
                    self.price_extra,
                })

    @api.multi
    def name_get(self):
        if not self._context.get('show_attribute',
                                 True):  # TDE FIXME: not used
            return super(ProductAttributevalue, self).name_get()
        return [(value.id, "%s: %s" % (value.attribute_id.name, value.name))
                for value in self]

    @api.multi
    def unlink(self):
        linked_products = self.env['product.product'].with_context(
            active_test=False).search([('attribute_value_ids', 'in', self.ids)
                                       ])
        if linked_products:
            raise UserError(
                _('The operation cannot be completed:\nYou are trying to delete an attribute value with a reference on a product variant.'
                  ))
        return super(ProductAttributevalue, self).unlink()

    @api.multi
    def _variant_name(self, variable_attributes):
        return ", ".join(
            [v.name for v in self if v.attribute_id in variable_attributes])
Beispiel #23
0
class HolidaysAllocation(models.Model):
    """ Allocation Requests Access specifications: similar to leave requests """
    _name = "hr.leave.allocation"
    _description = "Time Off Allocation"
    _order = "create_date desc"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _mail_post_access = 'read'

    def _default_holiday_status_id(self):
        if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
            domain = [('valid', '=', True)]
        else:
            domain = [('valid', '=', True),
                      ('allocation_type', '=', 'fixed_allocation')]
        return self.env['hr.leave.type'].search(domain, limit=1)

    def _holiday_status_id_domain(self):
        if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
            return [('valid', '=', True), ('allocation_type', '!=', 'no')]
        return [('valid', '=', True),
                ('allocation_type', '=', 'fixed_allocation')]

    name = fields.Char('Description',
                       compute='_compute_description',
                       inverse='_inverse_description',
                       search='_search_description',
                       compute_sudo=False)
    private_name = fields.Char('Allocation Description',
                               groups='hr_holidays.group_hr_holidays_user')
    state = fields.Selection(
        [('draft', 'To Submit'), ('cancel', 'Cancelled'),
         ('confirm', 'To Approve'), ('refuse', 'Refused'),
         ('validate1', 'Second Approval'), ('validate', 'Approved')],
        string='Status',
        readonly=True,
        tracking=True,
        copy=False,
        default='confirm',
        help=
        "The status is set to 'To Submit', when an allocation request is created."
        +
        "\nThe status is 'To Approve', when an allocation request is confirmed by user."
        +
        "\nThe status is 'Refused', when an allocation request is refused by manager."
        +
        "\nThe status is 'Approved', when an allocation request is approved by manager."
    )
    date_from = fields.Datetime('Start Date',
                                readonly=True,
                                index=True,
                                copy=False,
                                default=fields.Date.context_today,
                                states={
                                    'draft': [('readonly', False)],
                                    'confirm': [('readonly', False)]
                                },
                                tracking=True)
    date_to = fields.Datetime('End Date',
                              compute='_compute_from_holiday_status_id',
                              store=True,
                              readonly=False,
                              copy=False,
                              tracking=True,
                              states={
                                  'cancel': [('readonly', True)],
                                  'refuse': [('readonly', True)],
                                  'validate1': [('readonly', True)],
                                  'validate': [('readonly', True)]
                              })
    holiday_status_id = fields.Many2one("hr.leave.type",
                                        compute='_compute_from_employee_id',
                                        store=True,
                                        string="Time Off Type",
                                        required=True,
                                        readonly=False,
                                        states={
                                            'cancel': [('readonly', True)],
                                            'refuse': [('readonly', True)],
                                            'validate1': [('readonly', True)],
                                            'validate': [('readonly', True)]
                                        },
                                        domain=_holiday_status_id_domain)
    employee_id = fields.Many2one('hr.employee',
                                  compute='_compute_from_holiday_type',
                                  store=True,
                                  string='Employee',
                                  index=True,
                                  readonly=False,
                                  ondelete="restrict",
                                  tracking=True,
                                  states={
                                      'cancel': [('readonly', True)],
                                      'refuse': [('readonly', True)],
                                      'validate1': [('readonly', True)],
                                      'validate': [('readonly', True)]
                                  })
    manager_id = fields.Many2one('hr.employee',
                                 compute='_compute_from_employee_id',
                                 store=True,
                                 string='Manager')
    notes = fields.Text('Reasons',
                        readonly=True,
                        states={
                            'draft': [('readonly', False)],
                            'confirm': [('readonly', False)]
                        })
    # duration
    number_of_days = fields.Float(
        'Number of Days',
        compute='_compute_from_holiday_status_id',
        store=True,
        readonly=False,
        tracking=True,
        default=1,
        help='Duration in days. Reference field to use when necessary.')
    number_of_days_display = fields.Float(
        'Duration (days)',
        compute='_compute_number_of_days_display',
        states={
            'draft': [('readonly', False)],
            'confirm': [('readonly', False)]
        },
        help=
        "If Accrual Allocation: Number of days allocated in addition to the ones you will get via the accrual' system."
    )
    number_of_hours_display = fields.Float(
        'Duration (hours)',
        compute='_compute_number_of_hours_display',
        help=
        "If Accrual Allocation: Number of hours allocated in addition to the ones you will get via the accrual' system."
    )
    duration_display = fields.Char(
        'Allocated (Days/Hours)',
        compute='_compute_duration_display',
        help=
        "Field allowing to see the allocation duration in days or hours depending on the type_request_unit"
    )
    # details
    parent_id = fields.Many2one('hr.leave.allocation', string='Parent')
    linked_request_ids = fields.One2many('hr.leave.allocation',
                                         'parent_id',
                                         string='Linked Requests')
    first_approver_id = fields.Many2one(
        'hr.employee',
        string='First Approval',
        readonly=True,
        copy=False,
        help=
        'This area is automatically filled by the user who validates the allocation'
    )
    second_approver_id = fields.Many2one(
        'hr.employee',
        string='Second Approval',
        readonly=True,
        copy=False,
        help=
        'This area is automatically filled by the user who validates the allocation with second level (If allocation type need second validation)'
    )
    validation_type = fields.Selection(
        string='Validation Type',
        related='holiday_status_id.allocation_validation_type',
        readonly=True)
    can_reset = fields.Boolean('Can reset', compute='_compute_can_reset')
    can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
    type_request_unit = fields.Selection(
        related='holiday_status_id.request_unit', readonly=True)
    # mode
    holiday_type = fields.Selection(
        [('employee', 'By Employee'), ('company', 'By Company'),
         ('department', 'By Department'), ('category', 'By Employee Tag')],
        string='Allocation Mode',
        readonly=True,
        required=True,
        default='employee',
        states={
            'draft': [('readonly', False)],
            'confirm': [('readonly', False)]
        },
        help=
        "Allow to create requests in batchs:\n- By Employee: for a specific employee"
        "\n- By Company: all employees of the specified company"
        "\n- By Department: all employees of the specified department"
        "\n- By Employee Tag: all employees of the specific employee group category"
    )
    mode_company_id = fields.Many2one('res.company',
                                      compute='_compute_from_holiday_type',
                                      store=True,
                                      string='Company Mode',
                                      readonly=False,
                                      states={
                                          'cancel': [('readonly', True)],
                                          'refuse': [('readonly', True)],
                                          'validate1': [('readonly', True)],
                                          'validate': [('readonly', True)]
                                      })
    department_id = fields.Many2one('hr.department',
                                    compute='_compute_department_id',
                                    store=True,
                                    string='Department',
                                    states={
                                        'draft': [('readonly', False)],
                                        'confirm': [('readonly', False)]
                                    })
    category_id = fields.Many2one('hr.employee.category',
                                  compute='_compute_from_holiday_type',
                                  store=True,
                                  string='Employee Tag',
                                  readonly=False,
                                  states={
                                      'cancel': [('readonly', True)],
                                      'refuse': [('readonly', True)],
                                      'validate1': [('readonly', True)],
                                      'validate': [('readonly', True)]
                                  })
    # accrual configuration
    allocation_type = fields.Selection([('regular', 'Regular Allocation'),
                                        ('accrual', 'Accrual Allocation')],
                                       string="Allocation Type",
                                       default="regular",
                                       required=True,
                                       readonly=True,
                                       states={
                                           'draft': [('readonly', False)],
                                           'confirm': [('readonly', False)]
                                       })
    accrual_limit = fields.Integer(
        'Balance limit',
        default=0,
        help="Maximum of allocation for accrual; 0 means no maximum.")
    number_per_interval = fields.Float(
        "Number of unit per interval",
        compute='_compute_from_holiday_status_id',
        store=True,
        readonly=False,
        states={
            'cancel': [('readonly', True)],
            'refuse': [('readonly', True)],
            'validate1': [('readonly', True)],
            'validate': [('readonly', True)]
        })
    interval_number = fields.Integer("Number of unit between two intervals",
                                     compute='_compute_from_holiday_status_id',
                                     store=True,
                                     readonly=False,
                                     states={
                                         'cancel': [('readonly', True)],
                                         'refuse': [('readonly', True)],
                                         'validate1': [('readonly', True)],
                                         'validate': [('readonly', True)]
                                     })
    unit_per_interval = fields.Selection(
        [('hours', 'Hours'), ('days', 'Days')],
        compute='_compute_from_holiday_status_id',
        store=True,
        string="Unit of time added at each interval",
        readonly=False,
        states={
            'cancel': [('readonly', True)],
            'refuse': [('readonly', True)],
            'validate1': [('readonly', True)],
            'validate': [('readonly', True)]
        })
    interval_unit = fields.Selection(
        [('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months'),
         ('years', 'Years')],
        compute='_compute_from_holiday_status_id',
        store=True,
        string="Unit of time between two intervals",
        readonly=False,
        states={
            'cancel': [('readonly', True)],
            'refuse': [('readonly', True)],
            'validate1': [('readonly', True)],
            'validate': [('readonly', True)]
        })
    nextcall = fields.Date("Date of the next accrual allocation",
                           default=False,
                           readonly=True)
    max_leaves = fields.Float(compute='_compute_leaves')
    leaves_taken = fields.Float(compute='_compute_leaves')

    _sql_constraints = [
        ('type_value',
         "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or "
         "(holiday_type='category' AND category_id IS NOT NULL) or "
         "(holiday_type='department' AND department_id IS NOT NULL) or "
         "(holiday_type='company' AND mode_company_id IS NOT NULL))",
         "The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee."
         ),
        ('duration_check', "CHECK ( number_of_days >= 0 )",
         "The number of days must be greater than 0."),
        ('number_per_interval_check', "CHECK(number_per_interval > 0)",
         "The number per interval should be greater than 0"),
        ('interval_number_check', "CHECK(interval_number > 0)",
         "The interval number should be greater than 0"),
    ]

    @api.model
    def _update_accrual(self):
        """
            Method called by the cron task in order to increment the number_of_days when
            necessary.
        """
        today = fields.Date.from_string(fields.Date.today())

        holidays = self.search([('allocation_type', '=', 'accrual'),
                                ('employee_id.active', '=', True),
                                ('state', '=', 'validate'),
                                ('holiday_type', '=', 'employee'), '|',
                                ('date_to', '=', False),
                                ('date_to', '>', fields.Datetime.now()), '|',
                                ('nextcall', '=', False),
                                ('nextcall', '<=', today)])

        for holiday in holidays:
            values = {}

            delta = relativedelta(days=0)

            if holiday.interval_unit == 'days':
                delta = relativedelta(days=holiday.interval_number)
            if holiday.interval_unit == 'weeks':
                delta = relativedelta(weeks=holiday.interval_number)
            if holiday.interval_unit == 'months':
                delta = relativedelta(months=holiday.interval_number)
            if holiday.interval_unit == 'years':
                delta = relativedelta(years=holiday.interval_number)

            values['nextcall'] = (holiday.nextcall
                                  if holiday.nextcall else today) + delta

            period_start = datetime.combine(today, time(0, 0, 0)) - delta
            period_end = datetime.combine(today, time(0, 0, 0))

            # We have to check when the employee has been created
            # in order to not allocate him/her too much leaves
            start_date = holiday.employee_id._get_date_start_work()
            # If employee is created after the period, we cancel the computation
            if period_end <= start_date or period_end < holiday.date_from:
                holiday.write(values)
                continue

            # If employee created during the period, taking the date at which he has been created
            if period_start <= start_date:
                period_start = start_date

            employee = holiday.employee_id
            worked = employee._get_work_days_data_batch(
                period_start,
                period_end,
                domain=[('holiday_id.holiday_status_id.unpaid', '=', True),
                        ('time_type', '=', 'leave')])[employee.id]['days']
            left = employee._get_leave_days_data_batch(
                period_start,
                period_end,
                domain=[('holiday_id.holiday_status_id.unpaid', '=', True),
                        ('time_type', '=', 'leave')])[employee.id]['days']
            prorata = worked / (left + worked) if worked else 0

            days_to_give = holiday.number_per_interval
            if holiday.unit_per_interval == 'hours':
                # As we encode everything in days in the database we need to convert
                # the number of hours into days for this we use the
                # mean number of hours set on the employee's calendar
                days_to_give = days_to_give / (
                    employee.resource_calendar_id.hours_per_day
                    or HOURS_PER_DAY)

            values[
                'number_of_days'] = holiday.number_of_days + days_to_give * prorata
            if holiday.accrual_limit > 0:
                values['number_of_days'] = min(values['number_of_days'],
                                               holiday.accrual_limit)

            holiday.write(values)

    @api.depends_context('uid')
    def _compute_description(self):
        self.check_access_rights('read')
        self.check_access_rule('read')

        is_officer = self.env.user.has_group(
            'hr_holidays.group_hr_holidays_user')

        for allocation in self:
            if is_officer or allocation.employee_id.user_id == self.env.user or allocation.manager_id == self.env.user:
                allocation.name = allocation.sudo().private_name
            else:
                allocation.name = '*****'

    def _inverse_description(self):
        is_officer = self.env.user.has_group(
            'hr_holidays.group_hr_holidays_user')
        for allocation in self:
            if is_officer or allocation.employee_id.user_id == self.env.user or allocation.manager_id == self.env.user:
                allocation.sudo().private_name = allocation.name

    def _search_description(self, operator, value):
        is_officer = self.env.user.has_group(
            'hr_holidays.group_hr_holidays_user')
        domain = [('private_name', operator, value)]

        if not is_officer:
            domain = expression.AND(
                [domain, [('employee_id.user_id', '=', self.env.user.id)]])

        allocations = self.sudo().search(domain)
        return [('id', 'in', allocations.ids)]

    @api.depends('employee_id', 'holiday_status_id')
    def _compute_leaves(self):
        for allocation in self:
            leave_type = allocation.holiday_status_id.with_context(
                employee_id=allocation.employee_id.id)
            allocation.max_leaves = leave_type.max_leaves
            allocation.leaves_taken = leave_type.leaves_taken

    @api.depends('number_of_days')
    def _compute_number_of_days_display(self):
        for allocation in self:
            allocation.number_of_days_display = allocation.number_of_days

    @api.depends('number_of_days', 'employee_id')
    def _compute_number_of_hours_display(self):
        for allocation in self:
            if allocation.parent_id and allocation.parent_id.type_request_unit == "hour":
                allocation.number_of_hours_display = allocation.number_of_days * HOURS_PER_DAY
            elif allocation.number_of_days:
                allocation.number_of_hours_display = allocation.number_of_days * (
                    allocation.employee_id.sudo(
                    ).resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
            else:
                allocation.number_of_hours_display = 0.0

    @api.depends('number_of_hours_display', 'number_of_days_display')
    def _compute_duration_display(self):
        for allocation in self:
            allocation.duration_display = '%g %s' % (
                (float_round(allocation.number_of_hours_display,
                             precision_digits=2)
                 if allocation.type_request_unit == 'hour' else float_round(
                     allocation.number_of_days_display, precision_digits=2)),
                _('hours')
                if allocation.type_request_unit == 'hour' else _('days'))

    @api.depends('state', 'employee_id', 'department_id')
    def _compute_can_reset(self):
        for allocation in self:
            try:
                allocation._check_approval_update('draft')
            except (AccessError, UserError):
                allocation.can_reset = False
            else:
                allocation.can_reset = True

    @api.depends('state', 'employee_id', 'department_id')
    def _compute_can_approve(self):
        for allocation in self:
            try:
                if allocation.state == 'confirm' and allocation.validation_type == 'both':
                    allocation._check_approval_update('validate1')
                else:
                    allocation._check_approval_update('validate')
            except (AccessError, UserError):
                allocation.can_approve = False
            else:
                allocation.can_approve = True

    @api.depends('holiday_type')
    def _compute_from_holiday_type(self):
        for allocation in self:
            if allocation.holiday_type == 'employee':
                if not allocation.employee_id:
                    allocation.employee_id = self.env.user.employee_id
                allocation.mode_company_id = False
                allocation.category_id = False
            if allocation.holiday_type == 'company':
                allocation.employee_id = False
                if not allocation.mode_company_id:
                    allocation.mode_company_id = self.env.company
                allocation.category_id = False
            elif allocation.holiday_type == 'department':
                allocation.employee_id = False
                allocation.mode_company_id = False
                allocation.category_id = False
            elif allocation.holiday_type == 'category':
                allocation.employee_id = False
                allocation.mode_company_id = False
            elif not allocation.employee_id and not allocation._origin.employee_id:
                allocation.employee_id = self.env.context.get(
                    'default_employee_id') or self.env.user.employee_id

    @api.depends('holiday_type', 'employee_id')
    def _compute_department_id(self):
        for allocation in self:
            if allocation.holiday_type == 'employee':
                allocation.department_id = allocation.employee_id.department_id
            elif allocation.holiday_type == 'department':
                if not allocation.department_id:
                    allocation.department_id = self.env.user.employee_id.department_id
            elif allocation.holiday_type == 'category':
                allocation.department_id = False

    @api.depends('employee_id')
    def _compute_from_employee_id(self):
        default_holiday_status_id = self._default_holiday_status_id()
        for holiday in self:
            holiday.manager_id = holiday.employee_id and holiday.employee_id.parent_id
            if holiday.employee_id.user_id != self.env.user and holiday._origin.employee_id != holiday.employee_id:
                holiday.holiday_status_id = False
            elif not holiday.holiday_status_id and not holiday._origin.holiday_status_id:
                holiday.holiday_status_id = default_holiday_status_id

    @api.depends('holiday_status_id', 'allocation_type',
                 'number_of_hours_display', 'number_of_days_display')
    def _compute_from_holiday_status_id(self):
        for allocation in self:
            allocation.number_of_days = allocation.number_of_days_display
            if allocation.type_request_unit == 'hour':
                allocation.number_of_days = allocation.number_of_hours_display / (
                    allocation.employee_id.sudo(
                    ).resource_calendar_id.hours_per_day or HOURS_PER_DAY)

            # set default values
            if not allocation.interval_number and not allocation._origin.interval_number:
                allocation.interval_number = 1
            if not allocation.number_per_interval and not allocation._origin.number_per_interval:
                allocation.number_per_interval = 1
            if not allocation.unit_per_interval and not allocation._origin.unit_per_interval:
                allocation.unit_per_interval = 'hours'
            if not allocation.interval_unit and not allocation._origin.interval_unit:
                allocation.interval_unit = 'weeks'

            if allocation.holiday_status_id.validity_stop and allocation.date_to:
                new_date_to = datetime.combine(
                    allocation.holiday_status_id.validity_stop, time.max)
                if new_date_to < allocation.date_to:
                    allocation.date_to = new_date_to

            if allocation.allocation_type == 'accrual':
                if allocation.holiday_status_id.request_unit == 'hour':
                    allocation.unit_per_interval = 'hours'
                else:
                    allocation.unit_per_interval = 'days'
            else:
                allocation.interval_number = 1
                allocation.interval_unit = 'weeks'
                allocation.number_per_interval = 1
                allocation.unit_per_interval = 'hours'

    ####################################################
    # ORM Overrides methods
    ####################################################

    def name_get(self):
        res = []
        for allocation in self:
            if allocation.holiday_type == 'company':
                target = allocation.mode_company_id.name
            elif allocation.holiday_type == 'department':
                target = allocation.department_id.name
            elif allocation.holiday_type == 'category':
                target = allocation.category_id.name
            else:
                target = allocation.employee_id.sudo().name

            res.append((
                allocation.id,
                _("Allocation of %(allocation_name)s : %(duration).2f %(duration_type)s to %(person)s",
                  allocation_name=allocation.holiday_status_id.sudo().name,
                  duration=allocation.number_of_hours_display
                  if allocation.type_request_unit == 'hour' else
                  allocation.number_of_days,
                  duration_type='hours'
                  if allocation.type_request_unit == 'hour' else 'days',
                  person=target)))
        return res

    def add_follower(self, employee_id):
        employee = self.env['hr.employee'].browse(employee_id)
        if employee.user_id:
            self.message_subscribe(partner_ids=employee.user_id.partner_id.ids)

    @api.constrains('holiday_status_id')
    def _check_leave_type_validity(self):
        for allocation in self:
            if allocation.holiday_status_id.validity_stop:
                vstop = allocation.holiday_status_id.validity_stop
                today = fields.Date.today()

                if vstop < today:
                    raise ValidationError(
                        _('You can allocate %(allocation_type)s only before %(date)s.',
                          allocation_type=allocation.holiday_status_id.
                          display_name,
                          date=allocation.holiday_status_id.validity_stop))

    @api.model
    def create(self, values):
        """ Override to avoid automatic logging of creation """
        employee_id = values.get('employee_id', False)
        if not values.get('department_id'):
            values.update({
                'department_id':
                self.env['hr.employee'].browse(employee_id).department_id.id
            })
        holiday = super(
            HolidaysAllocation,
            self.with_context(mail_create_nosubscribe=True)).create(values)
        holiday.add_follower(employee_id)
        if holiday.validation_type == 'hr':
            holiday.message_subscribe(partner_ids=(
                holiday.employee_id.parent_id.user_id.partner_id
                | holiday.employee_id.leave_manager_id.partner_id).ids)
        if not self._context.get('import_file'):
            holiday.activity_update()
        return holiday

    def write(self, values):
        employee_id = values.get('employee_id', False)
        if values.get('state'):
            self._check_approval_update(values['state'])
        result = super(HolidaysAllocation, self).write(values)
        self.add_follower(employee_id)
        return result

    def unlink(self):
        state_description_values = {
            elem[0]: elem[1]
            for elem in self._fields['state']._description_selection(self.env)
        }
        for holiday in self.filtered(lambda holiday: holiday.state not in
                                     ['draft', 'cancel', 'confirm']):
            raise UserError(
                _('You cannot delete an allocation request which is in %s state.'
                  ) % (state_description_values.get(holiday.state), ))
        return super(HolidaysAllocation, self).unlink()

    def _get_mail_redirect_suggested_company(self):
        return self.holiday_status_id.company_id

    ####################################################
    # Business methods
    ####################################################

    def _prepare_holiday_values(self, employee):
        self.ensure_one()
        values = {
            'name': self.name,
            'holiday_type': 'employee',
            'holiday_status_id': self.holiday_status_id.id,
            'notes': self.notes,
            'number_of_days': self.number_of_days,
            'parent_id': self.id,
            'employee_id': employee.id,
            'allocation_type': self.allocation_type,
            'date_from': self.date_from,
            'date_to': self.date_to,
            'interval_unit': self.interval_unit,
            'interval_number': self.interval_number,
            'number_per_interval': self.number_per_interval,
            'unit_per_interval': self.unit_per_interval,
        }
        return values

    def action_draft(self):
        if any(holiday.state not in ['confirm', 'refuse'] for holiday in self):
            raise UserError(
                _('Allocation request state must be "Refused" or "To Approve" in order to be reset to Draft.'
                  ))
        self.write({
            'state': 'draft',
            'first_approver_id': False,
            'second_approver_id': False,
        })
        linked_requests = self.mapped('linked_request_ids')
        if linked_requests:
            linked_requests.action_draft()
            linked_requests.unlink()
        self.activity_update()
        return True

    def action_confirm(self):
        if self.filtered(lambda holiday: holiday.state != 'draft'):
            raise UserError(
                _('Allocation request must be in Draft state ("To Submit") in order to confirm it.'
                  ))
        res = self.write({'state': 'confirm'})
        self.activity_update()
        return res

    def action_approve(self):
        # if validation_type == 'both': this method is the first approval approval
        # if validation_type != 'both': this method calls action_validate() below
        if any(holiday.state != 'confirm' for holiday in self):
            raise UserError(
                _('Allocation request must be confirmed ("To Approve") in order to approve it.'
                  ))

        current_employee = self.env.user.employee_id

        self.filtered(lambda hol: hol.validation_type == 'both').write({
            'state':
            'validate1',
            'first_approver_id':
            current_employee.id
        })
        self.filtered(
            lambda hol: not hol.validation_type == 'both').action_validate()
        self.activity_update()

    def action_validate(self):
        current_employee = self.env.user.employee_id
        for holiday in self:
            if holiday.state not in ['confirm', 'validate1']:
                raise UserError(
                    _('Allocation request must be confirmed in order to approve it.'
                      ))

            holiday.write({'state': 'validate'})
            if holiday.validation_type == 'both':
                holiday.write({'second_approver_id': current_employee.id})
            else:
                holiday.write({'first_approver_id': current_employee.id})

            holiday._action_validate_create_childs()
        self.activity_update()
        return True

    def _action_validate_create_childs(self):
        childs = self.env['hr.leave.allocation']
        if self.state == 'validate' and self.holiday_type in [
                'category', 'department', 'company'
        ]:
            if self.holiday_type == 'category':
                employees = self.category_id.employee_ids
            elif self.holiday_type == 'department':
                employees = self.department_id.member_ids
            else:
                employees = self.env['hr.employee'].search([
                    ('company_id', '=', self.mode_company_id.id)
                ])

            for employee in employees:
                childs += self.with_context(
                    mail_notify_force_send=False,
                    mail_activity_automation_skip=True).create(
                        self._prepare_holiday_values(employee))
            # TODO is it necessary to interleave the calls?
            childs.action_approve()
            if childs and self.validation_type == 'both':
                childs.action_validate()
        return childs

    def action_refuse(self):
        current_employee = self.env.user.employee_id
        if any(holiday.state not in ['confirm', 'validate', 'validate1']
               for holiday in self):
            raise UserError(
                _('Allocation request must be confirmed or validated in order to refuse it.'
                  ))

        validated_holidays = self.filtered(
            lambda hol: hol.state == 'validate1')
        validated_holidays.write({
            'state': 'refuse',
            'first_approver_id': current_employee.id
        })
        (self - validated_holidays).write({
            'state':
            'refuse',
            'second_approver_id':
            current_employee.id
        })
        # If a category that created several holidays, cancel all related
        linked_requests = self.mapped('linked_request_ids')
        if linked_requests:
            linked_requests.action_refuse()
        self.activity_update()
        return True

    def _check_approval_update(self, state):
        """ Check if target state is achievable. """
        if self.env.is_superuser():
            return
        current_employee = self.env.user.employee_id
        if not current_employee:
            return
        is_officer = self.env.user.has_group(
            'hr_holidays.group_hr_holidays_user')
        is_manager = self.env.user.has_group(
            'hr_holidays.group_hr_holidays_manager')
        for holiday in self:
            val_type = holiday.holiday_status_id.sudo(
            ).allocation_validation_type
            if state == 'confirm':
                continue

            if state == 'draft':
                if holiday.employee_id != current_employee and not is_manager:
                    raise UserError(
                        _('Only a time off Manager can reset other people allocation.'
                          ))
                continue

            if not is_officer and self.env.user != holiday.employee_id.leave_manager_id:
                raise UserError(
                    _('Only a time off Officer/Responsible or Manager can approve or refuse time off requests.'
                      ))

            if is_officer or self.env.user == holiday.employee_id.leave_manager_id:
                # use ir.rule based first access check: department, members, ... (see security.xml)
                holiday.check_access_rule('write')

            if holiday.employee_id == current_employee and not is_manager:
                raise UserError(
                    _('Only a time off Manager can approve its own requests.'))

            if (state == 'validate1'
                    and val_type == 'both') or (state == 'validate'
                                                and val_type == 'manager'):
                if self.env.user == holiday.employee_id.leave_manager_id and self.env.user != holiday.employee_id.user_id:
                    continue
                manager = holiday.employee_id.parent_id or holiday.employee_id.department_id.manager_id
                if (manager != current_employee) and not is_manager:
                    raise UserError(
                        _('You must be either %s\'s manager or time off manager to approve this time off'
                          ) % (holiday.employee_id.name))

            if state == 'validate' and val_type == 'both':
                if not is_officer:
                    raise UserError(
                        _('Only a Time off Approver can apply the second approval on allocation requests.'
                          ))

    # ------------------------------------------------------------
    # Activity methods
    # ------------------------------------------------------------

    def _get_responsible_for_approval(self):
        self.ensure_one()
        responsible = self.env.user

        if self.validation_type == 'manager' or (self.validation_type == 'both'
                                                 and self.state == 'confirm'):
            if self.employee_id.leave_manager_id:
                responsible = self.employee_id.leave_manager_id
        elif self.validation_type == 'hr' or (self.validation_type == 'both'
                                              and self.state == 'validate1'):
            if self.holiday_status_id.responsible_id:
                responsible = self.holiday_status_id.responsible_id

        return responsible

    def activity_update(self):
        to_clean, to_do = self.env['hr.leave.allocation'], self.env[
            'hr.leave.allocation']
        for allocation in self:
            note = _(
                'New Allocation Request created by %(user)s: %(count)s Days of %(allocation_type)s',
                user=allocation.create_uid.name,
                count=allocation.number_of_days,
                allocation_type=allocation.holiday_status_id.name)
            if allocation.state == 'draft':
                to_clean |= allocation
            elif allocation.state == 'confirm':
                allocation.activity_schedule(
                    'hr_holidays.mail_act_leave_allocation_approval',
                    note=note,
                    user_id=allocation.sudo()._get_responsible_for_approval(
                    ).id or self.env.user.id)
            elif allocation.state == 'validate1':
                allocation.activity_feedback(
                    ['hr_holidays.mail_act_leave_allocation_approval'])
                allocation.activity_schedule(
                    'hr_holidays.mail_act_leave_allocation_second_approval',
                    note=note,
                    user_id=allocation.sudo()._get_responsible_for_approval(
                    ).id or self.env.user.id)
            elif allocation.state == 'validate':
                to_do |= allocation
            elif allocation.state == 'refuse':
                to_clean |= allocation
        if to_clean:
            to_clean.activity_unlink([
                'hr_holidays.mail_act_leave_allocation_approval',
                'hr_holidays.mail_act_leave_allocation_second_approval'
            ])
        if to_do:
            to_do.activity_feedback([
                'hr_holidays.mail_act_leave_allocation_approval',
                'hr_holidays.mail_act_leave_allocation_second_approval'
            ])

    ####################################################
    # Messaging methods
    ####################################################

    def _track_subtype(self, init_values):
        if 'state' in init_values and self.state == 'validate':
            allocation_notif_subtype_id = self.holiday_status_id.allocation_notif_subtype_id
            return allocation_notif_subtype_id or self.env.ref(
                'hr_holidays.mt_leave_allocation')
        return super(HolidaysAllocation, self)._track_subtype(init_values)

    def _notify_get_groups(self, msg_vals=None):
        """ Handle HR users and officers recipients that can validate or refuse holidays
        directly from email. """
        groups = super(HolidaysAllocation,
                       self)._notify_get_groups(msg_vals=msg_vals)
        msg_vals = msg_vals or {}

        self.ensure_one()
        hr_actions = []
        if self.state == 'confirm':
            app_action = self._notify_get_action_link(
                'controller', controller='/allocation/validate', **msg_vals)
            hr_actions += [{'url': app_action, 'title': _('Approve')}]
        if self.state in ['confirm', 'validate', 'validate1']:
            ref_action = self._notify_get_action_link(
                'controller', controller='/allocation/refuse', **msg_vals)
            hr_actions += [{'url': ref_action, 'title': _('Refuse')}]

        holiday_user_group_id = self.env.ref(
            'hr_holidays.group_hr_holidays_user').id
        new_group = ('group_hr_holidays_user', lambda pdata: pdata['type'] ==
                     'user' and holiday_user_group_id in pdata['groups'], {
                         'actions': hr_actions,
                     })

        return [new_group] + groups

    def message_subscribe(self,
                          partner_ids=None,
                          channel_ids=None,
                          subtype_ids=None):
        # due to record rule can not allow to add follower and mention on validated leave so subscribe through sudo
        if self.state in ['validate', 'validate1']:
            self.check_access_rights('read')
            self.check_access_rule('read')
            return super(HolidaysAllocation, self.sudo()).message_subscribe(
                partner_ids=partner_ids,
                channel_ids=channel_ids,
                subtype_ids=subtype_ids)
        return super(HolidaysAllocation,
                     self).message_subscribe(partner_ids=partner_ids,
                                             channel_ids=channel_ids,
                                             subtype_ids=subtype_ids)
Beispiel #24
0
class AccountAssetAsset(models.Model):
    _name = 'account.asset.asset'
    _description = 'Asset/Revenue Recognition'
    _inherit = ['mail.thread']

    entry_count = fields.Integer(compute='_entry_count',
                                 string='# Asset Entries')
    name = fields.Char(string='Asset Name',
                       required=True,
                       readonly=True,
                       states={'draft': [('readonly', False)]})
    code = fields.Char(string='Reference',
                       size=32,
                       readonly=True,
                       states={'draft': [('readonly', False)]})
    value = fields.Float(string='Gross Value',
                         required=True,
                         readonly=True,
                         digits=0,
                         states={'draft': [('readonly', False)]},
                         oldname='purchase_value')
    currency_id = fields.Many2one(
        'res.currency',
        string='Currency',
        required=True,
        readonly=True,
        states={'draft': [('readonly', False)]},
        default=lambda self: self.env.user.company_id.currency_id.id)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 required=True,
                                 readonly=True,
                                 states={'draft': [('readonly', False)]},
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('account.asset.asset'))
    note = fields.Text()
    category_id = fields.Many2one('account.asset.category',
                                  string='Category',
                                  required=True,
                                  change_default=True,
                                  readonly=True,
                                  states={'draft': [('readonly', False)]})
    date = fields.Date(string='Date',
                       required=True,
                       readonly=True,
                       states={'draft': [('readonly', False)]},
                       default=fields.Date.context_today,
                       oldname="purchase_date")
    state = fields.Selection(
        [('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')],
        'Status',
        required=True,
        copy=False,
        default='draft',
        help="When an asset is created, the status is 'Draft'.\n"
        "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n"
        "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status."
    )
    active = fields.Boolean(default=True)
    partner_id = fields.Many2one('res.partner',
                                 string='Partner',
                                 readonly=True,
                                 states={'draft': [('readonly', False)]})
    method = fields.Selection(
        [('linear', 'Linear'), ('degressive', 'Degressive')],
        string='Computation Method',
        required=True,
        readonly=True,
        states={'draft': [('readonly', False)]},
        default='linear',
        help=
        "Choose the method to use to compute the amount of depreciation lines.\n  * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n"
        "  * Degressive: Calculated on basis of: Residual Value * Degressive Factor"
    )
    method_number = fields.Integer(
        string='Number of Depreciations',
        readonly=True,
        states={'draft': [('readonly', False)]},
        default=5,
        help="The number of depreciations needed to depreciate your asset")
    method_period = fields.Integer(
        string='Number of Months in a Period',
        required=True,
        readonly=True,
        default=12,
        states={'draft': [('readonly', False)]},
        help="The amount of time between two depreciations, in months")
    method_end = fields.Date(string='Ending Date',
                             readonly=True,
                             states={'draft': [('readonly', False)]})
    method_progress_factor = fields.Float(
        string='Degressive Factor',
        readonly=True,
        default=0.3,
        states={'draft': [('readonly', False)]})
    value_residual = fields.Float(compute='_amount_residual',
                                  method=True,
                                  digits=0,
                                  string='Residual Value')
    method_time = fields.Selection(
        [('number', 'Number of Entries'), ('end', 'Ending Date')],
        string='Time Method',
        required=True,
        readonly=True,
        default='number',
        states={'draft': [('readonly', False)]},
        help=
        "Choose the method to use to compute the dates and number of entries.\n"
        "  * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n"
        "  * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."
    )
    prorata = fields.Boolean(
        string='Prorata Temporis',
        readonly=True,
        states={'draft': [('readonly', False)]},
        help=
        'Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January / Start date of fiscal year'
    )
    depreciation_line_ids = fields.One2many('account.asset.depreciation.line',
                                            'asset_id',
                                            string='Depreciation Lines',
                                            readonly=True,
                                            states={
                                                'draft': [('readonly', False)],
                                                'open': [('readonly', False)]
                                            })
    salvage_value = fields.Float(
        string='Salvage Value',
        digits=0,
        readonly=True,
        states={'draft': [('readonly', False)]},
        help="It is the amount you plan to have that you cannot depreciate.")
    invoice_id = fields.Many2one('account.invoice',
                                 string='Invoice',
                                 states={'draft': [('readonly', False)]},
                                 copy=False)
    type = fields.Selection(related="category_id.type",
                            string='Type',
                            required=True)

    @api.multi
    def unlink(self):
        for asset in self:
            if asset.state in ['open', 'close']:
                raise UserError(
                    _('You cannot delete a document is in %s state.') %
                    (asset.state, ))
            for depreciation_line in asset.depreciation_line_ids:
                if depreciation_line.move_id:
                    raise UserError(
                        _('You cannot delete a document that contains posted entries.'
                          ))
        return super(AccountAssetAsset, self).unlink()

    @api.multi
    def _get_last_depreciation_date(self):
        """
        @param id: ids of a account.asset.asset objects
        @return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset
        """
        self.env.cr.execute(
            """
            SELECT a.id as id, COALESCE(MAX(m.date),a.date) AS date
            FROM account_asset_asset a
            LEFT JOIN account_asset_depreciation_line rel ON (rel.asset_id = a.id)
            LEFT JOIN account_move m ON (rel.move_id = m.id)
            WHERE a.id IN %s
            GROUP BY a.id, m.date """, (tuple(self.ids), ))
        result = dict(self.env.cr.fetchall())
        return result

    @api.model
    def _cron_generate_entries(self):
        self.compute_generated_entries(datetime.today())

    @api.model
    def compute_generated_entries(self, date, asset_type=None):
        # Entries generated : one by grouped category and one by asset from ungrouped category
        created_move_ids = []
        type_domain = []
        if asset_type:
            type_domain = [('type', '=', asset_type)]

        ungrouped_assets = self.env['account.asset.asset'].search(
            type_domain +
            [('state', '=', 'open'), ('category_id.group_entries', '=',
                                      False)])
        created_move_ids += ungrouped_assets._compute_entries(
            date, group_entries=False)

        for grouped_category in self.env['account.asset.category'].search(
                type_domain + [('group_entries', '=', True)]):
            assets = self.env['account.asset.asset'].search([
                ('state', '=', 'open'),
                ('category_id', '=', grouped_category.id)
            ])
            created_move_ids += assets._compute_entries(date,
                                                        group_entries=True)
        return created_move_ids

    def _compute_board_amount(self, sequence, residual_amount, amount_to_depr,
                              undone_dotation_number,
                              posted_depreciation_line_ids, total_days,
                              depreciation_date):
        amount = 0
        if sequence == undone_dotation_number:
            amount = residual_amount
        else:
            if self.method == 'linear':
                amount = amount_to_depr / (undone_dotation_number -
                                           len(posted_depreciation_line_ids))
                if self.prorata:
                    amount = amount_to_depr / self.method_number
                    if sequence == 1:
                        if self.method_period % 12 != 0:
                            date = datetime.strptime(self.date, '%Y-%m-%d')
                            month_days = calendar.monthrange(
                                date.year, date.month)[1]
                            days = month_days - date.day + 1
                            amount = (amount_to_depr /
                                      self.method_number) / month_days * days
                        else:
                            days = (self.company_id.compute_fiscalyear_dates(
                                depreciation_date)['date_to'] -
                                    depreciation_date).days + 1
                            amount = (amount_to_depr /
                                      self.method_number) / total_days * days
            elif self.method == 'degressive':
                amount = residual_amount * self.method_progress_factor
                if self.prorata:
                    if sequence == 1:
                        if self.method_period % 12 != 0:
                            date = datetime.strptime(self.date, '%Y-%m-%d')
                            month_days = calendar.monthrange(
                                date.year, date.month)[1]
                            days = month_days - date.day + 1
                            amount = (residual_amount *
                                      self.method_progress_factor
                                      ) / month_days * days
                        else:
                            days = (self.company_id.compute_fiscalyear_dates(
                                depreciation_date)['date_to'] -
                                    depreciation_date).days + 1
                            amount = (residual_amount *
                                      self.method_progress_factor
                                      ) / total_days * days
        return amount

    def _compute_board_undone_dotation_nb(self, depreciation_date, total_days):
        undone_dotation_number = self.method_number
        if self.method_time == 'end':
            end_date = datetime.strptime(self.method_end, DF).date()
            undone_dotation_number = 0
            while depreciation_date <= end_date:
                depreciation_date = date(
                    depreciation_date.year, depreciation_date.month,
                    depreciation_date.day) + relativedelta(
                        months=+self.method_period)
                undone_dotation_number += 1
        if self.prorata:
            undone_dotation_number += 1
        return undone_dotation_number

    @api.multi
    def compute_depreciation_board(self):
        self.ensure_one()

        posted_depreciation_line_ids = self.depreciation_line_ids.filtered(
            lambda x: x.move_check).sorted(key=lambda l: l.depreciation_date)
        unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(
            lambda x: not x.move_check)

        # Remove old unposted depreciation lines. We cannot use unlink() with One2many field
        commands = [(2, line_id.id, False)
                    for line_id in unposted_depreciation_line_ids]

        if self.value_residual != 0.0:
            amount_to_depr = residual_amount = self.value_residual
            if self.prorata:
                # if we already have some previous validated entries, starting date is last entry + method perio
                if posted_depreciation_line_ids and posted_depreciation_line_ids[
                        -1].depreciation_date:
                    last_depreciation_date = datetime.strptime(
                        posted_depreciation_line_ids[-1].depreciation_date,
                        DF).date()
                    depreciation_date = last_depreciation_date + relativedelta(
                        months=+self.method_period)
                else:
                    depreciation_date = datetime.strptime(
                        self._get_last_depreciation_date()[self.id],
                        DF).date()
            else:
                # depreciation_date = 1st of January of purchase year if annual valuation, 1st of
                # purchase month in other cases
                if self.method_period >= 12:
                    asset_date = datetime.strptime(self.date[:4] + '-01-01',
                                                   DF).date()
                else:
                    asset_date = datetime.strptime(self.date[:7] + '-01',
                                                   DF).date()
                # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period
                if posted_depreciation_line_ids and posted_depreciation_line_ids[
                        -1].depreciation_date:
                    last_depreciation_date = datetime.strptime(
                        posted_depreciation_line_ids[-1].depreciation_date,
                        DF).date()
                    depreciation_date = last_depreciation_date + relativedelta(
                        months=+self.method_period)
                else:
                    depreciation_date = asset_date
            day = depreciation_date.day
            month = depreciation_date.month
            year = depreciation_date.year
            total_days = (year % 4) and 365 or 366

            undone_dotation_number = self._compute_board_undone_dotation_nb(
                depreciation_date, total_days)

            for x in range(len(posted_depreciation_line_ids),
                           undone_dotation_number):
                sequence = x + 1
                amount = self._compute_board_amount(
                    sequence, residual_amount, amount_to_depr,
                    undone_dotation_number, posted_depreciation_line_ids,
                    total_days, depreciation_date)
                amount = self.currency_id.round(amount)
                if float_is_zero(amount,
                                 precision_rounding=self.currency_id.rounding):
                    continue
                residual_amount -= amount
                vals = {
                    'amount':
                    amount,
                    'asset_id':
                    self.id,
                    'sequence':
                    sequence,
                    'name': (self.code or '') + '/' + str(sequence),
                    'remaining_value':
                    residual_amount,
                    'depreciated_value':
                    self.value - (self.salvage_value + residual_amount),
                    'depreciation_date':
                    depreciation_date.strftime(DF),
                }
                commands.append((0, False, vals))
                # Considering Depr. Period as months
                depreciation_date = date(year, month, day) + relativedelta(
                    months=+self.method_period)
                day = depreciation_date.day
                month = depreciation_date.month
                year = depreciation_date.year

        self.write({'depreciation_line_ids': commands})

        return True

    @api.multi
    def validate(self):
        self.write({'state': 'open'})
        fields = [
            'method',
            'method_number',
            'method_period',
            'method_end',
            'method_progress_factor',
            'method_time',
            'salvage_value',
            'invoice_id',
        ]
        ref_tracked_fields = self.env['account.asset.asset'].fields_get(fields)
        for asset in self:
            tracked_fields = ref_tracked_fields.copy()
            if asset.method == 'linear':
                del (tracked_fields['method_progress_factor'])
            if asset.method_time != 'end':
                del (tracked_fields['method_end'])
            else:
                del (tracked_fields['method_number'])
            dummy, tracking_value_ids = asset._message_track(
                tracked_fields, dict.fromkeys(fields))
            asset.message_post(subject=_('Asset created'),
                               tracking_value_ids=tracking_value_ids)

    def _get_disposal_moves(self):
        move_ids = []
        for asset in self:
            unposted_depreciation_line_ids = asset.depreciation_line_ids.filtered(
                lambda x: not x.move_check)
            if unposted_depreciation_line_ids:
                old_values = {
                    'method_end': asset.method_end,
                    'method_number': asset.method_number,
                }

                # Remove all unposted depr. lines
                commands = [(2, line_id.id, False)
                            for line_id in unposted_depreciation_line_ids]

                # Create a new depr. line with the residual amount and post it
                sequence = len(asset.depreciation_line_ids) - len(
                    unposted_depreciation_line_ids) + 1
                today = datetime.today().strftime(DF)
                vals = {
                    'amount': asset.value_residual,
                    'asset_id': asset.id,
                    'sequence': sequence,
                    'name': (asset.code or '') + '/' + str(sequence),
                    'remaining_value': 0,
                    'depreciated_value': asset.value -
                    asset.salvage_value,  # the asset is completely depreciated
                    'depreciation_date': today,
                }
                commands.append((0, False, vals))
                asset.write({
                    'depreciation_line_ids': commands,
                    'method_end': today,
                    'method_number': sequence
                })
                tracked_fields = self.env['account.asset.asset'].fields_get(
                    ['method_number', 'method_end'])
                changes, tracking_value_ids = asset._message_track(
                    tracked_fields, old_values)
                if changes:
                    asset.message_post(subject=_(
                        'Asset sold or disposed. Accounting entry awaiting for validation.'
                    ),
                                       tracking_value_ids=tracking_value_ids)
                move_ids += asset.depreciation_line_ids[-1].create_move(
                    post_move=False)

        return move_ids

    @api.multi
    def set_to_close(self):
        move_ids = self._get_disposal_moves()
        if move_ids:
            name = _('Disposal Move')
            view_mode = 'form'
            if len(move_ids) > 1:
                name = _('Disposal Moves')
                view_mode = 'tree,form'
            return {
                'name': name,
                'view_type': 'form',
                'view_mode': view_mode,
                'res_model': 'account.move',
                'type': 'ir.actions.act_window',
                'target': 'current',
                'res_id': move_ids[0],
            }

    @api.multi
    def set_to_draft(self):
        self.write({'state': 'draft'})

    @api.one
    @api.depends('value', 'salvage_value', 'depreciation_line_ids.move_check',
                 'depreciation_line_ids.amount')
    def _amount_residual(self):
        total_amount = 0.0
        for line in self.depreciation_line_ids:
            if line.move_check:
                total_amount += line.amount
        self.value_residual = self.value - total_amount - self.salvage_value

    @api.onchange('company_id')
    def onchange_company_id(self):
        self.currency_id = self.company_id.currency_id.id

    @api.multi
    @api.depends('depreciation_line_ids.move_id')
    def _entry_count(self):
        for asset in self:
            res = self.env['account.asset.depreciation.line'].search_count([
                ('asset_id', '=', asset.id), ('move_id', '!=', False)
            ])
            asset.entry_count = res or 0

    @api.one
    @api.constrains('prorata', 'method_time')
    def _check_prorata(self):
        if self.prorata and self.method_time != 'number':
            raise ValidationError(
                _('Prorata temporis can be applied only for time method "number of depreciations".'
                  ))

    @api.onchange('category_id')
    def onchange_category_id(self):
        vals = self.onchange_category_id_values(self.category_id.id)
        # We cannot use 'write' on an object that doesn't exist yet
        if vals:
            for k, v in vals['value'].items():
                setattr(self, k, v)

    def onchange_category_id_values(self, category_id):
        if category_id:
            category = self.env['account.asset.category'].browse(category_id)
            return {
                'value': {
                    'method': category.method,
                    'method_number': category.method_number,
                    'method_time': category.method_time,
                    'method_period': category.method_period,
                    'method_progress_factor': category.method_progress_factor,
                    'method_end': category.method_end,
                    'prorata': category.prorata,
                }
            }

    @api.onchange('method_time')
    def onchange_method_time(self):
        if self.method_time != 'number':
            self.prorata = False

    @api.multi
    def copy_data(self, default=None):
        if default is None:
            default = {}
        default['name'] = self.name + _(' (copy)')
        return super(AccountAssetAsset, self).copy_data(default)

    @api.multi
    def _compute_entries(self, date, group_entries=False):
        depreciation_ids = self.env['account.asset.depreciation.line'].search([
            ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date),
            ('move_check', '=', False)
        ])
        if group_entries:
            return depreciation_ids.create_grouped_move()
        return depreciation_ids.create_move()

    @api.model
    def create(self, vals):
        asset = super(AccountAssetAsset,
                      self.with_context(mail_create_nolog=True)).create(vals)
        asset.compute_depreciation_board()
        return asset

    @api.multi
    def write(self, vals):
        res = super(AccountAssetAsset, self).write(vals)
        if 'depreciation_line_ids' not in vals and 'state' not in vals:
            for rec in self:
                rec.compute_depreciation_board()
        return res

    @api.multi
    def open_entries(self):
        move_ids = []
        for asset in self:
            for depreciation_line in asset.depreciation_line_ids:
                if depreciation_line.move_id:
                    move_ids.append(depreciation_line.move_id.id)
        return {
            'name': _('Journal Entries'),
            'view_type': 'form',
            'view_mode': 'tree,form',
            'res_model': 'account.move',
            'view_id': False,
            'type': 'ir.actions.act_window',
            'domain': [('id', 'in', move_ids)],
        }
Beispiel #25
0
class FleetVehicle(models.Model):
    _inherit = 'fleet.vehicle'

    co2_fee = fields.Float(compute='_compute_co2_fee', string="CO2 Fee")
    total_depreciated_cost = fields.Float(
        compute='_compute_total_depreciated_cost',
        string="Total Cost (Depreciated)",
        track_visibility="onchange",
        help="This includes all the depreciated costs and the CO2 fee")
    total_cost = fields.Float(
        compute='_compute_total_cost',
        string="Total Cost",
        help="This include all the costs and the CO2 fee")
    fuel_type = fields.Selection(required=True, default='diesel')
    atn = fields.Float(compute='_compute_car_atn', string="ATN")
    acquisition_date = fields.Date(required=True)

    @api.depends('co2_fee', 'log_contracts', 'log_contracts.state',
                 'log_contracts.recurring_cost_amount_depreciated')
    def _compute_total_depreciated_cost(self):
        for car in self:
            car.total_depreciated_cost = car.co2_fee + \
                sum(car.log_contracts.filtered(
                    lambda contract: contract.state == 'open'
                ).mapped('recurring_cost_amount_depreciated'))

    @api.depends('co2_fee', 'log_contracts', 'log_contracts.state',
                 'log_contracts.cost_generated')
    def _compute_total_cost(self):
        for car in self:
            car.total_cost = car.co2_fee
            contracts = car.log_contracts.filtered(
                lambda contract: contract.state == 'open' and contract.
                cost_frequency != 'no')
            for contract in contracts:
                if contract.cost_frequency == "daily":
                    car.total_cost += contract.cost_generated * 30.0
                elif contract.cost_frequency == "weekly":
                    car.total_cost += contract.cost_generated * 4.0
                elif contract.cost_frequency == "monthly":
                    car.total_cost += contract.cost_generated
                elif contract.cost_frequency == "yearly":
                    car.total_cost += contract.cost_generated / 12.0

    def _get_co2_fee(self, co2, fuel_type):
        fuel_coefficient = {
            'diesel': 600,
            'gasoline': 768,
            'lpg': 990,
            'electric': 0,
            'hybrid': 600
        }
        co2_fee = 0
        if fuel_type and fuel_type != 'electric':
            if not co2:
                co2 = 165 if fuel_type in ['diesel', 'hybrid'] else 182
            co2_fee = (((co2 * 9.0) - fuel_coefficient.get(fuel_type, 0)) *
                       144.97 / 114.08) / 12.0
        return max(co2_fee, 26.47)

    @api.depends('co2', 'fuel_type')
    def _compute_co2_fee(self):
        for car in self:
            car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type)

    @api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2')
    def _compute_car_atn(self):
        for car in self:
            car.atn = car._get_car_atn(car.acquisition_date, car.car_value,
                                       car.fuel_type, car.co2)

    @api.depends('model_id', 'license_plate', 'log_contracts',
                 'acquisition_date', 'co2_fee', 'log_contracts',
                 'log_contracts.state',
                 'log_contracts.recurring_cost_amount_depreciated')
    def _compute_vehicle_name(self):
        super(FleetVehicle, self)._compute_vehicle_name()
        for vehicle in self:
            acquisition_date = vehicle._get_acquisition_date()
            vehicle.name += u" \u2022 " + str(
                round(vehicle.total_depreciated_cost,
                      2)) + u" \u2022 " + acquisition_date

    def _get_acquisition_date(self):
        self.ensure_one()
        return babel.dates.format_date(
            date=Datetime.from_string(self.acquisition_date),
            format='MMMM y',
            locale=self._context.get('lang') or 'en_US')

    def _get_car_atn(self, acquisition_date, car_value, fuel_type, co2):
        # Compute the correction coefficient from the age of the car
        now = Datetime.from_string(Datetime.now())
        start = Datetime.from_string(acquisition_date)
        if start:
            number_of_month = (
                now.year - start.year) * 12.0 + now.month - start.month + int(
                    bool(now.day - start.day + 1))
            if number_of_month <= 12:
                age_coefficient = 1.00
            elif number_of_month <= 24:
                age_coefficient = 0.94
            elif number_of_month <= 36:
                age_coefficient = 0.88
            elif number_of_month <= 48:
                age_coefficient = 0.82
            elif number_of_month <= 60:
                age_coefficient = 0.76
            else:
                age_coefficient = 0.70
            car_value = car_value * age_coefficient
            # Compute atn value from corrected car_value
            magic_coeff = 6.0 / 7.0  # Don't ask me why
            if fuel_type == 'electric':
                atn = 0.0
            else:
                if fuel_type in ['diesel', 'hybrid']:
                    reference = 88.0
                else:
                    reference = 107.0

                if not co2:
                    co2 = 195 if fuel_type in ['diesel', 'hybrid'] else 205

                if co2 <= reference:
                    atn = car_value * max(0.04,
                                          (0.055 - 0.001 *
                                           (reference - co2))) * magic_coeff
                else:
                    atn = car_value * min(0.18,
                                          (0.055 + 0.001 *
                                           (co2 - reference))) * magic_coeff
            return max(1310, atn) / 12.0
Beispiel #26
0
class AccountAssetCategory(models.Model):
    _name = 'account.asset.category'
    _description = 'Asset category'

    active = fields.Boolean(default=True)
    name = fields.Char(required=True, index=True, string="Asset Type")
    account_analytic_id = fields.Many2one('account.analytic.account',
                                          string='Analytic Account')
    account_asset_id = fields.Many2one(
        'account.account',
        string='Asset Account',
        required=True,
        domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
        help=
        "Account used to record the purchase of the asset at its original price."
    )
    account_depreciation_id = fields.Many2one(
        'account.account',
        string='Depreciation Entries: Asset Account',
        required=True,
        domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
        help=
        "Account used in the depreciation entries, to decrease the asset value."
    )
    account_depreciation_expense_id = fields.Many2one(
        'account.account',
        string='Depreciation Entries: Expense Account',
        required=True,
        domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
        oldname='account_income_recognition_id',
        help=
        "Account used in the periodical entries, to record a part of the asset as expense."
    )
    journal_id = fields.Many2one('account.journal',
                                 string='Journal',
                                 required=True)
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        required=True,
        default=lambda self: self.env['res.company']._company_default_get(
            'account.asset.category'))
    method = fields.Selection(
        [('linear', 'Linear'), ('degressive', 'Degressive')],
        string='Computation Method',
        required=True,
        default='linear',
        help=
        "Choose the method to use to compute the amount of depreciation lines.\n"
        "  * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n"
        "  * Degressive: Calculated on basis of: Residual Value * Degressive Factor"
    )
    method_number = fields.Integer(
        string='Number of Depreciations',
        default=5,
        help="The number of depreciations needed to depreciate your asset")
    method_period = fields.Integer(
        string='Period Length',
        default=1,
        help="State here the time between 2 depreciations, in months",
        required=True)
    method_progress_factor = fields.Float('Degressive Factor', default=0.3)
    method_time = fields.Selection(
        [('number', 'Number of Entries'), ('end', 'Ending Date')],
        string='Time Method',
        required=True,
        default='number',
        help=
        "Choose the method to use to compute the dates and number of entries.\n"
        "  * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n"
        "  * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."
    )
    method_end = fields.Date('Ending date')
    prorata = fields.Boolean(
        string='Prorata Temporis',
        help=
        'Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first of January'
    )
    open_asset = fields.Boolean(
        string='Auto-confirm Assets',
        help=
        "Check this if you want to automatically confirm the assets of this category when created by invoices."
    )
    group_entries = fields.Boolean(
        string='Group Journal Entries',
        help=
        "Check this if you want to group the generated entries by categories.")
    type = fields.Selection([('sale', 'Sale: Revenue Recognition'),
                             ('purchase', 'Purchase: Asset')],
                            required=True,
                            index=True,
                            default='purchase')

    @api.onchange('account_asset_id')
    def onchange_account_asset(self):
        if self.type == "purchase":
            self.account_depreciation_id = self.account_asset_id
        elif self.type == "sale":
            self.account_depreciation_expense_id = self.account_asset_id

    @api.onchange('type')
    def onchange_type(self):
        if self.type == 'sale':
            self.prorata = True
            self.method_period = 1
        else:
            self.method_period = 12

    @api.onchange('method_time')
    def _onchange_method_time(self):
        if self.method_time != 'number':
            self.prorata = False
Beispiel #27
0
class account_move(models.Model):
    _inherit = "account.move"

    def _get_document_data(self, cr, uid, ids, name, arg, context=None):
        """ TODO """
        res = {}
        for record in self.browse(cr, uid, ids, context=context):
            document_number = False
            if record.model and record.res_id:
                document_number = self.pool[record.model].browse(
                    cr, uid, record.res_id, context=context).document_number
            res[record.id] = document_number
        return res

    @api.depends(
        'sii_document_number',
        'name',
        'document_class_id',
        'document_class_id.doc_code_prefix',
    )
    def _get_document_number(self):
        for r in self:
            if r.sii_document_number and r.document_class_id:
                document_number = (r.document_class_id.doc_code_prefix
                                   or '') + r.sii_document_number
            else:
                document_number = r.name
            r.document_number = document_number

    document_class_id = fields.Many2one(
        'sii.document_class',
        string='Document Type',
        copy=False,
        readonly=True,
        states={'draft': [('readonly', False)]},
    )
    sii_document_number = fields.Char(
        string='Document Number',
        copy=False,
        readonly=True,
        states={'draft': [('readonly', False)]},
    )

    canceled = fields.Boolean(
        string="Canceled?",
        readonly=True,
        states={'draft': [('readonly', False)]},
    )
    iva_uso_comun = fields.Boolean(
        string="Iva Uso Común",
        readonly=True,
        states={'draft': [('readonly', False)]},
    )
    no_rec_code = fields.Selection(
        [('1',
          'Compras destinadas a IVA a generar operaciones no gravados o exentas.'
          ), ('2', 'Facturas de proveedores registrados fuera de plazo.'),
         ('3', 'Gastos rechazados.'),
         ('4',
          'Entregas gratuitas (premios, bonificaciones, etc.) recibidos.'),
         ('9', 'Otros.')],
        string="Código No recuperable",
        readonly=True,
        states={'draft': [('readonly', False)]},
    )  # @TODO select 1 automático si es emisor 2Categoría
    document_number = fields.Char(
        compute='_get_document_number',
        string='Document Number',
        store=True,
        readonly=True,
        states={'draft': [('readonly', False)]},
    )
    sended = fields.Boolean(
        string="Enviado al SII",
        default=False,
        readonly=True,
        states={'draft': [('readonly', False)]},
    )
    factor_proporcionalidad = fields.Float(
        string="Factor proporcionalidad",
        default=0.00,
        readonly=True,
        states={'draft': [('readonly', False)]},
    )

    def _get_move_imps(self):
        imps = {}
        for l in self.line_ids:
            if l.tax_line_id:
                if l.tax_line_id:
                    if not l.tax_line_id.id in imps:
                        imps[l.tax_line_id.id] = {
                            'tax_id': l.tax_line_id.id,
                            'credit': 0,
                            'debit': 0,
                            'code': l.tax_line_id.sii_code
                        }
                    imps[l.tax_line_id.id]['credit'] += l.credit
                    imps[l.tax_line_id.id]['debit'] += l.debit
                    if l.tax_line_id.activo_fijo:
                        ActivoFijo[1] += l.credit
            elif l.tax_ids and l.tax_ids[0].amount == 0:  #caso monto exento
                if not l.tax_ids[0].id in imps:
                    imps[l.tax_ids[0].id] = {
                        'tax_id': l.tax_ids[0].id,
                        'credit': 0,
                        'debit': 0,
                        'code': l.tax_ids[0].sii_code
                    }
                imps[l.tax_ids[0].id]['credit'] += l.credit
                imps[l.tax_ids[0].id]['debit'] += l.debit
        return imps

    def totales_por_movimiento(self):
        move_imps = self._get_move_imps()
        imps = {
            'iva': 0,
            'exento': 0,
            'otros_imps': 0,
        }
        for key, i in move_imps.items():
            if i['code'] in [14]:
                imps['iva'] += (i['credit'] or i['debit'])
            elif i['code'] == 0:
                imps['exento'] += (i['credit'] or i['debit'])
            else:
                imps['otros_imps'] += (i['credit'] or i['debit'])
        imps['neto'] = self.amount - imps['otros_imps'] - imps[
            'exento'] - imps['iva']
        return imps
Beispiel #28
0
class AccountAssetDepreciationLine(models.Model):
    _name = 'account.asset.depreciation.line'
    _description = 'Asset depreciation line'

    name = fields.Char(string='Depreciation Name', required=True, index=True)
    sequence = fields.Integer(required=True)
    asset_id = fields.Many2one('account.asset.asset',
                               string='Asset',
                               required=True,
                               ondelete='cascade')
    parent_state = fields.Selection(related='asset_id.state',
                                    string='State of Asset')
    amount = fields.Float(string='Current Depreciation',
                          digits=0,
                          required=True)
    remaining_value = fields.Float(string='Next Period Depreciation',
                                   digits=0,
                                   required=True)
    depreciated_value = fields.Float(string='Cumulative Depreciation',
                                     required=True)
    depreciation_date = fields.Date('Depreciation Date', index=True)
    move_id = fields.Many2one('account.move', string='Depreciation Entry')
    move_check = fields.Boolean(compute='_get_move_check',
                                string='Linked',
                                track_visibility='always',
                                store=True)
    move_posted_check = fields.Boolean(compute='_get_move_posted_check',
                                       string='Posted',
                                       track_visibility='always',
                                       store=True)

    @api.multi
    @api.depends('move_id')
    def _get_move_check(self):
        for line in self:
            line.move_check = bool(line.move_id)

    @api.multi
    @api.depends('move_id.state')
    def _get_move_posted_check(self):
        for line in self:
            line.move_posted_check = True if line.move_id and line.move_id.state == 'posted' else False

    @api.multi
    def create_move(self, post_move=True):
        created_moves = self.env['account.move']
        prec = self.env['decimal.precision'].precision_get('Account')
        for line in self:
            category_id = line.asset_id.category_id
            depreciation_date = self.env.context.get(
                'depreciation_date'
            ) or line.depreciation_date or fields.Date.context_today(self)
            company_currency = line.asset_id.company_id.currency_id
            current_currency = line.asset_id.currency_id
            amount = current_currency.with_context(
                date=depreciation_date).compute(line.amount, company_currency)
            asset_name = line.asset_id.name + ' (%s/%s)' % (
                line.sequence, len(line.asset_id.depreciation_line_ids))
            move_line_1 = {
                'name':
                asset_name,
                'account_id':
                category_id.account_depreciation_id.id,
                'debit':
                0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0
                else -amount,
                'credit':
                amount if float_compare(amount, 0.0, precision_digits=prec) > 0
                else 0.0,
                'journal_id':
                category_id.journal_id.id,
                'partner_id':
                line.asset_id.partner_id.id,
                'analytic_account_id':
                category_id.account_analytic_id.id
                if category_id.type == 'sale' else False,
                'currency_id':
                company_currency != current_currency and current_currency.id
                or False,
                'amount_currency':
                company_currency != current_currency and -1.0 * line.amount
                or 0.0,
            }
            move_line_2 = {
                'name':
                asset_name,
                'account_id':
                category_id.account_depreciation_expense_id.id,
                'credit':
                0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0
                else -amount,
                'debit':
                amount if float_compare(amount, 0.0, precision_digits=prec) > 0
                else 0.0,
                'journal_id':
                category_id.journal_id.id,
                'partner_id':
                line.asset_id.partner_id.id,
                'analytic_account_id':
                category_id.account_analytic_id.id
                if category_id.type == 'purchase' else False,
                'currency_id':
                company_currency != current_currency and current_currency.id
                or False,
                'amount_currency':
                company_currency != current_currency and line.amount or 0.0,
            }
            move_vals = {
                'ref': line.asset_id.code,
                'date': depreciation_date or False,
                'journal_id': category_id.journal_id.id,
                'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)],
            }
            move = self.env['account.move'].create(move_vals)
            line.write({'move_id': move.id, 'move_check': True})
            created_moves |= move

        if post_move and created_moves:
            created_moves.filtered(lambda m: any(
                m.asset_depreciation_ids.mapped(
                    'asset_id.category_id.open_asset'))).post()
        return [x.id for x in created_moves]

    @api.multi
    def create_grouped_move(self, post_move=True):
        if not self.exists():
            return []

        created_moves = self.env['account.move']
        category_id = self[
            0].asset_id.category_id  # we can suppose that all lines have the same category
        depreciation_date = self.env.context.get(
            'depreciation_date') or fields.Date.context_today(self)
        amount = 0.0
        for line in self:
            # Sum amount of all depreciation lines
            company_currency = line.asset_id.company_id.currency_id
            current_currency = line.asset_id.currency_id
            amount += current_currency.compute(line.amount, company_currency)

        name = category_id.name + _(' (grouped)')
        move_line_1 = {
            'name':
            name,
            'account_id':
            category_id.account_depreciation_id.id,
            'debit':
            0.0,
            'credit':
            amount,
            'journal_id':
            category_id.journal_id.id,
            'analytic_account_id':
            category_id.account_analytic_id.id
            if category_id.type == 'sale' else False,
        }
        move_line_2 = {
            'name':
            name,
            'account_id':
            category_id.account_depreciation_expense_id.id,
            'credit':
            0.0,
            'debit':
            amount,
            'journal_id':
            category_id.journal_id.id,
            'analytic_account_id':
            category_id.account_analytic_id.id
            if category_id.type == 'purchase' else False,
        }
        move_vals = {
            'ref': category_id.name,
            'date': depreciation_date or False,
            'journal_id': category_id.journal_id.id,
            'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)],
        }
        move = self.env['account.move'].create(move_vals)
        self.write({'move_id': move.id, 'move_check': True})
        created_moves |= move

        if post_move and created_moves:
            self.post_lines_and_close_asset()
            created_moves.post()
        return [x.id for x in created_moves]

    @api.multi
    def post_lines_and_close_asset(self):
        # we re-evaluate the assets to determine whether we can close them
        for line in self:
            line.log_message_when_posted()
            asset = line.asset_id
            if asset.currency_id.is_zero(asset.value_residual):
                asset.message_post(body=_("Document closed."))
                asset.write({'state': 'close'})

    @api.multi
    def log_message_when_posted(self):
        def _format_message(message_description, tracked_values):
            message = ''
            if message_description:
                message = '<span>%s</span>' % message_description
            for name, values in tracked_values.items():
                message += '<div> &nbsp; &nbsp; &bull; <b>%s</b>: ' % name
                message += '%s</div>' % values
            return message

        for line in self:
            if line.move_id and line.move_id.state == 'draft':
                partner_name = line.asset_id.partner_id.name
                currency_name = line.asset_id.currency_id.name
                msg_values = {
                    _('Currency'): currency_name,
                    _('Amount'): line.amount
                }
                if partner_name:
                    msg_values[_('Partner')] = partner_name
                msg = _format_message(_('Depreciation line posted.'),
                                      msg_values)
                line.asset_id.message_post(body=msg)

    @api.multi
    def unlink(self):
        for record in self:
            if record.move_check:
                if record.asset_id.category_id.type == 'purchase':
                    msg = _("You cannot delete posted depreciation lines.")
                else:
                    msg = _("You cannot delete posted installment lines.")
                raise UserError(msg)
        return super(AccountAssetDepreciationLine, self).unlink()
Beispiel #29
0
class SaleOrder(models.Model):
    _inherit = 'sale.order'

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

    # override domain
    project_id = fields.Many2one(
        domain=
        "['|', ('bill_type', '=', 'customer_task'), ('pricing_type', '=', 'fixed_rate'), ('analytic_account_id', '!=', False), ('company_id', '=', company_id)]"
    )
    timesheet_encode_uom_id = fields.Many2one(
        'uom.uom', related='company_id.timesheet_encode_uom_id')
    timesheet_total_duration = fields.Integer(
        "Timesheet Total Duration",
        compute='_compute_timesheet_total_duration',
        help=
        "Total recorded duration, expressed in the encoding UoM, and rounded to the unit"
    )

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

    @api.depends('timesheet_ids', 'company_id.timesheet_encode_uom_id')
    def _compute_timesheet_total_duration(self):
        for sale_order in self:
            timesheets = sale_order.timesheet_ids if self.user_has_groups(
                'hr_timesheet.group_hr_timesheet_approver'
            ) else sale_order.timesheet_ids.filtered(
                lambda t: t.user_id.id == self.env.uid)
            total_time = 0.0
            for timesheet in timesheets.filtered(
                    lambda t: not t.non_allow_billable):
                # Timesheets may be stored in a different unit of measure, so first we convert all of them to the reference unit
                total_time += timesheet.unit_amount * timesheet.product_uom_id.factor_inv
            # Now convert to the proper unit of measure
            total_time *= sale_order.timesheet_encode_uom_id.factor
            sale_order.timesheet_total_duration = total_time

    def action_view_project_ids(self):
        self.ensure_one()
        # redirect to form or kanban view
        billable_projects = self.project_ids.filtered(
            lambda project: project.sale_line_id)
        if len(billable_projects) == 1 and self.env.user.has_group(
                'project.group_project_manager'):
            action = billable_projects[0].action_view_timesheet_plan()
        else:
            action = super().action_view_project_ids()
        return action

    def action_view_timesheet(self):
        self.ensure_one()
        action = self.env["ir.actions.actions"]._for_xml_id(
            "sale_timesheet.timesheet_action_from_sales_order")
        action['context'] = {
            'search_default_billable_timesheet': True
        }  # erase default filters
        if self.timesheet_count > 0:
            action['domain'] = [('so_line', 'in', self.order_line.ids)]
        else:
            action = {'type': 'ir.actions.act_window_close'}
        return action

    def _create_invoices(self, grouped=False, final=False, date=None):
        """Link timesheets to the created invoices. Date interval is injected in the
        context in sale_make_invoice_advance_inv wizard.
        """
        moves = super()._create_invoices(grouped=grouped,
                                         final=final,
                                         date=date)
        moves._link_timesheets_to_invoice(
            self.env.context.get("timesheet_start_date"),
            self.env.context.get("timesheet_end_date"))
        return moves
Beispiel #30
0
class biaya_registrasi(models.Model):
    _name = 'siswa_psb_ocb11.biaya_registrasi'

    name = fields.Char('Name',
                       default=lambda self: str(self.tahunajaran_id.name) + " "
                       + str(self.jenjang_id.name))
    tahunajaran_id = fields.Many2one('siswa_ocb11.tahunajaran',
                                     string="Tahun Ajaran",
                                     required=True,
                                     ondelete="cascade")
    jenjang_id = fields.Many2one('siswa_ocb11.jenjang',
                                 string="Jenjang",
                                 required=True,
                                 ondelete="cascade")
    biaya_ta_jenjang_ids = fields.Many2many(
        'siswa_keu_ocb11.biaya_ta_jenjang',
        relation='siswa_registrasi_biaya_ta_jenjang_rel',
        column1='biaya_registrasi_id',
        column2='biaya_ta_jenjang_id',
        string="Biaya")
    total_biaya = fields.Float('Total', default=0.0)
    total_biaya_alt = fields.Float('Total (alt)', default=0.0)

    @api.multi
    def write(self, vals):

        pprint(vals)
        if 'biaya_ta_jenjang_ids' in vals:
            by_ta_jj_ids = vals['biaya_ta_jenjang_ids'][0][2]
            get_biaya_ta_jenjang_ids = self.env[
                'siswa_keu_ocb11.biaya_ta_jenjang'].search([('id', 'in',
                                                             by_ta_jj_ids)])

            total_biaya_val = 0
            total_biaya_alt_val = 0

            for biaya in get_biaya_ta_jenjang_ids:
                total_biaya_val += biaya.harga
                total_biaya_alt_val += biaya.harga_alt

            vals['total_biaya'] = total_biaya_val
            vals['total_biaya_alt'] = total_biaya_alt_val

#             print('ada perubahan')
#             data_vals = vals['biaya_ta_jenjang_ids'][0][2]
#             for data in data_vals:
#                 print(data)

#         # update total_biaya
#         print('Update total biaya')
#         biaya_reg_id = self.env['siswa_psb_ocb11.biaya_registrasi'].search([('id', '=', self.id)])

#         for by_reg in biaya_reg_id:
#             if by_reg.biaya_ta_jenjang_ids:
#                 total_biaya_val = 0
#                 total_biaya_alt_val = 0
#
#                 for biaya in by_reg.biaya_ta_jenjang_ids:
#                     total_biaya_val += biaya.harga
#                     total_biaya_alt_val += biaya.harga_alt
#                     print(biaya.biaya_id.name)
#                     print(biaya.harga)
#                     print(biaya.harga_alt)
#                     print('------------------------------')
#
#                 print('Total Biaya : ' + str(total_biaya_val))
#                 print('Total Biaya : ' + str(total_biaya_alt_val))
#
#                 vals['total_biaya'] = total_biaya_val
#                 vals['total_biaya_alt'] = total_biaya_alt_val

        result = super(biaya_registrasi, self).write(vals)

        return result

    @api.model
    def create(self, vals):
        tahunajaran = self.env['siswa_ocb11.tahunajaran']
        jenjang = self.env['siswa_ocb11.jenjang']

        vals['name'] = str(
            tahunajaran.search(
                [('id', '=', vals['tahunajaran_id'])]).name) + " " + str(
                    jenjang.search([('id', '=', vals['jenjang_id'])]).name)
        result = super(biaya_registrasi, self).create(vals)

        return result

    @api.onchange('tahunajaran_id')
    def pembayaran_id_onchange(self):
        return self.recompute_biaya_ta_jenjang()

    @api.onchange('jenjang_id')
    def jenjang_id_onchange(self):
        return self.recompute_biaya_ta_jenjang()

    def recompute_biaya_ta_jenjang(self):
        domain = {
            'biaya_ta_jenjang_ids':
            [('tahunajaran_id', '=', self.tahunajaran_id.id),
             ('jenjang_id', '=', self.jenjang_id.id)]
        }
        return {'domain': domain, 'value': {'biaya_ta_jenjang_ids': []}}

    def generate_biaya_registrasi(self):
        print('Generate Biaya Registrasi')
        get_biaya_ta_jenjang_ids = self.env[
            'siswa_keu_ocb11.biaya_ta_jenjang'].search([
                ('tahunajaran_id', '=', self.tahunajaran_id.id),
                ('jenjang_id', '=', self.jenjang_id.id),
            ])
        #         self.biaya_ta_jenjang_ids =  [(0, 0, get_biaya_ta_jenjang_ids )]
        biaya_ids = []
        for by in get_biaya_ta_jenjang_ids:
            biaya_ids.append(by.id)

        self.biaya_ta_jenjang_ids = [(6, 0, biaya_ids)]

    @api.model
    def generate_init_on_install(self):
        tahunajaran_ids = self.env['siswa_ocb11.tahunajaran'].search([
            ('active', 'ilike', '%')
        ])
        jenjang_ids = self.env['siswa_ocb11.jenjang'].search([('id', 'ilike',
                                                               '%')])

        print('---------------------------------------')
        print('GENERATE INIT BIAYA REGISTRASI ON INSTALLING MODULE ')
        print('---------------------------------------')

        for ta in tahunajaran_ids:
            for jj in jenjang_ids:
                #                 print(jj.name + '   ' + ta.name)
                biaya_registrasi_ids = self.env[
                    'siswa_psb_ocb11.biaya_registrasi'].search([
                        ('tahunajaran_id', '=', ta.id),
                        ('jenjang_id', '=', jj.id),
                    ])
                if not biaya_registrasi_ids:
                    # create new data
                    new_biaya_registrasi = self.env[
                        'siswa_psb_ocb11.biaya_registrasi'].create({
                            'tahunajaran_id':
                            ta.id,
                            'jenjang_id':
                            jj.id
                        })
                    new_biaya_registrasi.recompute_biaya_ta_jenjang()