Exemple #1
0
class SaleOrderTemplateLine(models.Model):
    _name = "sale.order.template.line"
    _description = "Quotation Template Line"
    _order = 'sale_order_template_id, sequence, id'

    sequence = fields.Integer(
        'Sequence',
        help=
        "Gives the sequence order when displaying a list of sale quote lines.",
        default=10)
    sale_order_template_id = fields.Many2one('sale.order.template',
                                             'Quotation Template Reference',
                                             required=True,
                                             ondelete='cascade',
                                             index=True)
    name = fields.Text('Description', required=True, translate=True)
    product_id = fields.Many2one('product.product',
                                 'Product',
                                 domain=[('sale_ok', '=', True)])
    price_unit = fields.Float('Unit Price',
                              required=True,
                              digits=dp.get_precision('Product Price'))
    discount = fields.Float('Discount (%)',
                            digits=dp.get_precision('Discount'),
                            default=0.0)
    product_uom_qty = fields.Float('Quantity',
                                   required=True,
                                   digits=dp.get_precision('Product UoS'),
                                   default=1)
    product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure')

    display_type = fields.Selection([('line_section', "Section"),
                                     ('line_note', "Note")],
                                    default=False,
                                    help="Technical field for UX purpose.")

    @api.onchange('product_id')
    def _onchange_product_id(self):
        self.ensure_one()
        if self.product_id:
            name = self.product_id.name_get()[0][1]
            if self.product_id.description_sale:
                name += '\n' + self.product_id.description_sale
            self.name = name
            self.price_unit = self.product_id.lst_price
            self.product_uom_id = self.product_id.uom_id.id
            domain = {
                'product_uom_id':
                [('category_id', '=', self.product_id.uom_id.category_id.id)]
            }
            return {'domain': domain}

    @api.onchange('product_uom_id')
    def _onchange_product_uom(self):
        if self.product_id and self.product_uom_id:
            self.price_unit = self.product_id.uom_id._compute_price(
                self.product_id.lst_price, self.product_uom_id)

    @api.model
    def create(self, values):
        if values.get('display_type',
                      self.default_get(['display_type'])['display_type']):
            values.update(product_id=False,
                          price_unit=0,
                          product_uom_qty=0,
                          product_uom_id=False)
        return super(SaleOrderTemplateLine, self).create(values)

    @api.multi
    def write(self, values):
        if 'display_type' in values and self.filtered(
                lambda line: line.display_type != values.get('display_type')):
            raise UserError(
                _("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type."
                  ))
        return super(SaleOrderTemplateLine, self).write(values)

    _sql_constraints = [
        ('accountable_product_id_required',
         "CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND product_uom_id IS NOT NULL))",
         "Missing required product and UoM on accountable sale quote line."),
        ('non_accountable_fields_null',
         "CHECK(display_type IS NULL OR (product_id IS NULL AND price_unit = 0 AND product_uom_qty = 0 AND product_uom_id IS NULL))",
         "Forbidden product, unit price, quantity, and UoM on non-accountable sale quote line"
         ),
    ]
Exemple #2
0
class HrExpenseSheet(models.Model):
    """
        Here are the rights associated with the expense flow

        Action       Group                   Restriction
        =================================================================================
        Submit      Employee                Only his own
                    Officer                 If he is expense manager of the employee, manager of the employee
                                             or the employee is in the department managed by the officer
                    Manager                 Always
        Approve     Officer                 Not his own and he is expense manager of the employee, manager of the employee
                                             or the employee is in the department managed by the officer
                    Manager                 Always
        Post        Anybody                 State = approve and journal_id defined
        Done        Anybody                 State = approve and journal_id defined
        Cancel      Officer                 Not his own and he is expense manager of the employee, manager of the employee
                                             or the employee is in the department managed by the officer
                    Manager                 Always
        =================================================================================
    """
    _name = "hr.expense.sheet"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = "Expense Report"
    _order = "accounting_date desc, id desc"

    @api.model
    def _default_journal_id(self):
        journal = self.env.ref('hr_expense.hr_expense_account_journal',
                               raise_if_not_found=False)
        if not journal:
            journal = self.env['account.journal'].search(
                [('type', '=', 'purchase')], limit=1)
        return journal.id

    @api.model
    def _default_bank_journal_id(self):
        return self.env['account.journal'].search(
            [('type', 'in', ['cash', 'bank'])], limit=1)

    name = fields.Char('Expense Report Summary', required=True)
    expense_line_ids = fields.One2many('hr.expense',
                                       'sheet_id',
                                       string='Expense Lines',
                                       states={
                                           'approve': [('readonly', True)],
                                           'done': [('readonly', True)],
                                           'post': [('readonly', True)]
                                       },
                                       copy=False)
    state = fields.Selection([('draft', 'Draft'), ('submit', 'Submitted'),
                              ('approve', 'Approved'), ('post', 'Posted'),
                              ('done', 'Paid'), ('cancel', 'Refused')],
                             string='Status',
                             index=True,
                             readonly=True,
                             track_visibility='onchange',
                             copy=False,
                             default='draft',
                             required=True,
                             help='Expense Report State')
    employee_id = fields.Many2one(
        'hr.employee',
        string="Employee",
        required=True,
        readonly=True,
        states={'draft': [('readonly', False)]},
        default=lambda self: self.env['hr.employee'].search(
            [('user_id', '=', self.env.uid)], limit=1))
    address_id = fields.Many2one('res.partner', string="Employee Home Address")
    payment_mode = fields.Selection(
        [("own_account", "Employee (to reimburse)"),
         ("company_account", "Company")],
        related='expense_line_ids.payment_mode',
        default='own_account',
        readonly=True,
        string="Paid By")
    user_id = fields.Many2one('res.users',
                              'Manager',
                              readonly=True,
                              copy=False,
                              states={'draft': [('readonly', False)]},
                              track_visibility='onchange',
                              oldname='responsible_id')
    total_amount = fields.Monetary('Total Amount',
                                   currency_field='currency_id',
                                   compute='_compute_amount',
                                   store=True,
                                   digits=dp.get_precision('Account'))
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True,
                                 states={'draft': [('readonly', False)]},
                                 default=lambda self: self.env.user.company_id)
    currency_id = fields.Many2one(
        'res.currency',
        string='Currency',
        readonly=True,
        states={'draft': [('readonly', False)]},
        default=lambda self: self.env.user.company_id.currency_id)
    attachment_number = fields.Integer(compute='_compute_attachment_number',
                                       string='Number of Attachments')
    journal_id = fields.Many2one(
        'account.journal',
        string='Expense Journal',
        states={
            'done': [('readonly', True)],
            'post': [('readonly', True)]
        },
        default=_default_journal_id,
        help="The journal used when the expense is done.")
    bank_journal_id = fields.Many2one(
        'account.journal',
        string='Bank Journal',
        states={
            'done': [('readonly', True)],
            'post': [('readonly', True)]
        },
        default=_default_bank_journal_id,
        help="The payment method used when the expense is paid by the company."
    )
    accounting_date = fields.Date("Date")
    account_move_id = fields.Many2one('account.move',
                                      string='Journal Entry',
                                      ondelete='restrict',
                                      copy=False)
    department_id = fields.Many2one('hr.department',
                                    string='Department',
                                    states={
                                        'post': [('readonly', True)],
                                        'done': [('readonly', True)]
                                    })
    is_multiple_currency = fields.Boolean(
        "Handle lines with different currencies",
        compute='_compute_is_multiple_currency')
    can_reset = fields.Boolean('Can Reset', compute='_compute_can_reset')

    @api.depends('expense_line_ids.total_amount_company')
    def _compute_amount(self):
        for sheet in self:
            sheet.total_amount = sum(
                sheet.expense_line_ids.mapped('total_amount_company'))

    @api.multi
    def _compute_attachment_number(self):
        for sheet in self:
            sheet.attachment_number = sum(
                sheet.expense_line_ids.mapped('attachment_number'))

    @api.depends('expense_line_ids.currency_id')
    def _compute_is_multiple_currency(self):
        for sheet in self:
            sheet.is_multiple_currency = len(
                sheet.expense_line_ids.mapped('currency_id')) > 1

    @api.multi
    def _compute_can_reset(self):
        is_expense_user = self.user_has_groups(
            'hr_expense.group_hr_expense_user')
        for sheet in self:
            sheet.can_reset = is_expense_user if is_expense_user else sheet.employee_id.user_id == self.env.user

    @api.onchange('employee_id')
    def _onchange_employee_id(self):
        self.address_id = self.employee_id.sudo().address_home_id
        self.department_id = self.employee_id.department_id
        self.user_id = self.employee_id.expense_manager_id or self.employee_id.parent_id.user_id

    @api.multi
    @api.constrains('expense_line_ids')
    def _check_payment_mode(self):
        for sheet in self:
            expense_lines = sheet.mapped('expense_line_ids')
            if expense_lines and any(
                    expense.payment_mode != expense_lines[0].payment_mode
                    for expense in expense_lines):
                raise ValidationError(
                    _("Expenses must be paid by the same entity (Company or employee)."
                      ))

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

    @api.model
    def create(self, vals):
        sheet = super(
            HrExpenseSheet,
            self.with_context(mail_create_nosubscribe=True)).create(vals)
        sheet.activity_update()
        return sheet

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

    # --------------------------------------------
    # Mail Thread
    # --------------------------------------------

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

    def _message_auto_subscribe_followers(self, updated_values, subtype_ids):
        res = super(HrExpenseSheet, self)._message_auto_subscribe_followers(
            updated_values, subtype_ids)
        if updated_values.get('employee_id'):
            employee = self.env['hr.employee'].browse(
                updated_values['employee_id'])
            if employee.user_id:
                res.append(
                    (employee.user_id.partner_id.id, subtype_ids, False))
        return res

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

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

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

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

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

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

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

    # --------------------------------------------
    # Business
    # --------------------------------------------

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

    @api.multi
    def action_submit_sheet(self):
        self.write({'state': 'submit'})
        self.activity_update()

    @api.multi
    def approve_expense_sheets(self):
        if not self.user_has_groups('hr_expense.group_hr_expense_user'):
            raise UserError(
                _("Only Managers and HR Officers can approve expenses"))
        elif not self.user_has_groups('hr_expense.group_hr_expense_manager'):
            current_managers = self.employee_id.parent_id.user_id | self.employee_id.department_id.manager_id.user_id | self.employee_id.expense_manager_id

            if self.employee_id.user_id == self.env.user:
                raise UserError(_("You cannot approve your own expenses"))

            if not self.env.user in current_managers:
                raise UserError(
                    _("You can only approve your department expenses"))

        responsible_id = self.user_id.id or self.env.user.id
        self.write({'state': 'approve', 'user_id': responsible_id})
        self.activity_update()

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

    @api.multi
    def refuse_sheet(self, reason):
        if not self.user_has_groups('hr_expense.group_hr_expense_user'):
            raise UserError(
                _("Only Managers and HR Officers can approve expenses"))
        elif not self.user_has_groups('hr_expense.group_hr_expense_manager'):
            current_managers = self.employee_id.parent_id.user_id | self.employee_id.department_id.manager_id.user_id | self.employee_id.expense_manager_id

            if self.employee_id.user_id == self.env.user:
                raise UserError(_("You cannot refuse your own expenses"))

            if not self.env.user in current_managers:
                raise UserError(
                    _("You can only refuse your department expenses"))

        self.write({'state': 'cancel'})
        for sheet in self:
            sheet.message_post_with_view(
                'hr_expense.hr_expense_template_refuse_reason',
                values={
                    'reason': reason,
                    'is_sheet': True,
                    'name': self.name
                })
        self.activity_update()

    @api.multi
    def reset_expense_sheets(self):
        if not self.can_reset:
            raise UserError(
                _("Only HR Officers or the concerned employee can reset to draft."
                  ))
        self.mapped('expense_line_ids').write({'is_refused': False})
        self.write({'state': 'draft'})
        self.activity_update()
        return True

    def _get_responsible_for_approval(self):
        if self.user_id:
            return self.user_id
        elif self.employee_id.parent_id.user_id:
            return self.employee_id.parent_id.user_id
        elif self.employee_id.department_id.manager_id.user_id:
            return self.employee_id.department_id.manager_id.user_id
        return self.env['res.users']

    def activity_update(self):
        for expense_report in self.filtered(lambda hol: hol.state == 'submit'):
            self.activity_schedule('hr_expense.mail_act_expense_approval',
                                   user_id=expense_report.sudo(
                                   )._get_responsible_for_approval().id
                                   or self.env.user.id)
        self.filtered(lambda hol: hol.state == 'approve').activity_feedback(
            ['hr_expense.mail_act_expense_approval'])
        self.filtered(lambda hol: hol.state == 'cancel').activity_unlink(
            ['hr_expense.mail_act_expense_approval'])
Exemple #3
0
class EventTicket(models.Model):
    _name = 'event.event.ticket'
    _description = 'Event Ticket'

    def _default_product_id(self):
        return self.env.ref('event_sale.product_product_event', raise_if_not_found=False)

    name = fields.Char(string='Name', required=True, translate=True)
    event_type_id = fields.Many2one('event.type', string='Event Category', ondelete='cascade')
    event_id = fields.Many2one('event.event', string="Event", ondelete='cascade')
    product_id = fields.Many2one('product.product', string='Product',
        required=True, domain=[("event_ok", "=", True)],
        default=_default_product_id)
    registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations')
    price = fields.Float(string='Price', digits=dp.get_precision('Product Price'))
    deadline = fields.Date(string="Sales End")
    is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired')

    price_reduce = fields.Float(string="Price Reduce", compute="_compute_price_reduce", digits=dp.get_precision('Product Price'))
    price_reduce_taxinc = fields.Float(compute='_get_price_reduce_tax', string='Price Reduce Tax inc')
    # seats fields
    seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')],
        string='Available Seat', required=True, store=True, compute='_compute_seats', default="limited")
    seats_max = fields.Integer(string='Maximum Available Seats',
       help="Define the number of available tickets. If you have too much registrations you will "
            "not be able to sell tickets anymore. Set 0 to ignore this rule set as unlimited.")
    seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=True)
    seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=True)
    seats_unconfirmed = fields.Integer(string='Unconfirmed Seat Reservations', compute='_compute_seats', store=True)
    seats_used = fields.Integer(compute='_compute_seats', store=True)

    @api.multi
    def _compute_is_expired(self):
        for record in self:
            if record.deadline:
                current_date = fields.Date.context_today(record.with_context(tz=record.event_id.date_tz))
                record.is_expired = record.deadline < current_date
            else:
                record.is_expired = False

    @api.multi
    def _compute_price_reduce(self):
        for record in self:
            product = record.product_id
            discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0
            record.price_reduce = (1.0 - discount) * record.price

    def _get_price_reduce_tax(self):
        for record in self:
            # sudo necessary here since the field is most probably accessed through the website
            tax_ids = record.sudo().product_id.taxes_id.filtered(lambda r: r.company_id == record.event_id.company_id)
            taxes = tax_ids.compute_all(record.price_reduce, record.event_id.company_id.currency_id, 1.0, product=record.product_id)
            record.price_reduce_taxinc = taxes['total_included']

    @api.multi
    @api.depends('seats_max', 'registration_ids.state')
    def _compute_seats(self):
        """ Determine reserved, available, reserved but unconfirmed and used seats. """
        # initialize fields to 0 + compute seats availability
        for ticket in self:
            ticket.seats_availability = 'unlimited' if ticket.seats_max == 0 else 'limited'
            ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0
        # aggregate registrations by ticket and by state
        if self.ids:
            state_field = {
                'draft': 'seats_unconfirmed',
                'open': 'seats_reserved',
                'done': 'seats_used',
            }
            query = """ SELECT event_ticket_id, state, count(event_id)
                        FROM event_registration
                        WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done')
                        GROUP BY event_ticket_id, state
                    """
            self.env.cr.execute(query, (tuple(self.ids),))
            for event_ticket_id, state, num in self.env.cr.fetchall():
                ticket = self.browse(event_ticket_id)
                ticket[state_field[state]] += num
        # compute seats_available
        for ticket in self:
            if ticket.seats_max > 0:
                ticket.seats_available = ticket.seats_max - (ticket.seats_reserved + ticket.seats_used)

    @api.multi
    @api.constrains('registration_ids', 'seats_max')
    def _check_seats_limit(self):
        for record in self:
            if record.seats_max and record.seats_available < 0:
                raise ValidationError(_('No more available seats for this ticket type.'))

    @api.constrains('event_type_id', 'event_id')
    def _constrains_event(self):
        if any(ticket.event_type_id and ticket.event_id for ticket in self):
            raise UserError(_('Ticket cannot belong to both the event category and the event itself.'))

    @api.onchange('product_id')
    def _onchange_product_id(self):
        self.price = self.product_id.list_price or 0

    def get_ticket_multiline_description_sale(self):
        """ Compute a multiline description of this ticket, in the context of sales.
            It will often be used as the default description of a sales order line referencing this ticket.

        1. the first line is the ticket name
        2. the second line is the event name (if it exists, which should be the case with a normal workflow) or the product name (if it exists)

        We decided to ignore entirely the product name and the product description_sale because they are considered to be replaced by the ticket name and event name.
            -> the workflow of creating a new event also does not lead to filling them correctly, as the product is created through the event interface
        """

        name = self.display_name

        if self.event_id:
            name += '\n' + self.event_id.display_name
        elif self.product_id:
            name += '\n' + self.product_id.display_name

        return name
