class ResPartner(models.Model): _inherit = 'res.partner' sale_discount = fields.Float( digits=dp.get_precision('Discount'), string='Discount (%)', company_dependent=True, )
class ResCompany(models.Model): _inherit = 'res.company' plafond_secu = fields.Float(string='Plafond de la Securite Sociale', digits=dp.get_precision('Payroll')) nombre_employes = fields.Integer(string='Nombre d\'employes') cotisation_prevoyance = fields.Float(string='Cotisation Patronale Prevoyance', digits=dp.get_precision('Payroll')) org_ss = fields.Char(string='Organisme de securite sociale') conv_coll = fields.Char(string='Convention collective')
class HrContract(models.Model): """ Employee contract allows to add different values in fields. Fields are used in salary rule computation. """ _inherit = 'hr.contract' tds = fields.Float(string='TDS', digits=dp.get_precision('Payroll'), help='Amount for Tax Deduction at Source') driver_salay = fields.Boolean(string='Driver Salary', help='Check this box if you provide allowance for driver') medical_insurance = fields.Float(string='Medical Insurance', digits=dp.get_precision('Payroll'), help='Deduction towards company provided medical insurance') voluntary_provident_fund = fields.Float(string='Voluntary Provident Fund (%)', digits=dp.get_precision('Payroll'), help='VPF is a safe option wherein you can contribute more than the PF ceiling of 12% that has been mandated by the government and VPF computed as percentage(%)') house_rent_allowance_metro_nonmetro = fields.Float(string='House Rent Allowance (%)', digits=dp.get_precision('Payroll'), help='HRA is an allowance given by the employer to the employee for taking care of his rental or accommodation expenses for metro city it is 50% and for non metro 40%. \nHRA computed as percentage(%)') supplementary_allowance = fields.Float(string='Supplementary Allowance', digits=dp.get_precision('Payroll'))
class ProductSetLine(models.Model): _name = 'product.set.line' _description = 'Product set line' _rec_name = 'product_id' _order = 'sequence' product_id = fields.Many2one( comodel_name='product.product', domain=[('sale_ok', '=', True)], string='Product', required=True ) quantity = fields.Float( string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1 ) product_set_id = fields.Many2one( 'product.set', string='Set', ondelete='cascade', ) active = fields.Boolean( string="Active", related="product_set_id.active", store=True, readonly=True, ) sequence = fields.Integer( string='Sequence', required=True, default=0, ) discount = fields.Float( string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0 )
class SaleOrder(models.Model): _inherit = 'sale.order' general_discount = fields.Float( digits=dp.get_precision('Discount'), string='Discount (%)', ) @api.onchange('partner_id') def onchange_partner_id(self): super().onchange_partner_id() self.general_discount = self.partner_id.sale_discount return @api.onchange('general_discount') def onchange_general_discount(self): self.mapped('order_line').update({ 'discount': self.general_discount, }) @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): """The purpose of this is to write a context on "order_line" field respecting other contexts on this field. There is a PR (https://github.com/eagle/eagle/pull/26607) to eagle for avoiding this. If merged, remove this method and add the attribute in the field. """ res = super(SaleOrder, self).fields_view_get( view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu, ) if view_type == 'form': order_xml = etree.XML(res['arch']) order_line_fields = order_xml.xpath("//field[@name='order_line']") if order_line_fields: order_line_field = order_line_fields[0] context = order_line_field.attrib.get("context", "{}").replace( "{", "{'default_discount': general_discount, ", 1, ) order_line_field.attrib['context'] = context res['arch'] = etree.tostring(order_xml) return res
class StockChangeStandardPrice(models.TransientModel): _name = "stock.change.standard.price" _description = "Change Standard Price" new_price = fields.Float( 'Price', digits=dp.get_precision('Product Price'), required=True, help= "If cost price is increased, stock variation account will be debited " "and stock output account will be credited with the value = (difference of amount * quantity available).\n" "If cost price is decreased, stock variation account will be creadited and stock input account will be debited." ) counterpart_account_id = fields.Many2one('account.account', string="Counter-Part Account", domain=[('deprecated', '=', False) ]) counterpart_account_id_required = fields.Boolean( string="Counter-Part Account Required") @api.model def default_get(self, fields): res = super(StockChangeStandardPrice, self).default_get(fields) product_or_template = self.env[self._context['active_model']].browse( self._context['active_id']) if 'new_price' in fields and 'new_price' not in res: res['new_price'] = product_or_template.standard_price if 'counterpart_account_id' in fields and 'counterpart_account_id' not in res: res['counterpart_account_id'] = product_or_template.property_account_expense_id.id or product_or_template.categ_id.property_account_expense_categ_id.id res['counterpart_account_id_required'] = bool( product_or_template.valuation == 'real_time') return res @api.multi def change_price(self): """ Changes the Standard Price of Product and creates an account move accordingly. """ self.ensure_one() if self._context['active_model'] == 'product.template': products = self.env['product.template'].browse( self._context['active_id']).product_variant_ids else: products = self.env['product.product'].browse( self._context['active_id']) products.do_change_standard_price(self.new_price, self.counterpart_account_id.id) return {'type': 'ir.actions.act_window_close'}
class LunchProduct(models.Model): """ Products available to order. A product is linked to a specific vendor. """ _name = 'lunch.product' _description = 'Lunch Product' name = fields.Char('Product', required=True) category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True) description = fields.Text('Description') price = fields.Float('Price', digits=dp.get_precision('Account')) supplier = fields.Many2one('res.partner', 'Vendor') active = fields.Boolean(default=True) available = fields.Boolean(compute='_get_available_product', search='_search_available_products') @api.depends('supplier') def _get_available_product(self): for product in self: if not product.supplier: product.available = True else: alerts = self.env['lunch.alert'].search([ ('partner_id', '=', self.supplier.id) ]) if alerts and not any(alert.display for alert in alerts): # every alert is not available product.available = False else: # no alert for the supplier or at least one is not available product.available = True def _search_available_products(self, operator, value): alerts = self.env['lunch.alert'].search([]) supplier_w_alerts = alerts.mapped('partner_id') available_suppliers = alerts.filtered(lambda a: a.display).mapped('partner_id') available_products = self.search([ '|', ('supplier', 'not in', supplier_w_alerts.ids), ('supplier', 'in', available_suppliers.ids) ]) if (operator in expression.NEGATIVE_TERM_OPERATORS and value) or \ (operator not in expression.NEGATIVE_TERM_OPERATORS and not value): # e.g. (available = False) or (available != True) return [('id', 'not in', available_products.ids)] else: # e.g. (available = True) or (available != False) return [('id', 'in', available_products.ids)]
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): for order in self: order.margin = sum( order.order_line.filtered( lambda r: r.state != 'cancel').mapped('margin'))
class StockMove(models.Model): _inherit = 'stock.move' weight = fields.Float(compute='_cal_move_weight', digits=dp.get_precision('Stock Weight'), store=True) @api.depends('product_id', 'product_uom_qty', 'product_uom') def _cal_move_weight(self): for move in self.filtered( lambda moves: moves.product_id.weight > 0.00): move.weight = (move.product_qty * move.product_id.weight) def _get_new_picking_values(self): vals = super(StockMove, self)._get_new_picking_values() vals['carrier_id'] = self.sale_line_id.order_id.carrier_id.id return vals
class ProductSetAdd(models.TransientModel): _name = 'product.set.add' _rec_name = 'product_set_id' _description = "Wizard model to add product set into a quotation" product_set_id = fields.Many2one( 'product.set', 'Product set', required=True) quantity = fields.Float( digits=dp.get_precision('Product Unit of Measure'), required=True, default=1) @api.multi def add_set(self): """ Add product set, multiplied by quantity in sale order line """ so_id = self._context['active_id'] if not so_id: return so = self.env['sale.order'].browse(so_id) max_sequence = 0 if so.order_line: max_sequence = max([line.sequence for line in so.order_line]) sale_order_line_env = self.env['sale.order.line'] sale_order_line = self.env['sale.order.line'] for set_line in self.product_set_id.set_line_ids: sale_order_line |= sale_order_line_env.create( self.prepare_sale_order_line_data( so_id, set_line, max_sequence=max_sequence)) return sale_order_line @api.multi def prepare_sale_order_line_data(self, sale_order_id, set_line, max_sequence=0): self.ensure_one() sale_line = self.env['sale.order.line'].new({ 'order_id': sale_order_id, 'product_id': set_line.product_id.id, 'product_uom_qty': set_line.quantity * self.quantity, 'product_uom': set_line.product_id.uom_id.id, 'sequence': max_sequence + set_line.sequence, 'discount': set_line.discount, }) sale_line.product_id_change() line_values = sale_line._convert_to_write(sale_line._cache) return line_values
class ReturnPickingLine(models.TransientModel): _name = "stock.return.picking.line" _rec_name = 'product_id' _description = 'Return Picking Line' product_id = fields.Many2one('product.product', string="Product", required=True, domain="[('id', '=', product_id)]") quantity = fields.Float("Quantity", digits=dp.get_precision('Product Unit of Measure'), required=True) uom_id = fields.Many2one('uom.uom', string='Unit of Measure', related='move_id.product_uom', readonly=False) wizard_id = fields.Many2one('stock.return.picking', string="Wizard") move_id = fields.Many2one('stock.move', "Move")
class ProductAttributeValue(models.Model): _inherit = "product.attribute.value" @api.multi def _compute_get_price_factor(self): active_id = self.env.context.get("active_id") if not active_id: return for obj in self: for price_id in obj.price_ids: if price_id.product_tmpl_id.id == active_id: obj.price_factor = price_id.price_factor def _set_price_factor(self): value = self.price_factor active_id = self.env.context.get("active_id") if not active_id: return p_obj = self.env["product.template.attribute.value"] p_ids = p_obj.search( [ ("product_attribute_value_id", "=", self.id), ("product_tmpl_id", "=", active_id), ] ) if p_ids: p_ids.write({"price_factor": value}) else: p_obj.create( { "product_tmpl_id": active_id, "product_attribute_value_id": self.id, "price_factor": value, } ) price_factor = fields.Float( compute="_compute_get_price_factor", string="Attribute Price Factor", inverse=_set_price_factor, digits=dp.get_precision("Product Price"), )
class ProductTemplateAttributeValue(models.Model): """Materialized relationship between attribute values and product template generated by the product.template.attribute.line""" _name = "product.template.attribute.value" _order = 'product_attribute_value_id, id' _description = 'Product Attribute Value' name = fields.Char('Value', related="product_attribute_value_id.name") product_attribute_value_id = fields.Many2one( 'product.attribute.value', string='Attribute Value', required=True, ondelete='cascade', index=True) product_tmpl_id = fields.Many2one( 'product.template', string='Product Template', required=True, ondelete='cascade', index=True) attribute_id = fields.Many2one( 'product.attribute', string='Attribute', related="product_attribute_value_id.attribute_id") sequence = fields.Integer('Sequence', related="product_attribute_value_id.sequence") price_extra = fields.Float( string='Attribute Price Extra', default=0.0, digits=dp.get_precision('Product Price'), help="""Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.""") exclude_for = fields.One2many( 'product.template.attribute.exclusion', 'product_template_attribute_value_id', string="Exclude for", relation="product_template_attribute_exclusion", help="""Make this attribute value not compatible with other values of the product or some attribute values of optional and accessory products.""") @api.multi def name_get(self): if not self._context.get('show_attribute', True): # TDE FIXME: not used return super(ProductTemplateAttributeValue, self).name_get() return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self] @api.multi def _without_no_variant_attributes(self): return self.filtered(lambda ptav: ptav.attribute_id.create_variant != 'no_variant')
class HrPayrollAdviceLine(models.Model): ''' Bank Advice Lines ''' _name = 'hr.payroll.advice.line' _description = 'Bank Advice Lines' advice_id = fields.Many2one('hr.payroll.advice', string='Bank Advice') name = fields.Char('Bank Account No.', required=True) ifsc_code = fields.Char(string='IFSC Code') employee_id = fields.Many2one('hr.employee', string='Employee', required=True) bysal = fields.Float(string='By Salary', digits=dp.get_precision('Payroll')) debit_credit = fields.Char(string='C/D', default='C') company_id = fields.Many2one('res.company', related='advice_id.company_id', string='Company', store=True, readonly=False) ifsc = fields.Boolean(related='advice_id.neft', string='IFSC', readonly=False) @api.onchange('employee_id') def onchange_employee_id(self): self.name = self.employee_id.bank_account_id.acc_number self.ifsc_code = self.employee_id.bank_account_id.bank_bic or ''
class ProductPriceHistory(models.Model): """ Keep track of the ``product.template`` standard prices as they are changed. """ _name = 'product.price.history' _rec_name = 'datetime' _order = 'datetime desc' _description = 'Product Price List History' def _get_default_company_id(self): return self._context.get('force_company', self.env.user.company_id.id) company_id = fields.Many2one('res.company', string='Company', default=_get_default_company_id, required=True) product_id = fields.Many2one('product.product', 'Product', ondelete='cascade', required=True) datetime = fields.Datetime('Date', default=fields.Datetime.now) cost = fields.Float('Cost', digits=dp.get_precision('Product Price'))
class SaleOrderLine(models.Model): _inherit = 'sale.order.line' is_delivery = fields.Boolean(string="Is a Delivery", default=False) product_qty = fields.Float( compute='_compute_product_qty', string='Quantity', digits=dp.get_precision('Product Unit of Measure')) @api.depends('product_id', 'product_uom', 'product_uom_qty') def _compute_product_qty(self): for line in self: if not line.product_id or not line.product_uom or not line.product_uom_qty: continue line.product_qty = line.product_uom._compute_quantity( line.product_uom_qty, line.product_id.uom_id) def _is_delivery(self): self.ensure_one() return self.is_delivery
class StockMoveLine(models.Model): _inherit = 'stock.move.line' workorder_id = fields.Many2one('mrp.workorder', 'Work Order') production_id = fields.Many2one('mrp.production', 'Production Order') lot_produced_id = fields.Many2one('stock.production.lot', 'Finished Lot/Serial Number') lot_produced_qty = fields.Float( 'Quantity Finished Product', digits=dp.get_precision('Product Unit of Measure'), help="Informative, not used in matching") done_wo = fields.Boolean('Done for Work Order', default=True, help="Technical Field which is False when temporarily filled in in work order") # TDE FIXME: naming done_move = fields.Boolean('Move Done', related='move_id.is_done', readonly=False, store=True) # TDE FIXME: naming def _get_similar_move_lines(self): lines = super(StockMoveLine, self)._get_similar_move_lines() if self.move_id.production_id: finished_moves = self.move_id.production_id.move_finished_ids finished_move_lines = finished_moves.mapped('move_line_ids') lines |= finished_move_lines.filtered(lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name) and ml.done_wo == self.done_wo) if self.move_id.raw_material_production_id: raw_moves = self.move_id.raw_material_production_id.move_raw_ids raw_moves_lines = raw_moves.mapped('move_line_ids') raw_moves_lines |= self.move_id.active_move_line_ids lines |= raw_moves_lines.filtered(lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name) and ml.done_wo == self.done_wo) return lines @api.multi def write(self, vals): for move_line in self: if move_line.move_id.production_id and 'lot_id' in vals: move_line.production_id.move_raw_ids.mapped('move_line_ids')\ .filtered(lambda r: r.done_wo and not r.done_move and r.lot_produced_id == move_line.lot_id)\ .write({'lot_produced_id': vals['lot_id']}) production = move_line.move_id.production_id or move_line.move_id.raw_material_production_id if production and move_line.state == 'done' and any(field in vals for field in ('lot_id', 'location_id', 'qty_done')): move_line._log_message(production, move_line, 'mrp.track_production_move_template', vals) return super(StockMoveLine, self).write(vals)
class SaleOrderOption(models.Model): _name = "sale.order.option" _description = "Sale Options" _order = 'sequence, id' order_id = fields.Many2one('sale.order', 'Sales Order Reference', ondelete='cascade', index=True) line_id = fields.Many2one('sale.order.line', on_delete="set null") name = fields.Text('Description', required=True) product_id = fields.Many2one('product.product', 'Product', required=True, 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')) uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True) quantity = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1) sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of optional products.") @api.onchange('product_id', 'uom_id') def _onchange_product_id(self): if not self.product_id: return product = self.product_id.with_context(lang=self.order_id.partner_id.lang) self.price_unit = product.list_price self.name = product.get_product_multiline_description_sale() self.uom_id = self.uom_id or product.uom_id pricelist = self.order_id.pricelist_id if pricelist and product: partner_id = self.order_id.partner_id.id self.price_unit = pricelist.with_context(uom=self.uom_id.id).get_product_price(product, self.quantity, partner_id) domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]} return {'domain': domain} @api.multi def button_add_to_order(self): self.add_option_to_order() return {'type': 'ir.actions.client', 'tag': 'reload'} @api.multi def add_option_to_order(self): self.ensure_one() sale_order = self.order_id if sale_order.state not in ['draft', 'sent']: raise UserError(_('You cannot add options to a confirmed order.')) values = self._get_values_to_add_to_order() order_line = self.env['sale.order.line'].create(values) order_line._compute_tax_id() self.write({'line_id': order_line.id}) @api.multi def _get_values_to_add_to_order(self): self.ensure_one() return { 'order_id': self.order_id.id, 'price_unit': self.price_unit, 'name': self.name, 'product_id': self.product_id.id, 'product_uom_qty': self.quantity, 'product_uom': self.uom_id.id, 'discount': self.discount, 'company_id': self.order_id.company_id.id, }
class account_voucher_line_inherit(models.Model): _inherit = "account.voucher.line" # _order = "move_line_id" type = fields.Selection([('dr', 'Debit'), ('cr', 'Credit')], 'Dr/Cr') amount = fields.Float('Amount', digits_compute=dp.get_precision('Account')) move_line_id = fields.Many2one('account.move.line', 'Journal Item', copy=False) date_due = fields.Date(Related='move_line_id.date_maturity', relation='account.move.line', string='Due Date', readonly=1) date_original = fields.Date(Related='move_line_id.date', relation='account.move.line', string='Date', readonly=1) amount_original = fields.Float(compute='_compute_balance', multi='dc', string='Original Amount', store=True, digits_compute=dp.get_precision('Account')) amount_unreconciled = fields.Float( compute='_compute_balance', multi='dc', string='Open Balance', store=True, digits_compute=dp.get_precision('Account')) voucher_id = fields.Many2one('account.voucher', 'Voucher', required=1, ondelete='cascade') state = fields.Char(Related='voucher_id.state', string='State', readonly=True) def _compute_balance(self): currency_pool = self.pool.get('res.currency') rs_data = {} for line in self.browse(): ctx = context.copy() ctx.update({'date': line.voucher_id.date}) voucher_rate = self.pool.get('res.currency').read( line.voucher_id.currency_id.id, ['rate'], context=ctx)['rate'] ctx.update({ 'voucher_special_currency': line.voucher_id.payment_rate_currency_id and line.voucher_id.payment_rate_currency_id.id or False, 'voucher_special_currency_rate': line.voucher_id.payment_rate * voucher_rate }) res = {} company_currency = line.voucher_id.journal_id.company_id.currency_id.id voucher_currency = line.voucher_id.currency_id and line.voucher_id.currency_id.id or company_currency move_line = line.move_line_id or False if not move_line: res['amount_original'] = 0.0 res['amount_unreconciled'] = 0.0 elif move_line.currency_id and voucher_currency == move_line.currency_id.id: res['amount_original'] = abs(move_line.amount_currency) res['amount_unreconciled'] = abs( move_line.amount_residual_currency) else: #always use the amount booked in the company currency as the basis of the conversion into the voucher currency res['amount_original'] = currency_pool.compute( cr, uid, company_currency, voucher_currency, move_line.credit or move_line.debit or 0.0, context=ctx) res['amount_unreconciled'] = currency_pool.compute( cr, uid, company_currency, voucher_currency, abs(move_line.amount_residual), context=ctx) rs_data[line.id] = res return rs_data @api.onchange('move_line_id') def onchange_move_line_id(self): """ Returns a dict that contains new values and context @param move_line_id: latest value from user input for field move_line_id @param args: other arguments @param context: context arguments, like lang, time zone @return: Returns a dict which contains new values, and context """ res = {} move_line_pool = self.env['account.move.line'] if self.move_line_id: move_line = move_line_pool.browse() if move_line.credit: ttype = 'dr' else: ttype = 'cr' res.update({ 'account_id': move_line.account_id.id, 'type': ttype, 'currency_id': move_line.currency_id and move_line.currency_id.id or move_line.company_id.currency_id.id, }) return { 'value': res, }
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) 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 @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: sale_order._create_delivery_line(self.carrier_id, self.carrier_price) @api.multi def open_website_url(self): self.ensure_one() if not self.carrier_tracking_url: raise UserError( _("Your delivery method has no redirect on courier provider's website to track this order." )) 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 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 ContractAbstractContractLine(models.AbstractModel): _name = 'contract.abstract.contract.line' _description = 'Abstract Recurring Contract Line' product_id = fields.Many2one('product.product', string='Product', required=True) name = fields.Text(string='Description', required=True) quantity = fields.Float(default=1.0, required=True) uom_id = fields.Many2one('uom.uom', string='Unit of Measure', required=True) automatic_price = fields.Boolean( string="Auto-price?", help="If this is marked, the price will be obtained automatically " "applying the pricelist to the product. If not, you will be " "able to introduce a manual price", ) specific_price = fields.Float(string='Specific Price') price_unit = fields.Float( string='Unit Price', compute="_compute_price_unit", inverse="_inverse_price_unit", ) price_subtotal = fields.Float( compute='_compute_price_subtotal', digits=dp.get_precision('Account'), string='Sub Total', ) discount = fields.Float( string='Discount (%)', digits=dp.get_precision('Discount'), help='Discount that is applied in generated invoices.' ' It should be less or equal to 100', ) sequence = fields.Integer( string="Sequence", default=10, help="Sequence of the contract line when displaying contracts", ) recurring_rule_type = fields.Selection( [ ('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('monthlylastday', 'Month(s) last day'), ('yearly', 'Year(s)'), ], default='monthly', string='Recurrence', help="Specify Interval for automatic invoice generation.", required=True, ) recurring_invoicing_type = fields.Selection( [('pre-paid', 'Pre-paid'), ('post-paid', 'Post-paid')], default='pre-paid', string='Invoicing type', help=("Specify if the invoice must be generated at the beginning " "(pre-paid) or end (post-paid) of the period."), required=True, ) recurring_invoicing_offset = fields.Integer( compute="_compute_recurring_invoicing_offset", string="Invoicing offset", help=("Number of days to offset the invoice from the period end " "date (in post-paid mode) or start date (in pre-paid mode).")) recurring_interval = fields.Integer( default=1, string='Invoice Every', help="Invoice every (Days/Week/Month/Year)", required=True, ) date_start = fields.Date(string='Date Start') recurring_next_date = fields.Date(string='Date of Next Invoice') last_date_invoiced = fields.Date(string='Last Date Invoiced') is_canceled = fields.Boolean(string="Canceled", default=False) is_auto_renew = fields.Boolean(string="Auto Renew", default=False) auto_renew_interval = fields.Integer( default=1, string='Renew Every', help="Renew every (Days/Week/Month/Year)", ) auto_renew_rule_type = fields.Selection( [ ('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('yearly', 'Year(s)'), ], default='yearly', string='Renewal type', help="Specify Interval for automatic renewal.", ) termination_notice_interval = fields.Integer( default=1, string='Termination Notice Before') termination_notice_rule_type = fields.Selection( [('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)')], default='monthly', string='Termination Notice type', ) contract_id = fields.Many2one( string='Contract', comodel_name='contract.abstract.contract', required=True, ondelete='cascade', ) @api.model def _get_default_recurring_invoicing_offset(self, recurring_invoicing_type, recurring_rule_type): if (recurring_invoicing_type == 'pre-paid' or recurring_rule_type == 'monthlylastday'): return 0 else: return 1 @api.depends('recurring_invoicing_type', 'recurring_rule_type') def _compute_recurring_invoicing_offset(self): for rec in self: rec.recurring_invoicing_offset = ( self._get_default_recurring_invoicing_offset( rec.recurring_invoicing_type, rec.recurring_rule_type)) @api.depends( 'automatic_price', 'specific_price', 'product_id', 'quantity', 'contract_id.pricelist_id', 'contract_id.partner_id', ) def _compute_price_unit(self): """Get the specific price if no auto-price, and the price obtained from the pricelist otherwise. """ for line in self: if line.automatic_price: product = line.product_id.with_context( quantity=line.env.context.get( 'contract_line_qty', line.quantity, ), pricelist=line.contract_id.pricelist_id.id, partner=line.contract_id.partner_id.id, date=line.env.context.get('old_date', fields.Date.context_today(line)), ) line.price_unit = product.price else: line.price_unit = line.specific_price # Tip in https://github.com/eagle/eagle/issues/23891#issuecomment-376910788 @api.onchange('price_unit') def _inverse_price_unit(self): """Store the specific price in the no auto-price records.""" for line in self.filtered(lambda x: not x.automatic_price): line.specific_price = line.price_unit @api.multi @api.depends('quantity', 'price_unit', 'discount') def _compute_price_subtotal(self): for line in self: subtotal = line.quantity * line.price_unit discount = line.discount / 100 subtotal *= 1 - discount if line.contract_id.pricelist_id: cur = line.contract_id.pricelist_id.currency_id line.price_subtotal = cur.round(subtotal) else: line.price_subtotal = subtotal @api.multi @api.constrains('discount') def _check_discount(self): for line in self: if line.discount > 100: raise ValidationError( _("Discount should be less or equal to 100")) @api.multi @api.onchange('product_id') def _onchange_product_id(self): if not self.product_id: return {'domain': {'uom_id': []}} vals = {} domain = { 'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } if not self.uom_id or (self.product_id.uom_id.category_id.id != self.uom_id.category_id.id): vals['uom_id'] = self.product_id.uom_id date = self.recurring_next_date or fields.Date.context_today(self) partner = self.contract_id.partner_id or self.env.user.partner_id product = self.product_id.with_context( lang=partner.lang, partner=partner.id, quantity=self.quantity, date=date, pricelist=self.contract_id.pricelist_id.id, uom=self.uom_id.id, ) vals['name'] = self.product_id.get_product_multiline_description_sale() vals['price_unit'] = product.price self.update(vals) return {'domain': domain}
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 MrpProductProduce(models.TransientModel): _name = "mrp.product.produce" _description = "Record Production" @api.model def default_get(self, fields): res = super(MrpProductProduce, self).default_get(fields) if self._context and self._context.get('active_id'): production = self.env['mrp.production'].browse( self._context['active_id']) serial_finished = (production.product_id.tracking == 'serial') todo_uom = production.product_uom_id.id if serial_finished: todo_quantity = 1.0 if production.product_uom_id.uom_type != 'reference': todo_uom = self.env['uom.uom'].search([ ('category_id', '=', production.product_uom_id.category_id.id), ('uom_type', '=', 'reference') ]).id else: main_product_moves = production.move_finished_ids.filtered( lambda x: x.product_id.id == production.product_id.id) todo_quantity = production.product_qty - sum( main_product_moves.mapped('quantity_done')) todo_quantity = todo_quantity if (todo_quantity > 0) else 0 if 'production_id' in fields: res['production_id'] = production.id if 'product_id' in fields: res['product_id'] = production.product_id.id if 'product_uom_id' in fields: res['product_uom_id'] = todo_uom if 'serial' in fields: res['serial'] = bool(serial_finished) if 'product_qty' in fields: res['product_qty'] = todo_quantity return res serial = fields.Boolean('Requires Serial') production_id = fields.Many2one('mrp.production', 'Production') product_id = fields.Many2one('product.product', 'Product') product_qty = fields.Float( string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True) product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure') lot_id = fields.Many2one('stock.production.lot', string='Lot/Serial Number') produce_line_ids = fields.One2many('mrp.product.produce.line', 'product_produce_id', string='Product to Track') product_tracking = fields.Selection(related="product_id.tracking", readonly=True) @api.multi def do_produce(self): # Nothing to do for lots since values are created using default data (stock.move.lots) quantity = self.product_qty if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0: raise UserError( _("The production order for '%s' has no quantity specified.") % self.product_id.display_name) for move in self.production_id.move_finished_ids: if move.product_id.tracking == 'none' and move.state not in ( 'done', 'cancel'): rounding = move.product_uom.rounding if move.product_id.id == self.production_id.product_id.id: move.quantity_done += float_round( quantity, precision_rounding=rounding) elif move.unit_factor: # byproducts handling move.quantity_done += float_round( quantity * move.unit_factor, precision_rounding=rounding) self.check_finished_move_lots() if self.production_id.state == 'confirmed': self.production_id.write({ 'state': 'progress', 'date_start': datetime.now(), }) return {'type': 'ir.actions.act_window_close'} @api.multi def check_finished_move_lots(self): produce_move = self.production_id.move_finished_ids.filtered( lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel')) if produce_move and produce_move.product_id.tracking != 'none': if not self.lot_id: raise UserError( _('You need to provide a lot for the finished product.')) existing_move_line = produce_move.move_line_ids.filtered( lambda x: x.lot_id == self.lot_id) if existing_move_line: if self.product_id.tracking == 'serial': raise UserError( _('You cannot produce the same serial number twice.')) produced_qty = self.product_uom_id._compute_quantity( self.product_qty, existing_move_line.product_uom_id) existing_move_line.product_uom_qty += produced_qty existing_move_line.qty_done += produced_qty else: location_dest_id = produce_move.location_dest_id.get_putaway_strategy( self.product_id).id or produce_move.location_dest_id.id vals = { 'move_id': produce_move.id, 'product_id': produce_move.product_id.id, 'production_id': self.production_id.id, 'product_uom_qty': self.product_qty, 'product_uom_id': self.product_uom_id.id, 'qty_done': self.product_qty, 'lot_id': self.lot_id.id, 'location_id': produce_move.location_id.id, 'location_dest_id': location_dest_id, } self.env['stock.move.line'].create(vals) for pl in self.produce_line_ids: if pl.qty_done: if pl.product_id.tracking != 'none' and not pl.lot_id: raise UserError( _('Please enter a lot or serial number for %s !' % pl.product_id.display_name)) if not pl.move_id: # Find move_id that would match move_id = self.production_id.move_raw_ids.filtered( lambda m: m.product_id == pl.product_id and m.state not in ('done', 'cancel')) if move_id: pl.move_id = move_id else: # create a move and put it in there order = self.production_id pl.move_id = self.env['stock.move'].create({ 'name': order.name, 'product_id': pl.product_id.id, 'product_uom': pl.product_uom_id.id, 'location_id': order.location_src_id.id, 'location_dest_id': self.product_id.property_stock_production.id, 'raw_material_production_id': order.id, 'group_id': order.procurement_group_id.id, 'origin': order.name, 'state': 'confirmed' }) pl.move_id._generate_consumed_move_line(pl.qty_done, self.lot_id, lot=pl.lot_id) return True @api.onchange('product_qty') def _onchange_product_qty(self): lines = [] qty_todo = self.product_uom_id._compute_quantity( self.product_qty, self.production_id.product_uom_id, round=False) for move in self.production_id.move_raw_ids.filtered( lambda m: m.state not in ('done', 'cancel') and m.bom_line_id): qty_to_consume = float_round( qty_todo * move.unit_factor, precision_rounding=move.product_uom.rounding) for move_line in move.move_line_ids: if float_compare( qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: break if move_line.lot_produced_id or float_compare( move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0: continue to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty) lines.append({ 'move_id': move.id, 'qty_to_consume': to_consume_in_line, 'qty_done': to_consume_in_line, 'lot_id': move_line.lot_id.id, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, 'qty_reserved': min(to_consume_in_line, move_line.product_uom_qty), }) qty_to_consume -= to_consume_in_line if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: if move.product_id.tracking == 'serial': while float_compare( qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: lines.append({ 'move_id': move.id, 'qty_to_consume': 1, 'qty_done': 1, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, }) qty_to_consume -= 1 else: lines.append({ 'move_id': move.id, 'qty_to_consume': qty_to_consume, 'qty_done': qty_to_consume, 'product_uom_id': move.product_uom.id, 'product_id': move.product_id.id, }) self.produce_line_ids = [(5, )] + [(0, 0, x) for x in lines]
class ProductTemplateAttributeValue(models.Model): _inherit = "product.template.attribute.value" price_factor = fields.Float( "Price Factor", digits=dp.get_precision("Product Price"), default=1.0 )
class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' asset_category_id = fields.Many2one('account.asset.category', string='Asset Category') asset_start_date = fields.Date(string='Asset Start Date', compute='_get_asset_date', readonly=True, store=True) asset_end_date = fields.Date(string='Asset End Date', compute='_get_asset_date', readonly=True, store=True) asset_mrr = fields.Float(string='Monthly Recurring Revenue', compute='_get_asset_date', readonly=True, digits=dp.get_precision('Account'), store=True) @api.one @api.depends('asset_category_id', 'invoice_id.date_invoice') def _get_asset_date(self): self.asset_mrr = 0 self.asset_start_date = False self.asset_end_date = False cat = self.asset_category_id if cat: if cat.method_number == 0 or cat.method_period == 0: raise UserError(_('The number of depreciations or the period length of your asset category cannot be 0.')) months = cat.method_number * cat.method_period if self.invoice_id.type in ['out_invoice', 'out_refund']: self.asset_mrr = self.price_subtotal_signed / months if self.invoice_id.date_invoice: start_date = self.invoice_id.date_invoice.replace(day=1) end_date = (start_date + relativedelta(months=months, days=-1)) self.asset_start_date = start_date self.asset_end_date = end_date @api.one def asset_create(self): if self.asset_category_id: vals = { 'name': self.name, 'code': self.invoice_id.number or False, 'category_id': self.asset_category_id.id, 'value': self.price_subtotal_signed, 'partner_id': self.invoice_id.partner_id.id, 'company_id': self.invoice_id.company_id.id, 'currency_id': self.invoice_id.company_currency_id.id, 'date': self.invoice_id.date_invoice, 'invoice_id': self.invoice_id.id, } changed_vals = self.env['account.asset.asset'].onchange_category_id_values(vals['category_id']) vals.update(changed_vals['value']) asset = self.env['account.asset.asset'].create(vals) if self.asset_category_id.open_asset: asset.validate() return True @api.onchange('asset_category_id') def onchange_asset_category_id(self): if self.invoice_id.type == 'out_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id elif self.invoice_id.type == 'in_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id @api.onchange('uom_id') def _onchange_uom_id(self): result = super(AccountInvoiceLine, self)._onchange_uom_id() self.onchange_asset_category_id() return result @api.onchange('product_id') def _onchange_product_id(self): vals = super(AccountInvoiceLine, self)._onchange_product_id() if self.product_id: if self.invoice_id.type == 'out_invoice': self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id elif self.invoice_id.type == 'in_invoice': self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id return vals def _set_additional_fields(self, invoice): if not self.asset_category_id: if invoice.type == 'out_invoice': self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id elif invoice.type == 'in_invoice': self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id self.onchange_asset_category_id() super(AccountInvoiceLine, self)._set_additional_fields(invoice) def get_invoice_line_account(self, type, product, fpos, company): return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
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 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 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', 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.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.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 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', readonly=False) 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') 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, picking_type=self.bom_id.picking_type_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) }