class StockMoveLine(models.Model): _inherit = "stock.move.line" product_version_id = fields.Many2one(comodel_name="product.version", related="move_id.product_version_id", store="True") real_in = fields.Float(string="Real In", compute="_compute_move_in_out_qty", store="True") real_out = fields.Float(string="Real Out", compute="_compute_move_in_out_qty", store="True") virtual_in = fields.Float(string="Virtual In", compute="_compute_move_in_out_qty", store="True") virtual_out = fields.Float(string="Virtual Out", compute="_compute_move_in_out_qty", store="True") api.depends("location_id", "location_dest_id", "product_qty") def _compute_move_in_out_qty(self): for move in self: move.real_in = 1 move.real_out = 2 move.virtual_in = 3 move.virtual_out = 4
class Phonebook(models.Model): _name = 'phone.book' _description = "Phone Book" name = fields.Char(string="Name", required=True) related_partner = fields.Many2one('res.partner', string="Related Partner") date_of_joining = fields.Date(string='Date of Joining') category_id = fields.Many2many('res.partner.category', string="Tags") city = fields.Char(string="City", required=True) street = fields.Char(string="Street", required=True) country_id = fields.Many2one('res.country', string="Country", required=True) address = fields.Char('Full Address', compute='_calculate_address') address_for_printing = fields.Char('Printing Address') def print_name(self): print("Name of Record: %s" % self.name) return True api.depends('country_id', 'city', 'street') def _calculate_address(self): full_address = self.country_id.name + ' ,' + self.city + ' ,' + self.street self.address = full_address @api.model @api.onchange('name') def return_full_address(self): if self.name and self.address: self.address_for_printing = 'customer: ' + self.name + ' ' + self.address @api.model def create(self, values): if 'name' in values: values['name'] = values['name'].upper() new_rec = super(Phonebook, self).create(values) return new_rec @api.multi def write(self, values, context=None): if 'name' in values: values['name'] = values['name'].upper() old_rec = super(Phonebook, self).write(values) else: old_rec = super(Phonebook, self).write(values) return True @api.multi def unlink(self): for rec in self: if rec.name == 'JOHN': raise NameError( 'El campo con el nombre de JOHN no tienes permitido borrarlo' )
class ResPartner(models.Model): _inherit = 'res.partner' _order = 'name' published_book_ids = fields.One2many('library.book', 'publisher_id', string='Published Books') authored_book_ids = fields.Many2many( 'library.book', string='Authored Books', relation='library_book_res_partner_rel') count_books = fields.Integer('Number of Authored Books', compute='_compute_count_books') api.depends('authored_book_ids') def _compute_count_books(self): for r in self: r.count_books = len(r.authored_book_ids) @api.multi def create_partner(self): today_str = fields.Date.context_today(self) val1 = { 'name': 'Eric Idle', 'email': '*****@*****.**', 'date': today_str } val2 = { 'name': 'John Clesse', 'email': '*****@*****.**', 'date': today_str } partner_val = { 'name': 'Flying Circus', 'email': '*****@*****.**', 'date': today_str, 'is_company': True, 'child_ids': [ (0, 0, val1), (0, 0, val2), ] } record = self.env['res.partner'].create(partner_val) return record
class PhoneBook(models.Model): _name = 'phone.book' # db name in psql : phone_book (dot turns to underscore in psql) _description = "Phone Book" # ORM side of things | Similar to peewee name = fields.Char(string="Name", required= True) related_partner = fields.Many2one(comodel_name='res.partner', string="Related Partner") date_of_joining = fields.Date(string="Date Of Joining") category_id = fields.Many2many(comodel_name= 'res.partner.category', string="Tags") city = fields.Char(string="City", required=True) street = fields.Char(string="Street", required=True) country_id = fields.Many2one(comodel_name='res.country', string="Country") address = fields.Char(string="Full Address", compute='_calculate_address') address_for_printing = fields.Char(string="Printing Address", compute='return_full_address') def print_name(self): print("Name of record: %s" %self.name) return True api.depends('country_id','city','street') def _calculate_address(self): # if country_id is None: # full_address = self.city + ' ,' + self.street # else: full_address = self.country_id.name + ' ,' + self.city + ' ,' + self.street self.address = full_address @api.model @api.onchange('name','address') # if change 'name' or 'address', everything will change automatically def return_full_address(self): if self.name and self.address: self.address_for_printing = 'customer: ' + self.name + ' ,' + self.address # see oreilly section6 last chapter on explanation @api.one , self.ensure_one() , @api.model # using create() function to upper() case the 'name' enterred when we create() new form @api.model def create(self,values): if 'name' in values: values['name'] = values['name'].upper() new_rec = super(PhoneBook, self).create(values) return new_rec # write(), same as create(), but applies for editing. create() meant for create new file ONLY @api.multi def write(self, values, context = None): if 'name' in values: values['name'] = values['name'].upper() old_rec = super(PhoneBook, self).write(values) else: old_rec = super(PhoneBook, self).write(values) return True # unlink(), ensuring that you can/cant delete something if the criteria are satisfied @api.multi def unlink(self): for rec in self: if rec.name == 'JOHN': raise NameError('Name that is exactly "JOHN" cant be deleted...because f**k you, thats why lmao')
additional_landed_cost = fields.Float( 'Additional Landed Cost', digits='Product Price') final_cost = fields.Float( ======= former_cost = fields.Monetary( 'Original Value') additional_landed_cost = fields.Monetary( 'Additional Landed Cost') final_cost = fields.Monetary( >>>>>>> f0a66d05e70e432d35dc68c9fb1e1cc6e51b40b8 'New Value', compute='_compute_final_cost', store=True) currency_id = fields.Many2one('res.currency', related='cost_id.company_id.currency_id') @api.depends('cost_line_id.name', 'product_id.code', 'product_id.name') def _compute_name(self): for line in self: name = '%s - ' % (line.cost_line_id.name if line.cost_line_id else '') line.name = name + (line.product_id.code or line.product_id.name or '') @api.depends('former_cost', 'additional_landed_cost') def _compute_final_cost(self): for line in self: line.final_cost = line.former_cost + line.additional_landed_cost def _create_accounting_entries(self, move, qty_out): # TDE CLEANME: product chosen for computation ? cost_product = self.cost_line_id.product_id if not cost_product: return False
def get_lab_warning_icon(self): if (self.warning): self.lab_warning_icon = 'medical-warning' @api.depends('result')
def _onchange_partner(self): if self.partner_id: self.delivery_id = self.partner_id # MAJ d'un autre champ # OU vals = {'delivery_id': self.partner_id.id} self.update(vals) # M2M : 2 possibilités : # - liste d'IDs, mais ça va AJOUTER les ids, comme (4, [IDs]) # - [(6, 0, [IDs])], ce qui va remplacer les ids # (cf module product_category_tax dans akretion/odoo-usability) # On utilise un autre M2M pour le mettre à jour, on peut faire # self.champ_M2M_ids.ids -> ça donne la liste des IDs # M2O : recordset (ou ID) # O2M : exemple v10 dans purchase/models/account_invoice.py # méthode purchase_order_change() # là, Odoo va jouer automatiquement le @api.onchange du champ delivery_id # pas besoin d'appeler le onchange de delivery_id dans notre code # Here, all form values are set on self # assigned values are not written to DB, but returned to the client # It is not possible to output a warning # It is not possible to put a raise UserError() # in this function (it will crash odoo) res = {'warning': {'title': _('Be careful'), {'message': _('here is the msg')}} # pour un domaine res = {'domain': { 'champ1': "[('product_id', '=', product_id)]", 'champ2': "[]"}, } # si on a besoin de changer le contexte (astuce qui peut être utile # pour ne pas déclancher en cascade les autres api.onchange qui filtreraient # sur le contexte self.env.context = self.with_context(olive_onchange=True).env.context # astuce trouvée sur https://github.com/odoo/odoo/issues/7472 return res # si je n'ai ni warning ni domain, je n'ai pas besoin de faire un return # La fonction de calcul du champ function price_subtotal @api.one # auto-loop decorator @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity', 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id') # @api.depends est utilisé pour: invalidation cache, recalcul, onchange # donc, maintenant, le fait d'avoir un champ calculé fait qu'il est # automatiquement mis à jour dans la vue quand un de ses champs 'depends' # est modifié ! COOOOOL ! # ATTENTION : si chgt de @api.depends, faire -u module ! # Pour un one2many : ne PAS juste indiquer le nom du champ o2m, sinon il ne fait rien # il faut aussi indiquer un champ sur le O2M. Exemple : 'line_ids.request_id' # Apparemment, on peut mettre dans @api.depends un champ fonction stocké et ça va bien # faire le recalcul en cascade # (ça n'a pas l'air de marcher qd on met un api.depends sur un champ non stocké) def _compute_price(self): price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id) self.price_subtotal = taxes['total'] # calcul et stockage de la valeur self.second_field = 'iuit' # calcul et stockage d'un 2e champ # equivalent de multi='pouet' # Pour un champ O2M ou M2M, envoyer un recordset multiple ou une liste d'IDS # pour un champ M2O, donner le recordset ou l'ID if self.invoice_id: self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal) # Pas besoin de return ! # on ne peut PAS faire un self.write({}) dans la fonction de calcul d'un champ fonction # Pour un champ fonction, on peut aussi faire @api.multi: # untaxed = fields.Float(compute='_amounts') # taxes = fields.Float(compute='_amounts') # total = fields.Float(compute='_amounts') @api.multi @api.depends('lines.amount', 'lines.taxes') def _amounts(self): for order in self: order.untaxed = sum(line.amount for line in order.lines) order.taxes = sum(line.taxes for line in order.lines) order.total = order.untaxed + order + taxes # Champ fonction inverse='_inverse_price' @api.onchange('name') # add @api.onchange on an inverse method to have it apply immediately and not upon save def _inverse_loud(self): for rec in self: rec.name = (rec.loud or '').lower() # MAJ du ou des autres champs # Champ fonction search='_search_price' def _search_loud(self, operator, value): if value is not False: value = value.lower() today = fields.Date.context_today(self) self._cr.execute('SELECT id FROM [cur_obj] WHERE (fortress_type <> %s OR (fortress_type = %s AND effectivity_date is not null)) AND (end_date is null OR end_date > %s)', (today, )) res_ids = [x[0] for x in self._cr.fetchall()] res = [('id', 'in', res_ids)] # recherche sur les autres champs return res # Fonction default=_default_account @api.model def _default_account(self): return valeur_par_defaut # M2O : retourne un recordset ou un ID (ou False) # (NOTE: apparemment, en v8, il veut un ID) # OUTDATED (?) : ATTENTION, si on veut un M2O à False, il ne pas que la fonction # _default_account retourne False mais self.env['..'].browse(False) # O2M : retourne une liste de dict contenant la valeur des champs # M2M : retourne un recordset multiple ? # date : string ou objet datetime # Fonction pour fields.selection @api.model def _type_list_get(self): return [('key1', _('String1')), ('key2', _('String2'))] ### CHAMPS # id, create_uid, write_uid, create_date et write_date # sont déjà utilisable dans le code python sans re-définition active = fields.Boolean(default=True) # Par défaut, string = nom du champ avec majuscule pour chaque début de mot login = fields.Char( string='Login', size=16, translate=True, required=True, help="My help message") display_name = fields.Char( string='Display Name', compute='_compute_display_name', readonly=True, store=True) comment = fields.Text(string='Comment', translate=True) html = fields.Html(string='report', translate=True) code_digits = fields.Integer( string='# of Digits', track_visibility='onchange', default=12, groups='base.group_user') # OU groups=['base.group_user', 'base.group_hr_manager'] # groups = XMLID : restriction du read/write et invisible ds les vues ET EXPORT # v13: track_visibility='onchange' => tracking=X sequence = fields.Integer(default=10) # track_visibility = always ou onchange amount_untaxed = fields.Float( 'Amount untaxed', digits='Product Unit of Measure', group_operator="avg") # Utile pour un pourcentage par exemple # v13 : digits='Product Unit of Measure' # v12- : digits=dp.get_precision('Account') # digits=(precision, scale) exemple (16, 2) # Scale est le nombre de chiffres après la virgule # quand le float est un fields.float ou un fields.function, # on met l'option : digits=dp.get_precision('Account') # Autres valeurs possibles pour get_precision : product/product_data.xml # Product Price, Discount, Stock Weight, Product Unit of Measure, # Product UoS (v8 only) # fields.Monetary is only in version >= 9.0 debit = fields.Monetary(default=0.0, currency_field='company_currency_id') start_date = fields.Date( string='Start Date', copy=False, default=fields.Date.context_today, index=True) # similaire : fields.Datetime and fields.Time start_datetime = fields.Datetime( string='Start Date and Time', default=fields.Datetime.now) # index=True => the field will be indexed in the database # (much faster when you search on that field) type = fields.Selection([ ('import', 'Import'), ('export', 'Export'), ], string="Type", default=lambda self: self._context.get('type', 'export')) # FIELDS.SELECTION ac selection dynamique : # type = fields.Selection('_type_list_get', string='Type', help='Pouet'), # Plus besoin de la double fonction pour que la 2e soit héritable # Pour ajouter des champs à un fields.Selection existant: # fields.Selection( # selection_add=[('new_key1', 'My new key1'), ('new_key2', 'My New Key2')]) # v14 : ondelete={"new_key1": "set default"} # other possible options for ondelete: set null, cascade (delete the records !) # Pour afficher la valeur 'lisible' du champ selection (v12+): # rec._fields['type'].convert_to_export(rec.type, rec) picture = fields.Binary(string='Picture', attachment=True) # Pour fields.binary, il existe une option filters='*.png, *.gif', # qui restreint les formats de fichiers sélectionnables dans # la boite de dialogue, mais ça ne marche pas en GTK (on # ne peut rien sélectionner) et c'est pas supporté en Web, cf # https://bugs.launchpad.net/openobject-server/+bug/1076895 picture_filename = fields.Char(string='Filename') # Les champs "picture" et "picture_filename" sont liés ensemble dans la vue # via la balise filename="picture_filename" sur le champ 'picture' # Il faut que le champ 'picture_filename' soit présent dans la vue # (il peut être invisible) # Pour un fichier à télécharger d'Odoo, le nom du fichier aura la valeur de # picture_filename # Pour un fichier à uploader dans Odoo, 'picture_filename' vaudra le nom # du fichier uploadé par l'utilisateur # Exemple de champ fonction stocké price_subtotal = fields.Float( string='Amount', digits= dp.get_precision('Account'), store=True, readonly=True, compute='_compute_price') # Exemple de champ function non stocké avec fonction inverse loud = fields.Char( store=False, compute='_compute_loud', inverse='_inverse_loud', search='_search_loud') account_id = fields.Many2one('account.account', string='Account', required=True, domain=[('type', 'not in', ['view', 'closed'])], default=lambda self: self._default_account(), check_company=True) # L'utilisation de lambda permet d'hériter la fonction _default_account() sans # hériter le champ. Sinon, on peut aussi utiliser default=_default_account # Possibilité d'hériter un domaine: # domain=lambda self: [('reconcile', '=', True), ('user_type_id.id', '=', self.env.ref('account.data_account_type_current_assets').id), ('deprecated', '=', False)] company_id = fields.Many2one( 'res.company', string='Company', ondelete='cascade', required=True, default=lambda self: self.env['res.company']._company_default_get() default=lambda self: self.env.company) # v13 # si on veut que tous les args soient nommés : comodel_name='res.company' user_id = fields.Many2one( 'res.users', string='Salesman', default=lambda self: self.env.user) # ATTENTION : si j'ai déjà un domaine sur la vue, # c'est le domaine sur la vue qui prime ! # ondelete='cascade' : # le fait de supprimer la company va supprimer l'objet courant ! # ondelete='set null' (default) # si on supprime la company, le champ company_id est mis à 0 # ondelete='restrict' : # si on supprime la company, ça déclanche une erreur d'intégrité ! # Champ Relation company_currency_id = fields.Many2one( 'res.currency', string='Currency', related='company_id.currency_id', store=True) # option related_sudo=True by default # ATTENTION, en nouvelle API, on ne peut PAS faire un fields.Char qui # soit un related d'un fields.Selection (bloque le démarrage d'Odoo # sans message d'erreur !) line_ids = fields.One2many( 'product.code.line', 'parent_id', string='Product lines', states={'done': [('readonly', True)]}, copy=True) # OU comodel_name='product.code.line', inverse_name='parent_id' # 2e arg = nom du champ sur l'objet destination qui est le M20 inverse # en v8 : # copy=True pour que les lignes soient copiées lors d'un duplicate # sinon, mettre copy=False (ça ne peut être qu'un booléen) # Valeur par défaut du paramètre "copy": True for normal fields, False for # one2many and computed fields, including property fields and related fields # ATTENTION : pour que states={} marche sur le champ A et que le # champ A est dans la vue tree, alors il faut que le champ "state" # soit aussi dans la vue tree. partner_ids = fields.Many2many( 'res.partner', 'product_code_partner_rel', 'code_id', 'partner_id', 'Related Partners') # 2e arg = nom de la table relation # 3e arg ou column1 = nom de la colonne dans la table relation # pour stocker l'ID du product.code # 4e arg ou column2 = nom de la colonne dans la table relation # pour stocker l'ID du res.partner # OU partner_ids = fields.Many2many( 'res.partner', column1='code_id', column2='partner_id', string='Related Partners') # OU partner_ids = fields.Many2many( 'res.partner', string='Related Partners') # Pour les 2 dernières définitions du M2M, il ne faut pas avoir # plusieurs champs M2M qui pointent du même obj vers le même obj # Champ property: il suffit de définit le champ comme un champ normal # et d'ajouter un argument company_dependent=True # Quand on veut lire la valeur d'un champ property dans une société # qui n'est pas celle de l'utilisateur, il faut passer dans le context # 'force_company': 8 (8 = ID de la company) }
class ChangeProject(models.Model): _description = "Change Project" _name = 'change.project' _inherit = ['mail.activity.mixin', 'mail.thread'] _order = 'id desc' @api.model def _needaction_domain_get(self): return [('state', '!=', 'resuelto')] name = fields.Char('Código', default="Nuevo", copy=False) title = fields.Char('Título', default="Reunión") obs = fields.Text('Observación') obs_solucion = fields.Text('Observación Solución') entry_date = fields.Datetime('Fecha de Entrada', default=fields.Datetime.now) end_date = fields.Datetime('Fecha de Salida') end_will_end = fields.Datetime('Fecha Prevista') user_id = fields.Many2one('res.users', string='Creado', default=lambda self: self.env.user) notas = fields.Char('Notas') category_id = fields.Many2one('ticket.category', string='Categoría') ticket_id = fields.Many2one('ticket.pro', string='Ticket Soporte') total_horas = fields.Float('Horas', compute='_total_price_sum') total_price = fields.Float('Precio Total', compute='_total_price_sum') api.depends('hours_ids') def _total_price_sum(self): suma = 0 suma_horas = 0 for move in self: for line in move.hours_ids: suma += line.total_price suma_horas += line.cant_horas move.total_price = suma move.total_horas = suma_horas user_error_id = fields.Many2one('res.users', string='Usuario', default=lambda self: self.env.user) comprobante_01_name = fields.Char("Adjunto") comprobante_01 = fields.Binary(string='Adjunto', copy=False, help='Adjunto') company_id = fields.Many2one( 'res.company', string="Compañia", required=True, default=lambda self: self.env.user.company_id.id) state = fields.Selection([('borrador', 'Borrador'), ('aprobado', 'Aprobado'), ('trabajando', 'Trabajando'), ('resuelto', 'Resuelto'), ('calificado', 'Calificado')], string='Estatus', index=True, readonly=True, default='borrador', copy=False) calificacion = fields.Selection([('0', 'Malo'), ('1', 'Regular'), ('2', 'Bueno'), ('3', 'Excelente')], string='Calificación', default='0', copy=False) obs_calificacion = fields.Text('Nota Calificación') prioridad = fields.Selection([('baja', 'Baja'), ('media', 'Media'), ('alta', 'Alta')], default='baja', copy=False) def exe_autorizar_2(self): for record in self: record.state = 'aprobado' record.message_post(body=_("Ticket Aprobado por: %s") % record.env.user.name) def exe_work_2(self): for record in self: record.user_work_id = record.env.user record.state = 'trabajando' record.message_post(body=_("Iniciando el trabajo: %s") % record.env.user.name) def exe_resuelto_2(self): for record in self: record.user_work_id = record.env.user record.state = 'resuelto' record.message_post(body=_("Nota Solución: %s") % record.obs_solucion) record.end_date = fields.Datetime.now() def exe_abrir_2(self): for record in self: # record.numero_veces = record.numero_veces + 1 record.state = 'borrador' record.message_post(body=_("Se Abre de nuevo: %s") % record.env.user.name) def exe_close_2(self): if self.calificacion == '0': raise ValidationError( "Por favor califica nuestro trabajo así mejoramos con tu ayuda, muchas gracias." ) for record in self: record.state = 'calificado' record.message_post(body=_("Calificado como: %s") % record.calificacion) @api.model def create(self, vals): if vals.get('name', "Nuevo") == "Nuevo": vals['name'] = self.env['ir.sequence'].next_by_code( 'change.project') or "Nuevo" ticket = super(ChangeProject, self).create(vals) template = self.env.ref('ticket_pro.email_change_project') if ticket.comprobante_01: attachment = self.env['ir.attachment'].create({ 'name': ticket.comprobante_01_name, 'datas': ticket.comprobante_01, 'datas_fname': ticket.comprobante_01_name, 'res_model': 'change.project', 'type': 'binary' }) template.attachment_ids = [(6, 0, attachment.ids)] mail = template.send_mail(ticket.id, force_send=True) # envia mail if mail: ticket.message_post(body=_("Enviado email al Cliente: %s" % ticket.category_id.name)) return ticket hours_ids = fields.One2many('hours.task', 'cambios_id', string='Listado Horas')
class npa_expenses(models.Model): _name = 'npa.expense_details_hdr' _description = 'Apty Expense details table' _order = 'name' # Disable the DUPLICATE button def copy(self): raise ValidationError("Sorry you are unable to duplicate records") # Perform a soft delete def unlink(self): for rec in self: rec.active = False # Returns age of staff api.depends('to_date', 'from_date') def _compute_days(self): for rec in self: if rec.from_date and rec.to_date: dur = rec.to_date - rec.from_date rec.age = dur.days + 1 # Method to payment wizard def create_service_payment(self): mod_obj = self.env['ir.model.data'] form_res = mod_obj.get_object_reference('apty_acctmgmt', 'view_shop_payment_form')[1] return { 'name': "Create Payment", 'view_type': 'form', 'view_mode': "[form]", 'view_id': form_res, 'res_model': 'npa.shop_payment_amt', 'type': 'ir.actions.act_window', 'views': [(form_res, 'form')], 'target': 'new', 'context': { 'default_service_id': self.id } } # Method to post purchase request def get_service_request_wizard(self): try: form_id = self.env['ir.model.data'].get_object_reference( 'apty_acctmgmt', 'post_service_request_wizard')[1] except ValueError: form_id = False raise Warning( _("Cannot locate required 'post_service_request_wizard'. Please contact IT Support" )) return { 'name': "Post Service Transaction", 'view_type': 'form', 'view_mode': "[form]", 'view_id': False, 'res_model': 'npa.common_wizard', 'type': 'ir.actions.act_window', 'target': 'new', 'views': [(form_id, 'form')], } @api.model def create(self, vals): if not vals: vals = {} vals['name'] = self.env['ir.sequence'].\ next_by_code('npa.expense_details_hdr') or 'New' return super(npa_expenses, self).create(vals) # Calculate total transaction and total payment amount @api.depends('expense_ids.service_amt', 'payment_ids.payment_amt') def _compute_total_amt(self): total_amt = 0.0 total_payment = 0.0 for record in self: for line in record.expense_ids: total_amt += line.service_amt for line in record.payment_ids: total_payment += line.payment_amt print('****** payment total', total_payment) record.total_service_amt = total_amt record.payment_amt = total_payment record.amount_residual = total_amt - total_payment # field name = fields.Char(size=80, string='Service Order', readonly=True, required=True, copy=False, default='New') service_name = fields.Char(size=80, string='Service Name') from_date = fields.Date(string="From date/time") to_date = fields.Date(string="To date/time") age = fields.Integer(string='No. Of Days', readonly=True, store=False) service_desc = fields.Text(string="Service Description") active = fields.Boolean(string='Active', default=True) expense_ids = fields.One2many(comodel_name='npa.expense_details_sumry', inverse_name='service_id', string='Services') payment_ids = fields.One2many(comodel_name='npa.shop_payment_amt', inverse_name='service_id', string='Payment Amount') date_posted = fields.Date(string='Posted Date') posted_by = fields.Many2one(comodel_name='res.users', string='Posted by', ondelete='restrict') total_service_amt = fields.Float(string='Total Amount', digits=(5, 2), compute='_compute_total_amt', store=True) payment_amt = fields.Float(string='Amount Paid', digits=(5, 2), compute='_compute_total_amt', store=True) amount_residual = fields.Float(string='Amount Due', digits=(5, 2), compute='_compute_total_amt', store=True) state = fields.Selection([ ('New', 'New'), ('Posted', 'Posted'), ('Cancel', 'Cancel'), ], string='State', default='New')
class Session(models.Model): _name = 'academico.session' _description = "Acadêmico Sessions" name = fields.Char(required=True) start_date = fields.Date(default=fields.Date.today) duration = fields.Float(digits=(6, 2), help="Duration in days") seats = fields.Integer(string="Number of seats") active = fields.Boolean(default=True) color = fields.Integer() instructor_id = fields.Many2one('res.partner', string="Instructor", domain=['|', ('instructor', '=', True), ('category_id.name', 'ilike', "Teacher")]) course_id = fields.Many2one('academico.course', ondelete='cascade', string="Course", required=True) attendee_ids = fields.Many2many('res.partner', string="Attendees") taken_seats = fields.Float(string="Taken seats", compute='_taken_seats') end_date = fields.Date(string="End Date", store=True, compute='_get_end_date', inverse='_set_end_date') attendees_count = fields.Integer( string="Attendees count", compute='_get_attendees_count', store=True) @api.depends('seats', 'attendee_ids') def _taken_seats(self): for r in self: if not r.seats: r.taken_seats = 0.0 else: r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats api.depends('start_date', 'duration') def _get_end_date(self): for r in self: if not (r.start_date and r.duration): r.end_date = r.start_date continue # Add duration to start_date, but: Monday + 5 days = Saturday, so # subtract one second to get on Friday instead duration = timedelta(days=r.duration, seconds=-1) r.end_date = r.start_date + duration def _set_end_date(self): for r in self: if not (r.start_date and r.end_date): continue # Compute the difference between dates, but: Friday - Monday = 4 days, # so add one day to get 5 days instead r.duration = (r.end_date - r.start_date).days + 1 @api.onchange('seats', 'attendee_ids') def _verify_valid_seats(self): if self.seats < 0: return { 'warning': { 'title': "Incorrect 'seats' value", 'message': "The number of available seats may not be negative", }, } if self.seats < len(self.attendee_ids): return { 'warning': { 'title': "Too many attendees", 'message': "Increase seats or remove excess attendees", }, } @api.depends('attendee_ids') def _get_attendees_count(self): for r in self: r.attendees_count = len(r.attendee_ids) @api.constrains('instructor_id', 'attendee_ids') def _check_instructor_not_in_attendees(self): for r in self: if r.instructor_id and r.instructor_id in r.attendee_ids: raise exceptions.ValidationError("A session's instructor can't be an attendee")
def _onchange_partner(self): if self.partner_id: self.delivery_id = self.partner_id # MAJ d'un autre champ # M2M : 2 possibilités : # - liste d'IDs, mais ça va AJOUTER les ids, comme (4, [IDs]) # - [(6, 0, [IDs])], ce qui va remplacer les ids # (cf module product_category_tax dans akretion/odoo-usability) # On utilise un autre M2M pour le mettre à jour, on peut faire # self.champ_M2M_ids.ids -> ça donne la liste des IDs # M2O : recordset (ou ID) # O2M : exemple v10 dans purchase/models/account_invoice.py # méthode purchase_order_change() # là, Odoo va jouer automatiquement le @api.onchange du champ delivery_id # pas besoin d'appeler le onchange de delivery_id dans notre code # Here, all form values are set on self # assigned values are not written to DB, but returned to the client # It is not possible to output a warning # It is not possible to put a raise UserError() # in this function (it will crash odoo) res = {'warning': {'title': _('Be careful'), {'message': _('here is the msg')}} # pour un domaine res = {'domain': { 'champ1': "[('product_id', '=', product_id)]", 'champ2': "[]"}, } return res # si je n'ai ni warning ni domain, je n'ai pas besoin de faire un return # Fonction on_change déclarée dans la vue form/tree @api.multi def product_id_change(self, cr, uid, ids, champ1, champ2, context): # ATTENTION : a priori, on ne doit pas utiliser ids dans le code de la # fonction, car quand on fait un on_change avant le save, ids = [] # Dans la vue XML : # <field name="product_id" # on_change="product_id_change(champ1, champ2, context)" /> # Piège : quand un champ float est passé dans un on_change, # si la personne avait tapé un entier, il va être passé en argument en # tant que integer et non en tant que float! raise orm.except_orm() # => il ne remet PAS l'ancienne valeur qui a déclanché le on_change # Pour mettre à jour des valeurs : return {'value': {'champ1': updated_value1, 'champ2': updated_value2}} # => à savoir : les onchange de 'champ1' et 'champ2' sont joués à # leur tour car leur valeur a été changée # si ces nouveaux on_change changent le product_id, # le product_id_change ne sera pas rejoué # Pour mettre un domaine : return {'domain': { 'champ1': "[('product_id', '=', product_id)]", 'champ2': "[]"}, } # l'intégralité du domaine est dans une string # Pour retourner un message de warning : return {'warning': { 'title': _('Le titre du msg de warn'), 'message': _("Ce que j'ai à te dire %s") % (text)}} # Pour ne rien faire return False # return True, ça marche en 7.0 mais ça bug en 6.1 # La fonction de calcul du champ function price_subtotal @api.one # auto-loop decorator @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity', 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id') # @api.depends est utilisé pour: invalidation cache, recalcul, onchange # donc, maintenant, le fait d'avoir un champ calculé fait qu'il est # automatiquement mis à jour dans la vue quand un de ses champs 'depends' # est modifié ! COOOOOL ! # ATTENTION : si chgt de @api.depends, faire -u module ! # Pour un one2many : ne PAS juste indiquer le nom du champ o2m, sinon il ne fait rien # il faut aussi indiquer un champ sur le O2M. Exemple : 'line_ids.request_id' # Apparemment, on peut mettre dans @api.depends un champ fonction stocké et ça va bien # faire le recalcul en cascade # (ça n'a pas l'air de marcher qd on met un api.depends sur un champ non stocké) def _compute_price(self): price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id) self.price_subtotal = taxes['total'] # calcul et stockage de la valeur self.second_field = 'iuit' # calcul et stockage d'un 2e champ # equivalent de multi='pouet' # Pour un champ O2M ou M2M, envoyer un recordset multiple ou une liste d'IDS # pour un champ M2O, donner le recordset ou l'ID if self.invoice_id: self.price_subtotal = self.invoice_id.currency_id.round(self.price_subtotal) # Pas besoin de return ! # on ne peut PAS faire un self.write({}) dans la fonction de calcul d'un champ fonction # Pour un champ fonction, on peut aussi faire @api.multi: # untaxed = fields.Float(compute='_amounts') # taxes = fields.Float(compute='_amounts') # total = fields.Float(compute='_amounts') @api.multi @api.depends('lines.amount', 'lines.taxes') def _amounts(self): for order in self: order.untaxed = sum(line.amount for line in order.lines) order.taxes = sum(line.taxes for line in order.lines) order.total = order.untaxed + order + taxes # Champ fonction inverse='_inverse_price' @api.one def _inverse_loud(self): self.name = (self.loud or '').lower() # MAJ du ou des autres champs # Champ fonction search='_search_price' def _search_loud(self, operator, value): if value is not False: value = value.lower() today = fields.Date.context_today(self) self._cr.execute('SELECT id FROM [cur_obj] WHERE (fortress_type <> %s OR (fortress_type = %s AND effectivity_date is not null)) AND (end_date is null OR end_date > %s)', (today, )) res_ids = [x[0] for x in self._cr.fetchall()] res = [('id', 'in', res_ids)] # recherche sur les autres champs return res # Fonction default=_default_account @api.model def _default_account(self): return valeur_par_defaut # M2O : retourne un recordset ou un ID (ou False) # (NOTE: apparemment, en v8, il veut un ID) # OUTDATED (?) : ATTENTION, si on veut un M2O à False, il ne pas que la fonction # _default_account retourne False mais self.env['..'].browse(False) # O2M : retourne une liste de dict contenant la valeur des champs # M2M : retourne un recordset multiple ? # date : string ou objet datetime # Fonction pour fields.selection @api.model def _type_list_get(self): return [('key1', _('String1')), ('key2', _('String2'))] ### CHAMPS # id, create_uid, write_uid, create_date et write_date # sont déjà utilisable dans le code python sans re-définition active = fields.Boolean(default=True) # Par défaut, string = nom du champ avec majuscule pour chaque début de mot login = fields.Char( string='Login', size=16, translate=True, required=True, help="My help message") display_name = fields.Char( string='Display Name', compute='_compute_display_name', readonly=True, store=True) comment = fields.Text(string='Comment', translate=True) html = fields.Html(string='report', translate=True) code_digits = fields.Integer( string='# of Digits', track_visibility='onchange', default=12, groups='base.group_user') # OU groups=['base.group_user', 'base.group_hr_manager'] # groups = XMLID : restriction du read/write et invisible ds les vues sequence = fields.Integer(default=10) # track_visibility = always ou onchange amount_untaxed = fields.Float( 'Amount untaxed', digits=dp.get_precision('Account'), group_operator="avg") # Utile pour un pourcentage par exemple # digits=(precision, scale) exemple (16, 2) # Scale est le nombre de chiffres après la virgule # quand le float est un fields.float ou un fields.function, # on met l'option : digits=dp.get_precision('Account') # Autres valeurs possibles pour get_precision : product/product_data.xml # Product Price, Discount, Stock Weight, Product Unit of Measure, # Product UoS (v8 only) # fields.Monetary is only in version >= 9.0 debit = fields.Monetary(default=0.0, currency_field='company_currency_id') start_date = fields.Date( string='Start Date', copy=False, default=fields.Date.context_today, index=True) # similaire : fields.Datetime and fields.Time # index=True => the field will be indexed in the database # (much faster when you search on that field) type = fields.Selection([ ('import', 'Import'), ('export', 'Export'), ], string="Type", default=lambda self: self._context.get('type', 'export')) # FIELDS.SELECTION ac selection dynamique : # type = fields.Selection('_type_list_get', string='Type', help='Pouet'), # Plus besoin de la double fonction pour que la 2e soit héritable # Pour ajouter des champs à un fields.Selection existant: # fields.Selection( # selection_add=[('new_key1', 'My new key1'), ('new_key2', 'My New Key2')]) picture = fields.Binary(string='Picture') # Pour fields.binary, il existe une option filters='*.png, *.gif', # qui restreint les formats de fichiers sélectionnables dans # la boite de dialogue, mais ça ne marche pas en GTK (on # ne peut rien sélectionner) et c'est pas supporté en Web, cf # https://bugs.launchpad.net/openobject-server/+bug/1076895 picture_filename = fields.Char(string='Filename') # Les champs "picture" et "picture_filename" sont liés ensemble dans la vue # via la balise filename="picture_filename" sur le champ 'picture' # Il faut que le champ 'picture_filename' soit présent dans la vue # (il peut être invisible) # Pour un fichier à télécharger d'Odoo, le nom du fichier aura la valeur de # picture_filename # Pour un fichier à uploader dans Odoo, 'picture_filename' vaudra le nom # du fichier uploadé par l'utilisateur # Exemple de champ fonction stocké price_subtotal = fields.Float( string='Amount', digits= dp.get_precision('Account'), store=True, readonly=True, compute='_compute_price') # Exemple de champ function non stocké avec fonction inverse loud = fields.Char( store=False, compute='_compute_loud', inverse='_inverse_loud', search='_search_loud') account_id = fields.Many2one('account.account', string='Account', required=True, domain=[('type', 'not in', ['view', 'closed'])], default=lambda self: self._default_account()) # L'utilisation de lambda permet d'hériter la fonction _default_account() sans # hériter le champ. Sinon, on peut aussi utiliser default=_default_account company_id = fields.Many2one( 'res.company', string='Company', ondelete='cascade', required=True, default=lambda self: self.env['res.company']._company_default_get( 'product.code')) # si on veut que tous les args soient nommés : comodel_name='res.company' user_id = fields.Many2one( 'res.users', string='Salesman', default=lambda self: self.env.user) # ATTENTION : si j'ai déjà un domaine sur la vue, # c'est le domaine sur la vue qui prime ! # ondelete='cascade' : # le fait de supprimer la company va supprimer l'objet courant ! # ondelete='set null' (default) # si on supprime la company, le champ company_id est mis à 0 # ondelete='restrict' : # si on supprime la company, ça déclanche une erreur d'intégrité ! # Champ Relation company_currency_id = fields.Many2one( 'res.currency', string='Currency', related='company_id.currency_id', store=True, compute_sudo=True) # ATTENTION, en nouvelle API, on ne peut PAS faire un fields.Char qui # soit un related d'un fields.Selection (bloque le démarrage d'Odoo # sans message d'erreur !) line_ids = fields.One2many( 'product.code.line', 'parent_id', string='Product lines', states={'done': [('readonly', True)]}, copy=True) # OU comodel_name='product.code.line', inverse_name='parent_id' # 2e arg = nom du champ sur l'objet destination qui est le M20 inverse # en v8 : # copy=True pour que les lignes soient copiées lors d'un duplicate # sinon, mettre copy=False (ça ne peut être qu'un booléen) # Valeur par défaut du paramètre "copy": True for normal fields, False for # one2many and computed fields, including property fields and related fields # ATTENTION : pour que states={} marche sur le champ A et que le # champ A est dans la vue tree, alors il faut que le champ "state" # soit aussi dans la vue tree. partner_ids = fields.Many2many( 'res.partner', 'product_code_partner_rel', 'code_id', 'partner_id', 'Related Partners') # 2e arg = nom de la table relation # 3e arg ou id1 = nom de la colonne dans la table relation # pour stocker l'ID du product.code # 4e arg ou id2 = nom de la colonne dans la table relation # pour stocker l'ID du res.partner # OU partner_ids = fields.Many2many( 'res.partner', column1='code_id', column2='partner_id', string='Related Partners') # OU partner_ids = fields.Many2many( 'res.partner', string='Related Partners') # Pour les 2 dernières définitions du M2M, il ne faut pas avoir # plusieurs champs M2M qui pointent du même obj vers le même obj # Champ property: il suffit de définit le champ comme un champ normal # et d'ajouter un argument company_dependent=True # Quand on veut lire la valeur d'un champ property dans une société # qui n'est pas celle de l'utilisateur, il faut passer dans le context # 'force_company': 8 (8 = ID de la company) }