Exemple #4
0
class ProductTemplate(models.Model):
    _inherit = [
        "product.template", "website.seo.metadata",
        'website.published.multi.mixin', 'rating.mixin'
    ]
    _name = 'product.template'
    _mail_post_access = 'read'

    website_description = fields.Html('Description for the website',
                                      sanitize_attributes=False,
                                      translate=html_translate)
    alternative_product_ids = fields.Many2many(
        'product.template',
        'product_alternative_rel',
        'src_id',
        'dest_id',
        string='Alternative Products',
        help='Suggest alternatives to your customer'
        '(upsell strategy).Those product show up on the product page.')
    accessory_product_ids = fields.Many2many(
        'product.product',
        'product_accessory_rel',
        'src_id',
        'dest_id',
        string='Accessory Products',
        help='Accessories show up when the customer'
        'reviews the cart before payment (cross-sell strategy).')
    website_size_x = fields.Integer('Size X', default=1)
    website_size_y = fields.Integer('Size Y', default=1)
    website_style_ids = fields.Many2many('product.style', string='Styles')
    website_sequence = fields.Integer(
        'Website Sequence',
        help="Determine the display order in the Website E-commerce",
        default=lambda self: self._default_website_sequence(),
        copy=False)
    public_categ_ids = fields.Many2many(
        'product.public.category',
        string='Website Product Category',
        help=
        "The product will be available in each mentioned e-commerce category. Go to"
        "Shop > Customize and enable 'E-commerce categories' to view all e-commerce categories."
    )
    product_image_ids = fields.One2many('product.image',
                                        'product_tmpl_id',
                                        string='Images')

    # website_price deprecated, directly use _get_combination_info instead
    website_price = fields.Float('Website price',
                                 compute='_website_price',
                                 digits=dp.get_precision('Product Price'))
    # website_public_price deprecated, directly use _get_combination_info instead
    website_public_price = fields.Float(
        'Website public price',
        compute='_website_price',
        digits=dp.get_precision('Product Price'))
    # website_price_difference deprecated, directly use _get_combination_info instead
    website_price_difference = fields.Boolean('Website price difference',
                                              compute='_website_price')

    def _website_price(self):
        current_website = self.env['website'].get_current_website()
        for template in self.with_context(website_id=current_website.id):
            res = template._get_combination_info()
            template.website_price = res.get('price')
            template.website_public_price = res.get('list_price')
            template.website_price_difference = res.get('has_discounted_price')

    @api.multi
    def _has_no_variant_attributes(self):
        """Return whether this `product.template` has at least one no_variant
        attribute.

        :return: True if at least one no_variant attribute, False otherwise
        :rtype: bool
        """
        self.ensure_one()
        return any(a.create_variant == 'no_variant'
                   for a in self._get_valid_product_attributes())

    @api.multi
    def _has_is_custom_values(self):
        self.ensure_one()
        """Return whether this `product.template` has at least one is_custom
        attribute value.

        :return: True if at least one is_custom attribute value, False otherwise
        :rtype: bool
        """
        return any(v.is_custom
                   for v in self._get_valid_product_attribute_values())

    @api.multi
    def _is_quick_add_to_cart_possible(self, parent_combination=None):
        """
        It's possible to quickly add to cart if there's no optional product,
        there's only one possible combination and no value is set to is_custom.

        Attributes set to dynamic or no_variant don't have to be tested
        specifically because they will be taken into account when checking for
        the possible combinations.

        :param parent_combination: combination from which `self` is an
            optional or accessory product
        :type parent_combination: recordset `product.template.attribute.value`

        :return: True if it's possible to quickly add to cart, else False
        :rtype: bool
        """
        self.ensure_one()

        if not self._is_add_to_cart_possible(parent_combination):
            return False
        gen = self._get_possible_combinations(parent_combination)
        first_possible_combination = next(gen)
        if next(gen, False) is not False:
            # there are at least 2 possible combinations.
            return False
        if self._has_is_custom_values():
            return False
        if self.optional_product_ids.filtered(
                lambda p: p._is_add_to_cart_possible(first_possible_combination
                                                     )):
            return False
        return True

    @api.multi
    def _get_possible_variants_sorted(self, parent_combination=None):
        """Return the sorted recordset of variants that are possible.

        The order is based on the order of the attributes and their values.

        See `_get_possible_variants` for the limitations of this method with
        dynamic or no_variant attributes, and also for a warning about
        performances.

        :param parent_combination: combination from which `self` is an
            optional or accessory product
        :type parent_combination: recordset `product.template.attribute.value`

        :return: the sorted variants that are possible
        :rtype: recordset of `product.product`
        """
        self.ensure_one()

        def _sort_key_attribute_value(value):
            # if you change this order, keep it in sync with _order from `product.attribute`
            return (value.attribute_id.sequence, value.attribute_id.id)

        def _sort_key_variant(variant):
            """
                We assume all variants will have the same attributes, with only one value for each.
                    - first level sort: same as "product.attribute"._order
                    - second level sort: same as "product.attribute.value"._order
            """
            keys = []
            for attribute in variant.attribute_value_ids.sorted(
                    _sort_key_attribute_value):
                # if you change this order, keep it in sync with _order from `product.attribute.value`
                keys.append(attribute.sequence)
                keys.append(attribute.id)
            return keys

        return self._get_possible_variants(parent_combination).sorted(
            _sort_key_variant)

    @api.multi
    def _get_combination_info(self,
                              combination=False,
                              product_id=False,
                              add_qty=1,
                              pricelist=False,
                              parent_combination=False,
                              only_template=False):
        """Override for website, where we want to:
            - take the website pricelist if no pricelist is set
            - apply the b2b/b2c setting to the result

        This will work when adding website_id to the context, which is done
        automatically when called from routes with website=True.
        """
        self.ensure_one()

        current_website = False

        if self.env.context.get('website_id'):
            current_website = self.env['website'].get_current_website()
            if not pricelist:
                pricelist = current_website.get_current_pricelist()

        combination_info = super(ProductTemplate, self)._get_combination_info(
            combination=combination,
            product_id=product_id,
            add_qty=add_qty,
            pricelist=pricelist,
            parent_combination=parent_combination,
            only_template=only_template)

        if self.env.context.get('website_id'):
            partner = self.env.user.partner_id
            company_id = current_website.company_id
            product = self.env['product.product'].browse(
                combination_info['product_id']) or self

            tax_display = self.env.user.has_group(
                'account.group_show_line_subtotals_tax_excluded'
            ) and 'total_excluded' or 'total_included'
            Fpos_sudo = self.env['account.fiscal.position'].sudo()
            fpos_id = Fpos_sudo.with_context(
                force_company=company_id.id).get_fiscal_position(partner.id)
            taxes = Fpos_sudo.browse(fpos_id).map_tax(
                product.sudo().taxes_id.filtered(
                    lambda x: x.company_id == company_id), product, partner)

            # The list_price is always the price of one.
            quantity_1 = 1
            combination_info['price'] = self.env[
                'account.tax']._fix_tax_included_price_company(
                    combination_info['price'],
                    product.sudo().taxes_id, taxes, company_id)
            price = taxes.compute_all(combination_info['price'],
                                      pricelist.currency_id, quantity_1,
                                      product, partner)[tax_display]
            if pricelist.discount_policy == 'without_discount':
                combination_info['list_price'] = self.env[
                    'account.tax']._fix_tax_included_price_company(
                        combination_info['list_price'],
                        product.sudo().taxes_id, taxes, company_id)
                list_price = taxes.compute_all(combination_info['list_price'],
                                               pricelist.currency_id,
                                               quantity_1, product,
                                               partner)[tax_display]
            else:
                list_price = price
            has_discounted_price = pricelist.currency_id.compare_amounts(
                list_price, price) == 1

            combination_info.update(
                price=price,
                list_price=list_price,
                has_discounted_price=has_discounted_price,
            )

        return combination_info

    @api.multi
    def _create_first_product_variant(self, log_warning=False):
        """Create if necessary and possible and return the first product
        variant for this template.

        :param log_warning: whether a warning should be logged on fail
        :type log_warning: bool

        :return: the first product variant or none
        :rtype: recordset of `product.product`
        """
        return self._create_product_variant(
            self._get_first_possible_combination(), log_warning)

    @api.multi
    def _get_current_company_fallback(self, **kwargs):
        """Override: if a website is set on the product or given, fallback to
        the company of the website. Otherwise use the one from parent method."""
        res = super(ProductTemplate,
                    self)._get_current_company_fallback(**kwargs)
        website = self.website_id or kwargs.get('website')
        return website and website.company_id or res

    def _default_website_sequence(self):
        self._cr.execute("SELECT MIN(website_sequence) FROM %s" % self._table)
        min_sequence = self._cr.fetchone()[0]
        return min_sequence and min_sequence - 1 or 10

    def set_sequence_top(self):
        self.website_sequence = self.sudo().search(
            [], order='website_sequence desc', limit=1).website_sequence + 1

    def set_sequence_bottom(self):
        self.website_sequence = self.sudo().search(
            [], order='website_sequence', limit=1).website_sequence - 1

    def set_sequence_up(self):
        previous_product_tmpl = self.sudo().search(
            [('website_sequence', '>', self.website_sequence),
             ('website_published', '=', self.website_published)],
            order='website_sequence',
            limit=1)
        if previous_product_tmpl:
            previous_product_tmpl.website_sequence, self.website_sequence = self.website_sequence, previous_product_tmpl.website_sequence
        else:
            self.set_sequence_top()

    def set_sequence_down(self):
        next_prodcut_tmpl = self.search(
            [('website_sequence', '<', self.website_sequence),
             ('website_published', '=', self.website_published)],
            order='website_sequence desc',
            limit=1)
        if next_prodcut_tmpl:
            next_prodcut_tmpl.website_sequence, self.website_sequence = self.website_sequence, next_prodcut_tmpl.website_sequence
        else:
            return self.set_sequence_bottom()

    def _default_website_meta(self):
        res = super(ProductTemplate, self)._default_website_meta()
        res['default_opengraph']['og:description'] = res['default_twitter'][
            'twitter:description'] = self.description_sale
        res['default_opengraph']['og:title'] = res['default_twitter'][
            'twitter:title'] = self.name
        res['default_opengraph']['og:image'] = res['default_twitter'][
            'twitter:image'] = "/web/image/product.template/%s/image" % (
                self.id)
        return res

    @api.multi
    def _compute_website_url(self):
        super(ProductTemplate, self)._compute_website_url()
        for product in self:
            product.website_url = "/shop/product/%s" % (product.id, )
Exemple #5
0
class HrExpense(models.Model):

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

    @api.model
    def _default_employee_id(self):
        return self.env['hr.employee'].search([('user_id', '=', self.env.uid)],
                                              limit=1)

    @api.model
    def _default_product_uom_id(self):
        return self.env['uom.uom'].search([], limit=1, order='id')

    @api.model
    def _default_account_id(self):
        return self.env['ir.property'].get('property_account_expense_categ_id',
                                           'product.category')

    @api.model
    def _get_employee_id_domain(self):
        res = [('id', '=', 0)]  # Nothing accepted by domain, by default
        if self.user_has_groups(
                'hr_expense.group_hr_expense_manager') or self.user_has_groups(
                    'account.group_account_user'):
            res = []  # Then, domain accepts everything
        elif self.user_has_groups('hr_expense.group_hr_expense_user'
                                  ) and self.env.user.employee_ids:
            user = self.env.user
            employee = user.employee_ids[0]
            res = [
                '|',
                '|',
                '|',
                ('department_id.manager_id', '=', employee.id),
                ('parent_id', '=', employee.id),
                ('id', '=', employee.id),
                ('expense_manager_id', '=', user.id),
            ]
        elif self.env.user.employee_ids:
            employee = self.env.user.employee_ids[0]
            res = [('id', '=', employee.id)]
        return res

    name = fields.Char('Description',
                       readonly=True,
                       required=True,
                       states={
                           'draft': [('readonly', False)],
                           'reported': [('readonly', False)],
                           'refused': [('readonly', False)]
                       })
    date = fields.Date(readonly=True,
                       states={
                           'draft': [('readonly', False)],
                           'reported': [('readonly', False)],
                           'refused': [('readonly', False)]
                       },
                       default=fields.Date.context_today,
                       string="Date")
    employee_id = fields.Many2one(
        'hr.employee',
        string="Employee",
        required=True,
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'reported': [('readonly', False)],
            'refused': [('readonly', False)]
        },
        default=_default_employee_id,
        domain=lambda self: self._get_employee_id_domain())
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 readonly=True,
                                 states={
                                     'draft': [('readonly', False)],
                                     'reported': [('readonly', False)],
                                     'refused': [('readonly', False)]
                                 },
                                 domain=[('can_be_expensed', '=', True)],
                                 required=True)
    product_uom_id = fields.Many2one('uom.uom',
                                     string='Unit of Measure',
                                     required=True,
                                     readonly=True,
                                     states={
                                         'draft': [('readonly', False)],
                                         'refused': [('readonly', False)]
                                     },
                                     default=_default_product_uom_id)
    unit_amount = fields.Float("Unit Price",
                               readonly=True,
                               required=True,
                               states={
                                   'draft': [('readonly', False)],
                                   'reported': [('readonly', False)],
                                   'refused': [('readonly', False)]
                               },
                               digits=dp.get_precision('Product Price'))
    quantity = fields.Float(required=True,
                            readonly=True,
                            states={
                                'draft': [('readonly', False)],
                                'reported': [('readonly', False)],
                                'refused': [('readonly', False)]
                            },
                            digits=dp.get_precision('Product Unit of Measure'),
                            default=1)
    tax_ids = fields.Many2many('account.tax',
                               'expense_tax',
                               'expense_id',
                               'tax_id',
                               string='Taxes',
                               states={
                                   'done': [('readonly', True)],
                                   'post': [('readonly', True)]
                               })
    untaxed_amount = fields.Float("Subtotal",
                                  store=True,
                                  compute='_compute_amount',
                                  digits=dp.get_precision('Account'))
    total_amount = fields.Monetary("Total",
                                   compute='_compute_amount',
                                   store=True,
                                   currency_field='currency_id',
                                   digits=dp.get_precision('Account'))
    company_currency_id = fields.Many2one('res.currency',
                                          string="Report Company Currency",
                                          related='sheet_id.currency_id',
                                          store=True,
                                          readonly=False)
    total_amount_company = fields.Monetary(
        "Total (Company Currency)",
        compute='_compute_total_amount_company',
        store=True,
        currency_field='company_currency_id',
        digits=dp.get_precision('Account'))
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True,
                                 states={
                                     'draft': [('readonly', False)],
                                     'refused': [('readonly', False)]
                                 },
                                 default=lambda self: self.env.user.company_id)
    currency_id = fields.Many2one(
        'res.currency',
        string='Currency',
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'refused': [('readonly', False)]
        },
        default=lambda self: self.env.user.company_id.currency_id)
    analytic_account_id = fields.Many2one('account.analytic.account',
                                          string='Analytic Account',
                                          states={
                                              'post': [('readonly', True)],
                                              'done': [('readonly', True)]
                                          },
                                          oldname='analytic_account')
    analytic_tag_ids = fields.Many2many('account.analytic.tag',
                                        string='Analytic Tags',
                                        states={
                                            'post': [('readonly', True)],
                                            'done': [('readonly', True)]
                                        })
    account_id = fields.Many2one('account.account',
                                 string='Account',
                                 states={
                                     'post': [('readonly', True)],
                                     'done': [('readonly', True)]
                                 },
                                 default=_default_account_id,
                                 help="An expense account is expected")
    description = fields.Text('Notes...',
                              readonly=True,
                              states={
                                  'draft': [('readonly', False)],
                                  'reported': [('readonly', False)],
                                  'refused': [('readonly', False)]
                              })
    payment_mode = fields.Selection(
        [("own_account", "Employee (to reimburse)"),
         ("company_account", "Company")],
        default='own_account',
        states={
            'done': [('readonly', True)],
            'approved': [('readonly', True)],
            'reported': [('readonly', True)]
        },
        string="Paid By")
    attachment_number = fields.Integer('Number of Attachments',
                                       compute='_compute_attachment_number')
    state = fields.Selection([('draft', 'To Submit'),
                              ('reported', 'Submitted'),
                              ('approved', 'Approved'), ('done', 'Paid'),
                              ('refused', 'Refused')],
                             compute='_compute_state',
                             string='Status',
                             copy=False,
                             index=True,
                             readonly=True,
                             store=True,
                             help="Status of the expense.")
    sheet_id = fields.Many2one('hr.expense.sheet',
                               string="Expense Report",
                               readonly=True,
                               copy=False)
    reference = fields.Char("Bill Reference")
    is_refused = fields.Boolean(
        "Explicitely Refused by manager or acccountant",
        readonly=True,
        copy=False)

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

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

    @api.depends('date', 'total_amount', 'company_currency_id')
    def _compute_total_amount_company(self):
        for expense in self:
            amount = 0
            if expense.company_currency_id:
                date_expense = expense.date
                amount = expense.currency_id._convert(
                    expense.total_amount, expense.company_currency_id,
                    expense.company_id or expense.sheet_id.company_id,
                    date_expense or fields.Date.today())
            expense.total_amount_company = amount

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

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

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

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

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

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

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

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

    @api.multi
    def action_submit_expenses(self):
        if any(expense.state != 'draft' or expense.sheet_id
               for expense in self):
            raise UserError(_("You cannot report twice the same line!"))
        if len(self.mapped('employee_id')) != 1:
            raise UserError(
                _("You cannot report expenses for different employees in the same report."
                  ))

        todo = self.filtered(
            lambda x: x.payment_mode == 'own_account') or self.filtered(
                lambda x: x.payment_mode == 'company_account')
        return {
            'name': _('New Expense Report'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'hr.expense.sheet',
            'target': 'current',
            'context': {
                'default_expense_line_ids': todo.ids,
                'default_employee_id': self[0].employee_id.id,
                'default_name': todo[0].name if len(todo) == 1 else ''
            }
        }

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

    # ----------------------------------------
    # Business
    # ----------------------------------------

    @api.multi
    def _prepare_move_values(self):
        """
        This function prepares move values related to an expense
        """
        self.ensure_one()
        journal = self.sheet_id.bank_journal_id if self.payment_mode == 'company_account' else self.sheet_id.journal_id
        account_date = self.sheet_id.accounting_date or self.date
        move_values = {
            'journal_id': journal.id,
            'company_id': self.env.user.company_id.id,
            'date': account_date,
            'ref': self.sheet_id.name,
            # force the name to the default value, to avoid an eventual 'default_name' in the context
            # to set it to '' which cause no number to be given to the account.move when posted.
            'name': '/',
        }
        return move_values

    @api.multi
    def _get_account_move_by_sheet(self):
        """ Return a mapping between the expense sheet of current expense and its account move
            :returns dict where key is a sheet id, and value is an account move record
        """
        move_grouped_by_sheet = {}
        for expense in self:
            # create the move that will contain the accounting entries
            if expense.sheet_id.id not in move_grouped_by_sheet:
                move = self.env['account.move'].create(
                    expense._prepare_move_values())
                move_grouped_by_sheet[expense.sheet_id.id] = move
            else:
                move = move_grouped_by_sheet[expense.sheet_id.id]
        return move_grouped_by_sheet

    @api.multi
    def _get_expense_account_source(self):
        self.ensure_one()
        if self.account_id:
            account = self.account_id
        elif self.product_id:
            account = self.product_id.product_tmpl_id._get_product_accounts(
            )['expense']
            if not account:
                raise UserError(
                    _("No Expense account found for the product %s (or for its category), please configure one."
                      ) % (self.product_id.name))
        else:
            account = self.env['ir.property'].with_context(
                force_company=self.company_id.id).get(
                    'property_account_expense_categ_id', 'product.category')
            if not account:
                raise UserError(
                    _('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.'
                      ))
        return account

    @api.multi
    def _get_expense_account_destination(self):
        self.ensure_one()
        account_dest = self.env['account.account']
        if self.payment_mode == 'company_account':
            if not self.sheet_id.bank_journal_id.default_credit_account_id:
                raise UserError(
                    _("No credit account found for the %s journal, please configure one."
                      ) % (self.sheet_id.bank_journal_id.name))
            account_dest = self.sheet_id.bank_journal_id.default_credit_account_id.id
        else:
            if not self.employee_id.address_home_id:
                raise UserError(
                    _("No Home Address found for the employee %s, please configure one."
                      ) % (self.employee_id.name))
            account_dest = self.employee_id.address_home_id.property_account_payable_id.id
        return account_dest

    @api.multi
    def _get_account_move_line_values(self):
        move_line_values_by_expense = {}
        for expense in self:
            move_line_name = expense.employee_id.name + ': ' + expense.name.split(
                '\n')[0][:64]
            account_src = expense._get_expense_account_source()
            account_dst = expense._get_expense_account_destination()
            account_date = expense.sheet_id.accounting_date or expense.date or fields.Date.context_today(
                expense)

            company_currency = expense.company_id.currency_id
            different_currency = expense.currency_id and expense.currency_id != company_currency

            move_line_values = []
            taxes = expense.tax_ids.with_context(round=True).compute_all(
                expense.unit_amount, expense.currency_id, expense.quantity,
                expense.product_id)
            total_amount = 0.0
            total_amount_currency = 0.0
            partner_id = expense.employee_id.address_home_id.commercial_partner_id.id

            # source move line
            amount = taxes['total_excluded']
            amount_currency = False
            if different_currency:
                amount = expense.currency_id._convert(amount, company_currency,
                                                      expense.company_id,
                                                      account_date)
                amount_currency = taxes['total_excluded']
            move_line_src = {
                'name':
                move_line_name,
                'quantity':
                expense.quantity or 1,
                'debit':
                amount if amount > 0 else 0,
                'credit':
                -amount if amount < 0 else 0,
                'amount_currency':
                amount_currency if different_currency else 0.0,
                'account_id':
                account_src.id,
                'product_id':
                expense.product_id.id,
                'product_uom_id':
                expense.product_uom_id.id,
                'analytic_account_id':
                expense.analytic_account_id.id,
                'analytic_tag_ids': [(6, 0, expense.analytic_tag_ids.ids)],
                'expense_id':
                expense.id,
                'partner_id':
                partner_id,
                'tax_ids': [(6, 0, expense.tax_ids.ids)],
                'currency_id':
                expense.currency_id.id if different_currency else False,
            }
            move_line_values.append(move_line_src)
            total_amount += -move_line_src['debit'] or move_line_src['credit']
            total_amount_currency += -move_line_src[
                'amount_currency'] if move_line_src['currency_id'] else (
                    -move_line_src['debit'] or move_line_src['credit'])

            # taxes move lines
            for tax in taxes['taxes']:
                amount = tax['amount']
                amount_currency = False
                if different_currency:
                    amount = expense.currency_id._convert(
                        amount, company_currency, expense.company_id,
                        account_date)
                    amount_currency = tax['amount']
                move_line_tax_values = {
                    'name':
                    tax['name'],
                    'quantity':
                    1,
                    'debit':
                    amount if amount > 0 else 0,
                    'credit':
                    -amount if amount < 0 else 0,
                    'amount_currency':
                    amount_currency if different_currency else 0.0,
                    'account_id':
                    tax['account_id'] or move_line_src['account_id'],
                    'tax_line_id':
                    tax['id'],
                    'expense_id':
                    expense.id,
                    'partner_id':
                    partner_id,
                    'currency_id':
                    expense.currency_id.id if different_currency else False,
                    'analytic_account_id':
                    expense.analytic_account_id.id
                    if tax['analytic'] else False,
                    'analytic_tag_ids': [(6, 0, expense.analytic_tag_ids.ids)]
                    if tax['analytic'] else False,
                }
                total_amount -= amount
                total_amount_currency -= move_line_tax_values[
                    'amount_currency'] or amount
                move_line_values.append(move_line_tax_values)

            # destination move line
            move_line_dst = {
                'name':
                move_line_name,
                'debit':
                total_amount > 0 and total_amount,
                'credit':
                total_amount < 0 and -total_amount,
                'account_id':
                account_dst,
                'date_maturity':
                account_date,
                'amount_currency':
                total_amount_currency if different_currency else 0.0,
                'currency_id':
                expense.currency_id.id if different_currency else False,
                'expense_id':
                expense.id,
                'partner_id':
                partner_id,
            }
            move_line_values.append(move_line_dst)

            move_line_values_by_expense[expense.id] = move_line_values
        return move_line_values_by_expense

    @api.multi
    def action_move_create(self):
        '''
        main function that is called when trying to create the accounting entries related to an expense
        '''
        move_group_by_sheet = self._get_account_move_by_sheet()

        move_line_values_by_expense = self._get_account_move_line_values()

        for expense in self:
            company_currency = expense.company_id.currency_id
            different_currency = expense.currency_id != company_currency

            # get the account move of the related sheet
            move = move_group_by_sheet[expense.sheet_id.id]

            # get move line values
            move_line_values = move_line_values_by_expense.get(expense.id)
            move_line_dst = move_line_values[-1]
            total_amount = move_line_dst['debit'] or -move_line_dst['credit']
            total_amount_currency = move_line_dst['amount_currency']

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

            # link move lines to move, and move to expense sheet
            move.with_context(dont_create_taxes=True).write(
                {'line_ids': [(0, 0, line) for line in move_line_values]})
            expense.sheet_id.write({'account_move_id': move.id})

            if expense.payment_mode == 'company_account':
                expense.sheet_id.paid_expense_sheets()

        # post the moves
        for move in move_group_by_sheet.values():
            move.post()

        return move_group_by_sheet

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

    # ----------------------------------------
    # Mail Thread
    # ----------------------------------------

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

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

        employee = self.env['hr.employee'].search([
            '|', ('work_email', 'ilike', email_address),
            ('user_id.email', 'ilike', email_address)
        ],
                                                  limit=1)
        # The expenses alias is the same for all companies, we need to set the proper context
        company = employee.company_id or self.env.user.company_id
        self = self.with_context(force_company=company.id)

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

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

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

        custom_values.update({
            'name':
            expense_description.strip(),
            'employee_id':
            employee.id,
            'product_id':
            product.id,
            'product_uom_id':
            product.uom_id.id,
            'tax_ids':
            [(4, tax.id, False) for tax in product.supplier_taxes_id],
            'quantity':
            1,
            'unit_amount':
            price,
            'company_id':
            employee.company_id.id,
            'currency_id':
            employee.company_id.currency_id.id,
        })
        if account:
            custom_values['account_id'] = account.id
        return super(HrExpense, self).message_new(msg_dict, custom_values)
Exemple #6
0
class MrpBomLine(models.Model):
    _name = 'mrp.bom.line'
    _order = "sequence, id"
    _rec_name = "product_id"
    _description = 'Bill of Material Line'

    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)
    product_tmpl_id = fields.Many2one('product.template',
                                      'Product Template',
                                      related='product_id.product_tmpl_id')
    product_qty = fields.Float(
        'Quantity',
        default=1.0,
        digits=dp.get_precision('Product Unit of Measure'),
        required=True)
    product_uom_id = fields.Many2one(
        'uom.uom',
        'Product Unit of Measure',
        default=_get_default_product_uom_id,
        oldname='product_uom',
        required=True,
        help=
        "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"
    )
    sequence = fields.Integer('Sequence',
                              default=1,
                              help="Gives the sequence order when displaying.")
    routing_id = fields.Many2one(
        'mrp.routing',
        'Routing',
        related='bom_id.routing_id',
        store=True,
        readonly=False,
        help=
        "The list of operations to produce the finished product. The routing is mainly used to "
        "compute work center costs during operations and to plan future loads on work centers "
        "based on production planning.")
    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')
    valid_product_attribute_value_ids = fields.Many2many(
        'product.attribute.value',
        related='bom_id.product_tmpl_id.valid_product_attribute_value_ids')
    valid_product_attribute_value_wnva_ids = fields.Many2many(
        'product.attribute.value',
        related='bom_id.product_tmpl_id.valid_product_attribute_value_wnva_ids'
    )
    attribute_value_ids = fields.Many2many(
        'product.attribute.value',
        string='Apply on Variants',
        help="BOM Product Variants needed form apply this line.")
    operation_id = fields.Many2one(
        'mrp.routing.workcenter',
        'Consumed in Operation',
        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')
    has_attachments = fields.Boolean('Has Attachments',
                                     compute='_compute_has_attachments')

    _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.one
    @api.depends('product_id', 'bom_id')
    def _compute_child_bom_id(self):
        if not self.product_id:
            self.child_bom_id = False
        else:
            self.child_bom_id = self.env['mrp.bom']._bom_find(
                product_tmpl=self.product_id.product_tmpl_id,
                product=self.product_id)

    @api.one
    @api.depends('product_id')
    def _compute_has_attachments(self):
        nbr_attach = self.env['mrp.document'].search_count([
            '|', '&', ('res_model', '=', 'product.product'),
            ('res_id', '=', self.product_id.id), '&',
            ('res_model', '=', 'product.template'),
            ('res_id', '=', self.product_id.product_tmpl_id.id)
        ])
        self.has_attachments = bool(nbr_attach)

    @api.one
    @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 """
        self.child_line_ids = self.child_bom_id.bom_line_ids.ids

    @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.onchange('parent_product_tmpl_id')
    def onchange_parent_product(self):
        if not self.parent_product_tmpl_id:
            return {}
        return {
            'domain': {
                'attribute_value_ids':
                [('id', 'in',
                  self.parent_product_tmpl_id.
                  _get_valid_product_attribute_values().ids),
                 ('attribute_id.create_variant', '!=', 'no_variant')]
            }
        }

    @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 produce, can be inherited for add
        custom control. It currently checks that all variant values are in the
        product. """
        if self.attribute_value_ids:
            for att, att_values in groupby(self.attribute_value_ids,
                                           lambda l: l.attribute_id):
                values = self.env['product.attribute.value'].concat(
                    *list(att_values))
                if not (product.attribute_value_ids & values):
                    return True
        return False

    @api.multi
    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',
            'view_type':
            '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}" %
            ('product.product', self.product_id.id)
        }
