mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 17062022
This commit is contained in:
@@ -144,7 +144,7 @@ class account_journal(models.Model):
|
||||
|
||||
def get_bar_graph_datas(self):
|
||||
data = []
|
||||
today = fields.Datetime.now(self)
|
||||
today = fields.Date.today()
|
||||
data.append({'label': _('Due'), 'value':0.0, 'type': 'past'})
|
||||
day_of_week = int(format_datetime(today, 'e', locale=get_lang(self.env).code))
|
||||
first_day_of_week = today + timedelta(days=-day_of_week+1)
|
||||
@@ -166,24 +166,29 @@ class account_journal(models.Model):
|
||||
(select_sql_clause, query_args) = self._get_bar_graph_select_query()
|
||||
query = ''
|
||||
start_date = (first_day_of_week + timedelta(days=-7))
|
||||
weeks = []
|
||||
for i in range(0,6):
|
||||
if i == 0:
|
||||
query += "("+select_sql_clause+" and invoice_date_due < '"+start_date.strftime(DF)+"')"
|
||||
weeks.append((start_date.min, start_date))
|
||||
elif i == 5:
|
||||
query += " UNION ALL ("+select_sql_clause+" and invoice_date_due >= '"+start_date.strftime(DF)+"')"
|
||||
weeks.append((start_date, start_date.max))
|
||||
else:
|
||||
next_date = start_date + timedelta(days=7)
|
||||
query += " UNION ALL ("+select_sql_clause+" and invoice_date_due >= '"+start_date.strftime(DF)+"' and invoice_date_due < '"+next_date.strftime(DF)+"')"
|
||||
weeks.append((start_date, next_date))
|
||||
start_date = next_date
|
||||
# Ensure results returned by postgres match the order of data list
|
||||
query += " ORDER BY aggr_date ASC"
|
||||
self.env.cr.execute(query, query_args)
|
||||
query_results = self.env.cr.dictfetchall()
|
||||
is_sample_data = True
|
||||
for index in range(0, len(query_results)):
|
||||
if query_results[index].get('aggr_date') != None:
|
||||
is_sample_data = False
|
||||
data[index]['value'] = query_results[index].get('total')
|
||||
aggr_date = query_results[index]['aggr_date']
|
||||
week_index = next(i for i in range(0, len(weeks)) if weeks[i][0] <= aggr_date < weeks[i][1])
|
||||
data[week_index]['value'] = query_results[index].get('total')
|
||||
|
||||
[graph_title, graph_key] = self._graph_title_and_key()
|
||||
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
|
||||
<!-- Partners. -->
|
||||
<ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:BuyerReference t-esc="buyer_reference or record.ref"/>
|
||||
|
||||
<!-- Seller. -->
|
||||
<ram:SellerTradeParty>
|
||||
<!-- Address. -->
|
||||
@@ -161,8 +163,12 @@
|
||||
|
||||
<!-- Reference. -->
|
||||
<ram:BuyerOrderReferencedDocument>
|
||||
<ram:IssuerAssignedID t-esc="record.payment_reference if record.payment_reference else record.name"/>
|
||||
<ram:IssuerAssignedID t-esc="purchase_order_reference or record.payment_reference or record.name"/>
|
||||
</ram:BuyerOrderReferencedDocument>
|
||||
|
||||
<ram:ContractReferencedDocument t-if="contract_reference">
|
||||
<ram:IssuerAssignedID t-esc="contract_reference"/>
|
||||
</ram:ContractReferencedDocument>
|
||||
</ram:ApplicableHeaderTradeAgreement>
|
||||
|
||||
<!-- Delivery. Don't make a dependency with sale only for one field. -->
|
||||
|
||||
@@ -95,6 +95,10 @@ class AccountEdiFormat(models.Model):
|
||||
'invoice_line_values': [],
|
||||
'seller_specified_legal_organization': seller_siret,
|
||||
'buyer_specified_legal_organization': buyer_siret,
|
||||
# Chorus PRO fields
|
||||
'buyer_reference': 'buyer_reference' in invoice._fields and invoice.buyer_reference or '',
|
||||
'contract_reference': 'contract_reference' in invoice._fields and invoice.contract_reference or '',
|
||||
'purchase_order_reference': 'purchase_order_reference' in invoice._fields and invoice.purchase_order_reference or '',
|
||||
}
|
||||
# Tax lines.
|
||||
# The old system was making one total "line" per tax in the xml, by using the tax_line_id.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
import werkzeug
|
||||
|
||||
from flectra import http, _
|
||||
from flectra import http, tools, _
|
||||
from flectra.addons.auth_signup.models.res_users import SignupError
|
||||
from flectra.addons.web.controllers.main import ensure_db, Home, SIGN_UP_REQUEST_PARAMS
|
||||
from flectra.addons.base_setup.controllers.main import BaseSetup
|
||||
@@ -95,6 +95,7 @@ class AuthSignupHome(Home):
|
||||
|
||||
get_param = request.env['ir.config_parameter'].sudo().get_param
|
||||
return {
|
||||
'disable_database_manager': not tools.config['list_db'],
|
||||
'signup_enabled': request.env['res.users']._get_signup_invitation_scope() == 'b2c',
|
||||
'reset_password_enabled': get_param('auth_signup.reset_password') == 'True',
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class StockPicking(models.Model):
|
||||
for pick in self:
|
||||
if pick.carrier_id:
|
||||
if pick.carrier_id.integration_level == 'rate_and_ship' and pick.picking_type_code != 'incoming':
|
||||
pick.send_to_shipper()
|
||||
pick.sudo().send_to_shipper()
|
||||
pick._check_carrier_details_compliance()
|
||||
return super(StockPicking, self)._send_confirmation_email()
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class AccountMove(models.Model):
|
||||
if self.move_type == 'out_refund':
|
||||
internal_types_domain = ('internal_type', '=', 'credit_note')
|
||||
else:
|
||||
internal_types_domain = ('internal_type', 'not in', ['invoice_in', 'credit_note'])
|
||||
internal_types_domain = ('internal_type', 'in', ['invoice', 'debit_note'])
|
||||
domain = [('country_id.code', '=', 'CL'), internal_types_domain]
|
||||
if self.company_id.partner_id.l10n_cl_sii_taxpayer_type == '1':
|
||||
domain += [('code', '!=', '71')] # Companies with VAT Affected doesn't have "Boleta de honorarios Electrónica"
|
||||
|
||||
@@ -34,7 +34,10 @@
|
||||
<field name="l10n_in_code">CMS-CENTIMETERS</field>
|
||||
</record>
|
||||
<record id="uom.product_uom_litre" model="uom.uom">
|
||||
<field name="l10n_in_code">OTH-OTHERS</field>
|
||||
<field name="l10n_in_code">LTR-LITRES</field>
|
||||
</record>
|
||||
<record id="uom.product_uom_cubic_meter" model="uom.uom">
|
||||
<field name="l10n_in_code">CBM-CUBIC METERS</field>
|
||||
</record>
|
||||
<record id="uom.product_uom_lb" model="uom.uom">
|
||||
<field name="l10n_in_code">OTH-OTHERS</field>
|
||||
@@ -60,4 +63,10 @@
|
||||
<record id="uom.product_uom_gal" model="uom.uom">
|
||||
<field name="l10n_in_code">UGS-US GALLONS</field>
|
||||
</record>
|
||||
<record id="uom.product_uom_cubic_inch" model="uom.uom">
|
||||
<field name="l10n_in_code">OTH-OTHERS</field>
|
||||
</record>
|
||||
<record id="uom.product_uom_cubic_foot" model="uom.uom">
|
||||
<field name="l10n_in_code">OTH-OTHERS</field>
|
||||
</record>
|
||||
</flectra>
|
||||
|
||||
@@ -6,18 +6,6 @@ from flectra import api, fields, models, _
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = 'account.journal'
|
||||
|
||||
@api.model
|
||||
def _fill_missing_values(self, vals):
|
||||
super()._fill_missing_values(vals)
|
||||
|
||||
if vals.get('type') != 'purchase':
|
||||
return
|
||||
|
||||
company = self.env['res.company'].browse(vals['company_id']) if vals.get('company_id') else self.env.company
|
||||
if company.country_id.code == "NL" and not vals.get('type_control_ids', [(6, 0, [])])[0][2]:
|
||||
type_control_ids = self.env.ref('account.data_account_type_direct_costs').ids
|
||||
vals['type_control_ids'] = [(6, 0, type_control_ids)]
|
||||
|
||||
@api.model
|
||||
def _prepare_liquidity_account_vals(self, company, code, vals):
|
||||
# OVERRIDE
|
||||
|
||||
@@ -1250,6 +1250,14 @@ class MailThread(models.AbstractModel):
|
||||
body = etree.tostring(root, pretty_print=False, encoding='unicode')
|
||||
return {'body': body, 'attachments': attachments}
|
||||
|
||||
def _part_get_content_decoded(self, part):
|
||||
try:
|
||||
return part.get_content(errors='strict')
|
||||
except TypeError: # no "errors" argument on the underlyding content manager
|
||||
return tools.ustr(part.get_content())
|
||||
except UnicodeDecodeError:
|
||||
return part.get_payload(decode=True).decode(errors='replace')
|
||||
|
||||
def _message_parse_extract_payload(self, message, save_original=False):
|
||||
"""Extract body as HTML and attachments from the mail message"""
|
||||
attachments = []
|
||||
@@ -1265,7 +1273,7 @@ class MailThread(models.AbstractModel):
|
||||
# type="text/html"
|
||||
if message.get_content_maintype() == 'text':
|
||||
encoding = message.get_content_charset()
|
||||
body = message.get_content()
|
||||
body = self._part_get_content_decoded(message)
|
||||
body = tools.ustr(body, encoding, errors='replace')
|
||||
if message.get_content_type() == 'text/plain':
|
||||
# text/plain -> <pre/>
|
||||
@@ -1288,22 +1296,21 @@ class MailThread(models.AbstractModel):
|
||||
# 0) Inline Attachments -> attachments, with a third part in the tuple to match cid / attachment
|
||||
if filename and part.get('content-id'):
|
||||
inner_cid = part.get('content-id').strip('><')
|
||||
attachments.append(self._Attachment(filename, part.get_content(), {'cid': inner_cid}))
|
||||
attachments.append(self._Attachment(filename, self._part_get_content_decoded(part), {'cid': inner_cid}))
|
||||
continue
|
||||
# 1) Explicit Attachments -> attachments
|
||||
if filename or part.get('content-disposition', '').strip().startswith('attachment'):
|
||||
attachments.append(self._Attachment(filename or 'attachment', part.get_content(), {}))
|
||||
attachments.append(self._Attachment(filename or 'attachment', self._part_get_content_decoded(part), {}))
|
||||
continue
|
||||
# 2) text/plain -> <pre/>
|
||||
if part.get_content_type() == 'text/plain' and (not alternative or not body):
|
||||
body = tools.append_content_to_html(body, tools.ustr(part.get_content(),
|
||||
encoding, errors='replace'), preserve=True)
|
||||
body = tools.append_content_to_html(body, self._part_get_content_decoded(part), preserve=True)
|
||||
# 3) text/html -> raw
|
||||
elif part.get_content_type() == 'text/html':
|
||||
# mutlipart/alternative have one text and a html part, keep only the second
|
||||
# mixed allows several html parts, append html content
|
||||
append_content = not alternative or (html and mixed)
|
||||
html = tools.ustr(part.get_content(), encoding, errors='replace')
|
||||
html = self._part_get_content_decoded(part)
|
||||
if not append_content:
|
||||
body = html
|
||||
else:
|
||||
@@ -1312,7 +1319,7 @@ class MailThread(models.AbstractModel):
|
||||
body = tools.html_sanitize(body, sanitize_tags=False, strip_classes=True)
|
||||
# 4) Anything else -> attachment
|
||||
else:
|
||||
attachments.append(self._Attachment(filename or 'attachment', part.get_content(), {}))
|
||||
attachments.append(self._Attachment(filename or 'attachment', self._part_get_content_decoded(part), {}))
|
||||
|
||||
return self._message_parse_extract_payload_postprocess(message, {'body': body, 'attachments': attachments})
|
||||
|
||||
|
||||
@@ -713,6 +713,10 @@ var KanbanActivity = BasicActivity.extend({
|
||||
*/
|
||||
_renderDropdown: function () {
|
||||
var self = this;
|
||||
this.$el.dropdown({
|
||||
boundary: 'viewport',
|
||||
flip: false,
|
||||
});
|
||||
this.$('.o_activity')
|
||||
.toggleClass('dropdown-menu-right', config.device.isMobile)
|
||||
.html(QWeb.render('mail.KanbanActivityLoading'));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.o_activity_view {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
> table {
|
||||
background-color: white;
|
||||
thead > tr > th:first-of-type {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
<t t-name="mail.KanbanActivity">
|
||||
<div class="o_kanban_inline_block dropdown o_mail_activity">
|
||||
<a class="dropdown-toggle o-no-caret o_activity_btn" data-boundary="viewport" data-flip="false" data-toggle="dropdown" role="button">
|
||||
<!-- Dropdowns are created in JS to avoid some bugs, that's why the <a/> contains no args for the dropdown creation -->
|
||||
<a class="dropdown-toggle o-no-caret o_activity_btn" data-toggle="dropdown" role="button">
|
||||
<!-- span classes are generated dynamically (see _render) -->
|
||||
<span t-att-title="widget.selection[widget.activityState]" role="img" t-att-aria-label="widget.selection[widget.activity_state]"/>
|
||||
</a>
|
||||
|
||||
@@ -48,7 +48,10 @@ class StockPickingType(models.Model):
|
||||
remaining.count_mo_late = False
|
||||
|
||||
def get_mrp_stock_picking_action_picking_type(self):
|
||||
return self._get_action('mrp.mrp_production_action_picking_deshboard')
|
||||
action = self.env.ref('mrp.mrp_production_action_picking_deshboard').read()[0]
|
||||
if self:
|
||||
action['display_name'] = self.display_name
|
||||
return action
|
||||
|
||||
@api.onchange('code')
|
||||
def _onchange_code(self):
|
||||
|
||||
@@ -431,7 +431,7 @@ flectra.define('point_of_sale.tests.ProductScreen', function (require) {
|
||||
const product1el = parent.el.querySelector(
|
||||
'article.product[aria-labelledby="article_product_1"]'
|
||||
);
|
||||
assert.ok(product1el.querySelector('.product-img img[alt="Water"]'));
|
||||
assert.ok(product1el.querySelector('.product-img img[data-alt="Water"]'));
|
||||
assert.ok(product1el.querySelector('.product-img .price-tag').textContent.includes('$2'));
|
||||
await testUtils.dom.click(product1el);
|
||||
await testUtils.nextTick();
|
||||
|
||||
@@ -200,12 +200,15 @@ class Project(models.Model):
|
||||
],
|
||||
string='Visibility', required=True,
|
||||
default='portal',
|
||||
help="Defines the visibility of the tasks of the project:\n"
|
||||
"- Invited internal users: employees may only see the followed project and tasks.\n"
|
||||
"- All internal users: employees may see all project and tasks.\n"
|
||||
"- Invited portal and all internal users: employees may see everything."
|
||||
" Portal users may see project and tasks followed by\n"
|
||||
" them or by someone of their company.")
|
||||
help="People to whom this project and its tasks will be visible.\n\n"
|
||||
"- Invited internal users: when following a project, internal users will get access to all of its tasks without distinction. "
|
||||
"Otherwise, they will only get access to the specific tasks they are following.\n "
|
||||
"A user with the project > administrator access right level can still access this project and its tasks, even if they are not explicitly part of the followers.\n\n"
|
||||
"- All internal users: all internal users can access the project and all of its tasks without distinction.\n\n"
|
||||
"- Invited portal users and all internal users: all internal users can access the project and all of its tasks without distinction.\n"
|
||||
"When following a project, portal users will get access to all of its tasks without distinction. Otherwise, they will only get access to the specific tasks they are following.\n\n"
|
||||
"In any case, an internal user with no project access rights can still access a task, "
|
||||
"provided that they are given the corresponding URL (and that they are part of the followers if the project is private).")
|
||||
|
||||
allowed_user_ids = fields.Many2many('res.users', compute='_compute_allowed_users', inverse='_inverse_allowed_user')
|
||||
allowed_internal_user_ids = fields.Many2many('res.users', 'project_allowed_internal_users_rel',
|
||||
|
||||
@@ -305,13 +305,14 @@ class PurchaseOrderLine(models.Model):
|
||||
pass
|
||||
elif (
|
||||
move.location_dest_id.usage == "internal"
|
||||
and move.to_refund
|
||||
and move.location_id.usage != "supplier"
|
||||
and move.location_dest_id
|
||||
not in self.env["stock.location"].search(
|
||||
[("id", "child_of", move.warehouse_id.view_location_id.id)]
|
||||
)
|
||||
):
|
||||
total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
|
||||
if move.to_refund:
|
||||
total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
|
||||
else:
|
||||
total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
|
||||
line._track_qty_received(total)
|
||||
|
||||
@@ -27,6 +27,7 @@ flectra.define('sale.SaleOrderView', function (require) {
|
||||
_onOpenDiscountWizard(ev) {
|
||||
const orderLines = this.renderer.state.data.order_line.data.filter(line => !line.data.display_type);
|
||||
const recordData = ev.target.recordData;
|
||||
if (recordData.discount === orderLines[0].data.discount) return;
|
||||
const isEqualDiscount = orderLines.slice(1).every(line => line.data.discount === recordData.discount);
|
||||
if (orderLines.length >= 3 && recordData.sequence === orderLines[0].data.sequence && isEqualDiscount) {
|
||||
Dialog.confirm(this, _t("Do you want to apply this discount to all order lines?"), {
|
||||
|
||||
@@ -198,6 +198,7 @@ ProductConfiguratorWidget.include({
|
||||
this._super.apply(this, arguments);
|
||||
return;
|
||||
}
|
||||
this.restoreProductTemplateId = this.recordData.product_template_id;
|
||||
// If line has been set up through the product_configurator:
|
||||
this._openProductConfigurator({
|
||||
configuratorMode: 'edit',
|
||||
|
||||
@@ -54,6 +54,7 @@ ProductConfiguratorWidget.include({
|
||||
if (result && result[0].product_add_mode === 'matrix') {
|
||||
self._openGridConfigurator(productTemplateId, self.dataPointID, true);
|
||||
} else {
|
||||
self.restoreProductTemplateId = self.recordData.product_template_id;
|
||||
// Call super only if product_add_mode different than matrix
|
||||
// to avoid product configurator opening (which is the default case).
|
||||
self._openProductConfigurator({
|
||||
|
||||
@@ -164,7 +164,7 @@ class ProfitabilityAnalysis(models.Model):
|
||||
AAL.so_line AS sale_line_id,
|
||||
0.0 AS timesheet_unit_amount,
|
||||
0.0 AS timesheet_cost,
|
||||
AAL.amount AS other_revenues,
|
||||
AAL.amount + COALESCE(AAL_RINV.amount, 0) AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
@@ -191,10 +191,10 @@ class ProfitabilityAnalysis(models.Model):
|
||||
AND RINVL.parent_state = 'posted'
|
||||
AND RINVL.exclude_from_invoice_tab = 'f'
|
||||
AND RINVL.product_id = AML.product_id
|
||||
LEFT JOIN account_analytic_line AAL_RINV ON RINVL.id = AAL_RINV.move_id
|
||||
WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't'
|
||||
AND P.allow_timesheets = 't'
|
||||
AND BILLL.id IS NULL
|
||||
AND RINVL.id IS NULL
|
||||
AND (SOL.id IS NULL
|
||||
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE AND SOL.is_service IS NOT TRUE))
|
||||
|
||||
@@ -208,7 +208,7 @@ class ProfitabilityAnalysis(models.Model):
|
||||
0.0 AS timesheet_unit_amount,
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
AAL.amount AS expense_cost,
|
||||
AAL.amount + COALESCE(AML_RBILLL.amount, 0) AS expense_cost,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
@@ -232,12 +232,12 @@ class ProfitabilityAnalysis(models.Model):
|
||||
AND RBILLL.parent_state = 'posted'
|
||||
AND RBILLL.exclude_from_invoice_tab = 'f'
|
||||
AND RBILLL.product_id = AML.product_id
|
||||
LEFT JOIN account_analytic_line AML_RBILLL ON RBILLL.id = AML_RBILLL.move_id
|
||||
-- Check if the AAL is not related to a consumed downpayment (when the SOL is fully invoiced - with downpayment discounted.)
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINVDOWN ON SOINVDOWN.invoice_line_id = AML.id
|
||||
LEFT JOIN sale_order_line SOLDOWN on SOINVDOWN.order_line_id = SOLDOWN.id AND SOLDOWN.is_downpayment = 't'
|
||||
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL
|
||||
AND INVL.id IS NULL
|
||||
AND RBILLL.id IS NULL
|
||||
AND SOLDOWN.id IS NULL
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from flectra.osv import expression
|
||||
from flectra.tools import float_is_zero, float_compare
|
||||
from flectra.addons.sale_timesheet.tests.common_reporting import TestCommonReporting
|
||||
from flectra.tests import tagged
|
||||
from flectra.tests import tagged, Form
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
@@ -937,3 +937,126 @@ class TestReporting(TestCommonReporting):
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_cost'], 0, msg="The expense costs (credit note) of the project should be zero (not taken into account), after credit note.")
|
||||
self.assertAlmostEqual(project_stat['other_revenues'], 0, msg="The other revenues of the project should be zero, as it is balanced by credit note.")
|
||||
|
||||
def test_profitability_partial_refund_invoice(self):
|
||||
ProjectProfitabilityReport = self.env['project.profitability.report']
|
||||
analytic_account = self.project_global.analytic_account_id
|
||||
product = self.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'name': "Product",
|
||||
'standard_price': 100.0,
|
||||
'list_price': 100.0,
|
||||
'taxes_id': False,
|
||||
})
|
||||
test_invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'currency_id': self.env.user.company_id.currency_id,
|
||||
'partner_id': self.partner_a,
|
||||
'invoice_date': '2021-01-01',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'quantity': 2,
|
||||
'product_id': product.id,
|
||||
'price_unit': 100.0,
|
||||
'analytic_account_id': analytic_account.id,
|
||||
})]
|
||||
})
|
||||
test_invoice.action_post()
|
||||
ProjectProfitabilityReport.flush()
|
||||
|
||||
project_stat= ProjectProfitabilityReport.read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
self.assertAlmostEqual(project_stat['amount_untaxed_invoiced'], 0, msg="The invoiced amount of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['amount_untaxed_to_invoice'], 0, msg="The amount to invoice of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['timesheet_unit_amount'], 0, msg="The timesheet unit amount of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['timesheet_cost'], 0, msg="The timesheet cost of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_to_invoice'], 0, msg="The expense cost to reinvoice of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_cost'], 0, msg="The expense cost of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['other_revenues'], test_invoice.amount_total_signed, msg="The other revenues of the project should be equal to the the invoice line price, after credit note.")
|
||||
|
||||
refund_note_wizard = self.env['account.move.reversal'].with_context({
|
||||
'active_model': 'account.move',
|
||||
'active_ids': test_invoice.ids,
|
||||
'active_id': test_invoice.id,
|
||||
}).create({
|
||||
'refund_method': 'refund',
|
||||
'reason': 'no reason',
|
||||
})
|
||||
refund = self.env['account.move'].browse(refund_note_wizard.reverse_moves()["res_id"])
|
||||
|
||||
move_form = Form(refund)
|
||||
with move_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.quantity = 1
|
||||
refund = move_form.save()
|
||||
refund.action_post()
|
||||
ProjectProfitabilityReport.flush()
|
||||
|
||||
project_stat= ProjectProfitabilityReport.read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
self.assertAlmostEqual(project_stat['amount_untaxed_invoiced'], 0, msg="The invoiced amount of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['amount_untaxed_to_invoice'], 0, msg="The amount to invoice of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['timesheet_unit_amount'], 0, msg="The timesheet unit amount of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['timesheet_cost'], 0, msg="The timesheet cost of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_to_invoice'], 0, msg="The expense cost to reinvoice of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_cost'], 0, msg="The expense costs (credit note) of the project should be zero (not taken into account), after credit note.")
|
||||
self.assertAlmostEqual(project_stat['other_revenues'], test_invoice.amount_total_signed + refund.amount_total_signed, msg="The other revenues of the project should be zero, as it is balanced by credit note.")
|
||||
|
||||
|
||||
def test_profitability_partial_refund_vendor_bill(self):
|
||||
ProjectProfitabilityReport = self.env['project.profitability.report']
|
||||
analytic_account = self.project_global.analytic_account_id
|
||||
product = self.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'name': "Product",
|
||||
'standard_price': 100.0,
|
||||
'list_price': 100.0,
|
||||
'taxes_id': False,
|
||||
})
|
||||
test_invoice = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'currency_id': self.env.user.company_id.currency_id,
|
||||
'partner_id': self.partner_a,
|
||||
'invoice_date': '2021-01-01',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'quantity': 2,
|
||||
'product_id': product.id,
|
||||
'price_unit': 100.0,
|
||||
'analytic_account_id': analytic_account.id,
|
||||
})]
|
||||
})
|
||||
test_invoice.action_post()
|
||||
ProjectProfitabilityReport.flush()
|
||||
|
||||
project_stat= ProjectProfitabilityReport.read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
self.assertAlmostEqual(project_stat['amount_untaxed_invoiced'], 0, msg="The invoiced amount of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['amount_untaxed_to_invoice'], 0, msg="The amount to invoice of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['timesheet_unit_amount'], 0, msg="The timesheet unit amount of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['timesheet_cost'], 0, msg="The timesheet cost of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_to_invoice'], 0, msg="The expense cost to reinvoice of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_cost'], test_invoice.amount_total_signed, msg="The expense cost of the project should be zero, before credit note.")
|
||||
self.assertAlmostEqual(project_stat['other_revenues'], 0, msg="The other revenues of the project should be equal to the the invoice line price, after credit note.")
|
||||
|
||||
refund_note_wizard = self.env['account.move.reversal'].with_context({
|
||||
'active_model': 'account.move',
|
||||
'active_ids': test_invoice.ids,
|
||||
'active_id': test_invoice.id,
|
||||
}).create({
|
||||
'refund_method': 'refund',
|
||||
'reason': 'no reason',
|
||||
})
|
||||
refund = self.env['account.move'].browse(refund_note_wizard.reverse_moves()["res_id"])
|
||||
|
||||
move_form = Form(refund)
|
||||
with move_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.quantity = 1
|
||||
refund = move_form.save()
|
||||
refund.action_post()
|
||||
ProjectProfitabilityReport.flush()
|
||||
|
||||
project_stat= ProjectProfitabilityReport.read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
self.assertAlmostEqual(project_stat['amount_untaxed_invoiced'], 0, msg="The invoiced amount of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['amount_untaxed_to_invoice'], 0, msg="The amount to invoice of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['timesheet_unit_amount'], 0, msg="The timesheet unit amount of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['timesheet_cost'], 0, msg="The timesheet cost of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_to_invoice'], 0, msg="The expense cost to reinvoice of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, after credit note.")
|
||||
self.assertAlmostEqual(project_stat['expense_cost'], test_invoice.amount_total_signed + refund.amount_total_signed, msg="The expense costs (credit note) of the project should be zero (not taken into account), after credit note.")
|
||||
self.assertAlmostEqual(project_stat['other_revenues'], 0, msg="The other revenues of the project should be zero, as it is balanced by credit note.")
|
||||
|
||||
@@ -525,8 +525,10 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
)
|
||||
|
||||
if use_new_cursor:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
try:
|
||||
cr.commit()
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
@@ -19,9 +19,8 @@ class StockSchedulerCompute(models.TransientModel):
|
||||
_description = 'Run Scheduler Manually'
|
||||
|
||||
def _procure_calculation_orderpoint(self):
|
||||
with api.Environment.manage():
|
||||
# As this function is in a new thread, I need to open a new cursor, because the old one may be closed
|
||||
new_cr = self.pool.cursor()
|
||||
# As this function is in a new thread, I need to open a new cursor, because the old one may be closed
|
||||
with api.Environment.manage(), self.pool.cursor() as new_cr:
|
||||
self = self.with_env(self.env(cr=new_cr))
|
||||
scheduler_cron = self.sudo().env.ref('stock.ir_cron_scheduler_action')
|
||||
# Avoid to run the scheduler multiple times in the same time
|
||||
@@ -31,7 +30,6 @@ class StockSchedulerCompute(models.TransientModel):
|
||||
except Exception:
|
||||
_logger.info('Attempt to run procurement scheduler aborted, as already running')
|
||||
self._cr.rollback()
|
||||
self._cr.close()
|
||||
return {}
|
||||
|
||||
for company in self.env.user.company_ids:
|
||||
@@ -39,7 +37,7 @@ class StockSchedulerCompute(models.TransientModel):
|
||||
self.env['procurement.group'].with_context(allowed_company_ids=cids).run_scheduler(
|
||||
use_new_cursor=self._cr.dbname,
|
||||
company_id=company.id)
|
||||
new_cr.close()
|
||||
self._cr.rollback()
|
||||
return {}
|
||||
|
||||
def procure_calculation(self):
|
||||
|
||||
@@ -1013,3 +1013,22 @@ Remote-MTA: 10.245.192.40
|
||||
|
||||
|
||||
--_av-UfLe6y6qxNo54-urtAxbJQ--"""
|
||||
|
||||
|
||||
MAIL_WRONG_CONTENT_CHARSET = """\
|
||||
Return-Path: <whatever-2a840@postmaster.twitter.com>
|
||||
To: gaston.lagaffe@example.com
|
||||
Received: by mail1.openerp.com (Postfix, from userid 10002)
|
||||
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
|
||||
From: spirou@example.com
|
||||
Subject: Ze Subject
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----=_Part_4200734_24778174.1344608186754"
|
||||
------=_Part_4200734_24778174.1344608186754
|
||||
Content-Type: text/plain; charset=US-ASCII
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Tonton, ton th=C3=A9 t'a-t-il ot=C3=A9 ta toux ?
|
||||
------=_Part_4200734_24778174.1344608186754
|
||||
"""
|
||||
|
||||
@@ -89,6 +89,11 @@ class TestEmailParsing(TestMailCommon):
|
||||
res = self.env['mail.thread'].message_parse(self.from_string(mail))
|
||||
self.assertIn('<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>', res['body'])
|
||||
|
||||
def test_message_parse_wrong_content_charset(self):
|
||||
mail = test_mail_data.MAIL_WRONG_CONTENT_CHARSET
|
||||
res = self.env['mail.thread'].message_parse(self.from_string(mail))
|
||||
self.assertIn("Tonton, ton thé t'a-t-il oté ta toux ?", res['body'])
|
||||
|
||||
def test_message_parse_xhtml(self):
|
||||
# Test that the parsing of XHTML mails does not fail
|
||||
self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_XHTML))
|
||||
|
||||
@@ -79,3 +79,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_graph_measures_list {
|
||||
max-height: calc(100vh - #{$o-navbar-height} - 100px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -102,3 +102,8 @@
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.o_pivot_measures_list {
|
||||
max-height: calc(100vh - #{$o-navbar-height} - 100px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,30 @@ flectra.define('web.test_utils', async function (require) {
|
||||
const testUtilsPivot = require('web.test_utils_pivot');
|
||||
const tools = require('web.tools');
|
||||
|
||||
QUnit.begin(() => {
|
||||
// alt attribute causes issues with scroll tests. Indeed, alt is
|
||||
// displayed between the time we scroll to the bottom of a thread
|
||||
// and the time we assert for the scroll position. The src
|
||||
// attribute is removed as well to make sure images won't
|
||||
// trigger a GET request on the server.
|
||||
function replaceAttr(attrName, prefix, element) {
|
||||
const attrKey = `${prefix}${attrName}`;
|
||||
const attrValue = element.getAttribute(attrKey);
|
||||
element.removeAttribute(attrKey);
|
||||
element.setAttribute(`${prefix}data-${attrName}`, attrValue);
|
||||
}
|
||||
const attrsToRemove = ['alt', 'src'];
|
||||
const attrPrefixes = ['', 't-att-', 't-attf-'];
|
||||
const templates = new DOMParser().parseFromString(session.owlTemplates, "text/xml");
|
||||
for (const attrName of attrsToRemove) {
|
||||
for (const prefix of attrPrefixes) {
|
||||
for (const element of templates.querySelectorAll(`*[${prefix}${attrName}]`)) {
|
||||
replaceAttr(attrName, prefix, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
session.owlTemplates = templates.documentElement.outerHTML;
|
||||
});
|
||||
|
||||
function deprecated(fn, type) {
|
||||
const msg = `Helper 'testUtils.${fn.name}' is deprecated. ` +
|
||||
|
||||
@@ -988,7 +988,14 @@ class Website(models.Model):
|
||||
path = urls.url_quote_plus(request.httprequest.path, safe='/')
|
||||
lang_path = ('/' + lang.url_code) if lang != self.default_lang_id else ''
|
||||
canonical_query_string = '?%s' % urls.url_encode(canonical_params) if canonical_params else ''
|
||||
return self.get_base_url() + lang_path + path + canonical_query_string
|
||||
|
||||
if lang_path and path == '/':
|
||||
# We want `/fr_BE` not `/fr_BE/` for correct canonical on homepage
|
||||
localized_path = lang_path
|
||||
else:
|
||||
localized_path = lang_path + path
|
||||
|
||||
return self.get_base_url() + localized_path + canonical_query_string
|
||||
|
||||
def _get_canonical_url(self, canonical_params):
|
||||
"""Returns the canonical URL for the current request."""
|
||||
|
||||
@@ -248,3 +248,16 @@ body.editor_enable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Put the following rules in a file that is not sent to the visitors
|
||||
// in the right app (website_mass_mailing).
|
||||
body.editor_enable {
|
||||
.s_newsletter_subscribe_form {
|
||||
.o_enable_preview {
|
||||
display: block !important;
|
||||
}
|
||||
.o_disable_preview {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,4 @@ class TestBaseUrl(TestUrlCommon):
|
||||
self._assertCanonical('/?debug=1', self.domain + '/')
|
||||
self._assertCanonical('/a-page', self.domain + '/a-page')
|
||||
self._assertCanonical('/en_US', self.domain + '/')
|
||||
self._assertCanonical('/fr_FR', self.domain + '/fr/')
|
||||
self._assertCanonical('/fr_FR', self.domain + '/fr')
|
||||
|
||||
@@ -20,7 +20,7 @@ class TestLangUrl(HttpCase):
|
||||
|
||||
def test_01_url_lang(self):
|
||||
with MockRequest(self.env, website=self.website):
|
||||
self.assertEqual(url_lang('', '[lang]'), '/[lang]/hello/', "`[lang]` is used to be replaced in the url_return after installing a language, it should not be replaced or removed.")
|
||||
self.assertEqual(url_lang('', '[lang]'), '/[lang]/hello', "`[lang]` is used to be replaced in the url_return after installing a language, it should not be replaced or removed.")
|
||||
|
||||
def test_02_url_redirect(self):
|
||||
url = '/fr_WHATEVER/contactus'
|
||||
|
||||
@@ -90,7 +90,7 @@ def MockRequest(
|
||||
env=env,
|
||||
httprequest=Mock(
|
||||
host='localhost',
|
||||
path='/hello/',
|
||||
path='/hello',
|
||||
app=flectra.http.root,
|
||||
environ={'REMOTE_ADDR': '127.0.0.1'},
|
||||
cookies=cookies or {},
|
||||
|
||||
@@ -103,13 +103,14 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-if="request and request.is_frontend_multilang and website">
|
||||
<!-- `alternate`/`canonical` mainly useful to crawlers/bots/SEO tools, which test the website as public user -->
|
||||
<t t-if="request and request.is_frontend_multilang and website and website.is_public_user()">
|
||||
<t t-set="alternate_languages" t-value="website._get_alternate_languages(canonical_params=canonical_params)"/>
|
||||
<t t-foreach="alternate_languages" t-as="lg">
|
||||
<link rel="alternate" t-att-hreflang="lg['hreflang']" t-att-href="lg['href']"/>
|
||||
</t>
|
||||
</t>
|
||||
<link t-if="request and website" rel="canonical" t-att-href="website._get_canonical_url(canonical_params=canonical_params)"/>
|
||||
<link t-if="request and website and website.is_public_user()" rel="canonical" t-att-href="website._get_canonical_url(canonical_params=canonical_params)"/>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin=""/>
|
||||
</xpath>
|
||||
|
||||
@@ -54,6 +54,18 @@ options.registry.mailing_list_subscribe = options.Class.extend({
|
||||
});
|
||||
return def;
|
||||
},
|
||||
/**
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
toggleThanksButton(previewMode, widgetValue, params) {
|
||||
const subscribeBtnEl = this.$target[0].querySelector('.js_subscribe_btn');
|
||||
const thanksBtnEl = this.$target[0].querySelector('.js_subscribed_btn');
|
||||
|
||||
thanksBtnEl.classList.toggle('o_disable_preview', !widgetValue);
|
||||
thanksBtnEl.classList.toggle('o_enable_preview', widgetValue);
|
||||
subscribeBtnEl.classList.toggle('o_enable_preview', !widgetValue);
|
||||
subscribeBtnEl.classList.toggle('o_disable_preview', widgetValue);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -64,6 +76,42 @@ options.registry.mailing_list_subscribe = options.Class.extend({
|
||||
self.getParent()._onRemoveClick($.Event( "click" ));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
cleanForSave() {
|
||||
const previewClasses = ['o_disable_preview', 'o_enable_preview'];
|
||||
this.$target[0].querySelector('.js_subscribe_btn').classList.remove(...previewClasses);
|
||||
this.$target[0].querySelector('.js_subscribed_btn').classList.remove(...previewClasses);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_computeWidgetState(methodName, params) {
|
||||
if (methodName !== 'toggleThanksButton') {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
const subscribeBtnEl = this.$target[0].querySelector('.js_subscribe_btn');
|
||||
return subscribeBtnEl && subscribeBtnEl.classList.contains('o_disable_preview') ?
|
||||
'true' : '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_renderCustomXML(uiFragment) {
|
||||
const checkboxEl = document.createElement('we-checkbox');
|
||||
checkboxEl.setAttribute('string', _t("Display Thanks Button"));
|
||||
checkboxEl.dataset.toggleThanksButton = 'true';
|
||||
checkboxEl.dataset.noPreview = 'true';
|
||||
// Prevent this option from triggering a refresh of the public widget.
|
||||
checkboxEl.dataset.noWidgetRefresh = 'true';
|
||||
uiFragment.appendChild(checkboxEl);
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.recaptchaSubscribe = options.Class.extend({
|
||||
@@ -137,14 +185,17 @@ options.registry.newsletter_popup = options.registry.mailing_list_subscribe.exte
|
||||
var self = this;
|
||||
var content = this.$target.data('content');
|
||||
if (content) {
|
||||
const $layout = $('<div/>', {html: content});
|
||||
const previewClasses = ['o_disable_preview', 'o_enable_preview'];
|
||||
$layout[0].querySelector('.js_subscribe_btn').classList.remove(...previewClasses);
|
||||
$layout[0].querySelector('.js_subscribed_btn').classList.remove(...previewClasses);
|
||||
this.trigger_up('get_clean_html', {
|
||||
$layout: $('<div/>').html(content),
|
||||
$layout: $layout,
|
||||
callback: function (html) {
|
||||
self.$target.data('content', html);
|
||||
},
|
||||
});
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
|
||||
@@ -17,6 +17,8 @@ const session = require('web.session');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
let alertReCaptchaDisplayed;
|
||||
|
||||
publicWidget.registry.subscribe = publicWidget.Widget.extend({
|
||||
selector: ".js_subscribe",
|
||||
disabledInEditableMode: false,
|
||||
@@ -45,10 +47,9 @@ publicWidget.registry.subscribe = publicWidget.Widget.extend({
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
var def = this._super.apply(this, arguments);
|
||||
|
||||
if (!this._recaptcha && this.editableMode && session.is_admin) {
|
||||
if (!this._recaptcha && this.editableMode && session.is_admin && !alertReCaptchaDisplayed) {
|
||||
this.displayNotification({
|
||||
type: 'info',
|
||||
message: _t("Do you want to install Google reCAPTCHA to secure your newsletter subscriptions?"),
|
||||
@@ -79,6 +80,7 @@ publicWidget.registry.subscribe = publicWidget.Widget.extend({
|
||||
});
|
||||
}}],
|
||||
});
|
||||
alertReCaptchaDisplayed = true;
|
||||
}
|
||||
|
||||
this.$popup = this.$target.closest('.o_newsletter_modal');
|
||||
@@ -88,17 +90,12 @@ publicWidget.registry.subscribe = publicWidget.Widget.extend({
|
||||
return def;
|
||||
}
|
||||
|
||||
var always = function (data) {
|
||||
var isSubscriber = data.is_subscriber;
|
||||
self.$('.js_subscribe_btn').prop('disabled', isSubscriber);
|
||||
self.$('input.js_subscribe_email')
|
||||
.val(data.email || "")
|
||||
.prop('disabled', isSubscriber);
|
||||
// Compat: remove d-none for DBs that have the button saved with it.
|
||||
self.$target.removeClass('d-none');
|
||||
self.$('.js_subscribe_btn').toggleClass('d-none', !!isSubscriber);
|
||||
self.$('.js_subscribed_btn').toggleClass('d-none', !isSubscriber);
|
||||
};
|
||||
if (this.editableMode) {
|
||||
// Since there is an editor option to choose whether "Thanks" button
|
||||
// should be visible or not, we should not vary its visibility here.
|
||||
return def;
|
||||
}
|
||||
const always = this._updateView.bind(this);
|
||||
return Promise.all([def, this._rpc({
|
||||
route: '/website_mass_mailing/is_subscriber',
|
||||
params: {
|
||||
@@ -106,6 +103,38 @@ publicWidget.registry.subscribe = publicWidget.Widget.extend({
|
||||
},
|
||||
}).then(always).guardedCatch(always)]);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
this._updateView({is_subscriber: false});
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Modifies the elements to have the view of a subscriber/non-subscriber.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
_updateView(data) {
|
||||
const isSubscriber = data.is_subscriber;
|
||||
const subscribeBtnEl = this.$target[0].querySelector('.js_subscribe_btn');
|
||||
const thanksBtnEl = this.$target[0].querySelector('.js_subscribed_btn');
|
||||
const emailInputEl = this.$target[0].querySelector('input.js_subscribe_email');
|
||||
|
||||
subscribeBtnEl.disabled = isSubscriber;
|
||||
emailInputEl.value = data.email || '';
|
||||
emailInputEl.disabled = isSubscriber;
|
||||
// Compat: remove d-none for DBs that have the button saved with it.
|
||||
this.$target[0].classList.remove('d-none');
|
||||
|
||||
subscribeBtnEl.classList.toggle('d-none', !!isSubscriber);
|
||||
thanksBtnEl.classList.toggle('d-none', !isSubscriber);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
|
||||
Reference in New Issue
Block a user