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" ), ]
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'])
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
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, )
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)
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) }
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
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'
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
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']
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))
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
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
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
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 {}
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': {}}
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'}
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)
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, }