Exemple #7
0
class Repair(models.Model):
    _name = 'repair.order'
    _description = 'Repair Order'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'create_date desc'

    @api.model
    def _default_stock_location(self):
        warehouse = self.env['stock.warehouse'].search([], limit=1)
        if warehouse:
            return warehouse.lot_stock_id.id
        return False

    name = fields.Char('Repair Reference',
                       default=lambda self: self.env['ir.sequence'].
                       next_by_code('repair.order'),
                       copy=False,
                       required=True,
                       states={'confirmed': [('readonly', True)]})
    product_id = fields.Many2one('product.product',
                                 string='Product to Repair',
                                 readonly=True,
                                 required=True,
                                 states={'draft': [('readonly', False)]})
    product_qty = fields.Float(
        'Product Quantity',
        default=1.0,
        digits=dp.get_precision('Product Unit of Measure'),
        readonly=True,
        required=True,
        states={'draft': [('readonly', False)]})
    product_uom = fields.Many2one('uom.uom',
                                  'Product Unit of Measure',
                                  readonly=True,
                                  required=True,
                                  states={'draft': [('readonly', False)]})
    partner_id = fields.Many2one(
        'res.partner',
        'Customer',
        index=True,
        states={'confirmed': [('readonly', True)]},
        help=
        'Choose partner for whom the order will be invoiced and delivered. You can find a partner by its Name, TIN, Email or Internal Reference.'
    )
    address_id = fields.Many2one('res.partner',
                                 'Delivery Address',
                                 domain="[('parent_id','=',partner_id)]",
                                 states={'confirmed': [('readonly', True)]})
    default_address_id = fields.Many2one('res.partner',
                                         compute='_compute_default_address_id')
    state = fields.Selection(
        [('draft', 'Quotation'), ('cancel', 'Cancelled'),
         ('confirmed', 'Confirmed'), ('under_repair', 'Under Repair'),
         ('ready', 'Ready to Repair'), ('2binvoiced', 'To be Invoiced'),
         ('invoice_except', 'Invoice Exception'), ('done', 'Repaired')],
        string='Status',
        copy=False,
        default='draft',
        readonly=True,
        track_visibility='onchange',
        help=
        "* The \'Draft\' status is used when a user is encoding a new and unconfirmed repair order.\n"
        "* The \'Confirmed\' status is used when a user confirms the repair order.\n"
        "* The \'Ready to Repair\' status is used to start to repairing, user can start repairing only after repair order is confirmed.\n"
        "* The \'To be Invoiced\' status is used to generate the invoice before or after repairing done.\n"
        "* The \'Done\' status is set when repairing is completed.\n"
        "* The \'Cancelled\' status is used when user cancel repair order.")
    location_id = fields.Many2one(
        'stock.location',
        'Location',
        default=_default_stock_location,
        index=True,
        readonly=True,
        required=True,
        help="This is the location where the product to repair is located.",
        states={
            'draft': [('readonly', False)],
            'confirmed': [('readonly', True)]
        })
    lot_id = fields.Many2one(
        'stock.production.lot',
        'Lot/Serial',
        domain="[('product_id','=', product_id)]",
        help="Products repaired are all belonging to this lot",
        oldname="prodlot_id")
    guarantee_limit = fields.Date('Warranty Expiration',
                                  states={'confirmed': [('readonly', True)]})
    operations = fields.One2many('repair.line',
                                 'repair_id',
                                 'Parts',
                                 copy=True,
                                 readonly=True,
                                 states={'draft': [('readonly', False)]})
    pricelist_id = fields.Many2one(
        'product.pricelist',
        'Pricelist',
        default=lambda self: self.env['product.pricelist'].search([], limit=1
                                                                  ).id,
        help='Pricelist of the selected partner.')
    partner_invoice_id = fields.Many2one('res.partner', 'Invoicing Address')
    invoice_method = fields.Selection(
        [("none", "No Invoice"), ("b4repair", "Before Repair"),
         ("after_repair", "After Repair")],
        string="Invoice Method",
        default='none',
        index=True,
        readonly=True,
        required=True,
        states={'draft': [('readonly', False)]},
        help=
        'Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.'
    )
    invoice_id = fields.Many2one('account.invoice',
                                 'Invoice',
                                 copy=False,
                                 readonly=True,
                                 track_visibility="onchange")
    move_id = fields.Many2one('stock.move',
                              'Move',
                              copy=False,
                              readonly=True,
                              track_visibility="onchange",
                              help="Move created by the repair order")
    fees_lines = fields.One2many('repair.fee',
                                 'repair_id',
                                 'Operations',
                                 copy=True,
                                 readonly=True,
                                 states={'draft': [('readonly', False)]})
    internal_notes = fields.Text('Internal Notes')
    quotation_notes = fields.Text('Quotation Notes')
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('repair.order'))
    invoiced = fields.Boolean('Invoiced', copy=False, readonly=True)
    repaired = fields.Boolean('Repaired', copy=False, readonly=True)
    amount_untaxed = fields.Float('Untaxed Amount',
                                  compute='_amount_untaxed',
                                  store=True)
    amount_tax = fields.Float('Taxes', compute='_amount_tax', store=True)
    amount_total = fields.Float('Total', compute='_amount_total', store=True)
    tracking = fields.Selection('Product Tracking',
                                related="product_id.tracking",
                                readonly=False)

    @api.one
    @api.depends('partner_id')
    def _compute_default_address_id(self):
        if self.partner_id:
            self.default_address_id = self.partner_id.address_get(
                ['contact'])['contact']

    @api.one
    @api.depends('operations.price_subtotal', 'invoice_method',
                 'fees_lines.price_subtotal', 'pricelist_id.currency_id')
    def _amount_untaxed(self):
        total = sum(operation.price_subtotal for operation in self.operations)
        total += sum(fee.price_subtotal for fee in self.fees_lines)
        self.amount_untaxed = self.pricelist_id.currency_id.round(total)

    @api.one
    @api.depends('operations.price_unit', 'operations.product_uom_qty',
                 'operations.product_id', 'fees_lines.price_unit',
                 'fees_lines.product_uom_qty', 'fees_lines.product_id',
                 'pricelist_id.currency_id', 'partner_id')
    def _amount_tax(self):
        val = 0.0
        for operation in self.operations:
            if operation.tax_id:
                tax_calculate = operation.tax_id.compute_all(
                    operation.price_unit, self.pricelist_id.currency_id,
                    operation.product_uom_qty, operation.product_id,
                    self.partner_id)
                for c in tax_calculate['taxes']:
                    val += c['amount']
        for fee in self.fees_lines:
            if fee.tax_id:
                tax_calculate = fee.tax_id.compute_all(
                    fee.price_unit, self.pricelist_id.currency_id,
                    fee.product_uom_qty, fee.product_id, self.partner_id)
                for c in tax_calculate['taxes']:
                    val += c['amount']
        self.amount_tax = val

    @api.one
    @api.depends('amount_untaxed', 'amount_tax')
    def _amount_total(self):
        self.amount_total = self.pricelist_id.currency_id.round(
            self.amount_untaxed + self.amount_tax)

    _sql_constraints = [
        ('name', 'unique (name)',
         'The name of the Repair Order must be unique!'),
    ]

    @api.onchange('product_id')
    def onchange_product_id(self):
        self.guarantee_limit = False
        self.lot_id = False
        if self.product_id:
            self.product_uom = self.product_id.uom_id.id

    @api.onchange('product_uom')
    def onchange_product_uom(self):
        res = {}
        if not self.product_id or not self.product_uom:
            return res
        if self.product_uom.category_id != self.product_id.uom_id.category_id:
            res['warning'] = {
                'title':
                _('Warning'),
                'message':
                _('The product unit of measure you chose has a different category than the product unit of measure.'
                  )
            }
            self.product_uom = self.product_id.uom_id.id
        return res

    @api.onchange('partner_id')
    def onchange_partner_id(self):
        if not self.partner_id:
            self.address_id = False
            self.partner_invoice_id = False
            self.pricelist_id = self.env['product.pricelist'].search(
                [], limit=1).id
        else:
            addresses = self.partner_id.address_get(
                ['delivery', 'invoice', 'contact'])
            self.address_id = addresses['delivery'] or addresses['contact']
            self.partner_invoice_id = addresses['invoice']
            self.pricelist_id = self.partner_id.property_product_pricelist.id

    @api.multi
    def button_dummy(self):
        # TDE FIXME: this button is very interesting
        return True

    @api.multi
    def action_repair_cancel_draft(self):
        if self.filtered(lambda repair: repair.state != 'cancel'):
            raise UserError(
                _("Repair must be canceled in order to reset it to draft."))
        self.mapped('operations').write({'state': 'draft'})
        return self.write({'state': 'draft'})

    def action_validate(self):
        self.ensure_one()
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        available_qty_owner = self.env['stock.quant']._get_available_quantity(
            self.product_id,
            self.location_id,
            self.lot_id,
            owner_id=self.partner_id,
            strict=True)
        available_qty_noown = self.env['stock.quant']._get_available_quantity(
            self.product_id, self.location_id, self.lot_id, strict=True)
        for available_qty in [available_qty_owner, available_qty_noown]:
            if float_compare(available_qty,
                             self.product_qty,
                             precision_digits=precision) >= 0:
                return self.action_repair_confirm()
        else:
            return {
                'name':
                _('Insufficient Quantity'),
                'view_type':
                'form',
                'view_mode':
                'form',
                'res_model':
                'stock.warn.insufficient.qty.repair',
                'view_id':
                self.env.ref(
                    'repair.stock_warn_insufficient_qty_repair_form_view').id,
                'type':
                'ir.actions.act_window',
                'context': {
                    'default_product_id': self.product_id.id,
                    'default_location_id': self.location_id.id,
                    'default_repair_id': self.id
                },
                'target':
                'new'
            }

    @api.multi
    def action_repair_confirm(self):
        """ Repair order state is set to 'To be invoiced' when invoice method
        is 'Before repair' else state becomes 'Confirmed'.
        @param *arg: Arguments
        @return: True
        """
        if self.filtered(lambda repair: repair.state != 'draft'):
            raise UserError(_("Only draft repairs can be confirmed."))
        before_repair = self.filtered(
            lambda repair: repair.invoice_method == 'b4repair')
        before_repair.write({'state': '2binvoiced'})
        to_confirm = self - before_repair
        to_confirm_operations = to_confirm.mapped('operations')
        to_confirm_operations.write({'state': 'confirmed'})
        to_confirm.write({'state': 'confirmed'})
        return True

    @api.multi
    def action_repair_cancel(self):
        if self.filtered(lambda repair: repair.state == 'done'):
            raise UserError(_("Cannot cancel completed repairs."))
        if any(repair.invoiced for repair in self):
            raise UserError(_('The repair order is already invoiced.'))
        self.mapped('operations').write({'state': 'cancel'})
        return self.write({'state': 'cancel'})

    @api.multi
    def action_send_mail(self):
        self.ensure_one()
        template_id = self.env.ref('repair.mail_template_repair_quotation').id
        ctx = {
            'default_model': 'repair.order',
            'default_res_id': self.id,
            'default_use_template': bool(template_id),
            'default_template_id': template_id,
            'default_composition_mode': 'comment',
            'custom_layout': 'mail.mail_notification_light',
        }
        return {
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'mail.compose.message',
            'target': 'new',
            'context': ctx,
        }

    @api.multi
    def print_repair_order(self):
        return self.env.ref('repair.action_report_repair_order').report_action(
            self)

    def action_repair_invoice_create(self):
        for repair in self:
            repair.action_invoice_create()
            if repair.invoice_method == 'b4repair':
                repair.action_repair_ready()
            elif repair.invoice_method == 'after_repair':
                repair.write({'state': 'done'})
        return True

    @api.multi
    def action_invoice_create(self, group=False):
        """ Creates invoice(s) for repair order.
        @param group: It is set to true when group invoice is to be generated.
        @return: Invoice Ids.
        """
        res = dict.fromkeys(self.ids, False)
        invoices_group = {}
        InvoiceLine = self.env['account.invoice.line']
        Invoice = self.env['account.invoice']
        for repair in self.filtered(lambda repair: repair.state not in (
                'draft', 'cancel') and not repair.invoice_id):
            if not repair.partner_id.id and not repair.partner_invoice_id.id:
                raise UserError(
                    _('You have to select an invoice address in the repair form.'
                      ))
            comment = repair.quotation_notes
            if repair.invoice_method != 'none':
                if group and repair.partner_invoice_id.id in invoices_group:
                    invoice = invoices_group[repair.partner_invoice_id.id]
                    invoice.write({
                        'name':
                        invoice.name + ', ' + repair.name,
                        'origin':
                        invoice.origin + ', ' + repair.name,
                        'comment':
                        (comment and
                         (invoice.comment and invoice.comment + "\n" + comment
                          or comment))
                        or (invoice.comment and invoice.comment or ''),
                    })
                else:
                    if not repair.partner_id.property_account_receivable_id:
                        raise UserError(
                            _('No account defined for partner "%s".') %
                            repair.partner_id.name)
                    fp_id = repair.partner_id.property_account_position_id.id or self.env[
                        'account.fiscal.position'].get_fiscal_position(
                            repair.partner_id.id,
                            delivery_id=repair.address_id.id)
                    invoice = Invoice.create({
                        'name':
                        repair.name,
                        'origin':
                        repair.name,
                        'type':
                        'out_invoice',
                        'account_id':
                        repair.partner_id.property_account_receivable_id.id,
                        'partner_id':
                        repair.partner_invoice_id.id or repair.partner_id.id,
                        'currency_id':
                        repair.pricelist_id.currency_id.id,
                        'comment':
                        repair.quotation_notes,
                        'fiscal_position_id':
                        fp_id
                    })
                    invoices_group[repair.partner_invoice_id.id] = invoice
                repair.write({'invoiced': True, 'invoice_id': invoice.id})

                for operation in repair.operations:
                    if operation.type == 'add':
                        if group:
                            name = repair.name + '-' + operation.name
                        else:
                            name = operation.name

                        if operation.product_id.property_account_income_id:
                            account_id = operation.product_id.property_account_income_id.id
                        elif operation.product_id.categ_id.property_account_income_categ_id:
                            account_id = operation.product_id.categ_id.property_account_income_categ_id.id
                        else:
                            raise UserError(
                                _('No account defined for product "%s".') %
                                operation.product_id.name)

                        invoice_line = InvoiceLine.create({
                            'invoice_id':
                            invoice.id,
                            'name':
                            name,
                            'origin':
                            repair.name,
                            'account_id':
                            account_id,
                            'quantity':
                            operation.product_uom_qty,
                            'invoice_line_tax_ids':
                            [(6, 0, [x.id for x in operation.tax_id])],
                            'uom_id':
                            operation.product_uom.id,
                            'price_unit':
                            operation.price_unit,
                            'price_subtotal':
                            operation.product_uom_qty * operation.price_unit,
                            'product_id':
                            operation.product_id and operation.product_id.id
                            or False
                        })
                        operation.write({
                            'invoiced': True,
                            'invoice_line_id': invoice_line.id
                        })
                for fee in repair.fees_lines:
                    if group:
                        name = repair.name + '-' + fee.name
                    else:
                        name = fee.name
                    if not fee.product_id:
                        raise UserError(_('No product defined on fees.'))

                    if fee.product_id.property_account_income_id:
                        account_id = fee.product_id.property_account_income_id.id
                    elif fee.product_id.categ_id.property_account_income_categ_id:
                        account_id = fee.product_id.categ_id.property_account_income_categ_id.id
                    else:
                        raise UserError(
                            _('No account defined for product "%s".') %
                            fee.product_id.name)

                    invoice_line = InvoiceLine.create({
                        'invoice_id':
                        invoice.id,
                        'name':
                        name,
                        'origin':
                        repair.name,
                        'account_id':
                        account_id,
                        'quantity':
                        fee.product_uom_qty,
                        'invoice_line_tax_ids':
                        [(6, 0, [x.id for x in fee.tax_id])],
                        'uom_id':
                        fee.product_uom.id,
                        'product_id':
                        fee.product_id and fee.product_id.id or False,
                        'price_unit':
                        fee.price_unit,
                        'price_subtotal':
                        fee.product_uom_qty * fee.price_unit
                    })
                    fee.write({
                        'invoiced': True,
                        'invoice_line_id': invoice_line.id
                    })
                invoice.compute_taxes()
                res[repair.id] = invoice.id
        return res

    @api.multi
    def action_created_invoice(self):
        self.ensure_one()
        return {
            'name': _('Invoice created'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'account.invoice',
            'view_id': self.env.ref('account.invoice_form').id,
            'target': 'current',
            'res_id': self.invoice_id.id,
        }

    def action_repair_ready(self):
        self.mapped('operations').write({'state': 'confirmed'})
        return self.write({'state': 'ready'})

    @api.multi
    def action_repair_start(self):
        """ Writes repair order state to 'Under Repair'
        @return: True
        """
        if self.filtered(
                lambda repair: repair.state not in ['confirmed', 'ready']):
            raise UserError(
                _("Repair must be confirmed before starting reparation."))
        self.mapped('operations').write({'state': 'confirmed'})
        return self.write({'state': 'under_repair'})

    @api.multi
    def action_repair_end(self):
        """ Writes repair order state to 'To be invoiced' if invoice method is
        After repair else state is set to 'Ready'.
        @return: True
        """
        if self.filtered(lambda repair: repair.state != 'under_repair'):
            raise UserError(
                _("Repair must be under repair in order to end reparation."))
        for repair in self:
            repair.write({'repaired': True})
            vals = {'state': 'done'}
            vals['move_id'] = repair.action_repair_done().get(repair.id)
            if not repair.invoiced and repair.invoice_method == 'after_repair':
                vals['state'] = '2binvoiced'
            repair.write(vals)
        return True

    @api.multi
    def action_repair_done(self):
        """ Creates stock move for operation and stock move for final product of repair order.
        @return: Move ids of final products

        """
        if self.filtered(lambda repair: not repair.repaired):
            raise UserError(
                _("Repair must be repaired in order to make the product moves."
                  ))
        res = {}
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        Move = self.env['stock.move']
        for repair in self:
            # Try to create move with the appropriate owner
            owner_id = False
            available_qty_owner = self.env[
                'stock.quant']._get_available_quantity(
                    repair.product_id,
                    repair.location_id,
                    repair.lot_id,
                    owner_id=repair.partner_id,
                    strict=True)
            if float_compare(available_qty_owner,
                             repair.product_qty,
                             precision_digits=precision) >= 0:
                owner_id = repair.partner_id.id

            moves = self.env['stock.move']
            for operation in repair.operations:
                move = Move.create({
                    'name':
                    repair.name,
                    'product_id':
                    operation.product_id.id,
                    'product_uom_qty':
                    operation.product_uom_qty,
                    'product_uom':
                    operation.product_uom.id,
                    'partner_id':
                    repair.address_id.id,
                    'location_id':
                    operation.location_id.id,
                    'location_dest_id':
                    operation.location_dest_id.id,
                    'move_line_ids': [(
                        0,
                        0,
                        {
                            'product_id': operation.product_id.id,
                            'lot_id': operation.lot_id.id,
                            'product_uom_qty': 0,  # bypass reservation here
                            'product_uom_id': operation.product_uom.id,
                            'qty_done': operation.product_uom_qty,
                            'package_id': False,
                            'result_package_id': False,
                            'owner_id': owner_id,
                            'location_id':
                            operation.location_id.id,  #TODO: owner stuff
                            'location_dest_id': operation.location_dest_id.id,
                        })],
                    'repair_id':
                    repair.id,
                    'origin':
                    repair.name,
                })
                moves |= move
                operation.write({'move_id': move.id, 'state': 'done'})
            move = Move.create({
                'name':
                repair.name,
                'product_id':
                repair.product_id.id,
                'product_uom':
                repair.product_uom.id or repair.product_id.uom_id.id,
                'product_uom_qty':
                repair.product_qty,
                'partner_id':
                repair.address_id.id,
                'location_id':
                repair.location_id.id,
                'location_dest_id':
                repair.location_id.id,
                'move_line_ids': [(
                    0,
                    0,
                    {
                        'product_id':
                        repair.product_id.id,
                        'lot_id':
                        repair.lot_id.id,
                        'product_uom_qty':
                        0,  # bypass reservation here
                        'product_uom_id':
                        repair.product_uom.id or repair.product_id.uom_id.id,
                        'qty_done':
                        repair.product_qty,
                        'package_id':
                        False,
                        'result_package_id':
                        False,
                        'owner_id':
                        owner_id,
                        'location_id':
                        repair.location_id.id,  #TODO: owner stuff
                        'location_dest_id':
                        repair.location_id.id,
                    })],
                'repair_id':
                repair.id,
                'origin':
                repair.name,
            })
            consumed_lines = moves.mapped('move_line_ids')
            produced_lines = move.move_line_ids
            moves |= move
            moves._action_done()
            produced_lines.write(
                {'consume_line_ids': [(6, 0, consumed_lines.ids)]})
            res[repair.id] = move.id
        return res
Exemple #8
0
class MembershipLine(models.Model):
    _name = 'membership.membership_line'
    _rec_name = 'partner'
    _order = 'id desc'
    _description = 'Membership Line'

    partner = fields.Many2one('res.partner', string='Partner', ondelete='cascade', index=True)
    membership_id = fields.Many2one('product.product', string="Membership", required=True)
    date_from = fields.Date(string='From', readonly=True)
    date_to = fields.Date(string='To', readonly=True)
    date_cancel = fields.Date(string='Cancel date')
    date = fields.Date(string='Join Date',
        help="Date on which member has joined the membership")
    member_price = fields.Float(string='Membership Fee',
        digits=dp.get_precision('Product Price'), required=True,
        help='Amount for the membership')
    account_invoice_line = fields.Many2one('account.invoice.line', string='Account Invoice line', readonly=True, ondelete='cascade')
    account_invoice_id = fields.Many2one('account.invoice', related='account_invoice_line.invoice_id', string='Invoice', readonly=True)
    company_id = fields.Many2one('res.company', related='account_invoice_line.invoice_id.company_id', string="Company", readonly=True, store=True)
    state = fields.Selection(STATE, compute='_compute_state', string='Membership Status', store=True,
        help="It indicates the membership status.\n"
             "-Non Member: A member who has not applied for any membership.\n"
             "-Cancelled Member: A member who has cancelled his membership.\n"
             "-Old Member: A member whose membership date has expired.\n"
             "-Waiting Member: A member who has applied for the membership and whose invoice is going to be created.\n"
             "-Invoiced Member: A member whose invoice has been created.\n"
             "-Paid Member: A member who has paid the membership amount.")

    @api.depends('account_invoice_line.invoice_id.state',
                 'account_invoice_line.invoice_id.payment_ids',
                 'account_invoice_line.invoice_id.payment_ids.invoice_ids.type')
    def _compute_state(self):
        """Compute the state lines """
        Invoice = self.env['account.invoice']
        for line in self:
            self._cr.execute('''
            SELECT i.state, i.id FROM
            account_invoice i
            WHERE
            i.id = (
                SELECT l.invoice_id FROM
                account_invoice_line l WHERE
                l.id = (
                    SELECT  ml.account_invoice_line FROM
                    membership_membership_line ml WHERE
                    ml.id = %s
                    )
                )
            ''', (line.id,))
            fetched = self._cr.fetchone()
            if not fetched:
                line.state = 'canceled'
                continue
            istate = fetched[0]
            if istate == 'draft':
                line.state = 'waiting'
            elif istate == 'open':
                line.state = 'invoiced'
            elif istate == 'paid':
                line.state = 'paid'
                invoices = Invoice.browse(fetched[1]).payment_move_line_ids.mapped('invoice_id')
                if invoices.filtered(lambda invoice: invoice.type == 'out_refund'):
                    line.state = 'canceled'
            elif istate == 'cancel':
                line.state = 'canceled'
            else:
                line.state = 'none'
Exemple #9
0
class StockPicking(models.Model):
    _inherit = 'stock.picking'

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

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

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

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

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

    def _compute_weight_uom_id(self):
        weight_uom_id = self.env[
            'product.template']._get_weight_uom_id_from_ir_config_parameter()
        for picking in self:
            picking.weight_uom_id = weight_uom_id

    def get_multiple_carrier_tracking(self):
        self.ensure_one()
        try:
            return json.loads(self.carrier_tracking_url)
        except (ValueError, TypeError):
            return False

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

    @api.multi
    def action_done(self):
        res = super(StockPicking, self).action_done()
        for pick in self:
            if pick.carrier_id:
                if pick.carrier_id.integration_level == 'rate_and_ship':
                    pick.send_to_shipper()
                pick._add_delivery_cost_to_so()
        return res

    @api.multi
    def put_in_pack(self):
        res = super(StockPicking, self).put_in_pack()
        if isinstance(res, dict) and res.get('type'):
            return res
        if self.carrier_id and self.carrier_id.delivery_type not in [
                'base_on_rule', 'fixed'
        ]:
            view_id = self.env.ref(
                'delivery.choose_delivery_package_view_form').id
            return {
                'name':
                _('Package Details'),
                'type':
                'ir.actions.act_window',
                'view_mode':
                'form',
                'res_model':
                'choose.delivery.package',
                'view_id':
                view_id,
                'views': [(view_id, 'form')],
                'target':
                'new',
                'context':
                dict(
                    self.env.context,
                    current_package_carrier_type=self.carrier_id.delivery_type,
                    default_picking_id=self.id,  # DO NOT FORWARD PORT
                    default_stock_quant_package_id=res.id),
            }
        else:
            return res

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

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

    @api.multi
    def _add_delivery_cost_to_so(self):
        self.ensure_one()
        sale_order = self.sale_id
        if sale_order.invoice_shipping_on_delivery:
            carrier_price = self.carrier_price * (
                1.0 + (float(self.carrier_id.margin) / 100.0))
            sale_order._create_delivery_line(self.carrier_id, carrier_price)

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

        carrier_trackers = []
        try:
            carrier_trackers = json.loads(self.carrier_tracking_url)
        except ValueError:
            carrier_trackers = self.carrier_tracking_url
        else:
            msg = "Tracking links for shipment: <br/>"
            for tracker in carrier_trackers:
                msg += '<a href=' + tracker[1] + '>' + tracker[0] + '</a><br/>'
            self.message_post(body=msg)
            return self.env.ref('delivery.act_delivery_trackers_url').read()[0]

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

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

    @api.multi
    def check_packages_are_identical(self):
        '''Some shippers require identical packages in the same shipment. This utility checks it.'''
        self.ensure_one()
        if self.package_ids:
            packages = [p.packaging_id for p in self.package_ids]
            if len(set(packages)) != 1:
                package_names = ', '.join([str(p.name) for p in packages])
                raise UserError(
                    _('You are shipping different packaging types in the same shipment.\nPackaging Types: %s'
                      % package_names))
        return True
Exemple #10
0
class SaleOrder(models.Model):
    _inherit = "sale.order"

    margin = fields.Monetary(compute='_product_margin', help="It gives profitability by calculating the difference between the Unit Price and the cost.", currency_field='currency_id', digits=dp.get_precision('Product Price'), store=True)

    @api.depends('order_line.margin')
    def _product_margin(self):
        if self.env.in_onchange:
            for order in self:
                order.margin = sum(order.order_line.filtered(lambda r: r.state != 'cancel').mapped('margin'))
        else:
            # On batch records recomputation (e.g. at install), compute the margins
            # with a single read_group query for better performance.
            # This isn't done in an onchange environment because (part of) the data
            # may not be stored in database (new records or unsaved modifications).
            grouped_order_lines_data = self.env['sale.order.line'].read_group(
                [
                    ('order_id', 'in', self.ids),
                    ('state', '!=', 'cancel'),
                ], ['margin', 'order_id'], ['order_id'])
            for data in grouped_order_lines_data:
                order = self.browse(data['order_id'][0])
                order.margin = data['margin']
Exemple #11
0
class HrSalaryRule(models.Model):
    _name = 'hr.salary.rule'
    _order = 'sequence, id'
    _description = 'Salary Rule'

    name = fields.Char(required=True, translate=True)
    code = fields.Char(
        required=True,
        help=
        "The code of salary rules can be used as reference in computation of other rules. "
        "In that case, it is case sensitive.")
    sequence = fields.Integer(required=True,
                              index=True,
                              default=5,
                              help='Use to arrange calculation sequence')
    quantity = fields.Char(
        default='1.0',
        help="It is used in computation for percentage and fixed amount. "
        "For e.g. A rule for Meal Voucher having fixed amount of "
        u"1€ per worked day can have its quantity defined in expression "
        "like worked_days.WORK100.number_of_days.")
    category_id = fields.Many2one('hr.salary.rule.category',
                                  string='Category',
                                  required=True)
    active = fields.Boolean(
        default=True,
        help=
        "If the active field is set to false, it will allow you to hide the salary rule without removing it."
    )
    appears_on_payslip = fields.Boolean(
        string='Appears on Payslip',
        default=True,
        help="Used to display the salary rule on payslip.")
    parent_rule_id = fields.Many2one('hr.salary.rule',
                                     string='Parent Salary Rule',
                                     index=True)
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        default=lambda self: self.env['res.company']._company_default_get())
    condition_select = fields.Selection([('none', 'Always True'),
                                         ('range', 'Range'),
                                         ('python', 'Python Expression')],
                                        string="Condition Based on",
                                        default='none',
                                        required=True)
    condition_range = fields.Char(
        string='Range Based on',
        default='contract.wage',
        help=
        'This will be used to compute the % fields values; in general it is on basic, '
        'but you can also use categories code fields in lowercase as a variable names '
        '(hra, ma, lta, etc.) and the variable basic.')
    condition_python = fields.Text(
        string='Python Condition',
        required=True,
        default='''
                    # Available variables:
                    #----------------------
                    # payslip: object containing the payslips
                    # employee: hr.employee object
                    # contract: hr.contract object
                    # rules: object containing the rules code (previously computed)
                    # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
                    # worked_days: object containing the computed worked days
                    # inputs: object containing the computed inputs

                    # Note: returned value have to be set in the variable 'result'

                    result = rules.NET > categories.NET * 0.10''',
        help=
        'Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.'
    )
    condition_range_min = fields.Float(
        string='Minimum Range',
        help="The minimum amount, applied for this rule.")
    condition_range_max = fields.Float(
        string='Maximum Range',
        help="The maximum amount, applied for this rule.")
    amount_select = fields.Selection(
        [
            ('percentage', 'Percentage (%)'),
            ('fix', 'Fixed Amount'),
            ('code', 'Python Code'),
        ],
        string='Amount Type',
        index=True,
        required=True,
        default='fix',
        help="The computation method for the rule amount.")
    amount_fix = fields.Float(string='Fixed Amount',
                              digits=dp.get_precision('Payroll'))
    amount_percentage = fields.Float(
        string='Percentage (%)',
        digits=dp.get_precision('Payroll Rate'),
        help='For example, enter 50.0 to apply a percentage of 50%')
    amount_python_compute = fields.Text(string='Python Code',
                                        default='''
                    # Available variables:
                    #----------------------
                    # payslip: object containing the payslips
                    # employee: hr.employee object
                    # contract: hr.contract object
                    # rules: object containing the rules code (previously computed)
                    # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
                    # worked_days: object containing the computed worked days.
                    # inputs: object containing the computed inputs.

                    # Note: returned value have to be set in the variable 'result'

                    result = contract.wage * 0.10''')
    amount_percentage_base = fields.Char(
        string='Percentage based on',
        help='result will be affected to a variable')
    child_ids = fields.One2many('hr.salary.rule',
                                'parent_rule_id',
                                string='Child Salary Rule',
                                copy=True)
    register_id = fields.Many2one(
        'hr.contribution.register',
        string='Contribution Register',
        help=
        "Eventual third party involved in the salary payment of the employees."
    )
    input_ids = fields.One2many('hr.rule.input',
                                'input_id',
                                string='Inputs',
                                copy=True)
    note = fields.Text(string='Description')

    @api.constrains('parent_rule_id')
    def _check_parent_rule_id(self):
        if not self._check_recursion(parent='parent_rule_id'):
            raise ValidationError(
                _('Error! You cannot create recursive hierarchy of Salary Rules.'
                  ))

    @api.multi
    def _recursive_search_of_rules(self):
        """
        @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
        """
        children_rules = []
        for rule in self.filtered(lambda rule: rule.child_ids):
            children_rules += rule.child_ids._recursive_search_of_rules()
        return [(rule.id, rule.sequence) for rule in self] + children_rules

    #TODO should add some checks on the type of result (should be float)
    @api.multi
    def _compute_rule(self, localdict):
        """
        :param localdict: dictionary containing the environement in which to compute the rule
        :return: returns a tuple build as the base/amount computed, the quantity and the rate
        :rtype: (float, float, float)
        """
        self.ensure_one()
        if self.amount_select == 'fix':
            try:
                return self.amount_fix, float(
                    safe_eval(self.quantity, localdict)), 100.0
            except:
                raise UserError(
                    _('Wrong quantity defined for salary rule %s (%s).') %
                    (self.name, self.code))
        elif self.amount_select == 'percentage':
            try:
                return (float(safe_eval(self.amount_percentage_base,
                                        localdict)),
                        float(safe_eval(self.quantity,
                                        localdict)), self.amount_percentage)
            except:
                raise UserError(
                    _('Wrong percentage base or quantity defined for salary rule %s (%s).'
                      ) % (self.name, self.code))
        else:
            try:
                safe_eval(self.amount_python_compute,
                          localdict,
                          mode='exec',
                          nocopy=True)
                return float(
                    localdict['result']
                ), 'result_qty' in localdict and localdict[
                    'result_qty'] or 1.0, 'result_rate' in localdict and localdict[
                        'result_rate'] or 100.0
            except:
                raise UserError(
                    _('Wrong python code defined for salary rule %s (%s).') %
                    (self.name, self.code))

    @api.multi
    def _satisfy_condition(self, localdict):
        """
        @param contract_id: id of hr.contract to be tested
        @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
        """
        self.ensure_one()

        if self.condition_select == 'none':
            return True
        elif self.condition_select == 'range':
            try:
                result = safe_eval(self.condition_range, localdict)
                return self.condition_range_min <= result and result <= self.condition_range_max or False
            except:
                raise UserError(
                    _('Wrong range condition defined for salary rule %s (%s).')
                    % (self.name, self.code))
        else:  # python code
            try:
                safe_eval(self.condition_python,
                          localdict,
                          mode='exec',
                          nocopy=True)
                return 'result' in localdict and localdict['result'] or False
            except:
                raise UserError(
                    _('Wrong python condition defined for salary rule %s (%s).'
                      ) % (self.name, self.code))
