Odoo 11: How to correctly implement #api.onchange? - odoo

I want to compute a telephone number, if the number in the 8 position is either "0" or "1" I want to print just the last 4 numbers with a "(3)" before them, otherwise just print the 4 numbers, but what is happening is that my code is printing "0.0" and I don't know why, I'll appreciate your help...
This is my python code:
class Employee(models.Model):
_inherit = "hr.employee"
marcado_rapido = fields.Float("MarcadoRapido",compute='_compute_marcado_rapido')
#api.onchange("emp.marcado_rapido")
def onchange_compute_marcado_rapido(self):
for num in self:
num = "809-257-1457"
if num[8] in ('0','1'):
"(3)"+num[8:]
This is my xml code:
<td>
<t t-foreach="env['hr.employee'].search([('department_id', '=', dep.id)])" t-as="emp">
<div class="contact_form">
<img t-if="emp.image" t-att-src="'data:image/png;base64,%s' % to_text(emp.image)"/>
<div class="bloqueP">
<div class="bloque" t-field="emp.marcado_rapido"/>
</div>
</div>
</t>
</td>

#onchange only supports simple field names, dotted names (fields of relational fields e.g. partner_id.tz) are not supported and will be ignored
You can check the official documentation on how the onchange decorator works and what are the limitations.
0.0 is the default value for float fields and the value of marcado_rapido is computed using _compute_marcado_rapido function. If the field updated in onchange method depends on marcado_rapido field value, you can compute its value using the same method

You should use compute decoration instead of onchange, but compute method aproach always depend in someone else field. My sugestion is use a another computed field, something like this:
class Employee(models.Model):
_inherit = 'hr.employee'
# If your number contains special characters(like '-') you should use `Char` insted of `float`
num_telefono = fields.Char('Num. Telefono')
marcado_rapido = fields.Char('MarcadoRapido', compute='_compute_marcado_rapido')
#api.depends('num_telefono')
def _compute_marcado_rapido(self):
for rec in self:
num = rec.num_telefono[-4:]
rec.marcado_rapido = '(3){}'.format(num) if num[:1] in ('0','1') else num
Now you can call marcado_rapido from your XML.
I hope this answer can be helful for you.

Related

Angular 8 update text field in response to another field change

The image above represents a project I'm working on. Of the 3 fields, only purchase price data is manually entered. I use the following markup and TS code to set the Outstanding mortgage field to a percentage of the previously provided figure:
<input type="number" class="form-control" min='0' id="purchaseValueInput" formControlName="purchase_value" (ngModelChange)='setPercentages()'>
...
setPercentages() {
this.mortgage = this.analysisForm.value.market.purchase_value * 0.75;
}
My challenge is I need to do a similar thing for the mortgage payments field but as a percentage of the outstanding mortgage value. Because that value is not manually provided the ngModelChange strategy is not working.
How can I resolve that last step?
Update
In my setPercentages() function, I have attempted to set a variable for mortgage payments but I get the following error:
The specified value "NaN" cannot be parsed, or is out of range.
I suspect it's because the field is regarded as empty even though visually it has data in it. I used the following code:
setPercentages() {
this.mortgage = this.analysisForm.value.market.purchase_value * 0.75;
this.mortgagePayments = (this.analysisForm.value.market.outstanding_mortgage * 0.03) / 12;
}
<input type="number" class="form-control" min='0' id="mortgagePaymentValueInput" formControlName="mortgage_payments" [value]="mortgagePayments">
You could try to bind the mortgage payments to a member variable and in the setPercentages function of the other input, you also update the model via that variable.
If ngModelChange doesn't work for some reason you can try to set the value attribute of that input:
<input type="text" value="$300"></input>
I was assigning my percentage values to variables and interpolation to assign the variable as the value of my input field. That proved to the the wrong approach.
What does work is using form patchValue as shown below:
this.analysisForm.patchValue({
market: {
outstanding_mortgage: this.analysisForm.value.market.purchase_value * 0.75,
}
});

Does BeautifulSoup can locate the element basing on contained text? [duplicate]

