[PATCH] Upstream patch - 13112022

This commit is contained in:
Parthiv Patel
2022-11-13 08:34:47 +00:00
parent f8c302e615
commit 1d22b7b048
23 changed files with 460 additions and 131 deletions
+10 -2
View File
@@ -4,6 +4,7 @@ from flectra import api, fields, models, _
from flectra.exceptions import RedirectWarning, UserError, ValidationError, AccessError
from flectra.tools import float_compare, date_utils, email_split, email_re, float_is_zero
from flectra.tools.misc import formatLang, format_date, get_lang
from flectra.osv import expression
from datetime import date, timedelta
from collections import defaultdict
@@ -3874,6 +3875,13 @@ class AccountMoveLine(models.Model):
# Add the domain and order by in order to compute the cumulated balance in _compute_cumulated_balance
return super(AccountMoveLine, self.with_context(domain_cumulated_balance=to_tuple(domain or []), order_cumulated_balance=order)).search_read(domain, fields, offset, limit, order)
@api.model
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields, attributes)
if res.get('cumulated_balance'):
res['cumulated_balance']['exportable'] = False
return res
@api.depends_context('order_cumulated_balance', 'domain_cumulated_balance')
def _compute_cumulated_balance(self):
if not self.env.context.get('order_cumulated_balance'):
@@ -4381,11 +4389,11 @@ class AccountMoveLine(models.Model):
@api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
if operator == 'ilike':
args = ['|', '|',
domain = ['|', '|',
('name', 'ilike', name),
('move_id', 'ilike', name),
('product_id', 'ilike', name)]
return self._search(args, limit=limit, access_rights_uid=name_get_uid)
return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)
return super()._name_search(name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
@@ -293,7 +293,7 @@ class AccountPartialReconcile(models.Model):
'tax_repartition_line_id': tax_line.tax_repartition_line_id.id,
'tax_ids': [(6, 0, tax_line.tax_ids.ids)],
'tax_tag_ids': [(6, 0, tax_line._convert_tags_for_cash_basis(tax_line.tax_tag_ids).ids)],
'account_id': tax_line.tax_repartition_line_id.account_id.id or tax_line.account_id.id,
'account_id': tax_line.tax_repartition_line_id.account_id.id or tax_line.company_id.account_cash_basis_base_account_id.id or tax_line.account_id.id,
'amount_currency': amount_currency,
'currency_id': tax_line.currency_id.id,
'partner_id': tax_line.partner_id.id,
@@ -2833,3 +2833,103 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
._create_payments()
bill.button_draft()
def test_cash_basis_taxline_without_account(self):
"""
Make sure that cash basis taxlines that don't have an account are handled properly.
"""
self.env.company.tax_exigibility = True
tax = self.env['account.tax'].create({
'name': 'cash basis 20%',
'type_tax_use': 'purchase',
'amount': 20,
'tax_exigibility': 'on_payment',
'cash_basis_transition_account_id': self.cash_basis_transfer_account.id,
'invoice_repartition_line_ids': [
(0, 0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
(0, 0, {
'factor_percent': 40,
'account_id': self.tax_account_1.id,
'repartition_type': 'tax',
}),
(0, 0, {
'factor_percent': 60,
'repartition_type': 'tax',
}),
],
'refund_repartition_line_ids': [
(0, 0, {
'factor_percent': 100,
'repartition_type': 'base',
}),
(0, 0, {
'factor_percent': 40,
'account_id': self.tax_account_1.id,
'repartition_type': 'tax',
}),
(0, 0, {
'factor_percent': 60,
'repartition_type': 'tax',
}),
],
})
# create invoice
move_form = Form(self.env['account.move'].with_context(
default_move_type='in_invoice'))
move_form.partner_id = self.partner_a
move_form.invoice_date = fields.Date.from_string('2017-01-01')
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.product_a
line_form.tax_ids.clear()
line_form.tax_ids.add(tax)
invoice = move_form.save()
invoice.action_post()
# make payment
self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
'payment_date': invoice.date,
})._create_payments()
# check caba move
partial_rec = invoice.mapped('line_ids.matched_debit_ids')
caba_move = self.env['account.move'].search(
[('tax_cash_basis_rec_id', '=', partial_rec.id)])
expected_values = [
{
'account_id': self.cash_basis_base_account.id,
'debit': 0.0,
'credit': 800.0
},
{
'account_id': self.cash_basis_base_account.id,
'debit': 800.0,
'credit': 0.0
},
{
'account_id': self.cash_basis_transfer_account.id,
'debit': 0.0,
'credit': 64.0
},
{
'account_id': self.tax_account_1.id,
'debit': 64.0,
'credit': 0.0},
{
'account_id': self.cash_basis_transfer_account.id,
'debit': 0.0,
'credit': 96.0
},
{
'account_id': self.cash_basis_base_account.id,
'debit': 96.0,
'credit': 0.0
}
]
self.assertRecordValues(caba_move.line_ids, expected_values)
@@ -490,13 +490,9 @@ class AccountEdiFormat(models.Model):
res = False
try:
if file_data['type'] == 'xml':
res = edi_format\
.with_context(default_move_type=invoice.move_type)\
._update_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], invoice)
res = edi_format._update_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], invoice)
elif file_data['type'] == 'pdf':
res = edi_format\
.with_context(default_move_type=invoice.move_type)\
._update_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], invoice)
res = edi_format._update_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], invoice)
file_data['pdf_reader'].stream.close()
else: # file_data['type'] == 'binary'
res = edi_format._update_invoice_from_binary(file_data['filename'], file_data['content'], file_data['extension'], invoice)
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import fields, models
from flectra import api, fields, models
class ResCountry(models.Model):
@@ -16,3 +16,8 @@ class ResCountry(models.Model):
"\n%(street_number)s: the house number"
"\n%(street_number2)s: the door number",
default='%(street_number)s/%(street_number2)s %(street_name)s', required=True)
@api.onchange("street_format")
def onchange_street_format(self):
# Prevent unexpected truncation with whitespaces in front of the street format
self.street_format = self.street_format.strip()
+2 -1
View File
@@ -99,6 +99,7 @@ class ImDispatch(object):
def __init__(self):
self.channels = {}
self.started = False
self.Event = None
def poll(self, dbname, channels, last, options=None, timeout=TIMEOUT):
if options is None:
@@ -181,7 +182,7 @@ class ImDispatch(object):
def start(self):
if flectra.evented:
# gevent mode
import gevent
import gevent.event # pylint: disable=import-outside-toplevel
self.Event = gevent.event.Event
gevent.spawn(self.run)
else:
+1 -1
View File
@@ -742,7 +742,7 @@ class Meeting(models.Model):
if values.get('activity_ids'):
continue
res_model_id = values.get('res_model_id', defaults.get('res_model_id'))
res_id = values.get('res_id', defaults.get('res_id'))
values['res_id'] = res_id = values.get('res_id') or defaults.get('res_id')
user_id = values.get('user_id', defaults.get('user_id'))
if not res_model_id or not res_id:
continue
@@ -128,6 +128,6 @@ class EventRegistration(models.Model):
res.update({
'payment_status': self.payment_status,
'payment_status_value': dict(self._fields['payment_status']._description_selection(self.env))[self.payment_status],
'has_to_pay': not self.is_paid,
'has_to_pay': self.payment_status == 'to_pay',
})
return res
+6 -1
View File
@@ -528,8 +528,13 @@ class IrHttp(models.AbstractModel):
raise Exception("Rerouting limit exceeded")
request.httprequest.environ['PATH_INFO'] = path
# void werkzeug cached_property. TODO: find a proper way to do this
for key in ('path', 'full_path', 'url', 'base_url'):
for key in ('full_path', 'url', 'base_url'):
request.httprequest.__dict__.pop(key, None)
# since werkzeug 2.0 `path`` became an attribute and is not a cached property anymore
if hasattr(type(request.httprequest), 'path'): # cached property
request.httprequest.__dict__.pop('path', None)
else: # direct attribute
request.httprequest.path = '/' + path.lstrip('/')
return cls._dispatch()
@@ -3,7 +3,8 @@
import re
from werkzeug import urls, utils
from html import unescape
from werkzeug import urls
from flectra import api, models, tools
@@ -40,7 +41,7 @@ class MailRenderMixin(models.AbstractModel):
label = (match[3] or '').strip()
if not blacklist or not [s for s in blacklist if s in long_url] and not long_url.startswith(short_schema):
create_vals = dict(link_tracker_vals, url=utils.unescape(long_url), label=utils.unescape(label))
create_vals = dict(link_tracker_vals, url=unescape(long_url), label=unescape(label))
link = self.env['link.tracker'].create(create_vals)
if link.short_url:
new_href = href.replace(long_url, link.short_url)
@@ -69,7 +70,7 @@ class MailRenderMixin(models.AbstractModel):
if blacklist and any(item in parsed.path for item in blacklist):
continue
create_vals = dict(link_tracker_vals, url= utils.unescape(original_url))
create_vals = dict(link_tracker_vals, url=unescape(original_url))
link = self.env['link.tracker'].create(create_vals)
if link.short_url:
content = content.replace(original_url, link.short_url, 1)
@@ -4,7 +4,8 @@
from datetime import date, timedelta
import requests
import werkzeug
from html import unescape
from flectra import models, api, service
from flectra.tools.translate import _
@@ -69,7 +70,7 @@ class MercuryTransaction(models.Model):
try:
r = requests.post(url, data=xml_transaction, headers=headers, timeout=65)
r.raise_for_status()
response = werkzeug.utils.unescape(r.content.decode())
response = unescape(r.content.decode())
except Exception:
response = "timeout"
+1 -1
View File
@@ -19,7 +19,7 @@ class StockMove(models.Model):
purchase_line_id = fields.Many2one('purchase.order.line',
'Purchase Order Line', ondelete='set null', index=True, readonly=True)
created_purchase_line_id = fields.Many2one('purchase.order.line',
'Created Purchase Order Line', ondelete='set null', readonly=True, copy=False)
'Created Purchase Order Line', ondelete='set null', readonly=True, copy=False, index=True)
@api.model
def _prepare_merge_moves_distinct_fields(self):
@@ -2233,46 +2233,3 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
self.assertEqual(price, 10)
def test_kit_cost_calculation(self):
""" Check that the average cost price is computed correctly after SO confirmation:
BOM 1:
- 1 unit of “super kit”:
- 2 units of “component a”
BOM 2:
- 1 unit of “component a”:
- 3 units of "component b"
1 unit of "component b" = $10
1 unit of "super kit" = 2 * 3 * $10 = *$60
"""
super_kit = self._cls_create_product('Super Kit', self.uom_unit)
(super_kit + self.component_a + self.component_b).categ_id.property_cost_method = 'average'
self.env['mrp.bom'].create({
'product_tmpl_id': self.component_a.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom',
'bom_line_ids': [(0, 0, {
'product_id': self.component_b.id,
'product_qty': 3.0,
})]
})
self.env['mrp.bom'].create({
'product_tmpl_id': super_kit.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom',
'bom_line_ids': [(0, 0, {
'product_id': self.component_a.id,
'product_qty': 2.0,
})]
})
self.component_b.standard_price = 10
self.component_a.button_bom_cost()
super_kit.button_bom_cost()
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.partner_a
with so_form.order_line.new() as line:
line.product_id = super_kit
so = so_form.save()
self.assertEqual(so.order_line.purchase_price, 60)
so.action_confirm()
self.assertEqual(so.order_line.purchase_price, 60)
+22 -4
View File
@@ -33,11 +33,23 @@ class AccountMove(models.Model):
all_invoices_amls = current_invoice_amls.sale_line_ids.invoice_lines.filtered(lambda aml: aml.move_id.state == 'posted').sorted(lambda aml: (aml.date, aml.move_name, aml.id))
index = all_invoices_amls.ids.index(current_invoice_amls[:1].id) if current_invoice_amls[:1] in all_invoices_amls else 0
previous_amls = all_invoices_amls[:index]
previous_qties_invoiced = previous_amls._get_invoiced_qty_per_product()
invoiced_qties = current_invoice_amls._get_invoiced_qty_per_product()
invoiced_products = invoiced_qties.keys()
if self.move_type == 'out_invoice':
# filter out the invoices that have been fully refund and re-invoice otherwise, the quantities would be
# consumed by the reversed invoice and won't be print on the new draft invoice
previous_amls = previous_amls.filtered(lambda aml: aml.move_id.payment_state != 'reversed')
previous_qties_invoiced = previous_amls._get_invoiced_qty_per_product()
if self.move_type == 'out_refund':
# we swap the sign because it's a refund, and it would print negative number otherwise
for p in previous_qties_invoiced:
previous_qties_invoiced[p] = -previous_qties_invoiced[p]
for p in invoiced_qties:
invoiced_qties[p] = -invoiced_qties[p]
qties_per_lot = defaultdict(float)
previous_qties_delivered = defaultdict(float)
stock_move_lines = current_invoice_amls.sale_line_ids.move_ids.move_line_ids.filtered(lambda sml: sml.state == 'done' and sml.lot_id).sorted(lambda sml: (sml.date, sml.id))
@@ -48,7 +60,13 @@ class AccountMove(models.Model):
product_uom = product.uom_id
qty_done = sml.product_uom_id._compute_quantity(sml.qty_done, product_uom)
if sml.location_id.usage == 'customer':
# is it a stock return considering the document type (should it be it thought of as positively or negatively?)
is_stock_return = (
self.move_type == 'out_invoice' and (sml.location_id.usage, sml.location_dest_id.usage) == ('customer', 'internal')
or
self.move_type == 'out_refund' and (sml.location_id.usage, sml.location_dest_id.usage) == ('internal', 'customer')
)
if is_stock_return:
returned_qty = min(qties_per_lot[sml.lot_id], qty_done)
qties_per_lot[sml.lot_id] -= returned_qty
qty_done = returned_qty - qty_done
@@ -60,7 +78,7 @@ class AccountMove(models.Model):
# try to reach the previous_qty_invoiced
if float_compare(qty_done, 0, precision_rounding=product_uom.rounding) < 0 or \
float_compare(previous_qty_delivered, previous_qty_invoiced, precision_rounding=product_uom.rounding) < 0:
previously_done = qty_done if sml.location_id.usage == 'customer' else min(previous_qty_invoiced - previous_qty_delivered, qty_done)
previously_done = qty_done if is_stock_return else min(previous_qty_invoiced - previous_qty_delivered, qty_done)
previous_qties_delivered[product] += previously_done
qty_done -= previously_done
+108
View File
@@ -40,6 +40,8 @@ class TestSaleStockInvoices(TestSaleCommon):
'product_id': self.product_by_usn.id,
'company_id': self.env.company.id,
})
self.usn01 = usn01
self.usn02 = usn02
self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 10, lot_id=lot)
self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=usn01)
self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=usn02)
@@ -281,3 +283,109 @@ class TestSaleStockInvoices(TestSaleCommon):
self.assertRegex(text, r'Product By Lot\n6.00\nUnits\nLOT0002', "There should be a line that specifies 6 x LOT0002")
self.assertRegex(text, r'Product By Lot\n2.00\nUnits\nLOT0003', "There should be a line that specifies 2 x LOT0003")
self.assertNotIn('LOT0001', text)
def test_refund_cancel_invoices(self):
"""
Suppose the lots are printed on the invoices.
The user sells 2 tracked-by-usn products, he delivers 2 products and invoices them
Then he adds credit notes and issues a full refund. Receive the products.
The reversed invoice should also have correct USN
"""
report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
display_lots = self.env.ref('sale_stock.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 2}),
],
})
so.action_confirm()
picking = so.picking_ids
picking.move_lines.move_line_ids[0].qty_done = 1
picking.move_lines.move_line_ids[1].qty_done = 1
picking.button_validate()
invoice01 = so._create_invoices()
invoice01.action_post()
html = report._render_qweb_html(invoice01.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001")
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0002', "There should be a line that specifies 1 x USN0002")
# Refund the invoice
refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=[invoice01.id]).create({
'refund_method': 'cancel',
})
refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
# recieve the returned product
stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(active_ids=picking.ids, active_id=picking.sorted().ids[0], active_model='stock.picking'))
return_wiz = stock_return_picking_form.save()
res = return_wiz.create_returns()
pick_return = self.env['stock.picking'].browse(res['res_id'])
move_form = Form(pick_return.move_lines, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
line.lot_id = self.usn01
line.qty_done = 1
with move_form.move_line_nosuggest_ids.new() as line:
line.lot_id = self.usn02
line.qty_done = 1
move_form.save()
pick_return.button_validate()
# reversed invoice
html = report._render_qweb_html(refund_invoice.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001")
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0002', "There should be a line that specifies 1 x USN0002")
def test_refund_modify_invoices(self):
"""
Suppose the lots are printed on the invoices.
The user sells 1 tracked-by-usn products, he delivers 1 and invoices it
Then he adds credit notes and issues full refund and new draft invoice.
The new draft invoice should have correct USN
"""
report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
display_lots = self.env.ref('sale_stock.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 1}),
],
})
so.action_confirm()
picking = so.picking_ids
picking.move_lines.move_line_ids[0].qty_done = 1
picking.button_validate()
invoice01 = so._create_invoices()
invoice01.action_post()
html = report._render_qweb_html(invoice01.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001")
# Refund the invoice with full refund and new draft invoice
refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=[invoice01.id]).create({
'refund_method': 'modify',
})
invoice02 = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
invoice02.action_post()
# new draft invoice
html = report._render_qweb_html(invoice02.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001")
+8 -2
View File
@@ -762,11 +762,17 @@ class ProductTemplate(models.Model):
if 'type' in vals and vals['type'] != 'product' and sum(self.mapped('nbr_reordering_rules')) != 0:
raise UserError(_('You still have some active reordering rules on this product. Please archive or delete them first.'))
if any('type' in vals and vals['type'] != prod_tmpl.type for prod_tmpl in self):
existing_move_lines = self.env['stock.move.line'].search([
existing_done_move_lines = self.env['stock.move.line'].search([
('product_id', 'in', self.mapped('product_variant_ids').ids),
('state', '=', 'done'),
], limit=1)
if existing_done_move_lines:
raise UserError(_("You can not change the type of a product that was already used."))
existing_reserved_move_lines = self.env['stock.move.line'].search([
('product_id', 'in', self.mapped('product_variant_ids').ids),
('state', 'in', ['partially_available', 'assigned']),
])
if existing_move_lines:
if existing_reserved_move_lines:
raise UserError(_("You can not change the type of a product that is currently reserved on a stock move. If you need to change the type, you should first unreserve the stock move."))
if 'type' in vals and vals['type'] != 'product' and any(p.type == 'product' and not float_is_zero(p.qty_available, precision_rounding=p.uom_id.rounding) for p in self):
raise UserError(_("Available quantity should be set to zero before changing type"))
@@ -12,7 +12,7 @@ class StockInventory(models.Model):
help="Date at which the accounting entries will be created"
" in case of automated inventory valuation."
" If empty, the inventory date will be used.")
has_account_moves = fields.Boolean(compute='_compute_has_account_moves')
has_account_moves = fields.Boolean(compute='_compute_has_account_moves', compute_sudo=True)
def _compute_has_account_moves(self):
for inventory in self:
+1 -2
View File
@@ -1099,10 +1099,9 @@ class Proxy(http.Controller):
if not data:
raise werkzeug.exceptions.BadRequest()
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse
base_url = request.httprequest.base_url
query_string = request.httprequest.query_string
client = Client(http.root, BaseResponse)
client = Client(http.root, werkzeug.wrappers.Response)
headers = {'X-Openerp-Session-Id': request.session.sid}
return client.post('/' + path, base_url=base_url, query_string=query_string,
headers=headers, data=data)
+1 -1
View File
@@ -129,7 +129,7 @@ class Website(Home):
"""
if not redirect and request.params.get('login_success'):
if request.env['res.users'].browse(uid).has_group('base.group_user'):
redirect = b'/web?' + request.httprequest.query_string
redirect = '/web?' + request.httprequest.query_string.decode()
else:
redirect = '/my'
return super()._login_redirect(uid, redirect=redirect)
@@ -48,22 +48,46 @@ weSnippetEditor.Class.include({
* if not already defined.
*
* @private
* @param {boolean} [reconfigure=false]
* @param {boolean} [onlyIfUndefined=false]
* @param {boolean} [reconfigure=false] // TODO name is confusing "alwaysReconfigure" is better
* @param {boolean} [onlyIfUndefined=false] // TODO name is confusing "configureIfNecessary" is better
*/
async _configureGMapAPI({reconfigure, onlyIfUndefined}) {
if (!reconfigure && !onlyIfUndefined) {
return false;
}
const apiKey = await new Promise(resolve => {
this.getParent().trigger_up('gmap_api_key_request', {
onSuccess: key => resolve(key),
});
});
if (!reconfigure && (apiKey || !onlyIfUndefined)) {
const apiKeyValidation = apiKey ? await this._validateGMapAPIKey(apiKey) : {
isValid: false,
message: undefined,
};
if (!reconfigure && onlyIfUndefined && apiKey && apiKeyValidation.isValid) {
return false;
}
let websiteId;
this.trigger_up('context_get', {
callback: ctx => websiteId = ctx['website_id'],
});
function applyError(message) {
const $apiKeyInput = this.find('#api_key_input');
const $apiKeyHelp = this.find('#api_key_help');
$apiKeyInput.addClass('is-invalid');
$apiKeyHelp.empty().text(message);
}
const $content = $(qweb.render('website.s_google_map_modal', {
apiKey: apiKey,
}));
if (!apiKeyValidation.isValid && apiKeyValidation.message) {
applyError.call($content, apiKeyValidation.message);
}
return new Promise(resolve => {
let invalidated = false;
const dialog = new Dialog(this, {
@@ -71,55 +95,57 @@ weSnippetEditor.Class.include({
title: _t("Google Map API Key"),
buttons: [
{text: _t("Save"), classes: 'btn-primary', click: async (ev) => {
const $apiKeyInput = dialog.$('#api_key_input');
const valueAPIKey = $apiKeyInput.val();
const $apiKeyHelp = dialog.$('#api_key_help');
const valueAPIKey = dialog.$('#api_key_input').val();
if (!valueAPIKey) {
$apiKeyInput.addClass('is-invalid');
$apiKeyHelp.text(_t("Enter an API Key"));
applyError.call(dialog.$el, _t("Enter an API Key"));
return;
}
const $button = $(ev.currentTarget);
$button.prop('disabled', true);
try {
const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${valueAPIKey}`);
if (response.status === 200) {
await this._rpc({
model: 'website',
method: 'write',
args: [
[websiteId],
{google_maps_api_key: valueAPIKey},
],
});
invalidated = true;
dialog.close();
} else {
const text = await response.text();
$apiKeyInput.addClass('is-invalid');
$apiKeyHelp.empty().text(
_t("Invalid API Key. The following error was returned by Google:")
).append($('<i/>', {
text: text,
class: 'ml-1',
}));
}
} catch (e) {
$apiKeyHelp.text(_t("Check your connection and try again"));
} finally {
$button.prop("disabled", false);
const res = await this._validateGMapAPIKey(valueAPIKey);
if (res.isValid) {
await this._rpc({
model: 'website',
method: 'write',
args: [
[websiteId],
{google_maps_api_key: valueAPIKey},
],
});
invalidated = true;
dialog.close();
} else {
applyError.call(dialog.$el, res.message);
}
$button.prop("disabled", false);
}},
{text: _t("Cancel"), close: true}
],
$content: $(qweb.render('website.s_google_map_modal', {
apiKey: apiKey,
})),
$content: $content,
});
dialog.on('closed', this, () => resolve(invalidated));
dialog.open();
});
},
/**
* @private
*/
async _validateGMapAPIKey(key) {
try {
const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${key}`);
const isValid = (response.status === 200);
return {
isValid: isValid,
message: !isValid &&
_t("Invalid API Key. The following error was returned by Google:") + " " + (await response.text()),
};
} catch (err) {
return {
isValid: false,
message: _t("Check your connection and try again"),
};
}
},
/**
* @override
*/
@@ -279,7 +279,7 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({
});
const GPSPicker = InputUserValueWidget.extend({
events: { // Explicitely not consider all InputUserValueWidget events
events: { // Explicitly not consider all InputUserValueWidget events
'blur input': '_onInputBlur',
},
@@ -299,10 +299,20 @@ const GPSPicker = InputUserValueWidget.extend({
this.trigger_up('gmap_api_request', {
editableMode: true,
configureIfNecessary: true,
onSuccess: key => resolve(!!key),
onSuccess: key => {
if (!key) {
resolve(false);
return;
}
// TODO see _notifyGMapError, this tries to trigger an error
// early but this is not consistent with new gmap keys.
this._nearbySearch('(50.854975,4.3753899)', !!key)
.then(place => resolve(!!place));
},
});
});
if (!this._gmapLoaded) {
if (!this._gmapLoaded && !this._gmapErrorNotified) {
this.trigger_up('user_value_widget_critical');
return;
}
@@ -336,17 +346,36 @@ const GPSPicker = InputUserValueWidget.extend({
*/
async setValue() {
await this._super(...arguments);
if (!this._gmapLoaded) {
return;
}
await new Promise(resolve => {
const gps = this._value;
if (this._gmapCacheGPSToPlace[gps]) {
this._gmapPlace = this._gmapCacheGPSToPlace[gps];
resolve();
return;
}
this._gmapPlace = await this._nearbySearch(this._value);
if (this._gmapPlace) {
this.inputEl.value = this._gmapPlace.formatted_address;
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {string} gps
* @param {boolean} [notify=true]
* @returns {Promise}
*/
async _nearbySearch(gps, notify = true) {
if (this._gmapCacheGPSToPlace[gps]) {
return this._gmapCacheGPSToPlace[gps];
}
const p = gps.substring(1).slice(0, -1).split(',');
const location = new google.maps.LatLng(p[0] || 0, p[1] || 0);
return new Promise(resolve => {
const service = new google.maps.places.PlacesService(document.createElement('div'));
const p = gps.substring(1).slice(0, -1).split(',');
const location = new google.maps.LatLng(p[0] || 0, p[1] || 0);
service.nearbySearch({
// Do a 'nearbySearch' followed by 'getDetails' to avoid using
// GMap Geocoder which the user may not have enabled... but
@@ -361,23 +390,59 @@ const GPSPicker = InputUserValueWidget.extend({
placeId: results[0].place_id,
fields: ['geometry', 'formatted_address'],
}, (place, status) => {
resolve();
if (status === google.maps.places.PlacesServiceStatus.OK) {
this._gmapCacheGPSToPlace[gps] = place;
this._gmapPlace = place;
resolve(place);
} else if (GMAP_CRITICAL_ERRORS.includes(status)) {
this.trigger_up('user_value_widget_critical');
if (notify) {
this._notifyGMapError();
}
resolve();
}
});
} else if (GMAP_CRITICAL_ERRORS.includes(status)) {
if (notify) {
this._notifyGMapError();
}
resolve();
} else {
resolve();
this.trigger_up('user_value_widget_critical');
}
});
});
if (this._gmapPlace) {
this.inputEl.value = this._gmapPlace.formatted_address;
},
/**
* Indicates to the user there is an error with the google map API and
* re-opens the configuration dialog. For good measures, this also notifies
* a critical error which normally removes the related snippet entirely.
*
* @private
*/
_notifyGMapError() {
// TODO this should be better to detect all errors. This is random.
// When misconfigured (wrong APIs enabled), sometimes Google throw
// errors immediately (which then reaches this code), sometimes it
// throws them later (which then induces an error log in the console
// and random behaviors).
if (this._gmapErrorNotified) {
return;
}
this._gmapErrorNotified = true;
this.displayNotification({
type: 'danger',
sticky: true,
message: _t("A Google Map error occurred. Make sure to read the key configuration popup carefully."),
});
this.trigger_up('gmap_api_request', {
editableMode: true,
reconfigure: true,
onSuccess: () => {
this._gmapErrorNotified = false;
},
});
setTimeout(() => this.trigger_up('user_value_widget_critical'));
},
//--------------------------------------------------------------------------
@@ -393,11 +458,25 @@ const GPSPicker = InputUserValueWidget.extend({
if (gmapPlace && gmapPlace.geometry) {
this._gmapPlace = gmapPlace;
const location = this._gmapPlace.geometry.location;
const oldValue = this._value;
this._value = `(${location.lat()},${location.lng()})`;
this._gmapCacheGPSToPlace[this._value] = gmapPlace;
this._onUserValueChange(ev);
if (oldValue !== this._value) {
this._onUserValueChange(ev);
}
}
},
/**
* @override
*/
_onInputBlur() {
// As a stable fix: do not call the _super as we actually don't want
// input focusout messing with the google map API. Because of this,
// clicking on google map autocomplete suggestion on Firefox was not
// working properly. This is kept as an empty function because of stable
// policy (ensures custo can still extend this).
// TODO review in master.
},
});
options.userValueWidgetsRegistry['we-urlpicker'] = UrlPickerUserValueWidget;
@@ -66,7 +66,7 @@ publicWidget.registry.GoogleMap = publicWidget.Widget.extend({
map.setCenter(gps);
// Update Map on screen resize
google.maps.event.addDomListener(window, 'resize', () => {
window.addEventListener('resize', () => {
map.setCenter(gps);
});
@@ -143,6 +143,25 @@
Enable billing on your Google Project
</a>
</div>
<div class="alert alert-info mb-0 mt-3">
Make sure your settings are properly configured:
<ul class="mb-0">
<li>
Enable the right google map APIs in your google account
<ul>
<li>Maps Static API</li>
<li>Maps JavaScript API</li>
<li>Places API</li>
</ul>
</li>
<li>
Make sure billing is enabled
</li>
<li>
Make sure to wait if errors keep being shown: sometimes enabling an API allows to use it immediately but Google keeps triggering errors for a while
</li>
</ul>
</div>
</div>
</div>
</div>