Exemple #12
0
class PurchaseOrderLine(models.Model):
    _name = 'purchase.order.line'
    _description = 'Purchase Order Line'
    _order = 'order_id, sequence, id'

    name = fields.Text(string='Description', required=True)
    sequence = fields.Integer(string='Sequence', default=10)
    product_qty = fields.Float(
        string='Quantity',
        digits=dp.get_precision('Product Unit of Measure'),
        required=True)
    product_uom_qty = fields.Float(string='Total Quantity',
                                   compute='_compute_product_uom_qty',
                                   store=True)
    date_planned = fields.Datetime(string='Scheduled Date',
                                   required=True,
                                   index=True)
    taxes_id = fields.Many2many(
        'account.tax',
        string='Taxes',
        domain=['|', ('active', '=', False), ('active', '=', True)])
    product_uom = fields.Many2one('uom.uom',
                                  string='Product Unit of Measure',
                                  required=True)
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 domain=[('purchase_ok', '=', True)],
                                 change_default=True,
                                 required=True)
    product_image = fields.Binary(
        'Product Image',
        related="product_id.image",
        readonly=False,
        help=
        "Non-stored related field to allow portal user to see the image of the product he has ordered"
    )
    product_type = fields.Selection(related='product_id.type', readonly=True)
    price_unit = fields.Float(string='Unit Price',
                              required=True,
                              digits=dp.get_precision('Product Price'))

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

    order_id = fields.Many2one('purchase.order',
                               string='Order Reference',
                               index=True,
                               required=True,
                               ondelete='cascade')
    account_analytic_id = fields.Many2one('account.analytic.account',
                                          string='Analytic Account')
    analytic_tag_ids = fields.Many2many('account.analytic.tag',
                                        string='Analytic Tags')
    company_id = fields.Many2one('res.company',
                                 related='order_id.company_id',
                                 string='Company',
                                 store=True,
                                 readonly=True)
    state = fields.Selection(related='order_id.state',
                             store=True,
                             readonly=False)

    invoice_lines = fields.One2many('account.invoice.line',
                                    'purchase_line_id',
                                    string="Bill Lines",
                                    readonly=True,
                                    copy=False)

    # Replace by invoiced Qty
    qty_invoiced = fields.Float(
        compute='_compute_qty_invoiced',
        string="Billed Qty",
        digits=dp.get_precision('Product Unit of Measure'),
        store=True)
    qty_received = fields.Float(
        string="Received Qty",
        digits=dp.get_precision('Product Unit of Measure'),
        copy=False)

    partner_id = fields.Many2one('res.partner',
                                 related='order_id.partner_id',
                                 string='Partner',
                                 readonly=True,
                                 store=True)
    currency_id = fields.Many2one(related='order_id.currency_id',
                                  store=True,
                                  string='Currency',
                                  readonly=True)
    date_order = fields.Datetime(related='order_id.date_order',
                                 string='Order Date',
                                 readonly=True)

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

    def _prepare_compute_all_values(self):
        # Hook method to returns the different argument values for the
        # compute_all method, due to the fact that discounts mechanism
        # is not implemented yet on the purchase orders.
        # This method should disappear as soon as this feature is
        # also introduced like in the sales module.
        self.ensure_one()
        return {
            'price_unit': self.price_unit,
            'currency_id': self.order_id.currency_id,
            'product_qty': self.product_qty,
            'product': self.product_id,
            'partner': self.order_id.partner_id,
        }

    @api.multi
    def _compute_tax_id(self):
        for line in self:
            fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id
            # If company_id is set, always filter taxes by the company
            taxes = line.product_id.supplier_taxes_id.filtered(
                lambda r: not line.company_id or r.company_id == line.
                company_id)
            line.taxes_id = fpos.map_tax(
                taxes, line.product_id,
                line.order_id.partner_id) if fpos else taxes

    @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity')
    def _compute_qty_invoiced(self):
        for line in self:
            qty = 0.0
            for inv_line in line.invoice_lines:
                if inv_line.invoice_id.state not in ['cancel']:
                    if inv_line.invoice_id.type == 'in_invoice':
                        qty += inv_line.uom_id._compute_quantity(
                            inv_line.quantity, line.product_uom)
                    elif inv_line.invoice_id.type == 'in_refund':
                        qty -= inv_line.uom_id._compute_quantity(
                            inv_line.quantity, line.product_uom)
            line.qty_invoiced = qty

    @api.model
    def create(self, values):
        line = super(PurchaseOrderLine, self).create(values)
        if line.order_id.state == 'purchase':
            msg = _("Extra line with %s ") % (line.product_id.display_name, )
            line.order_id.message_post(body=msg)
        return line

    @api.multi
    def write(self, values):
        if 'product_qty' in values:
            for line in self:
                if line.order_id.state == 'purchase':
                    line.order_id.message_post_with_view(
                        'purchase.track_po_line_template',
                        values={
                            'line': line,
                            'product_qty': values['product_qty']
                        },
                        subtype_id=self.env.ref('mail.mt_note').id)
        return super(PurchaseOrderLine, self).write(values)

    @api.multi
    def unlink(self):
        for line in self:
            if line.order_id.state in ['purchase', 'done']:
                raise UserError(
                    _('Cannot delete a purchase order line which is in state \'%s\'.'
                      ) % (line.state, ))
        return super(PurchaseOrderLine, self).unlink()

    @api.model
    def _get_date_planned(self, seller, po=False):
        """Return the datetime value to use as Schedule Date (``date_planned``) for
           PO Lines that correspond to the given product.seller_ids,
           when ordered at `date_order_str`.

           :param Model seller: used to fetch the delivery delay (if no seller
                                is provided, the delay is 0)
           :param Model po: purchase.order, necessary only if the PO line is
                            not yet attached to a PO.
           :rtype: datetime
           :return: desired Schedule Date for the PO line
        """
        date_order = po.date_order if po else self.order_id.date_order
        if date_order:
            return date_order + relativedelta(
                days=seller.delay if seller else 0)
        else:
            return datetime.today() + relativedelta(
                days=seller.delay if seller else 0)

    @api.onchange('product_id')
    def onchange_product_id(self):
        result = {}
        if not self.product_id:
            return result

        # Reset date, price and quantity since _onchange_quantity will provide default values
        self.date_planned = datetime.today().strftime(
            DEFAULT_SERVER_DATETIME_FORMAT)
        self.price_unit = self.product_qty = 0.0
        self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id
        result['domain'] = {
            'product_uom':
            [('category_id', '=', self.product_id.uom_id.category_id.id)]
        }

        product_lang = self.product_id.with_context(
            lang=self.partner_id.lang,
            partner_id=self.partner_id.id,
        )
        self.name = product_lang.display_name
        if product_lang.description_purchase:
            self.name += '\n' + product_lang.description_purchase

        self._compute_tax_id()

        self._suggest_quantity()
        self._onchange_quantity()

        return result

    @api.onchange('product_id')
    def onchange_product_id_warning(self):
        if not self.product_id:
            return
        warning = {}
        title = False
        message = False

        product_info = self.product_id

        if product_info.purchase_line_warn != 'no-message':
            title = _("Warning for %s") % product_info.name
            message = product_info.purchase_line_warn_msg
            warning['title'] = title
            warning['message'] = message
            if product_info.purchase_line_warn == 'block':
                self.product_id = False
            return {'warning': warning}
        return {}

    @api.onchange('product_qty', 'product_uom')
    def _onchange_quantity(self):
        if not self.product_id:
            return
        params = {'order_id': self.order_id}
        seller = self.product_id._select_seller(
            partner_id=self.partner_id,
            quantity=self.product_qty,
            date=self.order_id.date_order and self.order_id.date_order.date(),
            uom_id=self.product_uom,
            params=params)

        if seller or not self.date_planned:
            self.date_planned = self._get_date_planned(seller).strftime(
                DEFAULT_SERVER_DATETIME_FORMAT)

        if not seller:
            if self.product_id.seller_ids.filtered(
                    lambda s: s.name.id == self.partner_id.id):
                self.price_unit = 0.0
            return

        price_unit = self.env['account.tax']._fix_tax_included_price_company(
            seller.price, self.product_id.supplier_taxes_id, self.taxes_id,
            self.company_id) if seller else 0.0
        if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
            price_unit = seller.currency_id._convert(
                price_unit, self.order_id.currency_id,
                self.order_id.company_id, self.date_order
                or fields.Date.today())

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

        self.price_unit = price_unit

    @api.multi
    @api.depends('product_uom', 'product_qty', 'product_id.uom_id')
    def _compute_product_uom_qty(self):
        for line in self:
            if line.product_id.uom_id != line.product_uom:
                line.product_uom_qty = line.product_uom._compute_quantity(
                    line.product_qty, line.product_id.uom_id)
            else:
                line.product_uom_qty = line.product_qty

    def _suggest_quantity(self):
        '''
        Suggest a minimal quantity based on the seller
        '''
        if not self.product_id:
            return
        seller_min_qty = self.product_id.seller_ids\
            .filtered(lambda r: r.name == self.order_id.partner_id and (not r.product_id or r.product_id == self.product_id))\
            .sorted(key=lambda r: r.min_qty)
        if seller_min_qty:
            self.product_qty = seller_min_qty[0].min_qty or 1.0
            self.product_uom = seller_min_qty[0].product_uom
        else:
            self.product_qty = 1.0