Observe the following problem:
import re
from bs4 import BeautifulSoup as BS
soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
Edit
</a>
""")
# This returns the <a> element
soup.find(
'a',
href="/customer-menu/1/accounts/1/update",
text=re.compile(".*Edit.*")
)
soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
<i class="fa fa-edit"></i> Edit
</a>
""")
# This returns None
soup.find(
'a',
href="/customer-menu/1/accounts/1/update",
text=re.compile(".*Edit.*")
)
For some reason, BeautifulSoup will not match the text, when the <i> tag is there as well. Finding the tag and showing its text produces
>>> a2 = soup.find(
'a',
href="/customer-menu/1/accounts/1/update"
)
>>> print(repr(a2.text))
'\n Edit\n'
Right. According to the Docs, soup uses the match function of the regular expression, not the search function. So I need to provide the DOTALL flag:
pattern = re.compile('.*Edit.*')
pattern.match('\n Edit\n') # Returns None
pattern = re.compile('.*Edit.*', flags=re.DOTALL)
pattern.match('\n Edit\n') # Returns MatchObject
Alright. Looks good. Let's try it with soup
soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
<i class="fa fa-edit"></i> Edit
</a>
""")
soup.find(
'a',
href="/customer-menu/1/accounts/1/update",
text=re.compile(".*Edit.*", flags=re.DOTALL)
) # Still return None... Why?!
Edit
My solution based on geckons answer: I implemented these helpers:
import re
MATCH_ALL = r'.*'
def like(string):
"""
Return a compiled regular expression that matches the given
string with any prefix and postfix, e.g. if string = "hello",
the returned regex matches r".*hello.*"
"""
string_ = string
if not isinstance(string_, str):
string_ = str(string_)
regex = MATCH_ALL + re.escape(string_) + MATCH_ALL
return re.compile(regex, flags=re.DOTALL)
def find_by_text(soup, text, tag, **kwargs):
"""
Find the tag in soup that matches all provided kwargs, and contains the
text.
If no match is found, return None.
If more than one match is found, raise ValueError.
"""
elements = soup.find_all(tag, **kwargs)
matches = []
for element in elements:
if element.find(text=like(text)):
matches.append(element)
if len(matches) > 1:
raise ValueError("Too many matches:\n" + "\n".join(matches))
elif len(matches) == 0:
return None
else:
return matches[0]
Now, when I want to find the element above, I just run find_by_text(soup, 'Edit', 'a', href='/customer-menu/1/accounts/1/update')
The problem is that your <a> tag with the <i> tag inside, doesn't have the string attribute you expect it to have. First let's take a look at what text="" argument for find() does.
NOTE: The text argument is an old name, since BeautifulSoup 4.4.0 it's called string.
From the docs:
Although string is for finding strings, you can combine it with
arguments that find tags: Beautiful Soup will find all tags whose
.string matches your value for string. This code finds the tags
whose .string is “Elsie”:
soup.find_all("a", string="Elsie")
# [Elsie]
Now let's take a look what Tag's string attribute is (from the docs again):
If a tag has only one child, and that child is a NavigableString, the
child is made available as .string:
title_tag.string
# u'The Dormouse's story'
(...)
If a tag contains more than one thing, then it’s not clear what
.string should refer to, so .string is defined to be None:
print(soup.html.string)
# None
This is exactly your case. Your <a> tag contains a text and <i> tag. Therefore, the find gets None when trying to search for a string and thus it can't match.
How to solve this?
Maybe there is a better solution but I would probably go with something like this:
import re
from bs4 import BeautifulSoup as BS
soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
<i class="fa fa-edit"></i> Edit
</a>
""")
links = soup.find_all('a', href="/customer-menu/1/accounts/1/update")
for link in links:
if link.find(text=re.compile("Edit")):
thelink = link
break
print(thelink)
I think there are not too many links pointing to /customer-menu/1/accounts/1/update so it should be fast enough.
in one line using lambda
soup.find(lambda tag:tag.name=="a" and "Edit" in tag.text)
You can pass a function that return True if a text contains "Edit" to .find
In [51]: def Edit_in_text(tag):
....: return tag.name == 'a' and 'Edit' in tag.text
....:
In [52]: soup.find(Edit_in_text, href="/customer-menu/1/accounts/1/update")
Out[52]:
<a href="/customer-menu/1/accounts/1/update">
<i class="fa fa-edit"></i> Edit
</a>
EDIT:
You can use the .get_text() method instead of the text in your function which gives the same result:
def Edit_in_text(tag):
return tag.name == 'a' and 'Edit' in tag.get_text()
With soupsieve 2.1.0 you can use :-soup-contains css pseudo class selector to target a node's text. This replaces the deprecated form of :contains().
from bs4 import BeautifulSoup as BS
soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
Edit
</a>
""")
single = soup.select_one('a:-soup-contains("Edit")').text.strip()
multiple = [i.text.strip() for i in soup.select('a:-soup-contains("Edit")')]
print(single, '\n', multiple)
Method - 1: Checking text property
pattern = 'Edit'
a2 = soup.find_all('a', string = pattern)[0]
Method - 2: Using lambda iterate through all elements
a2 = soup.find(lambda tag:tag.name=="a" and "Edit" in tag.text)
Good Luck

Odoo 12 num2words Amount To Text

I'm new in this community and I setting up Odoo Community version for my little comapny. I does all the things just doesn't know how to set up num2words to show Total Amount in Invoice Reports!
I found num2words api in res_currency.py in Base/Modules but I spend two days researching how to connect and nothing. What I have to inherite and how and also what to put in invoice document qweb?
I made module like this:
from num2words import num2words
class account_invoice(models.Model):
_inherit = "account.invoice"
#api.multi
def amount_to_text(self, amount):
self.ensure_one()
def _num2words(number, lang):
try:
return num2words(number, lang=lang).title()
except NotImplementedError:
return num2words(number, lang='en').title()
if num2words is None:
logging.getLogger(__name__).warning("The library 'num2words' is missing, cannot render textual amounts.")
return ""
formatted = "%.{0}f".format(self.decimal_places) % amount
parts = formatted.partition('.')
integer_value = int(parts[0])
fractional_value = int(parts[2] or 0)
lang_code = self.env.context.get('lang') or self.env.user.lang
lang = self.env['res.lang'].search([('code', '=', lang_code)])
amount_words = tools.ustr('{amt_value} {amt_word}').format(
amt_value=_num2words(integer_value, lang=lang.iso_code),
amt_word=self.currency_unit_label,
)
if not self.is_zero(amount - integer_value):
amount_words += ' ' + _('and') + tools.ustr(' {amt_value} {amt_word}').format(
amt_value=_num2words(fractional_value, lang=lang.iso_code),
amt_word=self.currency_subunit_label,
)
return amount_words
Got error like this:
Error to render compiling AST
AttributeError: 'NoneType' object has no attribute 'currency_id'
Template: account.report_invoice_document_with_payments
Path: /templates/t/t/div/p[1]/span
Node: <span t-if="doc.currency_id" t-esc="doc.currency_id.amount_to_text(doc.amount_total)"/>
In QWeb I put this:
<span t-if="doc.currency_id" t-esc="doc.currency_id.amount_to_text(doc.amount_total)"/>
Thank you in advance!
In your case "currency_id" is a Many2one field. The 'res.currency' model does not contain the class 'amount_to_text' function.
You have written amount_to_text function in 'account.invoice' model. So change your like this,
<span t-if="doc.currency_id" t-esc="doc.amount_to_text(doc.amount_total)"/>
OR(if you don't have currency_id field in your object)
<span t-if="doc.amount_to_text" t-esc="doc.amount_to_text(doc.amount_total)"/>
Please use the below code
<span t-if="o.currency_id" t-esc="o.amount_to_text(o.amount_total)"/>
You are getting the error because in the base report they are using o instead of doc. please see the below part of code from base.
<t t-foreach="docs" t-as="o">
So try to use o instead of doc

Empy list on return

I have this in my qweb report
<span t-esc="formatLang(get_routing_data(o)[-1]['total'] , digits=3)"/>
it works ok, but sometimes it returns an empty list and then i get error index tuple out of range. how can i avoid it?
You could set the return value of the call to get_routing_data into a variable and make check the value using t-if conditions before use it, like:
<t t-set="routing_data" t-value="get_routing_data(o)"/>
<span t-if="routing_data and len(routing_data) > 0 and routing_data[-1].get('total', False)" t-esc="formatLang(routing_data[-1]['total'], digits=3)"/>

Is it possible to change the value of a selection field dynamically in Odoo 10?

I would like to have my selections depend on the value of a Char field, for instance, a Char field defined as such:
my_char = fields.Char("Enter Something", readonly = False)
so I suppose the selection field should call a function, something like "_get_value"
my_selection = fields.Selection(selection = ' _get_value')
#api.model
def _get_value(self):
my_list = [('key1','value1')]
#no idea how to assign the value of my_char to value1
return my_list
Eventually, I would like to have the selections in the drop down list vary as the user input different strings in my_char.
Is this achievable in Odoo? Because if it's not, I should better start reorganizing my structure. Thanks a lot.
As far is i know, it isn't possible with field type Selection. But you can use a Many2one field for such a behaviour.
class MySelectionModel(model.Models):
_name = "my.selection.model"
name = fields.Char()
class MyModel(models.Model):
_name = "my.model"
my_char = fields.Char()
my_selection_id = fields.Many2one(
comodel_name="my.selection.model", string="My Selection")
#api.onchange("my_char")
def onchange_my_char(self):
return {'domain': {'my_selection_id': [('name', 'ilike', self.my_char)]}}
Or without a onchange method:
my_selection_id = fields.Many2one(
comodel_name="my.selection.model", string="My Selection",
domain="[('name', 'ilike', my_char)]")
To let the Many2one field look like a selection, add the widget="selection" on that field in the form view.
How the domain should look like, should be decided by you. Here it is just an example.
No need to write method here. Just declare the dictionary to a variable and call it in selection field.
VAR_LIST = [('a','ABC'),
('p','PQR'),
('x','XYZ')]
my_selection = fields.Selection(string="Field Name",VAR_LIST)