Python Notes

Compare / Convert The Database Time (UTC0) With Logged in User Timezone

By default, time and date inside Odoo postgresql is saved in UTC0 format. If you are working with times in Odoo, it is always recommended to convert the date and times into user’s timezone first. After you made some manipulation on those times in computer memory, simply save it back into fields.Datetime(). it will also converted back automatically into the UTC0 as well.

The recommended flow to work on the date in a nutshell is : fetch a date from postgresql (use ORM is also fine) -> convert the date into current user timezone -> make some manipulation -> store it back to fields.Datetime() -> it will saved back into postgresql

This is the block example :

from odoo import fields

# Initializing the records and user timezones
api_time = record['updatedAt'] # Assuming this is the date and time, fetch from external API or casual text format
api_time_in_datetime_format = fields.Datetime.from_string(api_time) # Convert api_time into Datetime format using odoo fields import
user_timezone = self.env.user.tz # This will get current user timezone, accessible from .env

# convert from UTC0 (server timezone) to user timezone
api_time_in_user_timezone = fields.Datetime.context_timestamp(self.with_context(tz=user_timezone), timestamp=api_time_in_datetime_format)
api_time_converted_as_strftime= api_time_in_user_timezone.strftime("%Y-%m-%d %H:%M:%S")
api_time_converted_as_strptime = datetime.strptime(api_time_converted_as_string, "%Y-%m-%d %H:%M:%S")

Forcefully Write Debit-Credit On account.move.line

Using .with_context() will help to forcefully write debit-credit of journal entry without cancelling it.

journal_entry_line = self.env['account.move.line'].search([('ref', '=', str(self.name + " - " + " Vendor Exchange Out" + " - " + line.main_product.name))])
for record in journal_entry_line:
    if record.debit > 0:
        record.with_context(check_move_validity=False).write({'debit': line.main_product_cost * line.main_product_qty})
    if record.credit > 0:
        record.with_context(check_move_validity=False).write({'credit': line.main_product_cost * line.main_product_qty})

Set Current Date Using fields

define_a_date = fields.Date.today()

# or

define_a_date = fields.Date.context_today(self)

We can use js_assign_outstanding_line() to link invoice with payment

def register_payment(self):
    payment_data = {
        'payment_type': 'inbound',
        'partner_id': self.partner_id.id,
        'amount': order_pending['grandTotal'],
        'ref': 'Payment for Invoice %s' % invoice.name,
        'date': datetime.strptime(order_pending['createdAt'], '%Y-%m-%d %H:%M:%S').date(),
        'journal_id': rule.auto_workflow.payment_journal.id
    }

    payment = self.env['account.payment'].create(payment_data)

    # Post the payment
    payment.action_post()

    # Link payment with the invoice
    receivable_line = payment.line_ids.filtered('credit')
    invoice.js_assign_outstanding_line(receivable_line.id)

Passing Context From other_method() Into create() Method

This also can be applied to another method but to simplify stuff we will use create() method example instead first we inherit a create() method into the module

@api.model_create_multi
def create(self, vals):
    if vals.get('payroll_number', 'New') == 'New':
        vals['payroll_number'] = self.env['ir.sequence'].next_by_code('payroll.number.sequence') or 'New'
    result = super(CentralPayroll, self).create(vals)

    return result

now, in another part of the block, we have cron_auto_create_monthly_transfer_payroll(). We need to create context as dictionary to be passed on when create() is called

def cron_auto_create_monthly_transfer_payroll(self):
    context = {
        'is_via_cron': True
    }

    payroll = self.env['central.payroll'].with_context(context).create({ # Put the context during creation with .with_context(args)
        'employee_id': employee.id,
        'periode_bulan': str(current_month),
        'periode_tahun': self.env['central.configuration.year'].search([('year','=',current_year)]).id,
        'attendance_start': start_date,
        'attendance_finish': finish_date,
        'account_debit': self.env['account.journal'].search([('name','=','BCA 1910928887 (IDR)')]).id
    })

if we go back to the create(), we can fetch the context using self.env.context

@api.model_create_multi
def create(self, vals):
    if vals.get('payroll_number', 'New') == 'New':
        vals['payroll_number'] = self.env['ir.sequence'].next_by_code('payroll.number.sequence') or 'New'
    result = super(CentralPayroll, self).create(vals)

    context = self.env.context
    print("Context:", context) # This shall print out all context
    return result

Track Invoice Payment

@api.depends('amount_residual', 'move_type', 'state', 'company_id')
def _compute_payment_state(self):
    res = super(InvoiceInherit, self)._compute_payment_state()

    for rec in self:
        if rec.type_name == 'Invoice':
            print(rec.id)
            print(rec.payment_state)
    return res

Auto Create Sequence

Generate sequence when record is created

class RekapOrder(models.Model):
    _name = 'rekap.order'
    _description = 'Rekap Order Pengiriman'
    _rec_name = 'name'

    name = fields.Char(readonly=True, required=True, copy=False, default='New')
    @api.model_create_multi
    def create(self, vals):
        # Auto Assign record name
        if vals.get('name', 'New') == 'New':
            vals['name'] = self.env['ir.sequence'].with_company(self.company_id.id).next_by_code('rekap.order.sequence') or 'New'
        result = super(OrderSetoran, self).create(vals)

        return result

Set Domain Based on Company

This will fetch current logged in user by getting from env

account_stock_journal = fields.Many2one('account.journal', ondelete='restrict', domain=lambda self: self._get_company_domain())

@api.model
def _get_company_domain(self):
    return [('company_id', '=', self.env.company.id)]