Exemple #13
0
class RepairFee(models.Model):
    _name = 'repair.fee'
    _description = 'Repair Fees'

    repair_id = fields.Many2one('repair.order',
                                'Repair Order Reference',
                                index=True,
                                ondelete='cascade',
                                required=True)
    name = fields.Text('Description', index=True, required=True)
    product_id = fields.Many2one('product.product', 'Product')
    product_uom_qty = fields.Float(
        'Quantity',
        digits=dp.get_precision('Product Unit of Measure'),
        required=True,
        default=1.0)
    price_unit = fields.Float('Unit Price', required=True)
    product_uom = fields.Many2one('uom.uom',
                                  'Product Unit of Measure',
                                  required=True)
    price_subtotal = fields.Float('Subtotal',
                                  compute='_compute_price_subtotal',
                                  store=True,
                                  digits=0)
    tax_id = fields.Many2many('account.tax', 'repair_fee_line_tax',
                              'repair_fee_line_id', 'tax_id', 'Taxes')
    invoice_line_id = fields.Many2one('account.invoice.line',
                                      'Invoice Line',
                                      copy=False,
                                      readonly=True)
    invoiced = fields.Boolean('Invoiced', copy=False, readonly=True)

    @api.one
    @api.depends('price_unit', 'repair_id', 'product_uom_qty', 'product_id')
    def _compute_price_subtotal(self):
        taxes = self.tax_id.compute_all(
            self.price_unit, self.repair_id.pricelist_id.currency_id,
            self.product_uom_qty, self.product_id, self.repair_id.partner_id)
        self.price_subtotal = taxes['total_excluded']

    @api.onchange('repair_id', 'product_id', 'product_uom_qty')
    def onchange_product_id(self):
        """ On change of product it sets product quantity, tax account, name,
        uom of product, unit price and price subtotal. """
        if not self.product_id:
            return

        partner = self.repair_id.partner_id
        pricelist = self.repair_id.pricelist_id

        if partner and self.product_id:
            fp = partner.property_account_position_id
            if not fp:
                # Check automatic detection
                fp_id = self.env[
                    'account.fiscal.position'].get_fiscal_position(
                        partner.id, delivery_id=self.repair_id.address_id.id)
                fp = self.env['account.fiscal.position'].browse(fp_id)
            self.tax_id = fp.map_tax(self.product_id.taxes_id, self.product_id,
                                     partner).ids
        if self.product_id:
            if partner:
                self.name = self.product_id.with_context(
                    lang=partner.lang).display_name
            else:
                self.name = self.product_id.display_name
            self.product_uom = self.product_id.uom_id.id
            if self.product_id.description_sale:
                if partner:
                    self.name += '\n' + self.product_id.with_context(
                        lang=partner.lang).description_sale
                else:
                    self.name += '\n' + self.product_id.description_sale

        warning = False
        if not pricelist:
            warning = {
                'title':
                _('No pricelist found.'),
                'message':
                _('You have to select a pricelist in the Repair form !\n Please set one before choosing a product.'
                  )
            }
            return {'warning': warning}
        else:
            self._onchange_product_uom()

    @api.onchange('product_uom')
    def _onchange_product_uom(self):
        partner = self.repair_id.partner_id
        pricelist = self.repair_id.pricelist_id
        if pricelist and self.product_id:
            price = pricelist.get_product_price(self.product_id,
                                                self.product_uom_qty,
                                                partner,
                                                uom_id=self.product_uom.id)
            if price is False:
                warning = {
                    'title':
                    _('No valid pricelist line found.'),
                    'message':
                    _("Couldn't find a pricelist line matching this product and quantity.\nYou have to change either the product, the quantity or the pricelist."
                      )
                }
                return {'warning': warning}
            else:
                self.price_unit = price
Exemple #14
0
class RepairLine(models.Model):
    _name = 'repair.line'
    _description = 'Repair Line (parts)'

    name = fields.Text('Description', required=True)
    repair_id = fields.Many2one('repair.order',
                                'Repair Order Reference',
                                index=True,
                                ondelete='cascade')
    type = fields.Selection([('add', 'Add'), ('remove', 'Remove')],
                            'Type',
                            default='add',
                            required=True)
    product_id = fields.Many2one('product.product', 'Product', required=True)
    invoiced = fields.Boolean('Invoiced', copy=False, readonly=True)
    price_unit = fields.Float('Unit Price',
                              required=True,
                              digits=dp.get_precision('Product Price'))
    price_subtotal = fields.Float('Subtotal',
                                  compute='_compute_price_subtotal',
                                  store=True,
                                  digits=0)
    tax_id = fields.Many2many('account.tax', 'repair_operation_line_tax',
                              'repair_operation_line_id', 'tax_id', 'Taxes')
    product_uom_qty = fields.Float(
        'Quantity',
        default=1.0,
        digits=dp.get_precision('Product Unit of Measure'),
        required=True)
    product_uom = fields.Many2one('uom.uom',
                                  'Product Unit of Measure',
                                  required=True)
    invoice_line_id = fields.Many2one('account.invoice.line',
                                      'Invoice Line',
                                      copy=False,
                                      readonly=True)
    location_id = fields.Many2one('stock.location',
                                  'Source Location',
                                  index=True,
                                  required=True)
    location_dest_id = fields.Many2one('stock.location',
                                       'Dest. Location',
                                       index=True,
                                       required=True)
    move_id = fields.Many2one('stock.move',
                              'Inventory Move',
                              copy=False,
                              readonly=True)
    lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial')
    state = fields.Selection(
        [('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'),
         ('cancel', 'Cancelled')],
        'Status',
        default='draft',
        copy=False,
        readonly=True,
        required=True,
        help=
        'The status of a repair line is set automatically to the one of the linked repair order.'
    )

    @api.constrains('lot_id', 'product_id')
    def constrain_lot_id(self):
        for line in self.filtered(
                lambda x: x.product_id.tracking != 'none' and not x.lot_id):
            raise ValidationError(
                _("Serial number is required for operation line with product '%s'"
                  ) % (line.product_id.name))

    @api.one
    @api.depends('price_unit', 'repair_id', 'product_uom_qty', 'product_id',
                 'repair_id.invoice_method')
    def _compute_price_subtotal(self):
        taxes = self.tax_id.compute_all(
            self.price_unit, self.repair_id.pricelist_id.currency_id,
            self.product_uom_qty, self.product_id, self.repair_id.partner_id)
        self.price_subtotal = taxes['total_excluded']

    @api.onchange('type', 'repair_id')
    def onchange_operation_type(self):
        """ On change of operation type it sets source location, destination location
        and to invoice field.
        @param product: Changed operation type.
        @param guarantee_limit: Guarantee limit of current record.
        @return: Dictionary of values.
        """
        if not self.type:
            self.location_id = False
            self.location_dest_id = False
        elif self.type == 'add':
            self.onchange_product_id()
            args = self.repair_id.company_id and [
                ('company_id', '=', self.repair_id.company_id.id)
            ] or []
            warehouse = self.env['stock.warehouse'].search(args, limit=1)
            self.location_id = warehouse.lot_stock_id
            self.location_dest_id = self.env['stock.location'].search(
                [('usage', '=', 'production')], limit=1).id
        else:
            self.price_unit = 0.0
            self.tax_id = False
            self.location_id = self.env['stock.location'].search(
                [('usage', '=', 'production')], limit=1).id
            self.location_dest_id = self.env['stock.location'].search(
                [('scrap_location', '=', True)], limit=1).id

    @api.onchange('repair_id', 'product_id', 'product_uom_qty')
    def onchange_product_id(self):
        """ On change of product it sets product quantity, tax account, name,
        uom of product, unit price and price subtotal. """
        partner = self.repair_id.partner_id
        pricelist = self.repair_id.pricelist_id
        if not self.product_id or not self.product_uom_qty:
            return
        if self.product_id:
            if partner:
                self.name = self.product_id.with_context(
                    lang=partner.lang).display_name
            else:
                self.name = self.product_id.display_name
            if self.product_id.description_sale:
                if partner:
                    self.name += '\n' + self.product_id.with_context(
                        lang=partner.lang).description_sale
                else:
                    self.name += '\n' + self.product_id.description_sale
            self.product_uom = self.product_id.uom_id.id
        if self.type != 'remove':
            if partner and self.product_id:
                fp = partner.property_account_position_id
                if not fp:
                    # Check automatic detection
                    fp_id = self.env[
                        'account.fiscal.position'].get_fiscal_position(
                            partner.id,
                            delivery_id=self.repair_id.address_id.id)
                    fp = self.env['account.fiscal.position'].browse(fp_id)
                self.tax_id = fp.map_tax(self.product_id.taxes_id,
                                         self.product_id, partner).ids
            warning = False
            if not pricelist:
                warning = {
                    'title':
                    _('No pricelist found.'),
                    'message':
                    _('You have to select a pricelist in the Repair form !\n Please set one before choosing a product.'
                      )
                }
                return {'warning': warning}
            else:
                self._onchange_product_uom()

    @api.onchange('product_uom')
    def _onchange_product_uom(self):
        partner = self.repair_id.partner_id
        pricelist = self.repair_id.pricelist_id
        if pricelist and self.product_id and self.type != 'remove':
            price = pricelist.get_product_price(self.product_id,
                                                self.product_uom_qty,
                                                partner,
                                                uom_id=self.product_uom.id)
            if price is False:
                warning = {
                    'title':
                    _('No valid pricelist line found.'),
                    'message':
                    _("Couldn't find a pricelist line matching this product and quantity.\nYou have to change either the product, the quantity or the pricelist."
                      )
                }
                return {'warning': warning}
            else:
                self.price_unit = price
