[PATCH] Upstream patch - 17062022

This commit is contained in:
Parthiv Patel
2022-06-17 08:34:44 +00:00
parent 8ce51106dc
commit 1e956479f7
36 changed files with 389 additions and 71 deletions

View File

@@ -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()

View File

@@ -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. -->

View File

@@ -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.

View File

@@ -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',
}

View File

@@ -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()

View File

@@ -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"

View File

@@ -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>

View File

@@ -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

View File

@@ -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})

View File

@@ -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'));

View File

@@ -1,5 +1,6 @@
.o_activity_view {
height: 100%;
overflow: auto;
> table {
background-color: white;
thead > tr > th:first-of-type {

View File

@@ -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>

View File

@@ -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):

View File

@@ -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();

View File

@@ -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',

View File

@@ -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)

View File

@@ -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?"), {

View File

@@ -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',

View File

@@ -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({

View File

@@ -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'

View File

@@ -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.")

View File

@@ -525,8 +525,10 @@ class StockWarehouseOrderpoint(models.Model):
)
if use_new_cursor:
cr.commit()
cr.close()
try:
cr.commit()
finally:
cr.close()
return {}

View File

@@ -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):

View File

@@ -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
"""

View File

@@ -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))

View File

@@ -79,3 +79,8 @@
}
}
}
.o_graph_measures_list {
max-height: calc(100vh - #{$o-navbar-height} - 100px);
overflow-y: auto;
}

View File

@@ -102,3 +102,8 @@
cursor: default;
}
}
.o_pivot_measures_list {
max-height: calc(100vh - #{$o-navbar-height} - 100px);
overflow-y: auto;
}

View File

@@ -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. ` +

View File

@@ -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."""

View File

@@ -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;
}
}
}

View File

@@ -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')

View File

@@ -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'

View File

@@ -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 {},

View File

@@ -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>

View File

@@ -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

View File

@@ -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