@api.onchange('ticket_number')
def compute_tax(self):
    taxes = self.line_id.tax_id
    for rec in self:
        ticket_amount = rec.ticket_number.total_amount

        if taxes:
            tax_computation = taxes.compute_all(ticket_amount, currency=self.ticket_number.currency_id, quantity=1)
            tax_total = tax_computation['total_included'] - tax_computation['total_excluded']
        else:
            tax_total = 0

        rec.ticket_tax = tax_total

Download Record as PDF And Save it To ir.attachment


report_pdf = self.env["ir.actions.report"].sudo()._render_qweb_pdf(self.env.ref('crm_project_task.report_well_information_project'), res_ids=self.id)

filename = 'attachment' + '.pdf'
attachment = self.env['ir.attachment'].create({
    'name': filename,
    'type': 'binary',
    'datas': base64.b64encode(report_pdf[0]),
    'res_model': 'project.task',
    'res_id': self.id,
    'mimetype': 'application/x-pdf'
})


Launch Record To a New View

def launch_other_view(self):
    action = {
        'type': 'ir.actions.act_window',
        'name': 'Email Record',
        'res_model': 'email.record',
        'view_mode': 'form',
        'res_id': record.id,
        'target': 'current',
    }
    return action

Apply Filter Domain on Many2one

tunjangan_type = fields.Selection([
    ('bonus', 'Bonus/Tunjangan Karyawan'),
    ('potongan', 'Potongan'),
    
], string='Tipe Tunjangan',default='bonus', states={
    'draft': [('readonly', False)],
    'paid': [('readonly', True)],
    'cancel': [('readonly', True)],
})

@api.onchange('tunjangan_type')
def _compute_tunjangan_domain(self):
    for record in self:
        if record.tunjangan_type == 'bonus':
            return {'domain': {'account_tunjangan': ['&', '|', '|', '|', 
                                                        ('name', 'ilike', 'bonus'), 
                                                        ('name', 'ilike', 'pesangon'), 
                                                        ('name', 'ilike', 'lembur'), 
                                                        ('name', 'ilike', 'seragam'), 
                                                        ('user_type_id', '=', 'Expenses')]}}

One2many Guideline

Here’s the basic One2many guidelines

class MainModels(models.Model):
    _name = 'main.models'

    the_field = fields.One2many("comodels.of.main", "model_id")

class ComodelsOfMain(models.Model):
    _name = 'comodels.of.main'

    model_id = fields.Many2one('main.models')

Create Mail Activity (To-Do)

self.env['mail.activity'].create({
    'res_model_id': self.env['ir.model']._get('field.ticket.generator').id,
    'res_id': field_ticket.id,
    'summary': str(self.name) + str(" - Field Ticket Needs To Be Created"),
    'user_id': bookkeeper.id,
    'date_deadline': fields.Datetime.context_timestamp(self.with_context(tz=user_timezone), timestamp=today_in_string),
    'activity_type_id': 4,
})

Assign Existing Record into Many2many

previous_well_info = fields.Many2many('well.information', 'previous_well_info', 'uwi', 'uwi_id', string='Legumes', readonly=True)

def fetch_previous_uwi(self):
     previous_uwi = self.env['well.information'].search([('uwi', '=', self.uwi), ('id', '!=', self.id)])
     self.previous_well_info = [(5, 0, 0)]

     if bool(previous_uwi):
         self.previous_well_info = previous_uwi.ids

post_init_hook

For version 17.0, it doesnt need cr and registry as parameter anymore but rather using env as code below

# -*- coding: utf-8 -*-
from . import models
from odoo import api, SUPERUSER_ID


def post_init_hook(env):
    # env = api.Environment(env, SUPERUSER_ID, {})
    companies = env['res.company'].search([])

    for company in companies:
        env['default.service'].create({
            'company_id': company.id,
            'name': 'Default Service',
        })

Define Single Digits

psn_depth = fields.Float(string="PSN Depth", digits=(6, 1))

Create Pop Up Notification

from odoo import _

message = _("Connection Test Successful!")
return {
    'type': 'ir.actions.client',
    'tag': 'display_notification',
    'params': {
        'message': message,
        'type': 'success',
        'sticky': False,
    }
}

‘type’ can be changed to ‘success’, ‘warning’, ‘danger’, ‘info’

Changing Form String Name Title (In Python Ways)

You can Add ‘name’ keys to pass wizard

    def open_project_task_wizard(self):
        return {
            'name': ("Create Project Task"),
            'type': 'ir.actions.act_window',
            'res_model': 'crm.project.task.wizard',
            'view_mode': 'form',
            'target': 'new',
        }

Find Record in Database Based on XML ID

record_id = self.env.ref('module_name.xml_id_of_record').id 

Add New Selection Options

You can add new option into Odoo’s fields.Selection(selection_add=[]) like :

print_format = fields.Selection(selection_add=[
    ('zpl', 'ZPL Labels'),
    ('zplxprice', 'ZPL Labels with price')]
, ondelete={'zpl': 'set default', 'zplxprice': 'set default'})

If the print_format is defined somewhere in other module, you can just _inherit that module and make extension of the selection with the block above. it works.


Create Access Token For A Record

Access token will mostly be used to access certain record, basically to let external user to see a record through certain page (like invoice). In order to let your record has access token, you should make inheritance to your module like this example :

_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin', 'sequence.mixin']
    
access_token = fields.Char()

Everything should have been exactly like example above, so it will allows you to make an access token.


Define And Create Attachment

attachment_id = fields.Many2many('ir.attachment')
<field name="attachment_id" widget="many2many_binary"/>