Exemple #15
0
class ChangeProductionQty(models.TransientModel):
    _name = 'change.production.qty'
    _description = 'Change Production Qty'

    # TDE FIXME: add production_id field
    mo_id = fields.Many2one('mrp.production',
                            'Manufacturing Order',
                            required=True)
    product_qty = fields.Float(
        'Quantity To Produce',
        digits=dp.get_precision('Product Unit of Measure'),
        required=True)

    @api.model
    def default_get(self, fields):
        res = super(ChangeProductionQty, self).default_get(fields)
        if 'mo_id' in fields and not res.get('mo_id') and self._context.get(
                'active_model') == 'mrp.production' and self._context.get(
                    'active_id'):
            res['mo_id'] = self._context['active_id']
        if 'product_qty' in fields and not res.get('product_qty') and res.get(
                'mo_id'):
            res['product_qty'] = self.env['mrp.production'].browse(
                res['mo_id']).product_qty
        return res

    @api.model
    def _update_product_to_produce(self, production, qty, old_qty):
        production_move = production.move_finished_ids.filtered(
            lambda x: x.product_id.id == production.product_id.id and x.state
            not in ('done', 'cancel'))
        if production_move:
            production_move._decrease_reserved_quanity(qty)
            production_move.with_context(do_not_unreserve=True).write(
                {'product_uom_qty': qty})
            production_move._recompute_state()
            production_move._action_assign()
        else:
            production_move = production._generate_finished_moves()
            production_move = production.move_finished_ids.filtered(
                lambda x: x.state not in ('done', 'cancel') and production.
                product_id.id == x.product_id.id)
            production_move.write({'product_uom_qty': qty})
        return {production_move: (qty, old_qty)}

    @api.multi
    def change_prod_qty(self):
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        for wizard in self:
            production = wizard.mo_id
            produced = sum(
                production.move_finished_ids.filtered(
                    lambda m: m.product_id == production.product_id).mapped(
                        'quantity_done'))
            if wizard.product_qty < produced:
                format_qty = '%.{precision}f'.format(precision=precision)
                raise UserError(
                    _("You have already processed %s. Please input a quantity higher than %s "
                      ) % (format_qty % produced, format_qty % produced))
            old_production_qty = production.product_qty
            production.write({'product_qty': wizard.product_qty})
            done_moves = production.move_finished_ids.filtered(
                lambda x: x.state == 'done' and x.product_id == production.
                product_id)
            qty_produced = production.product_id.uom_id._compute_quantity(
                sum(done_moves.mapped('product_qty')),
                production.product_uom_id)
            factor = production.product_uom_id._compute_quantity(
                production.product_qty - qty_produced, production.bom_id.
                product_uom_id) / production.bom_id.product_qty
            boms, lines = production.bom_id.explode(
                production.product_id,
                factor,
                picking_type=production.bom_id.picking_type_id)
            documents = {}
            for line, line_data in lines:
                move = production.move_raw_ids.filtered(
                    lambda x: x.bom_line_id.id == line.id and x.state not in
                    ('done', 'cancel'))
                if move:
                    move = move[0]
                    old_qty = move.product_uom_qty
                else:
                    old_qty = 0
                iterate_key = production._get_document_iterate_key(move)
                if iterate_key:
                    document = self.env[
                        'stock.picking']._log_activity_get_documents(
                            {move: (line_data['qty'], old_qty)}, iterate_key,
                            'UP')
                    for key, value in document.items():
                        if documents.get(key):
                            documents[key] += [value]
                        else:
                            documents[key] = [value]

                production._update_raw_move(line, line_data)

            production._log_manufacture_exception(documents)
            operation_bom_qty = {}
            for bom, bom_data in boms:
                for operation in bom.routing_id.operation_ids:
                    operation_bom_qty[operation.id] = bom_data['qty']
            finished_moves_modification = self._update_product_to_produce(
                production, production.product_qty - qty_produced,
                old_production_qty)
            production._log_downside_manufactured_quantity(
                finished_moves_modification)
            moves = production.move_raw_ids.filtered(lambda x: x.state not in
                                                     ('done', 'cancel'))
            moves._action_assign()
            for wo in production.workorder_ids:
                operation = wo.operation_id
                if operation_bom_qty.get(operation.id):
                    cycle_number = float_round(
                        operation_bom_qty[operation.id] /
                        operation.workcenter_id.capacity,
                        precision_digits=0,
                        rounding_method='UP')
                    wo.duration_expected = (
                        operation.workcenter_id.time_start +
                        operation.workcenter_id.time_stop +
                        cycle_number * operation.time_cycle * 100.0 /
                        operation.workcenter_id.time_efficiency)
                quantity = wo.qty_production - wo.qty_produced
                if production.product_id.tracking == 'serial':
                    quantity = 1.0 if not float_is_zero(
                        quantity, precision_digits=precision) else 0.0
                else:
                    quantity = quantity if (quantity > 0) else 0
                if float_is_zero(quantity, precision_digits=precision):
                    wo.final_lot_id = False
                    wo.active_move_line_ids.unlink()
                wo.qty_producing = quantity
                if wo.qty_produced < wo.qty_production and wo.state == 'done':
                    wo.state = 'progress'
                if wo.qty_produced == wo.qty_production and wo.state == 'progress':
                    wo.state = 'done'
                # assign moves; last operation receive all unassigned moves
                # TODO: following could be put in a function as it is similar as code in _workorders_create
                # TODO: only needed when creating new moves
                moves_raw = production.move_raw_ids.filtered(
                    lambda move: move.operation_id == operation and move.state
                    not in ('done', 'cancel'))
                if wo == production.workorder_ids[-1]:
                    moves_raw |= production.move_raw_ids.filtered(
                        lambda move: not move.operation_id)
                moves_finished = production.move_finished_ids.filtered(
                    lambda move: move.operation_id == operation
                )  #TODO: code does nothing, unless maybe by_products?
                moves_raw.mapped('move_line_ids').write(
                    {'workorder_id': wo.id})
                (moves_finished + moves_raw).write({'workorder_id': wo.id})
                if quantity > 0 and wo.move_raw_ids.filtered(
                        lambda x: x.product_id.tracking != 'none'
                ) and not wo.active_move_line_ids:
                    wo._generate_lot_ids()
        return {}
Exemple #16
0
class AccountVoucherLine(models.Model):
    _name = 'account.voucher.line'
    _description = 'Accounting Voucher Line'

    name = fields.Text(string='Description', required=True)
    sequence = fields.Integer(
        default=10,
        help="Gives the sequence of this line when displaying the voucher.")
    voucher_id = fields.Many2one('account.voucher',
                                 'Voucher',
                                 required=1,
                                 ondelete='cascade')
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 ondelete='set null',
                                 index=True)
    account_id = fields.Many2one(
        'account.account',
        string='Account',
        required=True,
        domain=[('deprecated', '=', False)],
        help="The income or expense account related to the selected product.")
    price_unit = fields.Float(string='Unit Price',
                              required=True,
                              digits=dp.get_precision('Product Price'),
                              oldname='amount')
    price_subtotal = fields.Monetary(string='Amount',
                                     store=True,
                                     readonly=True,
                                     compute='_compute_subtotal')
    quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'),
                            required=True,
                            default=1)
    account_analytic_id = fields.Many2one('account.analytic.account',
                                          'Analytic Account')
    analytic_tag_ids = fields.Many2many('account.analytic.tag',
                                        string='Analytic Tags')
    company_id = fields.Many2one('res.company',
                                 related='voucher_id.company_id',
                                 string='Company',
                                 store=True,
                                 readonly=True)
    tax_ids = fields.Many2many('account.tax',
                               string='Tax',
                               help="Only for tax excluded from price")
    currency_id = fields.Many2one('res.currency',
                                  related='voucher_id.currency_id',
                                  readonly=False)

    @api.one
    @api.depends('price_unit', 'tax_ids', 'quantity', 'product_id',
                 'voucher_id.currency_id')
    def _compute_subtotal(self):
        self.price_subtotal = self.quantity * self.price_unit
        if self.tax_ids:
            taxes = self.tax_ids.compute_all(
                self.price_unit,
                self.voucher_id.currency_id,
                self.quantity,
                product=self.product_id,
                partner=self.voucher_id.partner_id)
            self.price_subtotal = taxes['total_excluded']

    @api.onchange('product_id', 'voucher_id', 'price_unit', 'company_id')
    def _onchange_line_details(self):
        if not self.voucher_id or not self.product_id or not self.voucher_id.partner_id:
            return
        onchange_res = self.product_id_change(self.product_id.id,
                                              self.voucher_id.partner_id.id,
                                              self.price_unit,
                                              self.company_id.id,
                                              self.voucher_id.currency_id.id,
                                              self.voucher_id.voucher_type)
        for fname, fvalue in onchange_res['value'].items():
            setattr(self, fname, fvalue)

    def _get_account(self, product, fpos, type):
        accounts = product.product_tmpl_id.get_product_accounts(fpos)
        if type == 'sale':
            return accounts['income']
        return accounts['expense']

    @api.multi
    def product_id_change(self,
                          product_id,
                          partner_id=False,
                          price_unit=False,
                          company_id=None,
                          currency_id=None,
                          type=None):
        # TDE note: mix of old and new onchange badly written in 9, multi but does not use record set
        context = self._context
        company_id = company_id if company_id is not None else context.get(
            'company_id', False)
        company = self.env['res.company'].browse(company_id)
        currency = self.env['res.currency'].browse(currency_id)
        if not partner_id:
            raise UserError(_("You must first select a partner."))
        part = self.env['res.partner'].browse(partner_id)
        if part.lang:
            self = self.with_context(lang=part.lang)

        product = self.env['product.product'].browse(product_id)
        fpos = part.property_account_position_id
        account = self._get_account(product, fpos, type)
        values = {
            'name': product.partner_ref,
            'account_id': account.id,
        }

        if type == 'purchase':
            values['price_unit'] = price_unit or product.standard_price
            taxes = product.supplier_taxes_id or account.tax_ids
            if product.description_purchase:
                values['name'] += '\n' + product.description_purchase
        else:
            values['price_unit'] = price_unit or product.lst_price
            taxes = product.taxes_id or account.tax_ids
            if product.description_sale:
                values['name'] += '\n' + product.description_sale

        values['tax_ids'] = taxes.ids

        if company and currency:
            if company.currency_id != currency:
                if type == 'purchase':
                    values['price_unit'] = price_unit or product.standard_price
                values['price_unit'] = values['price_unit'] * currency.rate

        return {'value': values, 'domain': {}}
Exemple #17
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"

    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',
        domain="[('type', 'in', ['product', 'consu'])]",
        required=True)
    product_id = fields.Many2one(
        'product.product',
        'Product Variant',
        domain=
        "['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu'])]",
        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)
    product_qty = fields.Float('Quantity',
                               default=1.0,
                               digits=dp.get_precision('Unit of Measure'),
                               required=True)
    product_uom_id = fields.Many2one(
        'uom.uom',
        'Product Unit of Measure',
        default=_get_default_product_uom_id,
        oldname='product_uom',
        required=True,
        help=
        "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"
    )
    sequence = fields.Integer(
        'Sequence',
        help=
        "Gives the sequence order when displaying a list of bills of material."
    )
    routing_id = fields.Many2one(
        'mrp.routing',
        'Routing',
        track_visibility='onchange',
        help=
        "The operations for producing this BoM.  When a routing is specified, the production orders will "
        " be executed through work orders, otherwise everything is processed in the production order itself. "
    )
    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')],
        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',
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('mrp.bom'),
                                 required=True)

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

    @api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids')
    def _check_product_recursion(self):
        for bom in self:
            if bom.product_id:
                if bom.bom_line_ids.filtered(
                        lambda x: x.product_id == bom.product_id):
                    raise ValidationError(
                        _('BoM line product %s should not be same as BoM product.'
                          ) % bom.display_name)
            else:
                if bom.bom_line_ids.filtered(
                        lambda x: x.product_id.product_tmpl_id == bom.
                        product_tmpl_id):
                    raise ValidationError(
                        _('BoM line product %s should not be same as BoM product.'
                          ) % bom.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.attribute_value_ids = False

    @api.onchange('routing_id')
    def onchange_routing_id(self):
        for line in self.bom_line_ids:
            line.operation_id = False

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

    @api.multi
    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."
                  ))

    @api.multi
    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(self,
                  product_tmpl=None,
                  product=None,
                  picking_type=None,
                  company_id=False):
        """ Finds BoM for particular product, picking and company """
        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
            return False
        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', '=', company_id
                                or self.env.context.get('company_id'))]
        # order to prioritize bom with product_id over the one without
        return self.search(domain, order='sequence, product_id', limit=1)

    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)
            if bom.type == 'phantom':
                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'
        }]
class ProductChangeQuantity(models.TransientModel):
    _name = "stock.change.product.qty"
    _description = "Change Product Quantity"

    # TDE FIXME: strange dfeault method, was present before migration ? to check
    product_id = fields.Many2one('product.product', 'Product', required=True)
    product_tmpl_id = fields.Many2one('product.template', 'Template', required=True)
    product_variant_count = fields.Integer('Variant Count', related='product_tmpl_id.product_variant_count', readonly=False)
    new_quantity = fields.Float(
        'New Quantity on Hand', default=1,
        digits=dp.get_precision('Product Unit of Measure'), required=True,
        help='This quantity is expressed in the Default Unit of Measure of the product.')
    location_id = fields.Many2one('stock.location', 'Location', required=True, domain="[('usage', '=', 'internal')]")

    @api.model
    def default_get(self, fields):
        res = super(ProductChangeQuantity, self).default_get(fields)
        if 'location_id' in fields and not res.get('location_id'):
            company_user = self.env.user.company_id
            warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
            if warehouse:
                res['location_id'] = warehouse.lot_stock_id.id
        return res

    @api.onchange('location_id', 'product_id')
    def onchange_location_id(self):
        # TDE FIXME: should'nt we use context / location ?
        if self.location_id and self.product_id:
            availability = self.product_id.with_context(compute_child=False)._product_available()
            self.new_quantity = availability[self.product_id.id]['qty_available']

    @api.onchange('product_id')
    def onchange_product_id(self):
        if self.product_id:
            self.product_tmpl_id = self.onchange_product_id_dict(self.product_id.id)['product_tmpl_id']

    def _action_start_line(self):
        product = self.product_id.with_context(location=self.location_id.id)
        th_qty = product.qty_available

        res = {
               'product_qty': self.new_quantity,
               'location_id': self.location_id.id,
               'product_id': self.product_id.id,
               'product_uom_id': self.product_id.uom_id.id,
               'theoretical_qty': th_qty,
        }

        return res


    def onchange_product_id_dict(self, product_id):
        return {
            'product_tmpl_id': self.env['product.product'].browse(product_id).product_tmpl_id.id,
        }

    @api.model
    def create(self, values):
        if values.get('product_id'):
            values.update(self.onchange_product_id_dict(values['product_id']))
        return super(ProductChangeQuantity, self).create(values)

    @api.constrains('new_quantity')
    def check_new_quantity(self):
        if any(wizard.new_quantity < 0 for wizard in self):
            raise UserError(_('Quantity cannot be negative.'))

    def change_product_qty(self):
        """ Changes the Product Quantity by making a Physical Inventory. """
        Inventory = self.env['stock.inventory']
        for wizard in self:
            product = wizard.product_id.with_context(location=wizard.location_id.id)
            line_data = wizard._action_start_line()

            if wizard.product_id.id:
                inventory_filter = 'product'
            else:
                inventory_filter = 'none'
            inventory = Inventory.create({
                'name': _('INV: %s') % tools.ustr(wizard.product_id.display_name),
                'filter': inventory_filter,
                'product_id': wizard.product_id.id,
                'location_id': wizard.location_id.id,
                'line_ids': [(0, 0, line_data)],
            })
            inventory.action_validate()
        return {'type': 'ir.actions.act_window_close'}
