class SaleOrderLine(models.Model): _inherit = "sale.order.line" margin = fields.Float(compute='_product_margin', digits=dp.get_precision('Product Price'), store=True) purchase_price = fields.Float(string='Cost', digits=dp.get_precision('Product Price')) def _compute_margin(self, order_id, product_id, product_uom_id): frm_cur = self.env.user.company_id.currency_id to_cur = order_id.pricelist_id.currency_id purchase_price = product_id.standard_price if product_uom_id != product_id.uom_id: purchase_price = product_id.uom_id._compute_price(purchase_price, product_uom_id) ctx = self.env.context.copy() ctx['date'] = order_id.date_order price = frm_cur.with_context(ctx).compute(purchase_price, to_cur, round=False) return price @api.model def _get_purchase_price(self, pricelist, product, product_uom, date): frm_cur = self.env.user.company_id.currency_id to_cur = pricelist.currency_id purchase_price = product.standard_price if product_uom != product.uom_id: purchase_price = product.uom_id._compute_price(purchase_price, product_uom) ctx = self.env.context.copy() ctx['date'] = date price = frm_cur.with_context(ctx).compute(purchase_price, to_cur, round=False) return {'purchase_price': price} @api.onchange('product_id', 'product_uom') def product_id_change_margin(self): if not self.order_id.pricelist_id or not self.product_id or not self.product_uom: return self.purchase_price = self._compute_margin(self.order_id, self.product_id, self.product_uom) @api.model def create(self, vals): vals.update(self._prepare_add_missing_fields(vals)) # Calculation of the margin for programmatic creation of a SO line. It is therefore not # necessary to call product_id_change_margin manually if 'purchase_price' not in vals: order_id = self.env['sale.order'].browse(vals['order_id']) product_id = self.env['product.product'].browse(vals['product_id']) product_uom_id = self.env['product.uom'].browse(vals['product_uom']) vals['purchase_price'] = self._compute_margin(order_id, product_id, product_uom_id) return super(SaleOrderLine, self).create(vals) @api.depends('product_id', 'purchase_price', 'product_uom_qty', 'price_unit', 'price_subtotal') def _product_margin(self): for line in self: currency = line.order_id.pricelist_id.currency_id price = line.purchase_price if not price: from_cur = line.env.user.company_id.currency_id.with_context(date=line.order_id.date_order) price = from_cur.compute(line.product_id.standard_price, currency, round=False) line.margin = currency.round(line.price_subtotal - (price * line.product_uom_qty))
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 PriceRule(models.Model): _name = "delivery.price.rule" _description = "Delivery Price Rules" _order = 'sequence, list_price, id' @api.depends('variable', 'operator', 'max_value', 'list_base_price', 'list_price', 'variable_factor') def _compute_name(self): for rule in self: name = 'if %s %s %s then' % (rule.variable, rule.operator, rule.max_value) if rule.list_base_price and not rule.list_price: name = '%s fixed price %s' % (name, rule.list_base_price) elif rule.list_price and not rule.list_base_price: name = '%s %s times %s' % (name, rule.list_price, rule.variable_factor) else: name = '%s fixed price %s plus %s times %s' % ( name, rule.list_base_price, rule.list_price, rule.variable_factor) rule.name = name name = fields.Char(compute='_compute_name') sequence = fields.Integer(required=True, default=10) carrier_id = fields.Many2one('delivery.carrier', 'Carrier', required=True, ondelete='cascade') variable = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], required=True, default='weight') operator = fields.Selection([('==', '='), ('<=', '<='), ('<', '<'), ('>=', '>='), ('>', '>')], required=True, default='<=') max_value = fields.Float('Maximum Value', required=True) list_base_price = fields.Float(string='Sale Base Price', digits=dp.get_precision('Product Price'), required=True, default=0.0) list_price = fields.Float('Sale Price', digits=dp.get_precision('Product Price'), required=True, default=0.0) variable_factor = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], 'Variable Factor', required=True, default='weight')
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 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 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 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 ProductAttributevalue(models.Model): _name = "product.attribute.value" _order = 'sequence, attribute_id, id' name = fields.Char('Value', required=True, translate=True) sequence = fields.Integer('Sequence', help="Determine the display order") attribute_id = fields.Many2one('product.attribute', 'Attribute', ondelete='cascade', required=True) product_ids = fields.Many2many('product.product', string='Variants', readonly=True) price_extra = fields.Float( 'Attribute Price Extra', compute='_compute_price_extra', inverse='_set_price_extra', default=0.0, digits=dp.get_precision('Product Price'), help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.") price_ids = fields.One2many('product.attribute.price', 'value_id', 'Attribute Prices', readonly=True) _sql_constraints = [ ('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !') ] @api.one def _compute_price_extra(self): if self._context.get('active_id'): price = self.price_ids.filtered(lambda price: price.product_tmpl_id.id == self._context['active_id']) self.price_extra = price.price_extra else: self.price_extra = 0.0 def _set_price_extra(self): if not self._context.get('active_id'): return AttributePrice = self.env['product.attribute.price'] prices = AttributePrice.search([('value_id', 'in', self.ids), ('product_tmpl_id', '=', self._context['active_id'])]) updated = prices.mapped('value_id') if prices: prices.write({'price_extra': self.price_extra}) else: for value in self - updated: AttributePrice.create({ 'product_tmpl_id': self._context['active_id'], 'value_id': value.id, 'price_extra': self.price_extra, }) @api.multi def name_get(self): if not self._context.get('show_attribute', True): # TDE FIXME: not used return super(ProductAttributevalue, self).name_get() return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self] @api.multi def unlink(self): linked_products = self.env['product.product'].with_context(active_test=False).search([('attribute_value_ids', 'in', self.ids)]) if linked_products: raise UserError(_('The operation cannot be completed:\nYou are trying to delete an attribute value with a reference on a product variant.')) return super(ProductAttributevalue, self).unlink() @api.multi def _variant_name(self, variable_attributes): return ", ".join([v.name for v in self if v.attribute_id in variable_attributes])
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 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 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 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 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 SaleOrderLine(models.Model): _inherit = 'sale.order.line' is_delivery = fields.Boolean(string="Is a Delivery", default=False) product_qty = fields.Float( compute='_compute_product_qty', string='Quantity', digits=dp.get_precision('Product Unit of Measure')) @api.depends('product_id', 'product_uom', 'product_uom_qty') def _compute_product_qty(self): for line in self: if not line.product_id or not line.product_uom or not line.product_uom_qty: return 0.0 line.product_qty = line.product_uom._compute_quantity( line.product_uom_qty, line.product_id.uom_id)
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 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 HrSalaryRule(models.Model): _name = 'hr.salary.rule' name = fields.Char(required=True, translate=True) code = fields.Char( required=True, help= "The code of salary rules can be used as reference in computation of other rules. " "In that case, it is case sensitive.") sequence = fields.Integer(required=True, index=True, default=5, help='Use to arrange calculation sequence') quantity = fields.Char( default='1.0', help="It is used in computation for percentage and fixed amount. " "For e.g. A rule for Meal Voucher having fixed amount of " u"1€ per worked day can have its quantity defined in expression " "like worked_days.WORK100.number_of_days.") category_id = fields.Many2one('hr.salary.rule.category', string='Category', required=True) active = fields.Boolean( default=True, help= "If the active field is set to false, it will allow you to hide the salary rule without removing it." ) appears_on_payslip = fields.Boolean( string='Appears on Payslip', default=True, help="Used to display the salary rule on payslip.") parent_rule_id = fields.Many2one('hr.salary.rule', string='Parent Salary Rule', index=True) company_id = fields.Many2one( 'res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) condition_select = fields.Selection([('none', 'Always True'), ('range', 'Range'), ('python', 'Python Expression')], string="Condition Based on", default='none', required=True) condition_range = fields.Char( string='Range Based on', default='contract.wage', help= 'This will be used to compute the % fields values; in general it is on basic, ' 'but you can also use categories code fields in lowercase as a variable names ' '(hra, ma, lta, etc.) and the variable basic.') condition_python = fields.Text( string='Python Condition', required=True, default=''' # Available variables: #---------------------- # payslip: object containing the payslips # employee: hr.employee object # contract: hr.contract object # rules: object containing the rules code (previously computed) # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category). # worked_days: object containing the computed worked days # inputs: object containing the computed inputs # Note: returned value have to be set in the variable 'result' result = rules.NET > categories.NET * 0.10''', help= 'Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.' ) condition_range_min = fields.Float( string='Minimum Range', help="The minimum amount, applied for this rule.") condition_range_max = fields.Float( string='Maximum Range', help="The maximum amount, applied for this rule.") amount_select = fields.Selection( [ ('percentage', 'Percentage (%)'), ('fix', 'Fixed Amount'), ('code', 'Python Code'), ], string='Amount Type', index=True, required=True, default='fix', help="The computation method for the rule amount.") amount_fix = fields.Float(string='Fixed Amount', digits=dp.get_precision('Payroll')) amount_percentage = fields.Float( string='Percentage (%)', digits=dp.get_precision('Payroll Rate'), help='For example, enter 50.0 to apply a percentage of 50%') amount_python_compute = fields.Text(string='Python Code', default=''' # Available variables: #---------------------- # payslip: object containing the payslips # employee: hr.employee object # contract: hr.contract object # rules: object containing the rules code (previously computed) # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category). # worked_days: object containing the computed worked days. # inputs: object containing the computed inputs. # Note: returned value have to be set in the variable 'result' result = contract.wage * 0.10''') amount_percentage_base = fields.Char( string='Percentage based on', help='result will be affected to a variable') child_ids = fields.One2many('hr.salary.rule', 'parent_rule_id', string='Child Salary Rule', copy=True) register_id = fields.Many2one( 'hr.contribution.register', string='Contribution Register', help= "Eventual third party involved in the salary payment of the employees." ) input_ids = fields.One2many('hr.rule.input', 'input_id', string='Inputs', copy=True) note = fields.Text(string='Description') @api.multi def _recursive_search_of_rules(self): """ @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids """ children_rules = [] for rule in self.filtered(lambda rule: rule.child_ids): children_rules += rule.child_ids._recursive_search_of_rules() return [(rule.id, rule.sequence) for rule in self] + children_rules #TODO should add some checks on the type of result (should be float) @api.multi def _compute_rule(self, localdict): """ :param localdict: dictionary containing the environement in which to compute the rule :return: returns a tuple build as the base/amount computed, the quantity and the rate :rtype: (float, float, float) """ self.ensure_one() if self.amount_select == 'fix': try: return self.amount_fix, float( safe_eval(self.quantity, localdict)), 100.0 except: raise UserError( _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) elif self.amount_select == 'percentage': try: return (float(safe_eval(self.amount_percentage_base, localdict)), float(safe_eval(self.quantity, localdict)), self.amount_percentage) except: raise UserError( _('Wrong percentage base or quantity defined for salary rule %s (%s).' ) % (self.name, self.code)) else: try: safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) return float( localdict['result'] ), 'result_qty' in localdict and localdict[ 'result_qty'] or 1.0, 'result_rate' in localdict and localdict[ 'result_rate'] or 100.0 except: raise UserError( _('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) @api.multi def _satisfy_condition(self, localdict): """ @param contract_id: id of hr.contract to be tested @return: returns True if the given rule match the condition for the given contract. Return False otherwise. """ self.ensure_one() if self.condition_select == 'none': return True elif self.condition_select == 'range': try: result = safe_eval(self.condition_range, localdict) return self.condition_range_min <= result and result <= self.condition_range_max or False except: raise UserError( _('Wrong range condition defined for salary rule %s (%s).') % (self.name, self.code)) else: # python code try: safe_eval(self.condition_python, localdict, mode='exec', nocopy=True) return 'result' in localdict and localdict['result'] or False except: raise UserError( _('Wrong python condition defined for salary rule %s (%s).' ) % (self.name, self.code))
class 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 RepairLine(models.Model): _name = 'mrp.repair.line' _description = 'Repair Line' name = fields.Char('Description', required=True) repair_id = fields.Many2one('mrp.repair', 'Repair Order Reference', index=True, ondelete='cascade') type = fields.Selection([('add', 'Add'), ('remove', 'Remove')], 'Type', required=True) product_id = fields.Many2one('product.product', 'Product', required=True) invoiced = fields.Boolean('Invoiced', copy=False, readonly=True) price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price')) price_subtotal = fields.Float('Subtotal', compute='_compute_price_subtotal', digits=0) tax_id = fields.Many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes') product_uom_qty = fields.Float( 'Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), required=True) product_uom = fields.Many2one('product.uom', 'Product Unit of Measure', required=True) invoice_line_id = fields.Many2one('account.invoice.line', 'Invoice Line', copy=False, readonly=True) location_id = fields.Many2one('stock.location', 'Source Location', index=True, required=True) location_dest_id = fields.Many2one('stock.location', 'Dest. Location', index=True, required=True) move_id = fields.Many2one('stock.move', 'Inventory Move', copy=False, readonly=True) lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial') state = fields.Selection( [('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', default='draft', copy=False, readonly=True, required=True, help= 'The status of a repair line is set automatically to the one of the linked repair order.' ) @api.constrains('lot_id', 'product_id') def constrain_lot_id(self): for line in self.filtered( lambda x: x.product_id.tracking != 'none' and not x.lot_id): raise ValidationError( _("Serial number is required for operation line with product '%s'" ) % (line.product_id.name)) @api.one @api.depends('price_unit', 'repair_id', 'product_uom_qty', 'product_id', 'repair_id.invoice_method') def _compute_price_subtotal(self): taxes = self.tax_id.compute_all( self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id) self.price_subtotal = taxes['total_excluded'] @api.onchange('type', 'repair_id') def onchange_operation_type(self): """ On change of operation type it sets source location, destination location and to invoice field. @param product: Changed operation type. @param guarantee_limit: Guarantee limit of current record. @return: Dictionary of values. """ if not self.type: self.location_id = False self.location_dest_id = False elif self.type == 'add': self.onchange_product_id() args = self.repair_id.company_id and [ ('company_id', '=', self.repair_id.company_id.id) ] or [] warehouse = self.env['stock.warehouse'].search(args, limit=1) self.location_id = warehouse.lot_stock_id self.location_dest_id = self.env['stock.location'].search( [('usage', '=', 'production')], limit=1).id else: self.price_unit = 0.0 self.tax_id = False self.location_id = self.env['stock.location'].search( [('usage', '=', 'production')], limit=1).id self.location_dest_id = self.env['stock.location'].search( [('scrap_location', '=', True)], limit=1).id @api.onchange('repair_id', 'product_id', 'product_uom_qty') def onchange_product_id(self): """ On change of product it sets product quantity, tax account, name, uom of product, unit price and price subtotal. """ partner = self.repair_id.partner_id pricelist = self.repair_id.pricelist_id if not self.product_id or not self.product_uom_qty: return if self.product_id: if partner: self.name = self.product_id.with_context( lang=partner.lang).display_name else: self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id.id if self.type != 'remove': 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 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 Repair(models.Model): _name = 'mrp.repair' _description = 'Repair Order' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc' @api.model def _default_stock_location(self): warehouse = self.env['stock.warehouse'].search([], limit=1) if warehouse: return warehouse.lot_stock_id.id return False name = fields.Char('Repair Reference', default=lambda self: self.env['ir.sequence']. next_by_code('mrp.repair'), copy=False, required=True, states={'confirmed': [('readonly', True)]}) product_id = fields.Many2one('product.product', string='Product to Repair', readonly=True, required=True, states={'draft': [('readonly', False)]}) product_qty = fields.Float( 'Product Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), readonly=True, required=True, states={'draft': [('readonly', False)]}) product_uom = fields.Many2one('product.uom', 'Product Unit of Measure', readonly=True, required=True, states={'draft': [('readonly', False)]}) partner_id = fields.Many2one( 'res.partner', 'Customer', index=True, states={'confirmed': [('readonly', True)]}, help='Choose partner for whom the order will be invoiced and delivered.' ) address_id = fields.Many2one('res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]", states={'confirmed': [('readonly', True)]}) default_address_id = fields.Many2one('res.partner', compute='_compute_default_address_id') state = fields.Selection( [('draft', 'Quotation'), ('cancel', 'Cancelled'), ('confirmed', 'Confirmed'), ('under_repair', 'Under Repair'), ('ready', 'Ready to Repair'), ('2binvoiced', 'To be Invoiced'), ('invoice_except', 'Invoice Exception'), ('done', 'Repaired')], string='Status', copy=False, default='draft', readonly=True, track_visibility='onchange', help= "* The \'Draft\' status is used when a user is encoding a new and unconfirmed repair order.\n" "* The \'Confirmed\' status is used when a user confirms the repair order.\n" "* The \'Ready to Repair\' status is used to start to repairing, user can start repairing only after repair order is confirmed.\n" "* The \'To be Invoiced\' status is used to generate the invoice before or after repairing done.\n" "* The \'Done\' status is set when repairing is completed.\n" "* The \'Cancelled\' status is used when user cancel repair order.") location_id = fields.Many2one('stock.location', 'Current Location', default=_default_stock_location, index=True, readonly=True, required=True, states={ 'draft': [('readonly', False)], 'confirmed': [('readonly', True)] }) location_dest_id = fields.Many2one('stock.location', 'Delivery Location', readonly=True, required=True, states={ 'draft': [('readonly', False)], 'confirmed': [('readonly', True)] }) lot_id = fields.Many2one( 'stock.production.lot', 'Lot/Serial', domain="[('product_id','=', product_id)]", help="Products repaired are all belonging to this lot", oldname="prodlot_id") guarantee_limit = fields.Date('Warranty Expiration', states={'confirmed': [('readonly', True)]}) operations = fields.One2many('mrp.repair.line', 'repair_id', 'Parts', copy=True, readonly=True, states={'draft': [('readonly', False)]}) pricelist_id = fields.Many2one( 'product.pricelist', 'Pricelist', default=lambda self: self.env['product.pricelist'].search([], limit=1 ).id, help='Pricelist of the selected partner.') partner_invoice_id = fields.Many2one('res.partner', 'Invoicing Address') invoice_method = fields.Selection( [("none", "No Invoice"), ("b4repair", "Before Repair"), ("after_repair", "After Repair")], string="Invoice Method", default='none', index=True, readonly=True, required=True, states={'draft': [('readonly', False)]}, help= 'Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.' ) invoice_id = fields.Many2one('account.invoice', 'Invoice', copy=False, readonly=True, track_visibility="onchange") move_id = fields.Many2one('stock.move', 'Move', copy=False, readonly=True, track_visibility="onchange", help="Move created by the repair order") fees_lines = fields.One2many('mrp.repair.fee', 'repair_id', 'Operations', copy=True, readonly=True, states={'draft': [('readonly', False)]}) internal_notes = fields.Text('Internal Notes') quotation_notes = fields.Text('Quotation Notes') company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']. _company_default_get('mrp.repair')) invoiced = fields.Boolean('Invoiced', copy=False, readonly=True) repaired = fields.Boolean('Repaired', copy=False, readonly=True) amount_untaxed = fields.Float('Untaxed Amount', compute='_amount_untaxed', store=True) amount_tax = fields.Float('Taxes', compute='_amount_tax', store=True) amount_total = fields.Float('Total', compute='_amount_total', store=True) tracking = fields.Selection('Product Tracking', related="product_id.tracking") @api.one @api.depends('partner_id') def _compute_default_address_id(self): if self.partner_id: self.default_address_id = self.partner_id.address_get( ['contact'])['contact'] @api.one @api.depends('operations.price_subtotal', 'invoice_method', 'fees_lines.price_subtotal', 'pricelist_id.currency_id') def _amount_untaxed(self): total = sum(operation.price_subtotal for operation in self.operations) total += sum(fee.price_subtotal for fee in self.fees_lines) self.amount_untaxed = self.pricelist_id.currency_id.round(total) @api.one @api.depends('operations.price_unit', 'operations.product_uom_qty', 'operations.product_id', 'fees_lines.price_unit', 'fees_lines.product_uom_qty', 'fees_lines.product_id', 'pricelist_id.currency_id', 'partner_id') def _amount_tax(self): val = 0.0 for operation in self.operations: if operation.tax_id: tax_calculate = operation.tax_id.compute_all( operation.price_unit, self.pricelist_id.currency_id, operation.product_uom_qty, operation.product_id, self.partner_id) for c in tax_calculate['taxes']: val += c['amount'] for fee in self.fees_lines: if fee.tax_id: tax_calculate = fee.tax_id.compute_all( fee.price_unit, self.pricelist_id.currency_id, fee.product_uom_qty, fee.product_id, self.partner_id) for c in tax_calculate['taxes']: val += c['amount'] self.amount_tax = val @api.one @api.depends('amount_untaxed', 'amount_tax') def _amount_total(self): self.amount_total = self.pricelist_id.currency_id.round( self.amount_untaxed + self.amount_tax) _sql_constraints = [ ('name', 'unique (name)', 'The name of the Repair Order must be unique!'), ] @api.onchange('product_id') def onchange_product_id(self): self.guarantee_limit = False self.lot_id = False if self.product_id: self.product_uom = self.product_id.uom_id.id @api.onchange('product_uom') def onchange_product_uom(self): res = {} if not self.product_id or not self.product_uom: return res if self.product_uom.category_id != self.product_id.uom_id.category_id: res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } self.product_uom = self.product_id.uom_id.id return res @api.onchange('location_id') def onchange_location_id(self): self.location_dest_id = self.location_id.id @api.onchange('partner_id') def onchange_partner_id(self): if not self.partner_id: self.address_id = False self.partner_invoice_id = False self.pricelist_id = self.env['product.pricelist'].search( [], limit=1).id else: addresses = self.partner_id.address_get( ['delivery', 'invoice', 'contact']) self.address_id = addresses['delivery'] or addresses['contact'] self.partner_invoice_id = addresses['invoice'] self.pricelist_id = self.partner_id.property_product_pricelist.id @api.multi def button_dummy(self): # TDE FIXME: this button is very interesting return True @api.multi def action_repair_cancel_draft(self): if self.filtered(lambda repair: repair.state != 'cancel'): raise UserError( _("Repair must be canceled in order to reset it to draft.")) self.mapped('operations').write({'state': 'draft'}) return self.write({'state': 'draft'}) def action_validate(self): self.ensure_one() precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') available_qty = self.env['stock.quant']._get_available_quantity( self.product_id, self.location_id, self.lot_id, strict=True) if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0: return self.action_repair_confirm() else: return { 'name': _('Insufficient Quantity'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.warn.insufficient.qty.repair', 'view_id': self.env.ref( 'mrp_repair.stock_warn_insufficient_qty_repair_form_view'). id, 'type': 'ir.actions.act_window', 'context': { 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_repair_id': self.id }, 'target': 'new' } @api.multi def action_repair_confirm(self): """ Repair order state is set to 'To be invoiced' when invoice method is 'Before repair' else state becomes 'Confirmed'. @param *arg: Arguments @return: True """ if self.filtered(lambda repair: repair.state != 'draft'): raise UserError(_("Can only confirm draft repairs.")) before_repair = self.filtered( lambda repair: repair.invoice_method == 'b4repair') before_repair.write({'state': '2binvoiced'}) to_confirm = self - before_repair to_confirm_operations = to_confirm.mapped('operations') to_confirm_operations.write({'state': 'confirmed'}) to_confirm.write({'state': 'confirmed'}) return True @api.multi def action_repair_cancel(self): if self.filtered(lambda repair: repair.state == 'done'): raise UserError(_("Cannot cancel completed repairs.")) if any(repair.invoiced for repair in self): raise UserError(_('Repair order is already invoiced.')) self.mapped('operations').write({'state': 'cancel'}) return self.write({'state': 'cancel'}) @api.multi def action_send_mail(self): self.ensure_one() template_id = self.env.ref( 'mrp_repair.mail_template_mrp_repair_quotation').id ctx = { 'default_model': 'mrp.repair', 'default_res_id': self.id, 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_composition_mode': 'comment' } return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'target': 'new', 'context': ctx, } @api.multi def print_repair_order(self): return self.env.ref( 'mrp_repair.action_report_mrp_repair_order').report_action(self) def action_repair_invoice_create(self): for repair in self: repair.action_invoice_create() if repair.invoice_method == 'b4repair': repair.action_repair_ready() elif repair.invoice_method == 'after_repair': repair.write({'state': 'done'}) return True @api.multi def action_invoice_create(self, group=False): """ Creates invoice(s) for repair order. @param group: It is set to true when group invoice is to be generated. @return: Invoice Ids. """ res = dict.fromkeys(self.ids, False) invoices_group = {} InvoiceLine = self.env['account.invoice.line'] Invoice = self.env['account.invoice'] for repair in self.filtered(lambda repair: repair.state not in ( 'draft', 'cancel') and not repair.invoice_id): if not repair.partner_id.id and not repair.partner_invoice_id.id: raise UserError( _('You have to select a Partner Invoice Address in the repair form!' )) comment = repair.quotation_notes if repair.invoice_method != 'none': if group and repair.partner_invoice_id.id in invoices_group: invoice = invoices_group[repair.partner_invoice_id.id] invoice.write({ 'name': invoice.name + ', ' + repair.name, 'origin': invoice.origin + ', ' + repair.name, 'comment': (comment and (invoice.comment and invoice.comment + "\n" + comment or comment)) or (invoice.comment and invoice.comment or ''), }) else: if not repair.partner_id.property_account_receivable_id: raise UserError( _('No account defined for partner "%s".') % repair.partner_id.name) invoice = Invoice.create({ 'name': repair.name, 'origin': repair.name, 'type': 'out_invoice', 'account_id': repair.partner_id.property_account_receivable_id.id, 'partner_id': repair.partner_invoice_id.id or repair.partner_id.id, 'currency_id': repair.pricelist_id.currency_id.id, 'comment': repair.quotation_notes, 'fiscal_position_id': repair.partner_id.property_account_position_id.id }) invoices_group[repair.partner_invoice_id.id] = invoice repair.write({'invoiced': True, 'invoice_id': invoice.id}) for operation in repair.operations: if operation.type == 'add': if group: name = repair.name + '-' + operation.name else: name = operation.name if operation.product_id.property_account_income_id: account_id = operation.product_id.property_account_income_id.id elif operation.product_id.categ_id.property_account_income_categ_id: account_id = operation.product_id.categ_id.property_account_income_categ_id.id else: raise UserError( _('No account defined for product "%s".') % operation.product_id.name) invoice_line = InvoiceLine.create({ 'invoice_id': invoice.id, 'name': name, 'origin': repair.name, 'account_id': account_id, 'quantity': operation.product_uom_qty, 'invoice_line_tax_ids': [(6, 0, [x.id for x in operation.tax_id])], 'uom_id': operation.product_uom.id, 'price_unit': operation.price_unit, 'price_subtotal': operation.product_uom_qty * operation.price_unit, 'product_id': operation.product_id and operation.product_id.id or False }) operation.write({ 'invoiced': True, 'invoice_line_id': invoice_line.id }) for fee in repair.fees_lines: if group: name = repair.name + '-' + fee.name else: name = fee.name if not fee.product_id: raise UserError(_('No product defined on Fees!')) if fee.product_id.property_account_income_id: account_id = fee.product_id.property_account_income_id.id elif fee.product_id.categ_id.property_account_income_categ_id: account_id = fee.product_id.categ_id.property_account_income_categ_id.id else: raise UserError( _('No account defined for product "%s".') % fee.product_id.name) invoice_line = InvoiceLine.create({ 'invoice_id': invoice.id, 'name': name, 'origin': repair.name, 'account_id': account_id, 'quantity': fee.product_uom_qty, 'invoice_line_tax_ids': [(6, 0, [x.id for x in fee.tax_id])], 'uom_id': fee.product_uom.id, 'product_id': fee.product_id and fee.product_id.id or False, 'price_unit': fee.price_unit, 'price_subtotal': fee.product_uom_qty * fee.price_unit }) fee.write({ 'invoiced': True, 'invoice_line_id': invoice_line.id }) invoice.compute_taxes() res[repair.id] = invoice.id return res @api.multi def action_created_invoice(self): self.ensure_one() return { 'name': _('Invoice created'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'account.invoice', 'view_id': self.env.ref('account.invoice_form').id, 'target': 'current', 'res_id': self.invoice_id.id, } def action_repair_ready(self): self.mapped('operations').write({'state': 'confirmed'}) return self.write({'state': 'ready'}) @api.multi def action_repair_start(self): """ Writes repair order state to 'Under Repair' @return: True """ if self.filtered( lambda repair: repair.state not in ['confirmed', 'ready']): raise UserError( _("Repair must be confirmed before starting reparation.")) self.mapped('operations').write({'state': 'confirmed'}) return self.write({'state': 'under_repair'}) @api.multi def action_repair_end(self): """ Writes repair order state to 'To be invoiced' if invoice method is After repair else state is set to 'Ready'. @return: True """ if self.filtered(lambda repair: repair.state != 'under_repair'): raise UserError( _("Repair must be under repair in order to end reparation.")) for repair in self: repair.write({'repaired': True}) vals = {'state': 'done'} vals['move_id'] = repair.action_repair_done().get(repair.id) if not repair.invoiced and repair.invoice_method == 'after_repair': vals['state'] = '2binvoiced' repair.write(vals) return True @api.multi def action_repair_done(self): """ Creates stock move for operation and stock move for final product of repair order. @return: Move ids of final products """ if self.filtered(lambda repair: not repair.repaired): raise UserError( _("Repair must be repaired in order to make the product moves." )) res = {} Move = self.env['stock.move'] for repair in self: moves = self.env['stock.move'] for operation in repair.operations: move = Move.create({ 'name': repair.name, 'product_id': operation.product_id.id, 'product_uom_qty': operation.product_uom_qty, 'product_uom': operation.product_uom.id, 'partner_id': repair.address_id.id, 'location_id': operation.location_id.id, 'location_dest_id': operation.location_dest_id.id, 'move_line_ids': [( 0, 0, { 'product_id': operation.product_id.id, 'lot_id': operation.lot_id.id, 'product_uom_qty': 0, # bypass reservation here 'product_uom_id': operation.product_uom.id, 'qty_done': operation.product_uom_qty, 'package_id': False, 'result_package_id': False, 'location_id': operation.location_id.id, #TODO: owner stuff 'location_dest_id': operation.location_dest_id.id, })], 'repair_id': repair.id, 'origin': repair.name, }) moves |= move operation.write({'move_id': move.id, 'state': 'done'}) move = Move.create({ 'name': repair.name, 'product_id': repair.product_id.id, 'product_uom': repair.product_uom.id or repair.product_id.uom_id.id, 'product_uom_qty': repair.product_qty, 'partner_id': repair.address_id.id, 'location_id': repair.location_id.id, 'location_dest_id': repair.location_dest_id.id, 'move_line_ids': [( 0, 0, { 'product_id': repair.product_id.id, 'lot_id': repair.lot_id.id, 'product_uom_qty': 0, # bypass reservation here 'product_uom_id': repair.product_uom.id or repair.product_id.uom_id.id, 'qty_done': repair.product_qty, 'package_id': False, 'result_package_id': False, 'location_id': repair.location_id.id, #TODO: owner stuff 'location_dest_id': repair.location_dest_id.id, })], 'repair_id': repair.id, 'origin': repair.name, }) consumed_lines = moves.mapped('move_line_ids') produced_lines = move.move_line_ids moves |= move moves._action_done() produced_lines.write( {'consume_line_ids': [(6, 0, consumed_lines.ids)]}) res[repair.id] = move.id return res
class AccountVoucherLine(models.Model): _name = 'account.voucher.line' _description = 'Voucher Lines' name = fields.Text(string='Description', required=True) sequence = fields.Integer( default=10, help="Gives the sequence of this line when displaying the voucher.") voucher_id = fields.Many2one('account.voucher', 'Voucher', required=1, ondelete='cascade') product_id = fields.Many2one('product.product', string='Product', ondelete='set null', index=True) account_id = fields.Many2one( 'account.account', string='Account', required=True, domain=[('deprecated', '=', False)], help="The income or expense account related to the selected product.") price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price'), oldname='amount') price_subtotal = fields.Monetary(string='Amount', store=True, readonly=True, compute='_compute_subtotal') quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'), required=True, default=1) account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account') company_id = fields.Many2one('res.company', related='voucher_id.company_id', string='Company', store=True, readonly=True) tax_ids = fields.Many2many('account.tax', string='Tax', help="Only for tax excluded from price") currency_id = fields.Many2one('res.currency', related='voucher_id.currency_id') @api.one @api.depends('price_unit', 'tax_ids', 'quantity', 'product_id', 'voucher_id.currency_id') def _compute_subtotal(self): self.price_subtotal = self.quantity * self.price_unit if self.tax_ids: taxes = self.tax_ids.compute_all( self.price_unit, self.voucher_id.currency_id, self.quantity, product=self.product_id, partner=self.voucher_id.partner_id) self.price_subtotal = taxes['total_excluded'] @api.onchange('product_id', 'voucher_id', 'price_unit', 'company_id') def _onchange_line_details(self): if not self.voucher_id or not self.product_id or not self.voucher_id.partner_id: return onchange_res = self.product_id_change(self.product_id.id, self.voucher_id.partner_id.id, self.price_unit, self.company_id.id, self.voucher_id.currency_id.id, self.voucher_id.voucher_type) for fname, fvalue in onchange_res['value'].items(): setattr(self, fname, fvalue) def _get_account(self, product, fpos, type): accounts = product.product_tmpl_id.get_product_accounts(fpos) if type == 'sale': return accounts['income'] return accounts['expense'] @api.multi def product_id_change(self, product_id, partner_id=False, price_unit=False, company_id=None, currency_id=None, type=None): # TDE note: mix of old and new onchange badly written in 9, multi but does not use record set context = self._context company_id = company_id if company_id is not None else context.get( 'company_id', False) company = self.env['res.company'].browse(company_id) currency = self.env['res.currency'].browse(currency_id) if not partner_id: raise UserError(_("You must first select a partner!")) part = self.env['res.partner'].browse(partner_id) if part.lang: self = self.with_context(lang=part.lang) product = self.env['product.product'].browse(product_id) fpos = part.property_account_position_id account = self._get_account(product, fpos, type) values = { 'name': product.partner_ref, 'account_id': account.id, } if type == 'purchase': values['price_unit'] = price_unit or product.standard_price taxes = product.supplier_taxes_id or account.tax_ids if product.description_purchase: values['name'] += '\n' + product.description_purchase else: values['price_unit'] = price_unit or product.lst_price taxes = product.taxes_id or account.tax_ids if product.description_sale: values['name'] += '\n' + product.description_sale values['tax_ids'] = taxes.ids if company and currency: if company.currency_id != currency: if type == 'purchase': values['price_unit'] = price_unit or product.standard_price values['price_unit'] = values['price_unit'] * currency.rate return {'value': values, 'domain': {}}
class 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 EventTicket(models.Model): _name = 'event.event.ticket' _description = 'Event Ticket' def _default_product_id(self): return self.env.ref('event_sale.product_product_event', raise_if_not_found=False) name = fields.Char(string='Name', required=True, translate=True) event_type_id = fields.Many2one('event.type', string='Event Category', ondelete='cascade') event_id = fields.Many2one('event.event', string="Event", ondelete='cascade') product_id = fields.Many2one('product.product', string='Product', required=True, domain=[("event_ok", "=", True)], default=_default_product_id) registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations') price = fields.Float(string='Price', digits=dp.get_precision('Product Price')) deadline = fields.Date(string="Sales End") is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired') price_reduce = fields.Float(string="Price Reduce", compute="_compute_price_reduce", digits=dp.get_precision('Product Price')) price_reduce_taxinc = fields.Float(compute='_get_price_reduce_tax', string='Price Reduce Tax inc') # seats fields seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')], string='Available Seat', required=True, store=True, compute='_compute_seats', default="limited") seats_max = fields.Integer(string='Maximum Available Seats', help="Define the number of available tickets. If you have too much registrations you will " "not be able to sell tickets anymore. Set 0 to ignore this rule set as unlimited.") seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=True) seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=True) seats_unconfirmed = fields.Integer(string='Unconfirmed Seat Reservations', compute='_compute_seats', store=True) seats_used = fields.Integer(compute='_compute_seats', store=True) @api.multi def _compute_is_expired(self): for record in self: if record.deadline: current_date = fields.Date.context_today(record.with_context({'tz': record.event_id.date_tz})) record.is_expired = record.deadline < current_date else: record.is_expired = False @api.multi def _compute_price_reduce(self): for record in self: product = record.product_id discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0 record.price_reduce = (1.0 - discount) * record.price def _get_price_reduce_tax(self): for record in self: # sudo necessary here since the field is most probably accessed through the website tax_ids = record.sudo().product_id.taxes_id.filtered(lambda r: r.company_id == record.event_id.company_id) taxes = tax_ids.compute_all(record.price_reduce, record.event_id.company_id.currency_id, 1.0, product=record.product_id) record.price_reduce_taxinc = taxes['total_included'] @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 + compute seats availability for ticket in self: ticket.seats_availability = 'unlimited' if ticket.seats_max == 0 else 'limited' ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0 # aggregate registrations by ticket and by state if self.ids: state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT event_ticket_id, state, count(event_id) FROM event_registration WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_ticket_id, state """ self.env.cr.execute(query, (tuple(self.ids),)) for event_ticket_id, state, num in self.env.cr.fetchall(): ticket = self.browse(event_ticket_id) ticket[state_field[state]] += num # compute seats_available for ticket in self: if ticket.seats_max > 0: ticket.seats_available = ticket.seats_max - (ticket.seats_reserved + ticket.seats_used) @api.multi @api.constrains('registration_ids', 'seats_max') def _check_seats_limit(self): for record in self: if record.seats_max and record.seats_available < 0: raise ValidationError(_('No more available seats for the ticket')) @api.constrains('event_type_id', 'event_id') def _constrains_event(self): if any(ticket.event_type_id and ticket.event_id for ticket in self): raise UserError(_('Ticket should belong to either event category or event but not both')) @api.onchange('product_id') def _onchange_product_id(self): self.price = self.product_id.list_price or 0
class MrpProduction(models.Model): """ Manufacturing Orders """ _name = 'mrp.production' _description = 'Manufacturing Order' _date_name = 'date_planned_start' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_planned_start asc,id' @api.model def _get_default_picking_type(self): return self.env['stock.picking.type'].search([ ('code', '=', 'mrp_operation'), ('warehouse_id.company_id', 'in', [self.env.context.get('company_id', self.env.user.company_id.id), False])], limit=1).id @api.model def _get_default_location_src_id(self): location = False if self._context.get('default_picking_type_id'): location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id if not location: location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False) return location and location.id or False @api.model def _get_default_location_dest_id(self): location = False if self._context.get('default_picking_type_id'): location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id if not location: location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False) return location and location.id or False name = fields.Char( 'Reference', copy=False, readonly=True, default=lambda x: _('New')) origin = fields.Char( 'Source', copy=False, help="Reference of the document that generated this production order request.") product_id = fields.Many2one( 'product.product', 'Product', domain=[('type', 'in', ['product', 'consu'])], readonly=True, required=True, states={'confirmed': [('readonly', False)]}) product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id') product_qty = fields.Float( 'Quantity To Produce', default=1.0, digits=dp.get_precision('Product Unit of Measure'), readonly=True, required=True, track_visibility='onchange', states={'confirmed': [('readonly', False)]}) product_uom_id = fields.Many2one( 'product.uom', 'Product Unit of Measure', oldname='product_uom', readonly=True, required=True, states={'confirmed': [('readonly', False)]}) picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', default=_get_default_picking_type, required=True) location_src_id = fields.Many2one( 'stock.location', 'Raw Materials Location', default=_get_default_location_src_id, readonly=True, required=True, states={'confirmed': [('readonly', False)]}, help="Location where the system will look for components.") location_dest_id = fields.Many2one( 'stock.location', 'Finished Products Location', default=_get_default_location_dest_id, readonly=True, required=True, states={'confirmed': [('readonly', False)]}, help="Location where the system will stock the finished products.") date_planned_start = fields.Datetime( 'Deadline Start', copy=False, default=fields.Datetime.now, index=True, required=True, states={'confirmed': [('readonly', False)]}, oldname="date_planned") date_planned_finished = fields.Datetime( 'Deadline End', copy=False, default=fields.Datetime.now, index=True, states={'confirmed': [('readonly', False)]}) date_start = fields.Datetime('Start Date', copy=False, index=True, readonly=True) date_finished = fields.Datetime('End Date', copy=False, index=True, readonly=True) bom_id = fields.Many2one( 'mrp.bom', 'Bill of Material', readonly=True, states={'confirmed': [('readonly', False)]}, help="Bill of Materials allow you to define the list of required raw materials to make a finished product.") routing_id = fields.Many2one( 'mrp.routing', 'Routing', readonly=True, compute='_compute_routing', store=True, help="The list of operations (list of work centers) to produce the finished product. The routing " "is mainly used to compute work center costs during operations and to plan future loads on " "work centers based on production planning.") move_raw_ids = fields.One2many( 'stock.move', 'raw_material_production_id', 'Raw Materials', oldname='move_lines', copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain=[('scrapped', '=', False)]) move_finished_ids = fields.One2many( 'stock.move', 'production_id', 'Finished Products', copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain=[('scrapped', '=', False)]) finished_move_line_ids = fields.One2many( 'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product" ) workorder_ids = fields.One2many( 'mrp.workorder', 'production_id', 'Work Orders', copy=False, oldname='workcenter_lines', readonly=True) workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count') workorder_done_count = fields.Integer('# Done Work Orders', compute='_compute_workorder_done_count') move_dest_ids = fields.One2many('stock.move', 'created_production_id', string="Stock Movements of Produced Goods") state = fields.Selection([ ('confirmed', 'Confirmed'), ('planned', 'Planned'), ('progress', 'In Progress'), ('done', 'Done'), ('cancel', 'Cancelled')], string='State', copy=False, default='confirmed', track_visibility='onchange') availability = fields.Selection([ ('assigned', 'Available'), ('partially_available', 'Partially Available'), ('waiting', 'Waiting'), ('none', 'None')], string='Materials Availability', compute='_compute_availability', store=True) unreserve_visible = fields.Boolean( 'Allowed to Unreserve Inventory', compute='_compute_unreserve_visible', help='Technical field to check when we can unreserve') post_visible = fields.Boolean( 'Allowed to Post Inventory', compute='_compute_post_visible', help='Technical field to check when we can post') consumed_less_than_planned = fields.Boolean( compute='_compute_consumed_less_than_planned', help='Technical field used to see if we have to display a warning or not when confirming an order.') user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self._uid) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('mrp.production'), required=True) check_to_done = fields.Boolean(compute="_get_produced_qty", string="Check Produced Qty", help="Technical Field to see if we can show 'Mark as Done' button") qty_produced = fields.Float(compute="_get_produced_qty", string="Quantity Produced") procurement_group_id = fields.Many2one( 'procurement.group', 'Procurement Group', copy=False) propagate = fields.Boolean( 'Propagate cancel and split', help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too') has_moves = fields.Boolean(compute='_has_moves') scrap_ids = fields.One2many('stock.scrap', 'production_id', 'Scraps') scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move') priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority', readonly=True, states={'confirmed': [('readonly', False)]}, default='1') is_locked = fields.Boolean('Is Locked', default=True, copy=False) show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots') production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production') @api.depends('product_id.tracking') def _compute_show_lots(self): for production in self: production.show_final_lots = production.product_id.tracking != 'none' def _inverse_lines(self): """ Little hack to make sure that when you change something on these objects, it gets saved""" pass @api.depends('move_finished_ids.move_line_ids') def _compute_lines(self): for production in self: production.finished_move_line_ids = production.move_finished_ids.mapped('move_line_ids') @api.multi @api.depends('bom_id.routing_id', 'bom_id.routing_id.operation_ids') def _compute_routing(self): for production in self: if production.bom_id.routing_id.operation_ids: production.routing_id = production.bom_id.routing_id.id else: production.routing_id = False @api.multi @api.depends('workorder_ids') def _compute_workorder_count(self): data = self.env['mrp.workorder'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id']) count_data = dict((item['production_id'][0], item['production_id_count']) for item in data) for production in self: production.workorder_count = count_data.get(production.id, 0) @api.multi @api.depends('workorder_ids.state') def _compute_workorder_done_count(self): data = self.env['mrp.workorder'].read_group([ ('production_id', 'in', self.ids), ('state', '=', 'done')], ['production_id'], ['production_id']) count_data = dict((item['production_id'][0], item['production_id_count']) for item in data) for production in self: production.workorder_done_count = count_data.get(production.id, 0) @api.multi @api.depends('move_raw_ids.state', 'workorder_ids.move_raw_ids', 'bom_id.ready_to_produce') def _compute_availability(self): for order in self: if not order.move_raw_ids: order.availability = 'none' continue if order.bom_id.ready_to_produce == 'all_available': order.availability = any(move.state not in ('assigned', 'done', 'cancel') for move in order.move_raw_ids) and 'waiting' or 'assigned' else: move_raw_ids = order.move_raw_ids.filtered(lambda m: m.product_qty) partial_list = [x.state in ('partially_available', 'assigned') for x in move_raw_ids] assigned_list = [x.state in ('assigned', 'done', 'cancel') for x in move_raw_ids] order.availability = (all(assigned_list) and 'assigned') or (any(partial_list) and 'partially_available') or 'waiting' @api.depends('move_raw_ids', 'is_locked', 'state', 'move_raw_ids.quantity_done') def _compute_unreserve_visible(self): for order in self: already_reserved = order.is_locked and order.state not in ('done', 'cancel') and order.mapped('move_raw_ids.move_line_ids') any_quantity_done = any([m.quantity_done > 0 for m in order.move_raw_ids]) order.unreserve_visible = not any_quantity_done and already_reserved @api.multi @api.depends('move_raw_ids.quantity_done', 'move_finished_ids.quantity_done', 'is_locked') def _compute_post_visible(self): for order in self: if order.product_tmpl_id._is_cost_method_standard(): order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids | order.move_finished_ids) else: order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids) @api.multi @api.depends('move_raw_ids.quantity_done', 'move_raw_ids.product_uom_qty') def _compute_consumed_less_than_planned(self): for order in self: order.consumed_less_than_planned = any(order.move_raw_ids.filtered( lambda move: float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=move.product_uom.rounding) == -1) ) @api.multi @api.depends('workorder_ids.state', 'move_finished_ids', 'is_locked') def _get_produced_qty(self): for production in self: done_moves = production.move_finished_ids.filtered(lambda x: x.state != 'cancel' and x.product_id.id == production.product_id.id) qty_produced = sum(done_moves.mapped('quantity_done')) wo_done = True if any([x.state not in ('done', 'cancel') for x in production.workorder_ids]): wo_done = False production.check_to_done = production.is_locked and done_moves and (qty_produced >= production.product_qty) and (production.state not in ('done', 'cancel')) and wo_done production.qty_produced = qty_produced return True @api.multi @api.depends('move_raw_ids') def _has_moves(self): for mo in self: mo.has_moves = any(mo.move_raw_ids) @api.multi def _compute_scrap_move_count(self): data = self.env['stock.scrap'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id']) count_data = dict((item['production_id'][0], item['production_id_count']) for item in data) for production in self: production.scrap_count = count_data.get(production.id, 0) _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'), ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), ] @api.onchange('product_id', 'picking_type_id', 'company_id') def onchange_product_id(self): """ Finds UoM of changed product. """ if not self.product_id: self.bom_id = False else: bom = self.env['mrp.bom']._bom_find(product=self.product_id, picking_type=self.picking_type_id, company_id=self.company_id.id) if bom.type == 'normal': self.bom_id = bom.id else: self.bom_id = False self.product_uom_id = self.product_id.uom_id.id return {'domain': {'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}} @api.onchange('picking_type_id') def onchange_picking_type(self): location = self.env.ref('stock.stock_location_stock') self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id @api.multi def write (self, vals): res = super(MrpProduction, self).write(vals) if 'date_planned_start' in vals: moves = (self.mapped('move_raw_ids') + self.mapped('move_finished_ids')).filtered( lambda r: r.state not in ['done', 'cancel']) moves.write({ 'date_expected': vals['date_planned_start'], }) return res @api.model def create(self, values): if not values.get('name', False) or values['name'] == _('New'): if values.get('picking_type_id'): values['name'] = self.env['stock.picking.type'].browse(values['picking_type_id']).sequence_id.next_by_id() else: values['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New') if not values.get('procurement_group_id'): values['procurement_group_id'] = self.env["procurement.group"].create({'name': values['name']}).id production = super(MrpProduction, self).create(values) production._generate_moves() return production @api.multi def unlink(self): if any(production.state != 'cancel' for production in self): raise UserError(_('Cannot delete a manufacturing order not in cancel state')) return super(MrpProduction, self).unlink() def action_toggle_is_locked(self): self.ensure_one() self.is_locked = not self.is_locked return True @api.multi def _generate_moves(self): for production in self: production._generate_finished_moves() factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) / production.bom_id.product_qty boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id) production._generate_raw_moves(lines) # Check for all draft moves whether they are mto or not production._adjust_procure_method() production.move_raw_ids._action_confirm() return True def _generate_finished_moves(self): move = self.env['stock.move'].create({ 'name': self.name, 'date': self.date_planned_start, 'date_expected': self.date_planned_start, 'product_id': self.product_id.id, 'product_uom': self.product_uom_id.id, 'product_uom_qty': self.product_qty, 'location_id': self.product_id.property_stock_production.id, 'location_dest_id': self.location_dest_id.id, 'company_id': self.company_id.id, 'production_id': self.id, 'origin': self.name, 'group_id': self.procurement_group_id.id, 'propagate': self.propagate, 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids], }) move._action_confirm() return move def _generate_raw_moves(self, exploded_lines): self.ensure_one() moves = self.env['stock.move'] for bom_line, line_data in exploded_lines: moves += self._generate_raw_move(bom_line, line_data) return moves def _generate_raw_move(self, bom_line, line_data): quantity = line_data['qty'] # alt_op needed for the case when you explode phantom bom and all the lines will be consumed in the operation given by the parent bom line alt_op = line_data['parent_line'] and line_data['parent_line'].operation_id.id or False if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom': return self.env['stock.move'] if bom_line.product_id.type not in ['product', 'consu']: return self.env['stock.move'] if self.routing_id: routing = self.routing_id else: routing = self.bom_id.routing_id if routing and routing.location_id: source_location = routing.location_id else: source_location = self.location_src_id original_quantity = (self.product_qty - self.qty_produced) or 1.0 data = { 'sequence': bom_line.sequence, 'name': self.name, 'date': self.date_planned_start, 'date_expected': self.date_planned_start, 'bom_line_id': bom_line.id, 'product_id': bom_line.product_id.id, 'product_uom_qty': quantity, 'product_uom': bom_line.product_uom_id.id, 'location_id': source_location.id, 'location_dest_id': self.product_id.property_stock_production.id, 'raw_material_production_id': self.id, 'company_id': self.company_id.id, 'operation_id': bom_line.operation_id.id or alt_op, 'price_unit': bom_line.product_id.standard_price, 'procure_method': 'make_to_stock', 'origin': self.name, 'warehouse_id': source_location.get_warehouse().id, 'group_id': self.procurement_group_id.id, 'propagate': self.propagate, 'unit_factor': quantity / original_quantity, } return self.env['stock.move'].create(data) @api.multi def _adjust_procure_method(self): try: mto_route = self.env['stock.warehouse']._get_mto_route() except: mto_route = False for move in self.move_raw_ids: product = move.product_id routes = product.route_ids + product.route_from_categ_ids # TODO: optimize with read_group? pull = self.env['procurement.rule'].search([('route_id', 'in', [x.id for x in routes]), ('location_src_id', '=', move.location_id.id), ('location_id', '=', move.location_dest_id.id)], limit=1) if pull and (pull.procure_method == 'make_to_order'): move.procure_method = pull.procure_method elif not pull: # If there is no make_to_stock rule either if mto_route and mto_route.id in [x.id for x in routes]: move.procure_method = 'make_to_order' @api.multi def _update_raw_move(self, bom_line, line_data): quantity = line_data['qty'] self.ensure_one() move = self.move_raw_ids.filtered(lambda x: x.bom_line_id.id == bom_line.id and x.state not in ('done', 'cancel')) if move: if quantity > 0: move[0].write({'product_uom_qty': quantity}) elif quantity < 0: # Do not remove 0 lines if move[0].quantity_done > 0: raise UserError(_('Lines need to be deleted, but can not as you still have some quantities to consume in them. ')) move[0]._action_cancel() move[0].unlink() return move else: self._generate_raw_move(bom_line, line_data) @api.multi def action_assign(self): for production in self: production.move_raw_ids._action_assign() return True @api.multi def open_produce_product(self): self.ensure_one() action = self.env.ref('mrp.act_mrp_product_produce').read()[0] return action @api.multi def button_plan(self): """ Create work orders. And probably do stuff, like things. """ orders_to_plan = self.filtered(lambda order: order.routing_id and order.state == 'confirmed') for order in orders_to_plan: quantity = order.product_uom_id._compute_quantity(order.product_qty, order.bom_id.product_uom_id) / order.bom_id.product_qty boms, lines = order.bom_id.explode(order.product_id, quantity, picking_type=order.bom_id.picking_type_id) order._generate_workorders(boms) return orders_to_plan.write({'state': 'planned'}) @api.multi def _generate_workorders(self, exploded_boms): workorders = self.env['mrp.workorder'] original_one = False for bom, bom_data in exploded_boms: # If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing if bom.routing_id.id and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id): temp_workorders = self._workorders_create(bom, bom_data) workorders += temp_workorders if temp_workorders: # In order to avoid two "ending work orders" if original_one: temp_workorders[-1].next_work_order_id = original_one original_one = temp_workorders[0] return workorders def _workorders_create(self, bom, bom_data): """ :param bom: in case of recursive boms: we could create work orders for child BoMs """ workorders = self.env['mrp.workorder'] bom_qty = bom_data['qty'] # Initial qty producing if self.product_id.tracking == 'serial': quantity = 1.0 else: quantity = self.product_qty - sum(self.move_finished_ids.mapped('quantity_done')) quantity = quantity if (quantity > 0) else 0 for operation in bom.routing_id.operation_ids: # create workorder cycle_number = math.ceil(bom_qty / operation.workcenter_id.capacity) # TODO: float_round UP duration_expected = (operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) workorder = workorders.create({ 'name': operation.name, 'production_id': self.id, 'workcenter_id': operation.workcenter_id.id, 'operation_id': operation.id, 'duration_expected': duration_expected, 'state': len(workorders) == 0 and 'ready' or 'pending', 'qty_producing': quantity, 'capacity': operation.workcenter_id.capacity, }) if workorders: workorders[-1].next_work_order_id = workorder.id workorders += workorder # assign moves; last operation receive all unassigned moves (which case ?) moves_raw = self.move_raw_ids.filtered(lambda move: move.operation_id == operation) if len(workorders) == len(bom.routing_id.operation_ids): moves_raw |= self.move_raw_ids.filtered(lambda move: not move.operation_id) moves_finished = self.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write({'workorder_id': workorder.id}) (moves_finished + moves_raw).write({'workorder_id': workorder.id}) workorder._generate_lot_ids() return workorders @api.multi def action_cancel(self): """ Cancels production order, unfinished stock moves and set procurement orders in exception """ if any(workorder.state == 'progress' for workorder in self.mapped('workorder_ids')): raise UserError(_('You can not cancel production order, a work order is still in progress.')) for production in self: production.workorder_ids.filtered(lambda x: x.state != 'cancel').action_cancel() finish_moves = production.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) raw_moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) (finish_moves | raw_moves)._action_cancel() self.write({'state': 'cancel', 'is_locked': True}) return True def _cal_price(self, consumed_moves): self.ensure_one() return True @api.multi def post_inventory(self): for order in self: moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) for move in moves_to_do.filtered(lambda m: m.product_qty == 0.0 and m.quantity_done > 0): move.product_uom_qty = move.quantity_done moves_to_do._action_done() moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do order._cal_price(moves_to_do) moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel')) moves_to_finish._action_done() order.action_assign() consume_move_lines = moves_to_do.mapped('active_move_line_ids') for moveline in moves_to_finish.mapped('active_move_line_ids'): if moveline.product_id == order.product_id and moveline.move_id.has_tracking != 'none': if any([not ml.lot_produced_id for ml in consume_move_lines]): raise UserError(_('You can not consume without telling for which lot you consumed it')) # Link all movelines in the consumed with same lot_produced_id false or the correct lot_produced_id filtered_lines = consume_move_lines.filtered(lambda x: x.lot_produced_id == moveline.lot_id) moveline.write({'consume_line_ids': [(6, 0, [x for x in filtered_lines.ids])]}) else: # Link with everything moveline.write({'consume_line_ids': [(6, 0, [x for x in consume_move_lines.ids])]}) return True @api.multi def button_mark_done(self): self.ensure_one() for wo in self.workorder_ids: if wo.time_ids.filtered(lambda x: (not x.date_end) and (x.loss_type in ('productive', 'performance'))): raise UserError(_('Work order %s is still running') % wo.name) self.post_inventory() moves_to_cancel = (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')) moves_to_cancel._action_cancel() self.write({'state': 'done', 'date_finished': fields.Datetime.now()}) return self.write({'state': 'done'}) @api.multi def do_unreserve(self): for production in self: production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))._do_unreserve() return True @api.multi def button_unreserve(self): self.ensure_one() self.do_unreserve() return True @api.multi def button_scrap(self): self.ensure_one() return { 'name': _('Scrap'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.scrap', 'view_id': self.env.ref('stock.stock_scrap_form_view2').id, 'type': 'ir.actions.act_window', 'context': {'default_production_id': self.id, 'product_ids': (self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) | self.move_finished_ids.filtered(lambda x: x.state == 'done')).mapped('product_id').ids, }, 'target': 'new', } @api.multi def action_see_move_scrap(self): self.ensure_one() action = self.env.ref('stock.action_stock_scrap').read()[0] action['domain'] = [('production_id', '=', self.id)] return action
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 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' # 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 HrExpenseSheet(models.Model): _name = "hr.expense.sheet" _inherit = ['mail.thread'] _description = "Expense Report" _order = "accounting_date desc, id desc" name = fields.Char(string='Expense Report Summary', required=True) expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={ 'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)] }, copy=False) state = fields.Selection([('submit', 'Submitted'), ('approve', 'Approved'), ('post', 'Posted'), ('done', 'Paid'), ('cancel', 'Refused')], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='submit', required=True, help='Expense Report State') employee_id = fields.Many2one( 'hr.employee', string="Employee", required=True, readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1)) address_id = fields.Many2one('res.partner', string="Employee Home Address") payment_mode = fields.Selection( [("own_account", "Employee (to reimburse)"), ("company_account", "Company")], related='expense_line_ids.payment_mode', default='own_account', readonly=True, string="Payment By") responsible_id = fields.Many2one('res.users', 'Validation By', readonly=True, copy=False, states={ 'submit': [('readonly', False)], 'submit': [('readonly', False)] }) total_amount = fields.Float(string='Total Amount', store=True, compute='_compute_amount', digits=dp.get_precision('Account')) company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id) currency_id = fields.Many2one( 'res.currency', string='Currency', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id) attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments') journal_id = fields.Many2one( 'account.journal', string='Expense Journal', states={ 'done': [('readonly', True)], 'post': [('readonly', True)] }, default=lambda self: self.env['ir.model.data']. xmlid_to_object('hr_expense.hr_expense_account_journal') or self.env[ 'account.journal'].search([('type', '=', 'purchase')], limit=1), help="The journal used when the expense is done.") bank_journal_id = fields.Many2one( 'account.journal', string='Bank Journal', states={ 'done': [('readonly', True)], 'post': [('readonly', True)] }, default=lambda self: self.env['account.journal'].search( [('type', 'in', ['cash', 'bank'])], limit=1), help="The payment method used when the expense is paid by the company." ) accounting_date = fields.Date(string="Date") account_move_id = fields.Many2one('account.move', string='Journal Entry', ondelete='restrict', copy=False) department_id = fields.Many2one('hr.department', string='Department', states={ 'post': [('readonly', True)], 'done': [('readonly', True)] }) @api.multi def check_consistency(self): for rec in self: expense_lines = rec.expense_line_ids if not expense_lines: continue if any(expense.employee_id != rec.employee_id for expense in expense_lines): raise UserError( _("Expenses must belong to the same Employee.")) if any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines): raise UserError( _("Expenses must have been paid by the same entity (Company or employee)" )) @api.model def create(self, vals): self._create_set_followers(vals) sheet = super(HrExpenseSheet, self).create(vals) sheet.check_consistency() return sheet @api.multi def write(self, vals): res = super(HrExpenseSheet, self).write(vals) self.check_consistency() if vals.get('employee_id'): self._add_followers() return res @api.multi def unlink(self): for expense in self: if expense.state in ['post', 'done']: raise UserError( _('You cannot delete a posted or paid expense.')) super(HrExpenseSheet, self).unlink() @api.multi def set_to_paid(self): self.write({'state': 'done'}) @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'approve': return 'hr_expense.mt_expense_approved' elif 'state' in init_values and self.state == 'submit': return 'hr_expense.mt_expense_confirmed' elif 'state' in init_values and self.state == 'cancel': return 'hr_expense.mt_expense_refused' elif 'state' in init_values and self.state == 'done': return 'hr_expense.mt_expense_paid' return super(HrExpenseSheet, self)._track_subtype(init_values) def _get_users_to_subscribe(self, employee=False): users = self.env['res.users'] employee = employee or self.employee_id if employee.user_id: users |= employee.user_id if employee.parent_id: users |= employee.parent_id.user_id if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id: users |= employee.department_id.manager_id.user_id return users def _add_followers(self): users = self._get_users_to_subscribe() self.message_subscribe_users(user_ids=users.ids) @api.model def _create_set_followers(self, values): # Add the followers at creation, so they can be notified employee_id = values.get('employee_id') if not employee_id: return employee = self.env['hr.employee'].browse(employee_id) users = self._get_users_to_subscribe(employee=employee) - self.env.user values['message_follower_ids'] = [] MailFollowers = self.env['mail.followers'] for partner in users.mapped('partner_id'): values[ 'message_follower_ids'] += MailFollowers._add_follower_command( self._name, [], {partner.id: None}, {})[0] @api.onchange('employee_id') def _onchange_employee_id(self): self.address_id = self.employee_id.sudo().address_home_id self.department_id = self.employee_id.department_id @api.one @api.depends('expense_line_ids', 'expense_line_ids.total_amount', 'expense_line_ids.currency_id') def _compute_amount(self): total_amount = 0.0 for expense in self.expense_line_ids: total_amount += expense.currency_id.with_context( date=expense.date, company_id=expense.company_id.id).compute( expense.total_amount, self.currency_id) self.total_amount = total_amount @api.one def _compute_attachment_number(self): self.attachment_number = sum( self.expense_line_ids.mapped('attachment_number')) @api.multi def refuse_sheet(self, reason): if not self.user_has_groups('hr_expense.group_hr_expense_user'): raise UserError(_("Only HR Officers can refuse expenses")) self.write({'state': 'cancel'}) for sheet in self: sheet.message_post_with_view( 'hr_expense.hr_expense_template_refuse_reason', values={ 'reason': reason, 'is_sheet': True, 'name': self.name }) @api.multi def approve_expense_sheets(self): if not self.user_has_groups('hr_expense.group_hr_expense_user'): raise UserError(_("Only HR Officers can approve expenses")) self.write({'state': 'approve', 'responsible_id': self.env.user.id}) @api.multi def paid_expense_sheets(self): self.write({'state': 'done'}) @api.multi def reset_expense_sheets(self): self.mapped('expense_line_ids').write({'is_refused': False}) return self.write({'state': 'submit'}) @api.multi def action_sheet_move_create(self): if any(sheet.state != 'approve' for sheet in self): raise UserError( _("You can only generate accounting entry for approved expense(s)." )) if any(not sheet.journal_id for sheet in self): raise UserError( _("Expenses must have an expense journal specified to generate accounting entries." )) expense_line_ids = self.mapped('expense_line_ids')\ .filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.user.company_id.currency_id).rounding)) res = expense_line_ids.action_move_create() if not self.accounting_date: self.accounting_date = self.account_move_id.date if self.payment_mode == 'own_account' and expense_line_ids: self.write({'state': 'post'}) else: self.write({'state': 'done'}) return res @api.multi def action_get_attachment_view(self): res = self.env['ir.actions.act_window'].for_xml_id( 'base', 'action_attachment') res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.expense_line_ids.ids)] res['context'] = { 'default_res_model': 'hr.expense.sheet', 'default_res_id': self.id, 'create': False, 'edit': False, } return res @api.one @api.constrains('expense_line_ids', 'employee_id') def _check_employee(self): employee_ids = self.expense_line_ids.mapped('employee_id') if len(employee_ids) > 1 or (len(employee_ids) == 1 and employee_ids != self.employee_id): raise ValidationError( _('You cannot add expense lines of another employee.')) @api.one @api.constrains('expense_line_ids') def _check_payment_mode(self): payment_mode = set(self.expense_line_ids.mapped('payment_mode')) if len(payment_mode) > 1: raise ValidationError( _('You cannot report expenses with different payment modes.'))