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 SaleQuoteOption(models.Model): _name = "sale.quote.option" _description = "Quotation Option" template_id = fields.Many2one('sale.quote.template', 'Quotation Template Reference', ondelete='cascade', index=True, required=True) name = fields.Text('Description', required=True, translate=True) product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True) layout_category_id = fields.Many2one('sale.layout_category', string='Section') website_description = fields.Html('Option Description', translate=html_translate, sanitize_attributes=False) 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('product.uom', 'Unit of Measure ', required=True) quantity = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1) @api.onchange('product_id') def _onchange_product_id(self): if not self.product_id: return product = self.product_id self.price_unit = product.list_price self.website_description = product.product_tmpl_id.quote_description self.name = product.name self.uom_id = product.uom_id domain = { 'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } return {'domain': domain} @api.onchange('uom_id') def _onchange_product_uom(self): if not self.product_id: return if not self.uom_id: self.price_unit = 0.0 return if self.uom_id.id != self.product_id.uom_id.id: self.price_unit = self.product_id.uom_id._compute_price( self.price_unit, self.uom_id)
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 PurchaseRequisitionLine(models.Model): _name = "purchase.requisition.line" _description = "Purchase Requisition Line" _rec_name = 'product_id' product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], required=True) product_uom_id = fields.Many2one('product.uom', string='Product Unit of Measure') product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure')) price_unit = fields.Float(string='Unit Price', digits=dp.get_precision('Product Price')) qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities') requisition_id = fields.Many2one('purchase.requisition', string='Purchase Agreement', ondelete='cascade') company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default= lambda self: self.env['res.company']._company_default_get('purchase.requisition.line')) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') schedule_date = fields.Date(string='Scheduled Date') move_dest_id = fields.Many2one('stock.move', 'Downstream Move') @api.multi @api.depends('requisition_id.purchase_ids.state') def _compute_ordered_qty(self): for line in self: total = 0.0 for po in line.requisition_id.purchase_ids.filtered(lambda purchase_order: purchase_order.state in ['purchase', 'done']): for po_line in po.order_line.filtered(lambda order_line: order_line.product_id == line.product_id): if po_line.product_uom != line.product_uom_id: total += po_line.product_uom._compute_quantity(po_line.product_qty, line.product_uom_id) else: total += po_line.product_qty line.qty_ordered = total @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_id self.product_qty = 1.0 if not self.account_analytic_id: self.account_analytic_id = self.requisition_id.account_analytic_id if not self.schedule_date: self.schedule_date = self.requisition_id.schedule_date @api.multi def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, taxes_ids=False): self.ensure_one() requisition = self.requisition_id return { 'name': name, 'product_id': self.product_id.id, 'product_uom': self.product_id.uom_po_id.id, 'product_qty': product_qty, 'price_unit': price_unit, 'taxes_id': [(6, 0, taxes_ids)], 'date_planned': requisition.schedule_date or fields.Date.today(), 'account_analytic_id': self.account_analytic_id.id, 'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or [] }
class HrPayslipLine(models.Model): _name = 'hr.payslip.line' _inherit = 'hr.salary.rule' _description = 'Payslip Line' _order = 'contract_id, sequence' slip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade') salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True) employee_id = fields.Many2one('hr.employee', string='Employee', required=True) contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True) rate = fields.Float(string='Rate (%)', digits=dp.get_precision('Payroll Rate'), default=100.0) amount = fields.Float(digits=dp.get_precision('Payroll')) quantity = fields.Float(digits=dp.get_precision('Payroll'), default=1.0) total = fields.Float(compute='_compute_total', string='Total', digits=dp.get_precision('Payroll'), store=True) @api.depends('quantity', 'amount', 'rate') def _compute_total(self): for line in self: line.total = float(line.quantity) * line.amount * line.rate / 100 @api.model def create(self, values): if 'employee_id' not in values or 'contract_id' not in values: payslip = self.env['hr.payslip'].browse(values.get('slip_id')) values['employee_id'] = values.get( 'employee_id') or payslip.employee_id.id values['contract_id'] = values.get( 'contract_id' ) or payslip.contract_id and payslip.contract_id.id if not values['contract_id']: raise UserError( _('You must set a contract to create a payslip line.')) return super(HrPayslipLine, self).create(values)
class StockMove(models.Model): _inherit = 'stock.move' def _default_uom(self): uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id return self.env['product.uom'].search( [('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1) weight = fields.Float(compute='_cal_move_weight', digits=dp.get_precision('Stock Weight'), store=True) weight_uom_id = fields.Many2one( 'product.uom', string='Weight Unit of Measure', required=True, readonly=True, help= "Unit of Measure (Unit of Measure) is the unit of measurement for Weight", default=_default_uom) @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 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) ifsc = fields.Boolean(related='advice_id.neft', string='IFSC') @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 Product(models.Model): _inherit = "product.product" website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price')) website_public_price = fields.Float('Website public price', compute='_website_price', digits=dp.get_precision('Product Price')) website_price_difference = fields.Boolean('Website price difference', compute='_website_price') def _website_price(self): qty = self._context.get('quantity', 1.0) partner = self.env.user.partner_id current_website = self.env['website'].get_current_website() pricelist = current_website.get_current_pricelist() company_id = current_website.company_id context = dict(self._context, pricelist=pricelist.id, partner=partner) self2 = self.with_context(context) if self._context != context else self ret = self.env.user.has_group('sale.group_show_price_subtotal') and 'total_excluded' or 'total_included' for p, p2 in pycompat.izip(self, self2): taxes = partner.property_account_position_id.map_tax(p.sudo().taxes_id.filtered(lambda x: x.company_id == company_id)) p.website_price = taxes.compute_all(p2.price, pricelist.currency_id, quantity=qty, product=p2, partner=partner)[ret] price_without_pricelist = taxes.compute_all(p.list_price, pricelist.currency_id)[ret] p.website_price_difference = False if float_is_zero(price_without_pricelist - p.website_price, precision_rounding=pricelist.currency_id.rounding) else True p.website_public_price = taxes.compute_all(p2.lst_price, quantity=qty, product=p2, partner=partner)[ret] @api.multi def website_publish_button(self): self.ensure_one() return self.product_tmpl_id.website_publish_button()
class RepairFee(models.Model): _name = 'mrp.repair.fee' _description = 'Repair Fees Line' repair_id = fields.Many2one( 'mrp.repair', 'Repair Order Reference', index=True, ondelete='cascade', required=True) name = fields.Char('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('product.uom', 'Product Unit of Measure', required=True) price_subtotal = fields.Float('Subtotal', compute='_compute_price_subtotal', 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: self.tax_id = partner.property_account_position_id.map_tax(self.product_id.taxes_id, self.product_id, partner).ids if self.product_id: self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id.id warning = False if not pricelist: warning = { 'title': _('No Pricelist!'), 'message': _('You have to select a pricelist in the Repair form !\n Please set one before choosing a product.')} else: price = pricelist.get_product_price(self.product_id, self.product_uom_qty, partner) 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.")} else: self.price_unit = price if warning: return {'warning': warning}
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') 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', 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 ProductAttributePrice(models.Model): _name = "product.attribute.price" product_tmpl_id = fields.Many2one('product.template', 'Product Template', ondelete='cascade', required=True) value_id = fields.Many2one('product.attribute.value', 'Product Attribute Value', ondelete='cascade', required=True) price_extra = fields.Float('Price Extra', digits=dp.get_precision('Product Price'))
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', '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)
class MrpProductProduceLine(models.TransientModel): _name = "mrp.product.produce.line" _description = "Record Production Line" product_produce_id = fields.Many2one('mrp.product.produce') product_id = fields.Many2one('product.product', 'Product') lot_id = fields.Many2one('stock.production.lot', 'Lot') qty_to_consume = fields.Float('To Consume', digits=dp.get_precision('Product Unit of Measure')) product_uom_id = fields.Many2one('product.uom', 'Unit of Measure') qty_done = fields.Float('Done', digits=dp.get_precision('Product Unit of Measure')) move_id = fields.Many2one('stock.move') @api.onchange('lot_id') def _onchange_lot_id(self): """ When the user is encoding a produce line for a tracked product, we apply some logic to help him. This onchange will automatically switch `qty_done` to 1.0. """ res = {} if self.product_id.tracking == 'serial': self.qty_done = 1 return res @api.onchange('qty_done') def _onchange_qty_done(self): """ When the user is encoding a produce line for a tracked product, we apply some logic to help him. This onchange will warn him if he set `qty_done` to a non-supported value. """ res = {} if self.product_id.tracking == 'serial': if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0: message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name res['warning'] = {'title': _('Warning'), 'message': message} return res @api.onchange('product_id') def _onchange_product_id(self): self.product_uom_id = self.product_id.uom_id.id
class ReturnPickingLine(models.TransientModel): _name = "stock.return.picking.line" _rec_name = 'product_id' 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('product.uom', string='Unit of Measure', related='move_id.product_uom') wizard_id = fields.Many2one('stock.return.picking', string="Wizard") move_id = fields.Many2one('stock.move', "Move")
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 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' 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 MembershipInvoice(models.TransientModel): _name = "membership.invoice" _description = "Membership Invoice" product_id = fields.Many2one('product.product', string='Membership', required=True) member_price = fields.Float(string='Member Price', digits=dp.get_precision('Product Price'), required=True) @api.onchange('product_id') def onchange_product(self): """This function returns value of product's member price based on product id. """ price_dict = self.product_id.price_compute('list_price') self.member_price = price_dict.get(self.product_id.id) or False @api.multi def membership_invoice(self): if self: datas = { 'membership_product_id': self.product_id.id, 'amount': self.member_price } invoice_list = self.env['res.partner'].browse( self._context.get('active_ids')).create_membership_invoice( datas=datas) search_view_ref = self.env.ref('account.view_account_invoice_filter', False) form_view_ref = self.env.ref('account.invoice_form', False) tree_view_ref = self.env.ref('account.invoice_tree', False) return { 'domain': [('id', 'in', invoice_list)], 'name': 'Membership Invoices', 'res_model': 'account.invoice', 'type': 'ir.actions.act_window', 'views': [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')], 'search_view_id': search_view_ref and search_view_ref.id, }
class LandedCostLine(models.Model): _name = 'stock.landed.cost.lines' _description = 'Stock Landed Cost Lines' name = fields.Char('Description') cost_id = fields.Many2one( 'stock.landed.cost', 'Landed Cost', required=True, ondelete='cascade') product_id = fields.Many2one('product.product', 'Product', required=True) price_unit = fields.Float('Cost', digits=dp.get_precision('Product Price'), required=True) split_method = fields.Selection(product.SPLIT_METHOD, string='Split Method', required=True) account_id = fields.Many2one('account.account', 'Account', domain=[('deprecated', '=', False)]) @api.onchange('product_id') def onchange_product_id(self): if not self.product_id: self.quantity = 0.0 self.name = self.product_id.name or '' self.split_method = self.product_id.split_method or 'equal' self.price_unit = self.product_id.standard_price or 0.0 self.account_id = self.product_id.property_account_expense_id.id or self.product_id.categ_id.property_account_expense_categ_id.id
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 MrpSubProduct(models.Model): _name = 'mrp.subproduct' _description = 'Byproduct' product_id = fields.Many2one('product.product', 'Product', required=True) product_qty = fields.Float( 'Product Qty', default=1.0, digits=dp.get_precision('Product Unit of Measure'), required=True) product_uom_id = fields.Many2one('product.uom', 'Unit of Measure', required=True) bom_id = fields.Many2one('mrp.bom', 'BoM', ondelete='cascade') operation_id = fields.Many2one('mrp.routing.workcenter', 'Produced at Operation') @api.onchange('product_id') def onchange_product_id(self): """ Changes UoM if product_id changes. """ if self.product_id: self.product_uom_id = self.product_id.uom_id.id @api.onchange('product_uom_id') def onchange_uom(self): res = {} if self.product_uom_id and self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id: res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } self.product_uom_id = self.product_id.uom_id.id return res
class SupplierInfo(models.Model): _name = "product.supplierinfo" _description = "Information about a product vendor" _order = 'sequence, min_qty desc, price' name = fields.Many2one('res.partner', 'Vendor', domain=[('supplier', '=', True)], ondelete='cascade', required=True, help="Vendor of this product") product_name = fields.Char( 'Vendor Product Name', help= "This vendor's product name will be used when printing a request for quotation. Keep empty to use the internal one." ) product_code = fields.Char( 'Vendor Product Code', help= "This vendor's product code will be used when printing a request for quotation. Keep empty to use the internal one." ) sequence = fields.Integer( 'Sequence', default=1, help="Assigns the priority to the list of product vendor.") product_uom = fields.Many2one('product.uom', 'Vendor Unit of Measure', readonly="1", related='product_tmpl_id.uom_po_id', help="This comes from the product form.") min_qty = fields.Float( 'Minimal Quantity', default=0.0, required=True, help= "The minimal quantity to purchase from this vendor, expressed in the vendor Product Unit of Measure if not any, in the default unit of measure of the product otherwise." ) price = fields.Float('Price', default=0.0, digits=dp.get_precision('Product Price'), required=True, help="The price to purchase a product") company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env.user.company_id.id, index=1) currency_id = fields.Many2one( 'res.currency', 'Currency', default=lambda self: self.env.user.company_id.currency_id.id, required=True) date_start = fields.Date('Start Date', help="Start date for this vendor price") date_end = fields.Date('End Date', help="End date for this vendor price") product_id = fields.Many2one( 'product.product', 'Product Variant', help= "If not set, the vendor price will apply to all variants of this products." ) product_tmpl_id = fields.Many2one('product.template', 'Product Template', index=True, ondelete='cascade', oldname='product_id') product_variant_count = fields.Integer( 'Variant Count', related='product_tmpl_id.product_variant_count') delay = fields.Integer( 'Delivery Lead Time', default=1, required=True, help= "Lead time in days between the confirmation of the purchase order and the receipt of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning." )
class ProductTemplate(models.Model): _inherit = ["product.template", "website.seo.metadata", 'website.published.mixin', 'rating.mixin'] _order = 'website_published desc, website_sequence desc, name' _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 more expensive alternatives to ' 'your customers (upsell strategy). Those products 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 paying (cross-sell strategy, e.g. for computers: mouse, keyboard, etc.). ' 'An algorithm figures out a list of accessories based on all the products added to cart.') 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()) public_categ_ids = fields.Many2many('product.public.category', string='Website Product Category', help="Categories can be published on the Shop page (online catalog grid) to help " "customers find all the items within a category. To publish them, go to the Shop page, " "hit Customize and turn *Product Categories* on. A product can belong to several categories.") product_image_ids = fields.One2many('product.image', 'product_tmpl_id', string='Images') website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price')) website_public_price = fields.Float('Website public price', compute='_website_price', digits=dp.get_precision('Product Price')) website_price_difference = fields.Boolean('Website price difference', compute='_website_price') def _website_price(self): # First filter out the ones that have no variant: # This makes sure that every template below has a corresponding product in the zipped result. self = self.filtered('product_variant_id') # use mapped who returns a recordset with only itself to prefetch (and don't prefetch every product_variant_ids) for template, product in pycompat.izip(self, self.mapped('product_variant_id')): template.website_price = product.website_price template.website_public_price = product.website_public_price template.website_price_difference = product.website_price_difference 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() @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 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', domain=[('sale_ok', '=', True)]) layout_category_id = fields.Many2one('sale.layout_category', string='Section') website_description = fields.Html('Line Description', sanitize_attributes=False, translate=html_translate) 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('product.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 suggested product." ) @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.website_description = product.quote_description or product.website_description self.name = product.name if product.description_sale: self.name += '\n' + product.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.ensure_one() order = self.order_id if order.state not in ['draft', 'sent']: return False order_line = order.order_line.filtered( lambda line: line.product_id == self.product_id) if order_line: order_line = order_line[0] order_line.product_uom_qty += 1 else: vals = { 'price_unit': self.price_unit, 'website_description': self.website_description, 'name': self.name, 'order_id': order.id, 'product_id': self.product_id.id, 'layout_category_id': self.layout_category_id.id, 'product_uom_qty': self.quantity, 'product_uom': self.uom_id.id, 'discount': self.discount, } order_line = self.env['sale.order.line'].create(vals) order_line._compute_tax_id() self.write({'line_id': order_line.id}) return {'type': 'ir.actions.client', 'tag': 'reload'}
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 null.' )) 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 = datetime.strptime(self.invoice_id.date_invoice, DF).replace(day=1) end_date = (start_date + relativedelta(months=months, days=-1)) self.asset_start_date = start_date.strftime(DF) self.asset_end_date = end_date.strftime(DF) @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 ProductTemplate(models.Model): _name = "product.template" _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Product Template" _order = "name" def _get_default_category_id(self): if self._context.get('categ_id') or self._context.get('default_categ_id'): return self._context.get('categ_id') or self._context.get('default_categ_id') category = self.env.ref('product.product_category_all', raise_if_not_found=False) if not category: category = self.env['product.category'].search([], limit=1) if category: return category.id else: err_msg = _('You must define at least one product category in order to be able to create products.') redir_msg = _('Go to Internal Categories') raise RedirectWarning(err_msg, self.env.ref('product.product_category_action_form').id, redir_msg) def _get_default_uom_id(self): return self.env["product.uom"].search([], limit=1, order='id').id name = fields.Char('Name', index=True, required=True, translate=True) sequence = fields.Integer('Sequence', default=1, help='Gives the sequence order when displaying a product list') description = fields.Text( 'Description', translate=True, help="A precise description of the Product, used only for internal information purposes.") description_purchase = fields.Text( 'Purchase Description', translate=True, help="A description of the Product that you want to communicate to your vendors. " "This description will be copied to every Purchase Order, Receipt and Vendor Bill/Credit Note.") description_sale = fields.Text( 'Sale Description', translate=True, help="A description of the Product that you want to communicate to your customers. " "This description will be copied to every Sales Order, Delivery Order and Customer Invoice/Credit Note") type = fields.Selection([ ('consu', 'Consumable'), ('service', 'Service')], string='Product Type', default='consu', required=True, help='A stockable product is a product for which you manage stock. The "Inventory" app has to be installed.\n' 'A consumable product, on the other hand, is a product for which stock is not managed.\n' 'A service is a non-material product you provide.\n' 'A digital content is a non-material product you sell online. The files attached to the products are the one that are sold on ' 'the e-commerce such as e-books, music, pictures,... The "Digital Product" module has to be installed.') rental = fields.Boolean('Can be Rent') categ_id = fields.Many2one( 'product.category', 'Internal Category', change_default=True, default=_get_default_category_id, required=True, help="Select category for the current product") currency_id = fields.Many2one( 'res.currency', 'Currency', compute='_compute_currency_id') # price fields price = fields.Float( 'Price', compute='_compute_template_price', inverse='_set_template_price', digits=dp.get_precision('Product Price')) list_price = fields.Float( 'Sales Price', default=1.0, digits=dp.get_precision('Product Price'), help="Base price to compute the customer price. Sometimes called the catalog price.") lst_price = fields.Float( 'Public Price', related='list_price', digits=dp.get_precision('Product Price')) standard_price = fields.Float( 'Cost', compute='_compute_standard_price', inverse='_set_standard_price', search='_search_standard_price', digits=dp.get_precision('Product Price'), groups="base.group_user", help = "Cost used for stock valuation in standard price and as a first price to set in average/fifo. " "Also used as a base price for pricelists. " "Expressed in the default unit of measure of the product. ") volume = fields.Float( 'Volume', compute='_compute_volume', inverse='_set_volume', help="The volume in m3.", store=True) weight = fields.Float( 'Weight', compute='_compute_weight', digits=dp.get_precision('Stock Weight'), inverse='_set_weight', store=True, help="The weight of the contents in Kg, not including any packaging, etc.") sale_ok = fields.Boolean( 'Can be Sold', default=True, help="Specify if the product can be selected in a sales order line.") purchase_ok = fields.Boolean('Can be Purchased', default=True) pricelist_id = fields.Many2one( 'product.pricelist', 'Pricelist', store=False, help='Technical field. Used for searching on pricelists, not stored in database.') uom_id = fields.Many2one( 'product.uom', 'Unit of Measure', default=_get_default_uom_id, required=True, help="Default Unit of Measure used for all stock operation.") uom_po_id = fields.Many2one( 'product.uom', 'Purchase Unit of Measure', default=_get_default_uom_id, required=True, help="Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure.") company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('product.template'), index=1) packaging_ids = fields.One2many( 'product.packaging', string="Product Packages", compute="_compute_packaging_ids", inverse="_set_packaging_ids", help="Gives the different ways to package the same product.") seller_ids = fields.One2many('product.supplierinfo', 'product_tmpl_id', 'Vendors') variant_seller_ids = fields.One2many('product.supplierinfo', 'product_tmpl_id') active = fields.Boolean('Active', default=True, help="If unchecked, it will allow you to hide the product without removing it.") color = fields.Integer('Color Index') is_product_variant = fields.Boolean(string='Is a product variant', compute='_compute_is_product_variant') attribute_line_ids = fields.One2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes') product_variant_ids = fields.One2many('product.product', 'product_tmpl_id', 'Products', required=True) # performance: product_variant_id provides prefetching on the first product variant only product_variant_id = fields.Many2one('product.product', 'Product', compute='_compute_product_variant_id') product_variant_count = fields.Integer( '# Product Variants', compute='_compute_product_variant_count') # related to display product product information if is_product_variant barcode = fields.Char('Barcode', oldname='ean13', related='product_variant_ids.barcode') default_code = fields.Char( 'Internal Reference', compute='_compute_default_code', inverse='_set_default_code', store=True) item_ids = fields.One2many('product.pricelist.item', 'product_tmpl_id', 'Pricelist Items') # image: all image fields are base64 encoded and PIL-supported image = fields.Binary( "Image", attachment=True, help="This field holds the image used as image for the product, limited to 1024x1024px.") image_medium = fields.Binary( "Medium-sized image", attachment=True, help="Medium-sized image of the product. It is automatically " "resized as a 128x128px image, with aspect ratio preserved, " "only when the image exceeds one of those sizes. Use this field in form views or some kanban views.") image_small = fields.Binary( "Small-sized image", attachment=True, help="Small-sized image of the product. It is automatically " "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") @api.depends('product_variant_ids') def _compute_product_variant_id(self): for p in self: p.product_variant_id = p.product_variant_ids[:1].id @api.multi def _compute_currency_id(self): try: main_company = self.sudo().env.ref('base.main_company') except ValueError: main_company = self.env['res.company'].sudo().search([], limit=1, order="id") for template in self: template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id @api.multi def _compute_template_price(self): prices = {} pricelist_id_or_name = self._context.get('pricelist') if pricelist_id_or_name: pricelist = None partner = self._context.get('partner') quantity = self._context.get('quantity', 1.0) # Support context pricelists specified as display_name or ID for compatibility if isinstance(pricelist_id_or_name, pycompat.string_types): pricelist_data = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) if pricelist_data: pricelist = self.env['product.pricelist'].browse(pricelist_data[0][0]) elif isinstance(pricelist_id_or_name, pycompat.integer_types): pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name) if pricelist: quantities = [quantity] * len(self) partners = [partner] * len(self) prices = pricelist.get_products_price(self, quantities, partners) for template in self: template.price = prices.get(template.id, 0.0) @api.multi def _set_template_price(self): if self._context.get('uom'): for template in self: value = self.env['product.uom'].browse(self._context['uom'])._compute_price(template.price, template.uom_id) template.write({'list_price': value}) else: self.write({'list_price': self.price}) @api.depends('product_variant_ids', 'product_variant_ids.standard_price') def _compute_standard_price(self): unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1) for template in unique_variants: template.standard_price = template.product_variant_ids.standard_price for template in (self - unique_variants): template.standard_price = 0.0 @api.one def _set_standard_price(self): if len(self.product_variant_ids) == 1: self.product_variant_ids.standard_price = self.standard_price def _search_standard_price(self, operator, value): products = self.env['product.product'].search([('standard_price', operator, value)], limit=None) return [('id', 'in', products.mapped('product_tmpl_id').ids)] @api.depends('product_variant_ids', 'product_variant_ids.volume') def _compute_volume(self): unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1) for template in unique_variants: template.volume = template.product_variant_ids.volume for template in (self - unique_variants): template.volume = 0.0 @api.one def _set_volume(self): if len(self.product_variant_ids) == 1: self.product_variant_ids.volume = self.volume @api.depends('product_variant_ids', 'product_variant_ids.weight') def _compute_weight(self): unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1) for template in unique_variants: template.weight = template.product_variant_ids.weight for template in (self - unique_variants): template.weight = 0.0 def _compute_is_product_variant(self): for template in self: template.is_product_variant = False @api.one def _set_weight(self): if len(self.product_variant_ids) == 1: self.product_variant_ids.weight = self.weight @api.one @api.depends('product_variant_ids.product_tmpl_id') def _compute_product_variant_count(self): # do not pollute variants to be prefetched when counting variants self.product_variant_count = len(self.with_prefetch().product_variant_ids) @api.depends('product_variant_ids', 'product_variant_ids.default_code') def _compute_default_code(self): unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1) for template in unique_variants: template.default_code = template.product_variant_ids.default_code for template in (self - unique_variants): template.default_code = '' @api.one def _set_default_code(self): if len(self.product_variant_ids) == 1: self.product_variant_ids.default_code = self.default_code @api.depends('product_variant_ids', 'product_variant_ids.packaging_ids') def _compute_packaging_ids(self): for p in self: if len(p.product_variant_ids) == 1: p.packaging_ids = p.product_variant_ids.packaging_ids def _set_packaging_ids(self): for p in self: if len(p.product_variant_ids) == 1: p.product_variant_ids.packaging_ids = p.packaging_ids @api.constrains('uom_id', 'uom_po_id') def _check_uom(self): if any(template.uom_id and template.uom_po_id and template.uom_id.category_id != template.uom_po_id.category_id for template in self): raise ValidationError(_('Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.')) return True @api.onchange('uom_id') def _onchange_uom_id(self): if self.uom_id: self.uom_po_id = self.uom_id.id @api.model def create(self, vals): ''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date''' # TDE FIXME: context brol tools.image_resize_images(vals) template = super(ProductTemplate, self).create(vals) if "create_product_product" not in self._context: template.with_context(create_from_tmpl=True).create_variant_ids() # This is needed to set given values to first variant after creation related_vals = {} if vals.get('barcode'): related_vals['barcode'] = vals['barcode'] if vals.get('default_code'): related_vals['default_code'] = vals['default_code'] if vals.get('standard_price'): related_vals['standard_price'] = vals['standard_price'] if vals.get('volume'): related_vals['volume'] = vals['volume'] if vals.get('weight'): related_vals['weight'] = vals['weight'] if related_vals: template.write(related_vals) return template @api.multi def write(self, vals): tools.image_resize_images(vals) res = super(ProductTemplate, self).write(vals) if 'attribute_line_ids' in vals or vals.get('active'): self.create_variant_ids() if 'active' in vals and not vals.get('active'): self.with_context(active_test=False).mapped('product_variant_ids').write({'active': vals.get('active')}) return res @api.multi def copy(self, default=None): # TDE FIXME: should probably be copy_data self.ensure_one() if default is None: default = {} if 'name' not in default: default['name'] = _("%s (copy)") % self.name return super(ProductTemplate, self).copy(default=default) @api.multi def name_get(self): return [(template.id, '%s%s' % (template.default_code and '[%s] ' % template.default_code or '', template.name)) for template in self] @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): # Only use the product.product heuristics if there is a search term and the domain # does not specify a match on `product.template` IDs. if not name or any(term[0] == 'id' for term in (args or [])): return super(ProductTemplate, self).name_search(name=name, args=args, operator=operator, limit=limit) Product = self.env['product.product'] templates = self.browse([]) while True: domain = templates and [('product_tmpl_id', 'not in', templates.ids)] or [] args = args if args is not None else [] products_ns = Product.name_search(name, args+domain, operator=operator) products = Product.browse([x[0] for x in products_ns]) templates |= products.mapped('product_tmpl_id') if (not products) or (limit and (len(templates) > limit)): break # re-apply product.template order + name_get return super(ProductTemplate, self).name_search( '', args=[('id', 'in', list(set(templates.ids)))], operator='ilike', limit=limit) @api.multi def price_compute(self, price_type, uom=False, currency=False, company=False): # TDE FIXME: delegate to template or not ? fields are reencoded here ... # compatibility about context keys used a bit everywhere in the code if not uom and self._context.get('uom'): uom = self.env['product.uom'].browse(self._context['uom']) if not currency and self._context.get('currency'): currency = self.env['res.currency'].browse(self._context['currency']) templates = self if price_type == 'standard_price': # standard_price field can only be seen by users in base.group_user # Thus, in order to compute the sale price from the cost for users not in this group # We fetch the standard price as the superuser templates = self.with_context(force_company=company and company.id or self._context.get('force_company', self.env.user.company_id.id)).sudo() prices = dict.fromkeys(self.ids, 0.0) for template in templates: prices[template.id] = template[price_type] or 0.0 if uom: prices[template.id] = template.uom_id._compute_price(prices[template.id], uom) # Convert from current user company currency to asked one # This is right cause a field cannot be in more than one currency if currency: prices[template.id] = template.currency_id.compute(prices[template.id], currency) return prices # compatibility to remove after v10 - DEPRECATED @api.model def _price_get(self, products, ptype='list_price'): return products.price_compute(ptype) @api.multi def create_variant_ids(self): Product = self.env["product.product"] AttributeValues = self.env['product.attribute.value'] for tmpl_id in self.with_context(active_test=False): # adding an attribute with only one value should not recreate product # write this attribute on every product to make sure we don't lose them variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: line.attribute_id.create_variant and len(line.value_ids) == 1).mapped('value_ids') for value_id in variant_alone: updated_products = tmpl_id.product_variant_ids.filtered(lambda product: value_id.attribute_id not in product.mapped('attribute_value_ids.attribute_id')) updated_products.write({'attribute_value_ids': [(4, value_id.id)]}) # iterator of n-uple of product.attribute.value *ids* variant_matrix = [ AttributeValues.browse(value_ids) for value_ids in itertools.product(*(line.value_ids.ids for line in tmpl_id.attribute_line_ids if line.value_ids[:1].attribute_id.create_variant)) ] # get the value (id) sets of existing variants existing_variants = {frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant).ids) for variant in tmpl_id.product_variant_ids} # -> for each value set, create a recordset of values to create a # variant for if the value set isn't already a variant to_create_variants = [ value_ids for value_ids in variant_matrix if set(value_ids.ids) not in existing_variants ] # check product variants_to_activate = self.env['product.product'] variants_to_unlink = self.env['product.product'] for product_id in tmpl_id.product_variant_ids: if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) in variant_matrix: variants_to_activate |= product_id elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) not in variant_matrix: variants_to_unlink |= product_id if variants_to_activate: variants_to_activate.write({'active': True}) # create new product for variant_ids in to_create_variants: new_variant = Product.create({ 'product_tmpl_id': tmpl_id.id, 'attribute_value_ids': [(6, 0, variant_ids.ids)] }) # unlink or inactive product for variant in variants_to_unlink: try: with self._cr.savepoint(), tools.mute_logger('noblecrm.sql_db'): variant.unlink() # We catch all kind of exception to be sure that the operation doesn't fail. except (psycopg2.Error, except_orm): variant.write({'active': False}) pass return True
class ChangeProductionQty(models.TransientModel): _name = 'change.production.qty' _description = 'Change Quantity of Products' # 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): 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.write({'product_uom_qty': qty}) 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}) @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)) 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) for line, line_data in lines: production._update_raw_move(line, line_data) 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'] self._update_product_to_produce( production, production.product_qty - qty_produced) 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 = math.ceil(operation_bom_qty[operation.id] / operation.workcenter_id.capacity ) # TODO: float_round 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 ProductProduct(models.Model): _name = "product.product" _description = "Product" _inherits = {'product.template': 'product_tmpl_id'} _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'default_code, name, id' price = fields.Float('Price', compute='_compute_product_price', digits=dp.get_precision('Product Price'), inverse='_set_product_price') price_extra = fields.Float( 'Variant Price Extra', compute='_compute_product_price_extra', digits=dp.get_precision('Product Price'), help="This is the sum of the extra price of all attributes") lst_price = fields.Float( 'Sale Price', compute='_compute_product_lst_price', digits=dp.get_precision('Product Price'), inverse='_set_product_lst_price', help= "The sale price is managed from the product template. Click on the 'Variant Prices' button to set the extra attribute prices." ) default_code = fields.Char('Internal Reference', index=True) code = fields.Char('Reference', compute='_compute_product_code') partner_ref = fields.Char('Customer Ref', compute='_compute_partner_ref') active = fields.Boolean( 'Active', default=True, help= "If unchecked, it will allow you to hide the product without removing it." ) product_tmpl_id = fields.Many2one('product.template', 'Product Template', auto_join=True, index=True, ondelete="cascade", required=True) barcode = fields.Char( 'Barcode', copy=False, oldname='ean13', help="International Article Number used for product identification.") attribute_value_ids = fields.Many2many('product.attribute.value', string='Attributes', ondelete='restrict') # image: all image fields are base64 encoded and PIL-supported image_variant = fields.Binary( "Variant Image", attachment=True, help= "This field holds the image used as image for the product variant, limited to 1024x1024px." ) image = fields.Binary( "Big-sized image", compute='_compute_images', inverse='_set_image', help= "Image of the product variant (Big-sized image of product template if false). It is automatically " "resized as a 1024x1024px image, with aspect ratio preserved.") image_small = fields.Binary( "Small-sized image", compute='_compute_images', inverse='_set_image_small', help= "Image of the product variant (Small-sized image of product template if false)." ) image_medium = fields.Binary( "Medium-sized image", compute='_compute_images', inverse='_set_image_medium', help= "Image of the product variant (Medium-sized image of product template if false)." ) is_product_variant = fields.Boolean(compute='_compute_is_product_variant') standard_price = fields.Float( 'Cost', company_dependent=True, digits=dp.get_precision('Product Price'), groups="base.group_user", help= "Cost used for stock valuation in standard price and as a first price to set in average/fifo. " "Also used as a base price for pricelists. " "Expressed in the default unit of measure of the product.") volume = fields.Float('Volume', help="The volume in m3.") weight = fields.Float( 'Weight', digits=dp.get_precision('Stock Weight'), help= "The weight of the contents in Kg, not including any packaging, etc.") pricelist_item_ids = fields.Many2many('product.pricelist.item', 'Pricelist Items', compute='_get_pricelist_items') packaging_ids = fields.One2many( 'product.packaging', 'product_id', 'Product Packages', help="Gives the different ways to package the same product.") _sql_constraints = [ ('barcode_uniq', 'unique(barcode)', "A barcode can only be assigned to one product !"), ] def _get_invoice_policy(self): return False def _compute_is_product_variant(self): for product in self: product.is_product_variant = True def _compute_product_price(self): prices = {} pricelist_id_or_name = self._context.get('pricelist') if pricelist_id_or_name: pricelist = None partner = self._context.get('partner', False) quantity = self._context.get('quantity', 1.0) # Support context pricelists specified as display_name or ID for compatibility if isinstance(pricelist_id_or_name, pycompat.string_types): pricelist_name_search = self.env[ 'product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) if pricelist_name_search: pricelist = self.env['product.pricelist'].browse( [pricelist_name_search[0][0]]) elif isinstance(pricelist_id_or_name, pycompat.integer_types): pricelist = self.env['product.pricelist'].browse( pricelist_id_or_name) if pricelist: quantities = [quantity] * len(self) partners = [partner] * len(self) prices = pricelist.get_products_price(self, quantities, partners) for product in self: product.price = prices.get(product.id, 0.0) def _set_product_price(self): for product in self: if self._context.get('uom'): value = self.env['product.uom'].browse( self._context['uom'])._compute_price( product.price, product.uom_id) else: value = product.price value -= product.price_extra product.write({'list_price': value}) def _set_product_lst_price(self): for product in self: if self._context.get('uom'): value = self.env['product.uom'].browse( self._context['uom'])._compute_price( product.lst_price, product.uom_id) else: value = product.lst_price value -= product.price_extra product.write({'list_price': value}) @api.depends('attribute_value_ids.price_ids.price_extra', 'attribute_value_ids.price_ids.product_tmpl_id') def _compute_product_price_extra(self): # TDE FIXME: do a real multi and optimize a bit ? for product in self: price_extra = 0.0 for attribute_price in product.mapped( 'attribute_value_ids.price_ids'): if attribute_price.product_tmpl_id == product.product_tmpl_id: price_extra += attribute_price.price_extra product.price_extra = price_extra @api.depends('list_price', 'price_extra') def _compute_product_lst_price(self): to_uom = None if 'uom' in self._context: to_uom = self.env['product.uom'].browse([self._context['uom']]) for product in self: if to_uom: list_price = product.uom_id._compute_price( product.list_price, to_uom) else: list_price = product.list_price product.lst_price = list_price + product.price_extra @api.one def _compute_product_code(self): for supplier_info in self.seller_ids: if supplier_info.name.id == self._context.get('partner_id'): self.code = supplier_info.product_code or self.default_code break else: self.code = self.default_code @api.one def _compute_partner_ref(self): for supplier_info in self.seller_ids: if supplier_info.name.id == self._context.get('partner_id'): product_name = supplier_info.product_name or self.default_code or self.name self.partner_ref = '%s%s' % (self.code and '[%s] ' % self.code or '', product_name) break else: self.partner_ref = self.name_get()[0][1] @api.one @api.depends('image_variant', 'product_tmpl_id.image') def _compute_images(self): if self._context.get('bin_size'): self.image_medium = self.image_variant self.image_small = self.image_variant self.image = self.image_variant else: resized_images = tools.image_get_resized_images( self.image_variant, return_big=True, avoid_resize_medium=True) self.image_medium = resized_images['image_medium'] self.image_small = resized_images['image_small'] self.image = resized_images['image'] if not self.image_medium: self.image_medium = self.product_tmpl_id.image_medium if not self.image_small: self.image_small = self.product_tmpl_id.image_small if not self.image: self.image = self.product_tmpl_id.image @api.one def _set_image(self): self._set_image_value(self.image) @api.one def _set_image_medium(self): self._set_image_value(self.image_medium) @api.one def _set_image_small(self): self._set_image_value(self.image_small) @api.one def _set_image_value(self, value): if isinstance(value, pycompat.text_type): value = value.encode('ascii') image = tools.image_resize_image_big(value) if self.product_tmpl_id.image: self.image_variant = image else: self.product_tmpl_id.image = image @api.one def _get_pricelist_items(self): self.pricelist_item_ids = self.env['product.pricelist.item'].search([ '|', ('product_id', '=', self.id), ('product_tmpl_id', '=', self.product_tmpl_id.id) ]).ids @api.constrains('attribute_value_ids') def _check_attribute_value_ids(self): for product in self: attributes = self.env['product.attribute'] for value in product.attribute_value_ids: if value.attribute_id in attributes: raise ValidationError( _('Error! It is not allowed to choose more than one value for a given attribute.' )) if value.attribute_id.create_variant: attributes |= value.attribute_id return True @api.onchange('uom_id', 'uom_po_id') def _onchange_uom(self): if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id: self.uom_po_id = self.uom_id @api.model def create(self, vals): product = super( ProductProduct, self.with_context(create_product_product=True)).create(vals) # When a unique variant is created from tmpl then the standard price is set by _set_standard_price if not (self.env.context.get('create_from_tmpl') and len(product.product_tmpl_id.product_variant_ids) == 1): product._set_standard_price(vals.get('standard_price') or 0.0) return product @api.multi def write(self, values): ''' Store the standard price change in order to be able to retrieve the cost of a product for a given date''' res = super(ProductProduct, self).write(values) if 'standard_price' in values: self._set_standard_price(values['standard_price']) return res @api.multi def unlink(self): unlink_products = self.env['product.product'] unlink_templates = self.env['product.template'] for product in self: # Check if product still exists, in case it has been unlinked by unlinking its template if not product.exists(): continue # Check if the product is last product of this template other_products = self.search([('product_tmpl_id', '=', product.product_tmpl_id.id), ('id', '!=', product.id)]) if not other_products: unlink_templates |= product.product_tmpl_id unlink_products |= product res = super(ProductProduct, unlink_products).unlink() # delete templates after calling super, as deleting template could lead to deleting # products due to ondelete='cascade' unlink_templates.unlink() return res @api.multi def copy(self, default=None): # TDE FIXME: clean context / variant brol if default is None: default = {} if self._context.get('variant'): # if we copy a variant or create one, we keep the same template default['product_tmpl_id'] = self.product_tmpl_id.id elif 'name' not in default: default['name'] = self.name return super(ProductProduct, self).copy(default=default) @api.model def search(self, args, offset=0, limit=None, order=None, count=False): # TDE FIXME: strange if self._context.get('search_default_categ_id'): args.append((('categ_id', 'child_of', self._context['search_default_categ_id']))) return super(ProductProduct, self).search(args, offset=offset, limit=limit, order=order, count=count) @api.multi def name_get(self): # TDE: this could be cleaned a bit I think def _name_get(d): name = d.get('name', '') code = self._context.get('display_default_code', True) and d.get( 'default_code', False) or False if code: name = '[%s] %s' % (code, name) return (d['id'], name) partner_id = self._context.get('partner_id') if partner_id: partner_ids = [ partner_id, self.env['res.partner'].browse( partner_id).commercial_partner_id.id ] else: partner_ids = [] # all user don't have access to seller and partner # check access and use superuser self.check_access_rights("read") self.check_access_rule("read") result = [] for product in self.sudo(): # display only the attributes with multiple possible values on the template variable_attributes = product.attribute_line_ids.filtered( lambda l: len(l.value_ids) > 1).mapped('attribute_id') variant = product.attribute_value_ids._variant_name( variable_attributes) name = variant and "%s (%s)" % (product.name, variant) or product.name sellers = [] if partner_ids: sellers = [ x for x in product.seller_ids if (x.name.id in partner_ids) and (x.product_id == product) ] if not sellers: sellers = [ x for x in product.seller_ids if (x.name.id in partner_ids) and not x.product_id ] if sellers: for s in sellers: seller_variant = s.product_name and ( variant and "%s (%s)" % (s.product_name, variant) or s.product_name) or False mydict = { 'id': product.id, 'name': seller_variant or name, 'default_code': s.product_code or product.default_code, } temp = _name_get(mydict) if temp not in result: result.append(temp) else: mydict = { 'id': product.id, 'name': name, 'default_code': product.default_code, } result.append(_name_get(mydict)) return result @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): if not args: args = [] if name: positive_operators = ['=', 'ilike', '=ilike', 'like', '=like'] products = self.env['product.product'] if operator in positive_operators: products = self.search([('default_code', '=', name)] + args, limit=limit) if not products: products = self.search([('barcode', '=', name)] + args, limit=limit) if not products and operator not in expression.NEGATIVE_TERM_OPERATORS: # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal # on a database with thousands of matching products, due to the huge merge+unique needed for the # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table # Performing a quick memory merge of ids in Python will give much better performance products = self.search(args + [('default_code', operator, name)], limit=limit) if not limit or len(products) < limit: # we may underrun the limit because of dupes in the results, that's fine limit2 = (limit - len(products)) if limit else False products += self.search(args + [('name', operator, name), ('id', 'not in', products.ids)], limit=limit2) elif not products and operator in expression.NEGATIVE_TERM_OPERATORS: domain = expression.OR([ [ '&', ('default_code', operator, name), ('name', operator, name) ], [ '&', ('default_code', '=', False), ('name', operator, name) ], ]) domain = expression.AND([args, domain]) products = self.search(domain, limit=limit) if not products and operator in positive_operators: ptrn = re.compile('(\[(.*?)\])') res = ptrn.search(name) if res: products = self.search( [('default_code', '=', res.group(2))] + args, limit=limit) # still no results, partner in context: search on supplier info as last hope to find something if not products and self._context.get('partner_id'): suppliers = self.env['product.supplierinfo'].search([ ('name', '=', self._context.get('partner_id')), '|', ('product_code', operator, name), ('product_name', operator, name) ]) if suppliers: products = self.search( [('product_tmpl_id.seller_ids', 'in', suppliers.ids)], limit=limit) else: products = self.search(args, limit=limit) return products.name_get() @api.model def view_header_get(self, view_id, view_type): res = super(ProductProduct, self).view_header_get(view_id, view_type) if self._context.get('categ_id'): return _('Products: ') + self.env['product.category'].browse( self._context['categ_id']).name return res @api.multi def open_product_template(self): """ Utility method used to add an "Open Template" button in product views """ self.ensure_one() return { 'type': 'ir.actions.act_window', 'res_model': 'product.template', 'view_mode': 'form', 'res_id': self.product_tmpl_id.id, 'target': 'new' } @api.multi def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False): self.ensure_one() if date is None: date = fields.Date.context_today(self) precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') res = self.env['product.supplierinfo'] for seller in self.seller_ids: # Set quantity in UoM of seller quantity_uom_seller = quantity if quantity_uom_seller and uom_id and uom_id != seller.product_uom: quantity_uom_seller = uom_id._compute_quantity( quantity_uom_seller, seller.product_uom) if seller.date_start and seller.date_start > date: continue if seller.date_end and seller.date_end < date: continue if partner_id and seller.name not in [ partner_id, partner_id.parent_id ]: continue if float_compare(quantity_uom_seller, seller.min_qty, precision_digits=precision) == -1: continue if seller.product_id and seller.product_id != self: continue res |= seller break return res @api.multi def price_compute(self, price_type, uom=False, currency=False, company=False): # TDE FIXME: delegate to template or not ? fields are reencoded here ... # compatibility about context keys used a bit everywhere in the code if not uom and self._context.get('uom'): uom = self.env['product.uom'].browse(self._context['uom']) if not currency and self._context.get('currency'): currency = self.env['res.currency'].browse( self._context['currency']) products = self if price_type == 'standard_price': # standard_price field can only be seen by users in base.group_user # Thus, in order to compute the sale price from the cost for users not in this group # We fetch the standard price as the superuser products = self.with_context( force_company=company and company.id or self._context.get( 'force_company', self.env.user.company_id.id)).sudo() prices = dict.fromkeys(self.ids, 0.0) for product in products: prices[product.id] = product[price_type] or 0.0 if price_type == 'list_price': prices[product.id] += product.price_extra if uom: prices[product.id] = product.uom_id._compute_price( prices[product.id], uom) # Convert from current user company currency to asked one # This is right cause a field cannot be in more than one currency if currency: prices[product.id] = product.currency_id.compute( prices[product.id], currency) return prices # compatibility to remove after v10 - DEPRECATED @api.multi def price_get(self, ptype='list_price'): return self.price_compute(ptype) @api.multi def _set_standard_price(self, value): ''' Store the standard price change in order to be able to retrieve the cost of a product for a given date''' PriceHistory = self.env['product.price.history'] for product in self: PriceHistory.create({ 'product_id': product.id, 'cost': value, 'company_id': self._context.get('force_company', self.env.user.company_id.id), }) @api.multi def get_history_price(self, company_id, date=None): history = self.env['product.price.history'].search( [('company_id', '=', company_id), ('product_id', 'in', self.ids), ('datetime', '<=', date or fields.Datetime.now())], order='datetime desc,id desc', limit=1) return history.cost or 0.0
class LunchOrderLine(models.Model): _name = 'lunch.order.line' _description = 'lunch order line' _order = 'date desc, id desc' name = fields.Char(related='product_id.name', string="Product Name", readonly=True) order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True) product_id = fields.Many2one('lunch.product', 'Product', required=True) category_id = fields.Many2one('lunch.product.category', string='Product Category', related='product_id.category_id', readonly=True, store=True) date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True) supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier', readonly=True, store=True) user_id = fields.Many2one('res.users', string='User', related='order_id.user_id', readonly=True, store=True) note = fields.Text('Note') price = fields.Float(related='product_id.price', readonly=True, store=True, digits=dp.get_precision('Account')) state = fields.Selection([('new', 'New'), ('confirmed', 'Received'), ('ordered', 'Ordered'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, default='new') cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') currency_id = fields.Many2one('res.currency', related='order_id.currency_id') @api.one def order(self): """ The order_line is ordered to the vendor but isn't received yet """ if self.user_has_groups("lunch.group_lunch_manager"): self.state = 'ordered' else: raise AccessError( _("Only your lunch manager processes the orders.")) @api.one def confirm(self): """ confirm one or more order line, update order status and create new cashmove """ if self.user_has_groups("lunch.group_lunch_manager"): if self.state != 'confirmed': values = { 'user_id': self.user_id.id, 'amount': -self.price, 'description': self.product_id.name, 'order_id': self.id, 'state': 'order', 'date': self.date, } self.env['lunch.cashmove'].create(values) self.state = 'confirmed' else: raise AccessError( _("Only your lunch manager sets the orders as received.")) @api.one def cancel(self): """ cancel one or more order.line, update order status and unlink existing cashmoves """ if self.user_has_groups("lunch.group_lunch_manager"): self.state = 'cancelled' self.cashmove.unlink() else: raise AccessError(_("Only your lunch manager cancels the orders."))
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['product.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( 'product.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', 'All components available'), ('asap', 'The components of 1st operation')], string='Manufacturing Readiness', default='asap', 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 procurement 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.constrains('product_id', 'product_tmpl_id', 'bom_line_ids') def _check_product_recursion(self): for bom in self: 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 @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
class MrpBomLine(models.Model): _name = 'mrp.bom.line' _order = "sequence, id" _rec_name = "product_id" def _get_default_product_uom_id(self): return self.env['product.uom'].search([], limit=1, order='id').id product_id = fields.Many2one('product.product', 'Product', required=True) product_qty = fields.Float( 'Product Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), required=True) product_uom_id = fields.Many2one( 'product.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, 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) attribute_value_ids = fields.Many2many( 'product.attribute.value', string='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['ir.attachment'].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.model def create(self, values): 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(values) 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: if not product or self.attribute_value_ids - product.attribute_value_ids: 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="oe_view_nocontent_create"> Click to 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) }