Exemple #19
0
class MrpProduction(models.Model):
    """ Manufacturing Orders """
    _name = 'mrp.production'
    _description = 'Production Order'
    _date_name = 'date_planned_start'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'date_planned_start asc,id'

    @api.model
    def _get_default_picking_type(self):
        return self.env['stock.picking.type'].search(
            [('code', '=', 'mrp_operation'),
             ('warehouse_id.company_id', 'in', [
                 self.env.context.get('company_id',
                                      self.env.user.company_id.id), False
             ])],
            limit=1).id

    @api.model
    def _get_default_location_src_id(self):
        location = False
        if self._context.get('default_picking_type_id'):
            location = self.env['stock.picking.type'].browse(
                self.env.context['default_picking_type_id']
            ).default_location_src_id
        if not location:
            location = self.env.ref('stock.stock_location_stock',
                                    raise_if_not_found=False)
            try:
                location.check_access_rule('read')
            except (AttributeError, AccessError):
                location = self.env['stock.warehouse'].search(
                    [('company_id', '=', self.env.user.company_id.id)],
                    limit=1).lot_stock_id
        return location and location.id or False

    @api.model
    def _get_default_location_dest_id(self):
        location = False
        if self._context.get('default_picking_type_id'):
            location = self.env['stock.picking.type'].browse(
                self.env.context['default_picking_type_id']
            ).default_location_dest_id
        if not location:
            location = self.env.ref('stock.stock_location_stock',
                                    raise_if_not_found=False)
            try:
                location.check_access_rule('read')
            except (AttributeError, AccessError):
                location = self.env['stock.warehouse'].search(
                    [('company_id', '=', self.env.user.company_id.id)],
                    limit=1).lot_stock_id
        return location and location.id or False

    name = fields.Char('Reference',
                       copy=False,
                       readonly=True,
                       default=lambda x: _('New'))
    origin = fields.Char(
        'Source',
        copy=False,
        help=
        "Reference of the document that generated this production order request."
    )

    product_id = fields.Many2one('product.product',
                                 'Product',
                                 domain=[('type', 'in', ['product', 'consu'])],
                                 readonly=True,
                                 required=True,
                                 states={'confirmed': [('readonly', False)]})
    product_tmpl_id = fields.Many2one('product.template',
                                      'Product Template',
                                      related='product_id.product_tmpl_id',
                                      readonly=True)
    product_qty = fields.Float(
        'Quantity To Produce',
        default=1.0,
        digits=dp.get_precision('Product Unit of Measure'),
        readonly=True,
        required=True,
        track_visibility='onchange',
        states={'confirmed': [('readonly', False)]})
    product_uom_id = fields.Many2one(
        'uom.uom',
        'Product Unit of Measure',
        oldname='product_uom',
        readonly=True,
        required=True,
        states={'confirmed': [('readonly', False)]})
    product_uom_qty = fields.Float(string='Total Quantity',
                                   compute='_compute_product_uom_qty',
                                   store=True)
    picking_type_id = fields.Many2one('stock.picking.type',
                                      'Operation Type',
                                      default=_get_default_picking_type,
                                      required=True)
    location_src_id = fields.Many2one(
        'stock.location',
        'Raw Materials Location',
        default=_get_default_location_src_id,
        readonly=True,
        required=True,
        states={'confirmed': [('readonly', False)]},
        help="Location where the system will look for components.")
    location_dest_id = fields.Many2one(
        'stock.location',
        'Finished Products Location',
        default=_get_default_location_dest_id,
        readonly=True,
        required=True,
        states={'confirmed': [('readonly', False)]},
        help="Location where the system will stock the finished products.")
    date_planned_start = fields.Datetime(
        'Deadline Start',
        copy=False,
        default=fields.Datetime.now,
        index=True,
        required=True,
        states={'confirmed': [('readonly', False)]},
        oldname="date_planned")
    date_planned_finished = fields.Datetime(
        'Deadline End',
        copy=False,
        default=fields.Datetime.now,
        index=True,
        states={'confirmed': [('readonly', False)]})
    date_start = fields.Datetime('Start Date',
                                 copy=False,
                                 index=True,
                                 readonly=True)
    date_finished = fields.Datetime('End Date',
                                    copy=False,
                                    index=True,
                                    readonly=True)
    bom_id = fields.Many2one(
        'mrp.bom',
        'Bill of Material',
        readonly=True,
        states={'confirmed': [('readonly', False)]},
        help=
        "Bill of Materials allow you to define the list of required raw materials to make a finished product."
    )
    routing_id = fields.Many2one(
        'mrp.routing',
        'Routing',
        readonly=True,
        compute='_compute_routing',
        store=True,
        help=
        "The list of operations (list of work centers) to produce the finished product. The routing "
        "is mainly used to compute work center costs during operations and to plan future loads on "
        "work centers based on production planning.")
    move_raw_ids = fields.One2many('stock.move',
                                   'raw_material_production_id',
                                   'Raw Materials',
                                   oldname='move_lines',
                                   copy=False,
                                   states={
                                       'done': [('readonly', True)],
                                       'cancel': [('readonly', True)]
                                   },
                                   domain=[('scrapped', '=', False)])
    move_finished_ids = fields.One2many('stock.move',
                                        'production_id',
                                        'Finished Products',
                                        copy=False,
                                        states={
                                            'done': [('readonly', True)],
                                            'cancel': [('readonly', True)]
                                        },
                                        domain=[('scrapped', '=', False)])
    finished_move_line_ids = fields.One2many('stock.move.line',
                                             compute='_compute_lines',
                                             inverse='_inverse_lines',
                                             string="Finished Product")
    workorder_ids = fields.One2many('mrp.workorder',
                                    'production_id',
                                    'Work Orders',
                                    copy=False,
                                    oldname='workcenter_lines',
                                    readonly=True)
    workorder_count = fields.Integer('# Work Orders',
                                     compute='_compute_workorder_count')
    workorder_done_count = fields.Integer(
        '# Done Work Orders', compute='_compute_workorder_done_count')
    move_dest_ids = fields.One2many('stock.move',
                                    'created_production_id',
                                    string="Stock Movements of Produced Goods")

    state = fields.Selection([('confirmed', 'Confirmed'),
                              ('planned', 'Planned'),
                              ('progress', 'In Progress'), ('done', 'Done'),
                              ('cancel', 'Cancelled')],
                             string='State',
                             copy=False,
                             default='confirmed',
                             track_visibility='onchange')
    availability = fields.Selection(
        [('assigned', 'Available'),
         ('partially_available', 'Partially Available'),
         ('waiting', 'Waiting'), ('none', 'None')],
        string='Materials Availability',
        compute='_compute_availability',
        store=True)

    unreserve_visible = fields.Boolean(
        'Allowed to Unreserve Inventory',
        compute='_compute_unreserve_visible',
        help='Technical field to check when we can unreserve')
    post_visible = fields.Boolean(
        'Allowed to Post Inventory',
        compute='_compute_post_visible',
        help='Technical field to check when we can post')
    consumed_less_than_planned = fields.Boolean(
        compute='_compute_consumed_less_than_planned',
        help=
        'Technical field used to see if we have to display a warning or not when confirming an order.'
    )

    user_id = fields.Many2one('res.users',
                              'Responsible',
                              default=lambda self: self._uid)
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('mrp.production'),
                                 required=True)

    check_to_done = fields.Boolean(
        compute="_get_produced_qty",
        string="Check Produced Qty",
        help="Technical Field to see if we can show 'Mark as Done' button")
    qty_produced = fields.Float(compute="_get_produced_qty",
                                string="Quantity Produced")
    procurement_group_id = fields.Many2one('procurement.group',
                                           'Procurement Group',
                                           copy=False)
    propagate = fields.Boolean(
        'Propagate cancel and split',
        help=
        'If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too'
    )
    has_moves = fields.Boolean(compute='_has_moves')
    scrap_ids = fields.One2many('stock.scrap', 'production_id', 'Scraps')
    scrap_count = fields.Integer(compute='_compute_scrap_move_count',
                                 string='Scrap Move')
    priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'),
                                 ('2', 'Urgent'), ('3', 'Very Urgent')],
                                'Priority',
                                readonly=True,
                                states={'confirmed': [('readonly', False)]},
                                default='1')
    is_locked = fields.Boolean('Is Locked', default=True, copy=False)
    show_final_lots = fields.Boolean('Show Final Lots',
                                     compute='_compute_show_lots')
    production_location_id = fields.Many2one(
        'stock.location',
        "Production Location",
        related='product_id.property_stock_production',
        readonly=False)
    picking_ids = fields.Many2many(
        'stock.picking',
        compute='_compute_picking_ids',
        string='Picking associated to this manufacturing order')
    delivery_count = fields.Integer(string='Delivery Orders',
                                    compute='_compute_picking_ids')

    @api.depends('procurement_group_id')
    def _compute_picking_ids(self):
        for order in self:
            order.picking_ids = self.env['stock.picking'].search([
                ('group_id', '=', order.procurement_group_id.id),
                ('group_id', '!=', False),
            ])
            order.delivery_count = len(order.picking_ids)

    def action_view_mo_delivery(self):
        """ This function returns an action that display picking related to
        manufacturing order orders. It can either be a in a list or in a form
        view, if there is only one picking to show.
        """
        self.ensure_one()
        action = self.env.ref('stock.action_picking_tree_all').read()[0]
        pickings = self.mapped('picking_ids')
        if len(pickings) > 1:
            action['domain'] = [('id', 'in', pickings.ids)]
        elif pickings:
            form_view = [(self.env.ref('stock.view_picking_form').id, 'form')]
            if 'views' in action:
                action['views'] = form_view + [
                    (state, view)
                    for state, view in action['views'] if view != 'form'
                ]
            else:
                action['views'] = form_view
            action['res_id'] = pickings.id
        return action

    @api.depends('product_uom_id', 'product_qty', 'product_id.uom_id')
    def _compute_product_uom_qty(self):
        for production in self:
            if production.product_id.uom_id != production.product_uom_id:
                production.product_uom_qty = production.product_uom_id._compute_quantity(
                    production.product_qty, production.product_id.uom_id)
            else:
                production.product_uom_qty = production.product_qty

    @api.depends('product_id.tracking')
    def _compute_show_lots(self):
        for production in self:
            production.show_final_lots = production.product_id.tracking != 'none'

    def _inverse_lines(self):
        """ Little hack to make sure that when you change something on these objects, it gets saved"""
        pass

    @api.depends('move_finished_ids.move_line_ids')
    def _compute_lines(self):
        for production in self:
            production.finished_move_line_ids = production.move_finished_ids.mapped(
                'move_line_ids')

    @api.multi
    @api.depends('bom_id.routing_id', 'bom_id.routing_id.operation_ids')
    def _compute_routing(self):
        for production in self:
            if production.bom_id.routing_id.operation_ids:
                production.routing_id = production.bom_id.routing_id.id
            else:
                production.routing_id = False

    @api.multi
    @api.depends('workorder_ids')
    def _compute_workorder_count(self):
        data = self.env['mrp.workorder'].read_group(
            [('production_id', 'in', self.ids)], ['production_id'],
            ['production_id'])
        count_data = dict(
            (item['production_id'][0], item['production_id_count'])
            for item in data)
        for production in self:
            production.workorder_count = count_data.get(production.id, 0)

    @api.multi
    @api.depends('workorder_ids.state')
    def _compute_workorder_done_count(self):
        data = self.env['mrp.workorder'].read_group(
            [('production_id', 'in', self.ids),
             ('state', '=', 'done')], ['production_id'], ['production_id'])
        count_data = dict(
            (item['production_id'][0], item['production_id_count'])
            for item in data)
        for production in self:
            production.workorder_done_count = count_data.get(production.id, 0)

    @api.multi
    @api.depends('move_raw_ids.state', 'workorder_ids.move_raw_ids',
                 'bom_id.ready_to_produce')
    def _compute_availability(self):
        for order in self:
            if not order.move_raw_ids:
                order.availability = 'none'
                continue
            if order.bom_id.ready_to_produce == 'all_available':
                order.availability = any(
                    move.state not in ('assigned', 'done', 'cancel')
                    for move in order.move_raw_ids) and 'waiting' or 'assigned'
            else:
                move_raw_ids = order.move_raw_ids.filtered(
                    lambda m: m.product_qty)
                partial_list = [
                    x.state in ('partially_available', 'assigned')
                    for x in move_raw_ids
                ]
                assigned_list = [
                    x.state in ('assigned', 'done', 'cancel')
                    for x in move_raw_ids
                ]
                order.availability = (all(assigned_list) and 'assigned') or (
                    any(partial_list) and 'partially_available') or 'waiting'

    @api.depends('move_raw_ids', 'is_locked', 'state',
                 'move_raw_ids.quantity_done')
    def _compute_unreserve_visible(self):
        for order in self:
            already_reserved = order.is_locked and order.state not in (
                'done',
                'cancel') and order.mapped('move_raw_ids.move_line_ids')
            any_quantity_done = any(
                [m.quantity_done > 0 for m in order.move_raw_ids])
            order.unreserve_visible = not any_quantity_done and already_reserved

    @api.multi
    @api.depends('move_raw_ids.quantity_done',
                 'move_finished_ids.quantity_done', 'is_locked')
    def _compute_post_visible(self):
        for order in self:
            if order.product_tmpl_id._is_cost_method_standard():
                order.post_visible = order.is_locked and any(
                    (x.quantity_done > 0 and x.state not in ['done', 'cancel'])
                    for x in order.move_raw_ids | order.move_finished_ids)
            else:
                order.post_visible = order.is_locked and any(
                    (x.quantity_done > 0 and x.state not in ['done', 'cancel'])
                    for x in order.move_finished_ids)
            order.post_visible &= all(wo.state in ['done', 'cancel']
                                      for wo in order.workorder_ids) or all(
                                          m.product_id.tracking == 'none'
                                          for m in order.move_raw_ids)

    @api.multi
    @api.depends('move_raw_ids.quantity_done', 'move_raw_ids.product_uom_qty')
    def _compute_consumed_less_than_planned(self):
        for order in self:
            order.consumed_less_than_planned = any(
                order.move_raw_ids.filtered(lambda move: float_compare(
                    move.quantity_done,
                    move.product_uom_qty,
                    precision_rounding=move.product_uom.rounding) == -1))

    @api.multi
    @api.depends('workorder_ids.state', 'move_finished_ids', 'is_locked')
    def _get_produced_qty(self):
        for production in self:
            done_moves = production.move_finished_ids.filtered(
                lambda x: x.state != 'cancel' and x.product_id.id == production
                .product_id.id)
            qty_produced = sum(done_moves.mapped('quantity_done'))
            wo_done = True
            if any([
                    x.state not in ('done', 'cancel')
                    for x in production.workorder_ids
            ]):
                wo_done = False
            production.check_to_done = (
                production.is_locked and done_moves and float_compare(
                    qty_produced,
                    production.product_qty,
                    precision_rounding=production.product_uom_id.rounding) !=
                -1 and (production.state not in ("done", "cancel"))
                and wo_done)
            production.qty_produced = qty_produced
        return True

    @api.multi
    @api.depends('move_raw_ids')
    def _has_moves(self):
        for mo in self:
            mo.has_moves = any(mo.move_raw_ids)

    @api.multi
    def _compute_scrap_move_count(self):
        data = self.env['stock.scrap'].read_group(
            [('production_id', 'in', self.ids)], ['production_id'],
            ['production_id'])
        count_data = dict(
            (item['production_id'][0], item['production_id_count'])
            for item in data)
        for production in self:
            production.scrap_count = count_data.get(production.id, 0)

    _sql_constraints = [
        ('name_uniq', 'unique(name, company_id)',
         'Reference must be unique per Company!'),
        ('qty_positive', 'check (product_qty > 0)',
         'The quantity to produce must be positive!'),
    ]

    @api.onchange('product_id', 'picking_type_id', 'company_id')
    def onchange_product_id(self):
        """ Finds UoM of changed product. """
        if not self.product_id:
            self.bom_id = False
        else:
            bom = self.env['mrp.bom']._bom_find(
                product=self.product_id,
                picking_type=self.picking_type_id,
                company_id=self.company_id.id)
            if bom.type == 'normal':
                self.bom_id = bom.id
                self.product_qty = self.bom_id.product_qty
                self.product_uom_id = self.bom_id.product_uom_id.id
            else:
                self.bom_id = False
                self.product_uom_id = self.product_id.uom_id.id
            return {
                'domain': {
                    'product_uom_id': [('category_id', '=',
                                        self.product_id.uom_id.category_id.id)]
                }
            }

    @api.onchange('bom_id')
    def _onchange_bom_id(self):
        self.product_qty = self.bom_id.product_qty
        self.product_uom_id = self.bom_id.product_uom_id.id
        self.picking_type_id = self.bom_id.picking_type_id or self.picking_type_id

    @api.onchange('picking_type_id', 'routing_id')
    def onchange_picking_type(self):
        location = self.env.ref('stock.stock_location_stock')
        try:
            location.check_access_rule('read')
        except (AttributeError, AccessError):
            location = self.env['stock.warehouse'].search(
                [('company_id', '=', self.env.user.company_id.id)],
                limit=1).lot_stock_id
        self.location_src_id = self.routing_id.location_id.id or self.picking_type_id.default_location_src_id.id or location.id
        self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id

    @api.multi
    def write(self, vals):
        res = super(MrpProduction, self).write(vals)
        if 'date_planned_start' in vals:
            moves = (self.mapped('move_raw_ids') +
                     self.mapped('move_finished_ids')
                     ).filtered(lambda r: r.state not in ['done', 'cancel'])
            moves.write({
                'date_expected': vals['date_planned_start'],
            })
        return res

    @api.model
    def create(self, values):
        if not values.get('name', False) or values['name'] == _('New'):
            picking_type_id = values.get(
                'picking_type_id') or self._get_default_picking_type()
            picking_type_id = self.env['stock.picking.type'].browse(
                picking_type_id)
            if picking_type_id:
                values['name'] = picking_type_id.sequence_id.next_by_id()
            else:
                values['name'] = self.env['ir.sequence'].next_by_code(
                    'mrp.production') or _('New')
        if not values.get('procurement_group_id'):
            values['procurement_group_id'] = self.env[
                "procurement.group"].create({
                    'name': values['name']
                }).id
        production = super(MrpProduction, self).create(values)
        production._generate_moves()
        return production

    @api.multi
    def unlink(self):
        if any(production.state != 'cancel' for production in self):
            raise UserError(
                _('Cannot delete a manufacturing order not in cancel state'))
        return super(MrpProduction, self).unlink()

    def action_toggle_is_locked(self):
        self.ensure_one()
        self.is_locked = not self.is_locked
        return True

    @api.multi
    def _generate_moves(self):
        for production in self:
            production._generate_finished_moves()
            factor = production.product_uom_id._compute_quantity(
                production.product_qty, production.bom_id.product_uom_id
            ) / production.bom_id.product_qty
            boms, lines = production.bom_id.explode(
                production.product_id,
                factor,
                picking_type=production.bom_id.picking_type_id)
            production._generate_raw_moves(lines)
            # Check for all draft moves whether they are mto or not
            production._adjust_procure_method()
            production.move_raw_ids._action_confirm()
        return True

    def _generate_finished_moves(self):
        move = self.env['stock.move'].create({
            'name':
            self.name,
            'date':
            self.date_planned_start,
            'date_expected':
            self.date_planned_start,
            'picking_type_id':
            self.picking_type_id.id,
            'product_id':
            self.product_id.id,
            'product_uom':
            self.product_uom_id.id,
            'product_uom_qty':
            self.product_qty,
            'location_id':
            self.product_id.property_stock_production.id,
            'location_dest_id':
            self.location_dest_id.id,
            'company_id':
            self.company_id.id,
            'production_id':
            self.id,
            'warehouse_id':
            self.location_dest_id.get_warehouse().id,
            'origin':
            self.name,
            'group_id':
            self.procurement_group_id.id,
            'propagate':
            self.propagate,
            'move_dest_ids': [(4, x.id) for x in self.move_dest_ids],
        })
        move._action_confirm()
        return move

    def _generate_raw_moves(self, exploded_lines):
        self.ensure_one()
        moves = self.env['stock.move'].create([
            d
            for d in itertools.starmap(self._get_raw_move_data, exploded_lines)
            if d
        ])
        return moves

    def _generate_raw_move(self, bom_line, line_data):
        v = self._get_raw_move_data(bom_line, line_data)
        if not v:
            return self.env['stock.move']
        v['state'] = 'confirmed'
        return self.env['stock.move'].create(v)

    def _get_raw_move_data(self, bom_line, line_data):
        quantity = line_data['qty']
        # alt_op needed for the case when you explode phantom bom and all the lines will be consumed in the operation given by the parent bom line
        alt_op = line_data['parent_line'] and line_data[
            'parent_line'].operation_id.id or False
        if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom':
            return
        if bom_line.product_id.type not in ['product', 'consu']:
            return
        if self.routing_id:
            routing = self.routing_id
        else:
            routing = self.bom_id.routing_id
        if routing and routing.location_id:
            source_location = routing.location_id
        else:
            source_location = self.location_src_id
        original_quantity = (self.product_qty - self.qty_produced) or 1.0
        return {
            'sequence': bom_line.sequence,
            'name': self.name,
            'date': self.date_planned_start,
            'date_expected': self.date_planned_start,
            'bom_line_id': bom_line.id,
            'picking_type_id': self.picking_type_id.id,
            'product_id': bom_line.product_id.id,
            'product_uom_qty': quantity,
            'product_uom': bom_line.product_uom_id.id,
            'location_id': source_location.id,
            'location_dest_id': self.product_id.property_stock_production.id,
            'raw_material_production_id': self.id,
            'company_id': self.company_id.id,
            'operation_id': bom_line.operation_id.id or alt_op,
            'price_unit': bom_line.product_id.standard_price,
            'procure_method': 'make_to_stock',
            'origin': self.name,
            'warehouse_id': source_location.get_warehouse().id,
            'group_id': self.procurement_group_id.id,
            'propagate': self.propagate,
            'unit_factor': quantity / original_quantity,
        }

    @api.multi
    def _adjust_procure_method(self):
        try:
            mto_route = self.env['stock.warehouse']._find_global_route(
                'stock.route_warehouse0_mto', _('Make To Order'))
        except:
            mto_route = False
        for move in self.move_raw_ids:
            product = move.product_id
            routes = product.route_ids + product.route_from_categ_ids + move.warehouse_id.route_ids
            # TODO: optimize with read_group?
            pull = self.env['stock.rule'].search(
                [('route_id', 'in', [x.id for x in routes]),
                 ('location_src_id', '=', move.location_id.id),
                 ('location_id', '=', move.location_dest_id.id),
                 ('action', '!=', 'push')],
                limit=1)
            if pull and (pull.procure_method == 'make_to_order'):
                move.procure_method = pull.procure_method
            elif not pull:  # If there is no make_to_stock rule either
                if mto_route and mto_route.id in [x.id for x in routes]:
                    move.procure_method = 'make_to_order'

    @api.multi
    def _update_raw_move(self, bom_line, line_data):
        """ :returns update_move, old_quantity, new_quantity """
        quantity = line_data['qty']
        self.ensure_one()
        move = self.move_raw_ids.filtered(
            lambda x: x.bom_line_id.id == bom_line.id and x.state not in
            ('done', 'cancel'))
        if move:
            old_qty = move[0].product_uom_qty
            if quantity > 0:
                production = move[0].raw_material_production_id
                production_qty = production.product_qty - production.qty_produced
                move[0].write({'product_uom_qty': quantity})
                move[0]._recompute_state()
                move[0]._action_assign()
                move[0].unit_factor = production_qty and (
                    quantity - move[0].quantity_done) / production_qty or 1.0
                return move[0], old_qty, quantity
            else:
                if move[0].quantity_done > 0:
                    raise UserError(
                        _('Lines need to be deleted, but can not as you still have some quantities to consume in them. '
                          ))
                move[0]._action_cancel()
                move[0].unlink()
                return self.env['stock.move'], old_qty, quantity
        else:
            move = self._generate_raw_move(bom_line, line_data)
            return move, 0, quantity

    @api.multi
    def action_assign(self):
        for production in self:
            production.move_raw_ids._action_assign()
        return True

    @api.multi
    def open_produce_product(self):
        self.ensure_one()
        if self.bom_id.type == 'phantom':
            raise UserError(
                _('You cannot produce a MO with a bom kit product.'))
        action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
        return action

    @api.multi
    def button_plan(self):
        """ Create work orders. And probably do stuff, like things. """
        orders_to_plan = self.filtered(
            lambda order: order.routing_id and order.state == 'confirmed')
        for order in orders_to_plan:
            quantity = order.product_uom_id._compute_quantity(
                order.product_qty,
                order.bom_id.product_uom_id) / order.bom_id.product_qty
            boms, lines = order.bom_id.explode(
                order.product_id,
                quantity,
                picking_type=order.bom_id.picking_type_id)
            order._generate_workorders(boms)
        return orders_to_plan.write({'state': 'planned'})

    @api.multi
    def _generate_workorders(self, exploded_boms):
        workorders = self.env['mrp.workorder']
        original_one = False
        for bom, bom_data in exploded_boms:
            # If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing
            if bom.routing_id.id and (
                    not bom_data['parent_line']
                    or bom_data['parent_line'].bom_id.routing_id.id !=
                    bom.routing_id.id):
                temp_workorders = self._workorders_create(bom, bom_data)
                workorders += temp_workorders
                if temp_workorders:  # In order to avoid two "ending work orders"
                    if original_one:
                        temp_workorders[-1].next_work_order_id = original_one
                    original_one = temp_workorders[0]
        return workorders

    def _workorders_create(self, bom, bom_data):
        """
        :param bom: in case of recursive boms: we could create work orders for child
                    BoMs
        """
        workorders = self.env['mrp.workorder']
        bom_qty = bom_data['qty']

        # Initial qty producing
        if self.product_id.tracking == 'serial':
            quantity = 1.0
        else:
            quantity = self.product_qty - sum(
                self.move_finished_ids.mapped('quantity_done'))
            quantity = quantity if (quantity > 0) else 0

        for operation in bom.routing_id.operation_ids:
            # create workorder
            cycle_number = float_round(bom_qty /
                                       operation.workcenter_id.capacity,
                                       precision_digits=0,
                                       rounding_method='UP')
            duration_expected = (operation.workcenter_id.time_start +
                                 operation.workcenter_id.time_stop +
                                 cycle_number * operation.time_cycle * 100.0 /
                                 operation.workcenter_id.time_efficiency)
            workorder = workorders.create({
                'name':
                operation.name,
                'production_id':
                self.id,
                'workcenter_id':
                operation.workcenter_id.id,
                'operation_id':
                operation.id,
                'duration_expected':
                duration_expected,
                'state':
                len(workorders) == 0 and 'ready' or 'pending',
                'qty_producing':
                quantity,
                'capacity':
                operation.workcenter_id.capacity,
            })
            if workorders:
                workorders[-1].next_work_order_id = workorder.id
                workorders[-1]._start_nextworkorder()
            workorders += workorder

            # assign moves; last operation receive all unassigned moves (which case ?)
            moves_raw = self.move_raw_ids.filtered(
                lambda move: move.operation_id == operation)
            if len(workorders) == len(bom.routing_id.operation_ids):
                moves_raw |= self.move_raw_ids.filtered(
                    lambda move: not move.operation_id)
            moves_finished = self.move_finished_ids.filtered(
                lambda move: move.operation_id == operation
            )  #TODO: code does nothing, unless maybe by_products?
            moves_raw.mapped('move_line_ids').write(
                {'workorder_id': workorder.id})
            (moves_finished + moves_raw).write({'workorder_id': workorder.id})

            workorder._generate_lot_ids()
        return workorders

    def _check_lots(self):
        # Check that the raw materials were consumed for lots that we have produced.
        if self.product_id.tracking != 'none':
            finished_lots = set(self.finished_move_line_ids.mapped('lot_id'))
            raw_finished_lots = set(
                self.move_raw_ids.mapped('move_line_ids.lot_produced_id'))
            if not (raw_finished_lots <= finished_lots):
                lots_short = raw_finished_lots - finished_lots
                error_msg = _(
                    'Some raw materials have been consumed for a lot/serial number that has not been produced. '
                    'Unlock the MO and click on the components lines to correct it.\n'
                    'List of the components:\n')
                move_lines = self.move_raw_ids.mapped(
                    'move_line_ids').filtered(
                        lambda x: x.lot_produced_id in lots_short)
                for ml in move_lines:
                    error_msg += ml.product_id.display_name + ' (' + ml.lot_produced_id.name + ')\n'
                raise UserError(error_msg)

    @api.multi
    def action_cancel(self):
        """ Cancels production order, unfinished stock moves and set procurement
        orders in exception """
        if any(workorder.state == 'progress'
               for workorder in self.mapped('workorder_ids')):
            raise UserError(
                _('You can not cancel production order, a work order is still in progress.'
                  ))
        documents = {}
        for production in self:
            for move_raw_id in production.move_raw_ids.filtered(
                    lambda m: m.state not in ('done', 'cancel')):
                iterate_key = self._get_document_iterate_key(move_raw_id)
                if iterate_key:
                    document = self.env[
                        'stock.picking']._log_activity_get_documents(
                            {move_raw_id: (move_raw_id.product_uom_qty, 0)},
                            iterate_key, 'UP')
                    for key, value in document.items():
                        if documents.get(key):
                            documents[key] += [value]
                        else:
                            documents[key] = [value]
            production.workorder_ids.filtered(
                lambda x: x.state != 'cancel').action_cancel()
            finish_moves = production.move_finished_ids.filtered(
                lambda x: x.state not in ('done', 'cancel'))
            raw_moves = production.move_raw_ids.filtered(
                lambda x: x.state not in ('done', 'cancel'))
            (finish_moves | raw_moves)._action_cancel()
            picking_ids = production.picking_ids.filtered(
                lambda x: x.state not in ('done', 'cancel'))
            picking_ids.action_cancel()
        self.write({'state': 'cancel', 'is_locked': True})
        if documents:
            filtered_documents = {}
            for (parent, responsible), rendering_context in documents.items():
                if not parent or parent._name == 'stock.picking' and parent.state == 'cancel' or parent == self:
                    continue
                filtered_documents[(parent, responsible)] = rendering_context
            self._log_manufacture_exception(filtered_documents, cancel=True)
        return True

    def _get_document_iterate_key(self, move_raw_id):
        return move_raw_id.move_orig_ids and 'move_orig_ids' or False

    def _cal_price(self, consumed_moves):
        self.ensure_one()
        return True

    @api.multi
    def post_inventory(self):
        for order in self:
            moves_not_to_do = order.move_raw_ids.filtered(
                lambda x: x.state == 'done')
            moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in
                                                      ('done', 'cancel'))
            for move in moves_to_do.filtered(
                    lambda m: m.product_qty == 0.0 and m.quantity_done > 0):
                move.product_uom_qty = move.quantity_done
            moves_to_do._action_done()
            moves_to_do = order.move_raw_ids.filtered(
                lambda x: x.state == 'done') - moves_not_to_do
            order._cal_price(moves_to_do)
            moves_to_finish = order.move_finished_ids.filtered(
                lambda x: x.state not in ('done', 'cancel'))
            moves_to_finish._action_done()
            order.action_assign()
            consume_move_lines = moves_to_do.mapped('active_move_line_ids')
            for moveline in moves_to_finish.mapped('active_move_line_ids'):
                if moveline.product_id == order.product_id and moveline.move_id.has_tracking != 'none':
                    if any(
                        [not ml.lot_produced_id for ml in consume_move_lines]):
                        raise UserError(
                            _('You can not consume without telling for which lot you consumed it'
                              ))
                    # Link all movelines in the consumed with same lot_produced_id false or the correct lot_produced_id
                    filtered_lines = consume_move_lines.filtered(
                        lambda x: x.lot_produced_id == moveline.lot_id)
                    moveline.write({
                        'consume_line_ids':
                        [(6, 0, [x for x in filtered_lines.ids])]
                    })
                else:
                    # Link with everything
                    moveline.write({
                        'consume_line_ids':
                        [(6, 0, [x for x in consume_move_lines.ids])]
                    })
        return True

    @api.multi
    def button_mark_done(self):
        self.ensure_one()
        for wo in self.workorder_ids:
            if wo.time_ids.filtered(lambda x: (not x.date_end) and (
                    x.loss_type in ('productive', 'performance'))):
                raise UserError(_('Work order %s is still running') % wo.name)
        self._check_lots()

        self.post_inventory()
        # Moves without quantity done are not posted => set them as done instead of canceling. In
        # case the user edits the MO later on and sets some consumed quantity on those, we do not
        # want the move lines to be canceled.
        (self.move_raw_ids | self.move_finished_ids
         ).filtered(lambda x: x.state not in ('done', 'cancel')).write({
             'state':
             'done',
             'product_uom_qty':
             0.0,
         })
        return self.write({
            'state': 'done',
            'date_finished': fields.Datetime.now()
        })

    @api.multi
    def do_unreserve(self):
        for production in self:
            production.move_raw_ids.filtered(
                lambda x: x.state not in ('done', 'cancel'))._do_unreserve()
        return True

    @api.multi
    def button_unreserve(self):
        self.ensure_one()
        self.do_unreserve()
        return True

    @api.multi
    def button_scrap(self):
        self.ensure_one()
        return {
            'name': _('Scrap'),
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'stock.scrap',
            'view_id': self.env.ref('stock.stock_scrap_form_view2').id,
            'type': 'ir.actions.act_window',
            'context': {
                'default_production_id':
                self.id,
                'product_ids':
                (self.move_raw_ids.filtered(lambda x: x.state not in
                                            ('done', 'cancel'))
                 | self.move_finished_ids.filtered(lambda x: x.state == 'done')
                 ).mapped('product_id').ids,
            },
            'target': 'new',
        }

    @api.multi
    def action_see_move_scrap(self):
        self.ensure_one()
        action = self.env.ref('stock.action_stock_scrap').read()[0]
        action['domain'] = [('production_id', '=', self.id)]
        return action

    @api.model
    def get_empty_list_help(self, help):
        self = self.with_context(
            empty_list_help_document_name=_("manufacturing order"), )
        return super(MrpProduction, self).get_empty_list_help(help)

    def _log_downside_manufactured_quantity(self, moves_modification):
        def _keys_in_sorted(move):
            """ sort by picking and the responsible for the product the
            move.
            """
            return (move.picking_id.id, move.product_id.responsible_id.id)

        def _keys_in_groupby(move):
            """ group by picking and the responsible for the product the
            move.
            """
            return (move.picking_id, move.product_id.responsible_id)

        def _render_note_exception_quantity_mo(rendering_context):
            values = {
                'production_order':
                self,
                'order_exceptions':
                dict((key, d[key]) for d in rendering_context for key in d),
                'impacted_pickings':
                False,
                'cancel':
                False
            }
            return self.env.ref('mrp.exception_on_mo').render(values=values)

        documents = {}
        for move, (old_qty, new_qty) in moves_modification.items():
            document = self.env['stock.picking']._log_activity_get_documents(
                {move: (old_qty, new_qty)}, 'move_dest_ids', 'DOWN',
                _keys_in_sorted, _keys_in_groupby)
            for key, value in document.items():
                if documents.get(key):
                    documents[key] += [value]
                else:
                    documents[key] = [value]
        self.env['stock.picking']._log_activity(
            _render_note_exception_quantity_mo, documents)

    def _log_manufacture_exception(self, documents, cancel=False):
        def _render_note_exception_quantity_mo(rendering_context):
            visited_objects = []
            order_exceptions = {}
            for exception in rendering_context:
                order_exception, visited = exception
                order_exceptions.update(order_exception)
                visited_objects += visited
            visited_objects = self.env[visited_objects[0]._name].concat(
                *visited_objects)
            impacted_object = []
            if visited_objects and visited_objects._name == 'stock.move':
                visited_objects |= visited_objects.mapped('move_orig_ids')
                impacted_object = visited_objects.filtered(
                    lambda m: m.state not in ('done', 'cancel')).mapped(
                        'picking_id')
            values = {
                'production_order': self,
                'order_exceptions': order_exceptions,
                'impacted_object': impacted_object,
                'cancel': cancel
            }
            return self.env.ref('mrp.exception_on_mo').render(values=values)

        self.env['stock.picking']._log_activity(
            _render_note_exception_quantity_mo, documents)
Exemple #20
0
class SaleAdvancePaymentInv(models.TransientModel):
    _name = "sale.advance.payment.inv"
    _description = "Sales Advance Payment Invoice"

    @api.model
    def _count(self):
        return len(self._context.get('active_ids', []))

    @api.model
    def _get_advance_payment_method(self):
        if self._count() == 1:
            sale_obj = self.env['sale.order']
            order = sale_obj.browse(self._context.get('active_ids'))[0]
            if order.order_line.filtered(lambda dp: dp.is_downpayment) and order.invoice_ids.filtered(lambda invoice: invoice.state != 'cancel') or order.order_line.filtered(lambda l: l.qty_to_invoice < 0):
                return 'all'
            else:
                return 'delivered'
        return 'all'

    @api.model
    def _default_product_id(self):
        product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id')
        return self.env['product.product'].browse(int(product_id)).exists()

    @api.model
    def _default_deposit_account_id(self):
        return self._default_product_id().property_account_income_id

    @api.model
    def _default_deposit_taxes_id(self):
        return self._default_product_id().taxes_id

    advance_payment_method = fields.Selection([
        ('delivered', 'Invoiceable lines'),
        ('all', 'Invoiceable lines (deduct down payments)'),
        ('percentage', 'Down payment (percentage)'),
        ('fixed', 'Down payment (fixed amount)')
        ], string='What do you want to invoice?', default=_get_advance_payment_method, required=True)
    product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')],
        default=_default_product_id)
    count = fields.Integer(default=_count, string='Order Count')
    amount = fields.Float('Down Payment Amount', digits=dp.get_precision('Account'), help="The amount to be invoiced in advance, taxes excluded.")
    deposit_account_id = fields.Many2one("account.account", string="Income Account", domain=[('deprecated', '=', False)],
        help="Account used for deposits", default=_default_deposit_account_id)
    deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes", help="Taxes used for deposits", default=_default_deposit_taxes_id)

    @api.onchange('advance_payment_method')
    def onchange_advance_payment_method(self):
        if self.advance_payment_method == 'percentage':
            return {'value': {'amount': 0}}
        return {}

    @api.multi
    def _create_invoice(self, order, so_line, amount):
        inv_obj = self.env['account.invoice']
        ir_property_obj = self.env['ir.property']

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

        if self.amount <= 0.00:
            raise UserError(_('The value of the down payment amount must be positive.'))
        context = {'lang': order.partner_id.lang}
        if self.advance_payment_method == 'percentage':
            amount = order.amount_untaxed * self.amount / 100
            name = _("Down payment of %s%%") % (self.amount,)
        else:
            amount = self.amount
            name = _('Down Payment')
        del context
        taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id)
        if order.fiscal_position_id and taxes:
            tax_ids = order.fiscal_position_id.map_tax(taxes, self.product_id, order.partner_shipping_id).ids
        else:
            tax_ids = taxes.ids

        invoice = inv_obj.create({
            'name': order.client_order_ref or order.name,
            'origin': order.name,
            'type': 'out_invoice',
            'reference': False,
            'account_id': order.partner_id.property_account_receivable_id.id,
            'partner_id': order.partner_invoice_id.id,
            'partner_shipping_id': order.partner_shipping_id.id,
            'invoice_line_ids': [(0, 0, {
                'name': name,
                'origin': order.name,
                'account_id': account_id,
                'price_unit': amount,
                'quantity': 1.0,
                'discount': 0.0,
                'uom_id': self.product_id.uom_id.id,
                'product_id': self.product_id.id,
                'sale_line_ids': [(6, 0, [so_line.id])],
                'invoice_line_tax_ids': [(6, 0, tax_ids)],
                'analytic_tag_ids': [(6, 0, so_line.analytic_tag_ids.ids)],
                'account_analytic_id': order.analytic_account_id.id or False,
            })],
            'currency_id': order.pricelist_id.currency_id.id,
            'payment_term_id': order.payment_term_id.id,
            'fiscal_position_id': order.fiscal_position_id.id or order.partner_id.property_account_position_id.id,
            'team_id': order.team_id.id,
            'user_id': order.user_id.id,
            'company_id': order.company_id.id,
            'comment': order.note,
        })
        invoice.compute_taxes()
        invoice.message_post_with_view('mail.message_origin_link',
                    values={'self': invoice, 'origin': order},
                    subtype_id=self.env.ref('mail.mt_note').id)
        return invoice

    @api.multi
    def create_invoices(self):
        sale_orders = self.env['sale.order'].browse(self._context.get('active_ids', []))

        if self.advance_payment_method == 'delivered':
            sale_orders.action_invoice_create()
        elif self.advance_payment_method == 'all':
            sale_orders.action_invoice_create(final=True)
        else:
            # Create deposit product if necessary
            if not self.product_id:
                vals = self._prepare_deposit_product()
                self.product_id = self.env['product.product'].create(vals)
                self.env['ir.config_parameter'].sudo().set_param('sale.default_deposit_product_id', self.product_id.id)

            sale_line_obj = self.env['sale.order.line']
            for order in sale_orders:
                if self.advance_payment_method == 'percentage':
                    amount = order.amount_untaxed * self.amount / 100
                else:
                    amount = self.amount
                if self.product_id.invoice_policy != 'order':
                    raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.'))
                if self.product_id.type != 'service':
                    raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product."))
                taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id)
                if order.fiscal_position_id and taxes:
                    tax_ids = order.fiscal_position_id.map_tax(taxes, self.product_id, order.partner_shipping_id).ids
                else:
                    tax_ids = taxes.ids
                context = {'lang': order.partner_id.lang}
                analytic_tag_ids = []
                for line in order.order_line:
                    analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in line.analytic_tag_ids]
                so_line = sale_line_obj.create({
                    'name': _('Advance: %s') % (time.strftime('%m %Y'),),
                    'price_unit': amount,
                    'product_uom_qty': 0.0,
                    'order_id': order.id,
                    'discount': 0.0,
                    'product_uom': self.product_id.uom_id.id,
                    'product_id': self.product_id.id,
                    'analytic_tag_ids': analytic_tag_ids,
                    'tax_id': [(6, 0, tax_ids)],
                    'is_downpayment': True,
                })
                del context
                self._create_invoice(order, so_line, amount)
        if self._context.get('open_invoices', False):
            return sale_orders.action_view_invoice()
        return {'type': 'ir.actions.act_window_close'}

    def _prepare_deposit_product(self):
        return {
            'name': 'Down payment',
            'type': 'service',
            'invoice_policy': 'order',
            'property_account_income_id': self.deposit_account_id.id,
            'taxes_id': [(6, 0, self.deposit_taxes_id.ids)],
            'company_id': False,
        }