Master upstream patch

This commit is contained in:
Parthiv Patel committed 2021-07-02 12:13:20 +00:00
1 parent 5b378526e9
commit ed061991dd
74 files changed
+2637 -122

No files matched your search

+1 -1
View File
@@ -82,7 +82,7 @@ class ResConfigSettings(models.TransientModel):
module_currency_rate_live = fields.Boolean(string="Automatic Currency Rates")
module_account_intrastat = fields.Boolean(string='Intrastat')
module_product_margin = fields.Boolean(string="Allow Product Margin")
module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT")
module_l10n_eu_service = fields.Boolean(string="EU Intra-community Distance Selling")
module_account_taxcloud = fields.Boolean(string="Account TaxCloud")
module_account_invoice_extract = fields.Boolean(string="Bill Digitalization")
module_snailmail_account = fields.Boolean(string="Snailmail")
@@ -98,12 +98,12 @@ class AccountInvoiceReport(models.Model):
line.quantity / NULLIF(COALESCE(uom_line.factor, 1) / COALESCE(uom_template.factor, 1), 0.0) * (CASE WHEN move.move_type IN ('in_invoice','out_refund','in_receipt') THEN -1 ELSE 1 END)
AS quantity,
-line.balance * currency_table.rate AS price_subtotal,
-COALESCE(line.balance
/ NULLIF(line.quantity, 0.0)
/ NULLIF(COALESCE(uom_line.factor, 1), 0.0)
/ NULLIF(COALESCE(uom_template.factor, 1), 0.0),
0.0) * currency_table.rate
AS price_average,
-COALESCE(
-- Average line price
(line.balance / NULLIF(line.quantity, 0.0))
-- convert to template uom
* (NULLIF(COALESCE(uom_line.factor, 1), 0.0) / NULLIF(COALESCE(uom_template.factor, 1), 0.0)),
0.0) * currency_table.rate AS price_average,
COALESCE(partner.country_id, commercial_partner.country_id) AS country_id
'''
@@ -14,7 +14,7 @@
<field name="name"/>
<field name="applicability"/>
<field name="tax_negate" readonly="1" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
<field name="country_id" readonly="1" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
<field name="country_id" attrs="{'invisible': [('applicability', '!=', 'taxes')], 'readonly': [('tax_report_line_ids', '!=', [])]}"/>
<field name="tax_report_line_ids" readonly="1" attrs="{'invisible': [('applicability', '!=', 'taxes')]}"/>
</group>
</sheet>
@@ -93,14 +93,15 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="eu_service" title="If you're selling digital goods to customers in the EU, you must charge VAT based on your customers' locations. This rule applies regardless of you are located. Digital goods are defined in the legislation as broadcasting, telecommunications, and services that are electronically supplied instead of shipped. Gift cards sent online are not included in the definition.">
<div class="col-12 col-lg-6 o_setting_box" id="eu_service" title="If you sell goods and services to customers in a foreign EU country, you must charge VAT based on the delivery address. This rule applies regardless of where you are located.">
<div class="o_setting_left_pane">
<field name="module_l10n_eu_service"/>
</div>
<div class="o_setting_right_pane" name="l10n_eu_service_right_pane">
<label for="module_l10n_eu_service"/>
<a href="https://doc.flectrahq.com/2.0/applications/finance/accounting/taxation/taxes/eu_distance_selling.html" title="Documentation" class="o_doc_link" target="_blank"></a>
<div class="text-muted">
Apply right VAT rates for digital products sold in EU
Apply VAT of the EU country to which goods and services are delivered.
</div>
</div>
</div>
@@ -83,7 +83,7 @@
<h3>Contractual Relationship</h3>
<p>The payment processing services ordered by you by placing this order will be
provided to you by Adyen N.V. (hereafter “Processor”), with which you are
entering into a direct agreement by confirming this order. FlectraHQ, Inc., Odoo S.A.
entering into a direct agreement by confirming this order. FlectraHQ Inc., Odoo S.A.
(hereafter “We /Us”) will assist and support you in your use of the services to be
provided by the Processor and we will provide you first line assistance with and
enable you to connect to the systems of Processor to be able to use its services.
+1 -1
View File
@@ -8,7 +8,7 @@
Allow users to login through OAuth2 Provider.
=============================================
""",
'maintainer': 'FlectraHQ Inc.., Odoo S.A.',
'maintainer': 'FlectraHQ Inc., Odoo S.A.',
'depends': ['base', 'web', 'base_setup', 'auth_signup'],
'data': [
'data/auth_oauth_data.xml',
+1 -1
View File
@@ -23,7 +23,7 @@ class TestCalendar(SavepointCaseWithUserDemo):
'stop': '2011-04-30 18:30:00',
'description': 'The Technical Presentation will cover following topics:\n* Creating Flectra class\n* Views\n* Wizards\n* Workflows',
'duration': 2.5,
'location': 'FlectraHQ, Inc., Odoo S.A.',
'location': 'FlectraHQ Inc., Odoo S.A.',
'name': 'Technical Presentation'
})
+1 -1
View File
@@ -259,7 +259,7 @@
<record id="employee_resume_line_admin_4" model="hr.resume.line">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="name">FlectraHQ, Inc.</field>
<field name="name">FlectraHQ Inc.</field>
<field name="date_start" eval="(datetime.now()+relativedelta(years=-3)).strftime('%Y-11-01')"/>
<field name="line_type_id" ref="resume_type_experience"/>
<field name="description">
+2 -1
View File
@@ -233,7 +233,8 @@
<br/><strong>Currency: </strong><span t-esc="'%s - %s' % (o.currency_id.name, o.currency_id.currency_unit_label)"/>
<br/><strong>Exchange rate: </strong> <span t-field="o.l10n_ar_currency_rate"/>
</t>
<t t-if="o.l10n_latam_document_type_id.code in ['201', '206', '211']">
<!-- Show CBU for FACTURA DE CREDITO ELECTRONICA MiPyMEs and NOTA DE DEBITO ELECTRONICA MiPyMEs -->
<t t-if="o.l10n_latam_document_type_id.code in ['201', '206', '211', '202', '207', '212'] and o.partner_bank_id">
<br/><strong>CBU for payment: </strong><span t-esc="o.partner_bank_id.acc_number or '' if o.partner_bank_id.acc_type == 'cbu' else ''"/>
</t>
+1 -1
View File
@@ -34,7 +34,7 @@ Wizards provided by this module:
**Path to access :** Invoicing/Reporting/Legal Reports/Belgium Statements/Annual Listing Of VAT-Subjected Customers
""",
'author': 'Noviat, FlectraHQ Inc..',
'author': 'Noviat, FlectraHQ Inc.',
'depends': [
'account',
'base_iban',
+1 -1
View File
@@ -11,7 +11,7 @@ However, if the new QR-IBAN field is filled, the value will be used as the QR-IB
This should help for reconciliation on the bank statements where the old IBAN code is still used.
""",
'version': '1.0',
'author': 'FlectraHQ Inc.., Odoo S.A',
'author': 'FlectraHQ Inc., Odoo S.A',
'category': 'Localization',
'depends': ['l10n_ch'],
'data': [
+8
View File
@@ -3,3 +3,11 @@
from . import wizard
from . import models
from flectra import api, SUPERUSER_ID
def l10n_eu_service_post_init(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
env['res.company']._map_all_eu_companies_taxes()
def l10n_eu_service_uninstall(cr, registry):
cr.execute("DELETE FROM ir_model_data WHERE module = 'l10n_eu_service' and model in ('account.tax.group', 'account.account');")
+12 -35
View File
@@ -2,48 +2,23 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
{
'name': 'EU Mini One Stop Shop (MOSS)',
'name': 'EU One Stop Shop (OSS)',
'category': 'Accounting/Localizations',
'description': """
EU Mini One Stop Shop (MOSS) VAT for telecommunications, broadcasting and electronic services
=============================================================================================
EU One Stop Shop (OSS) VAT
==========================
As of January 1rst, 2015, telecommunications, broadcasting
and electronic services sold within the European Union
have to be always taxed in the country where the customer
belongs. In order to simplify the application of this EU
directive, the Mini One Stop Shop (MOSS) registration scheme
allows businesses to make a unique tax declaration.
From July 1st 2021, EU businesses that are selling goods within the EU above EUR 10 000 to buyers located in another EU Member State need to register and pay VAT in the buyers’ Member State.
Below this new EU-wide threshold you can continue to apply the domestic rules for VAT on your cross-border sales. In order to simplify the application of this EU directive, the One Stop Shop (OSS) registration scheme allows businesses to make a unique tax declaration.
This module makes it possible by helping with the creation
of the required EU fiscal positions and taxes in order to
automatically apply and record the required taxes.
This module makes it possible by helping with the creation of the required EU fiscal positions and taxes in order to automatically apply and record the required taxes.
This module installs a wizard to help setup fiscal positions
and taxes for selling electronic services inside EU.
The wizard lets you select:
- the EU countries to which you are selling these
services
- your national VAT tax for services, to be mapped
to the target country's tax
- optionally: a template fiscal position, in order
to copy the account mapping. Should be your
existing B2C Intra-EU fiscal position. (defaults
to no account mapping)
- optionally: an account to use for collecting the
tax amounts (defaults to the account used by your
national VAT tax for services)
It creates the corresponding fiscal positions and taxes,
automatically applicable for EU sales with a customer
in the selected countries.
The wizard can be run again for adding more countries.
All you have to do is check that the proposed mapping is suitable for the products and services you sell.
References
++++++++++
- Directive 2008/8/EC
- Council Implementing Regulation (EU) No 1042/2013
Council Directive (EU) 2017/2455 Council Directive (EU) 2019/1995
Council Implementing Regulation (EU) 2019/2026
""",
'depends': ['account'],
@@ -51,6 +26,8 @@ References
'security/ir.model.access.csv',
'wizard/wizard.xml',
'data/l10n_eu_service.service_tax_rate.csv',
'views/res_config_settings_views.xml'
'views/res_config_settings_views.xml',
],
'post_init_hook': 'l10n_eu_service_post_init',
'uninstall_hook': 'l10n_eu_service_uninstall',
}
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import chart_template
from . import eu_service_tax_rate
from . import eu_tax_map
from . import res_company
from . import res_config_settings
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, fields, models, _
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
def _load(self, sale_tax_rate, purchase_tax_rate, company):
rslt = super()._load( sale_tax_rate, purchase_tax_rate, company)
if company.account_tax_fiscal_country_id in self.env.ref('base.europe').country_ids:
company._map_eu_taxes()
return rslt
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, fields, models, _
from .eu_tax_map import EU_TAX_MAP
class Company(models.Model):
_inherit = 'res.company'
@api.model
def _map_all_eu_companies_taxes(self):
''' Identifies EU companies and calls the _map_eu_taxes function
'''
eu_countries = self.env.ref('base.europe').country_ids
companies = self.search([('account_tax_fiscal_country_id','in', eu_countries.ids)])
companies._map_eu_taxes()
def _map_eu_taxes(self):
'''Creates or updates Fiscal Positions for each EU country excluding the company's account_tax_fiscal_country_id
'''
eu_countries = self.env.ref('base.europe').country_ids
oss_tax_groups = self.env['ir.model.data'].search([
('module', '=', 'l10n_eu_service'),
('model', '=', 'account.tax.group')])
for company in self:
invoice_repartition_lines, refund_repartition_lines = company._get_repartition_lines_oss()
taxes = self.env['account.tax'].search([
('type_tax_use', '=', 'sale'),
('amount_type', '=', 'percent'),
('company_id','=', company.id),
('tax_group_id', 'not in', oss_tax_groups.mapped('res_id'))])
for country in eu_countries - company.account_tax_fiscal_country_id:
mapping = []
fpos = self.env['account.fiscal.position'].search([
('country_id', '=', country.id),
('company_id', '=', company.id),
('auto_apply', '=', True),
('vat_required', '=', False)], limit=1)
if not fpos:
fpos = self.env['account.fiscal.position'].create({
'name': 'OSS B2C %s' % country.name,
'country_id': country.id,
'company_id': company.id,
'auto_apply': True,
})
foreign_taxes = {tax.amount: tax for tax in fpos.tax_ids.tax_dest_id if tax.amount_type == 'percent'}
for domestic_tax in taxes:
tax_amount = EU_TAX_MAP.get((company.account_tax_fiscal_country_id.code, domestic_tax.amount, country.code), False)
if tax_amount and domestic_tax not in fpos.tax_ids.tax_src_id:
if not foreign_taxes.get(tax_amount, False):
if not self.env['ir.model.data'].xmlid_to_object('l10n_eu_service.oss_tax_group_%s' % str(tax_amount).replace('.','_')):
self.env['ir.model.data'].create({
'name': 'oss_tax_group_%s' % str(tax_amount).replace('.','_'),
'module': 'l10n_eu_service',
'model': 'account.tax.group',
'res_id': self.env['account.tax.group'].create({'name': 'OSS %s%%' % tax_amount}).id,
'noupdate': True,
})
foreign_taxes[tax_amount] = self.env['account.tax'].create({
'name': '%(rate)s%% %(country)s %(label)s' % {'rate': tax_amount, 'country': country.code, 'label': country.vat_label},
'amount': tax_amount,
'invoice_repartition_line_ids': invoice_repartition_lines,
'refund_repartition_line_ids': refund_repartition_lines,
'type_tax_use': 'sale',
'description': "%s%%" % tax_amount,
'tax_group_id': self.env.ref('l10n_eu_service.oss_tax_group_%s' % str(tax_amount).replace('.','_')).id,
'sequence': 1000,
'company_id': company.id,
})
mapping.append((0, 0, {'tax_src_id': domestic_tax.id, 'tax_dest_id': foreign_taxes[tax_amount].id}))
if mapping:
fpos.write({
'tax_ids': mapping
})
def _get_repartition_lines_oss(self):
self.ensure_one()
defaults = self.env['account.tax'].with_company(self).default_get(['invoice_repartition_line_ids', 'refund_repartition_line_ids'])
oss_account = self._get_oss_account()
if oss_account:
defaults['invoice_repartition_line_ids'][1][2]['account_id'] = oss_account.id
defaults['refund_repartition_line_ids'][1][2]['account_id'] = oss_account.id
return defaults['invoice_repartition_line_ids'], defaults['refund_repartition_line_ids']
def _get_oss_account(self):
self.ensure_one()
if not self.env['ir.model.data'].xmlid_to_object('l10n_eu_service.oss_tax_account_company_%s' % self.id):
sales_tax_accounts = self.env['account.tax'].search([
('type_tax_use', '=', 'sale'),
('company_id', '=', self.id)
]).invoice_repartition_line_ids.mapped('account_id')
if not sales_tax_accounts:
return False
new_code = self.env['account.account']._search_new_account_code(self, len(sales_tax_accounts[0].code), sales_tax_accounts[0].code[:-2])
oss_account = self.env['account.account'].create({
'name': '%s OSS' % sales_tax_accounts[0].name,
'code': new_code,
'user_type_id': sales_tax_accounts[0].user_type_id.id,
'company_id': self.id,
})
self.env['ir.model.data'].create({
'name': 'oss_tax_account_company_%s' % self.id,
'module': 'l10n_eu_service',
'model': 'account.account',
'res_id': oss_account.id,
'noupdate': True,
})
return self.env.ref('l10n_eu_service.oss_tax_account_company_%s' % self.id)
@@ -9,6 +9,11 @@ class ResConfigSettings(models.TransientModel):
l10n_eu_services_eu_country = fields.Boolean('Is European country?', compute='_compute_l10n_eu_services_european_country')
def refresh_eu_tax_mapping(self):
self.env.companies._map_eu_taxes()
@api.depends('company_id')
def _compute_l10n_eu_services_european_country(self):
self.l10n_eu_services_eu_country = self.company_id.country_id in self.env.ref('base.europe').country_ids
european_countries = self.env.ref('base.europe').country_ids
for record in self:
record.l10n_eu_services_eu_country = record.company_id.account_tax_fiscal_country_id in european_countries
@@ -10,10 +10,11 @@
<field name="l10n_eu_services_eu_country" invisible="1"/>
<div class="content-group" attrs="{'invisible': [('module_l10n_eu_service', '=', False)]}">
<div class="mt8" attrs="{'invisible': [('l10n_eu_services_eu_country', '=', False)]}">
<button name="%(l10n_eu_service.action_eu_service)d" icon="fa-arrow-right" type="action" string="Add new countries" class="btn-link"/>
<button type="object" name="refresh_eu_tax_mapping" icon="fa-refresh" string="Refresh tax mapping" class="btn-link"/>
</div>
</div>
</div>
</field>
</record>
</flectra>
</flectra>
-3
View File
@@ -49,7 +49,4 @@
<field name="target">new</field>
</record>
<record id="open_l10n_eu_service" model="ir.actions.todo">
<field name="action_id" ref="action_eu_service"/>
</record>
</flectra>
+1 -1
View File
@@ -19,7 +19,7 @@ The module adds following features:
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
Access to download the mandatory Certificate of Conformity delivered by FlectraHQ Inc.. (only for Flectra Enterprise users)
Access to download the mandatory Certificate of Conformity delivered by FlectraHQ Inc. (only for Flectra Enterprise users)
""",
'depends': ['l10n_fr', 'point_of_sale'],
'installable': True,
+1 -1
View File
@@ -6,7 +6,7 @@
'version': '1.0',
'category': 'Accounting/Localizations/Account Charts',
'description': """ This is the base module to manage chart of accounting and localization for Hong Kong """,
'author': 'FlectraHQ Inc..',
'author': 'FlectraHQ Inc.',
'depends': ['account'],
'data': [
'data/account_chart_template_data.xml',
+1 -1
View File
@@ -5,7 +5,7 @@
"name" : "Mongolia - Accounting",
"version" : "1.0",
'category': 'Accounting/Localizations/Account Charts',
"author" : "BumanIT LLC, FlectraHQ Inc.., Odoo S.A.",
"author" : "BumanIT LLC, FlectraHQ Inc., Odoo S.A.",
"description": """
This is the module to manage the accounting chart for Mongolia.
===============================================================
+1 -1
View File
@@ -8,7 +8,7 @@
{
'name' : 'Venezuela - Accounting',
'author': ['FlectraHQ Inc.., Odoo S.A.', 'Vauxoo'],
'author': ['FlectraHQ Inc., Odoo S.A.', 'Vauxoo'],
'category': 'Accounting/Localizations/Account Charts',
'description':
"""
+7 -4
View File
@@ -74,10 +74,7 @@ class LunchProductCategory(models.Model):
res = super().toggle_active()
Product = self.env['lunch.product'].with_context(active_test=False)
all_products = Product.search([('category_id', 'in', self.ids)])
for category in self:
all_products.filtered(
lambda p: p.category_id == category and p.active != category.active
).toggle_active()
all_products._sync_active_from_related()
return res
class LunchTopping(models.Model):
@@ -122,7 +119,13 @@ class LunchProduct(models.Model):
new_until = fields.Date('New Until')
favorite_user_ids = fields.Many2many('res.users', 'lunch_product_favorite_user_rel', 'product_id', 'user_id', check_company=True)
def _sync_active_from_related(self):
""" Archive/unarchive product after related field is archived/unarchived """
return self.filtered(lambda p: (p.category_id.active and p.supplier_id.active) != p.active).toggle_active()
def toggle_active(self):
if self.filtered(lambda product: not product.active and not product.category_id.active):
raise UserError(_("The product category is archived. The user have to unarchive the category or change the category of the product."))
if self.filtered(lambda product: not product.active and not product.supplier_id.active):
raise UserError(_("The product supplier is archived. The user have to unarchive the supplier or change the supplier of the product."))
return super().toggle_active()
+8
View File
@@ -103,6 +103,14 @@ class LunchSupplier(models.Model):
res.append((supplier.id, supplier.name))
return res
def toggle_active(self):
""" Archiving related lunch product """
res = super().toggle_active()
Product = self.env['lunch.product'].with_context(active_test=False)
all_products = Product.search([('supplier_id', 'in', self.ids)])
all_products._sync_active_from_related()
return res
@api.model
def _auto_email_send(self):
"""
+4 -1
View File
@@ -2354,7 +2354,10 @@ class MailThread(models.AbstractModel):
if add_sign:
signature = "<p>-- <br/>%s</p>" % author.name
company = self.company_id.sudo() if self and 'company_id' in self else user.company_id
# company value should fall back on env.company if:
# - no company_id field on record
# - company_id field available but not set
company = self.company_id.sudo() if self and 'company_id' in self and self.company_id else self.env.company
if company.website:
website_url = 'http://%s' % company.website if not company.website.lower().startswith(('http:', 'https:')) else company.website
else:
@@ -3,6 +3,8 @@ flectra.define('mail.activity_view_tests', function (require) {
var ActivityView = require('mail.ActivityView');
var testUtils = require('web.test_utils');
const ActivityRenderer = require('mail.ActivityRenderer');
const domUtils = require('web.dom');
var createActionManager = testUtils.createActionManager;
@@ -518,5 +520,43 @@ QUnit.test('Activity view: discard an activity creation dialog', async function
actionManager.destroy();
});
QUnit.test("Activity view: on_destroy_callback doesn't crash", async function (assert) {
assert.expect(3);
const params = {
View: ActivityView,
model: 'task',
data: this.data,
arch: '<activity string="Task">' +
'<templates>' +
'<div t-name="activity-box">' +
'<field name="foo"/>' +
'</div>' +
'</templates>'+
'</activity>',
};
ActivityRenderer.patch('test_mounted_unmounted', T =>
class extends T {
mounted() {
assert.step('mounted');
}
willUnmount() {
assert.step('willUnmount');
}
});
const activity = await createView(params);
domUtils.detach([{widget: activity}]);
assert.verifySteps([
'mounted',
'willUnmount'
]);
ActivityRenderer.unpatch('test_mounted_unmounted');
activity.destroy();
});
});
});
+4 -4
View File
@@ -385,12 +385,12 @@ class StockMove(models.Model):
# skip service since we never deliver them
if bom_line.product_id.type == 'service':
continue
if float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding):
# As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those
# to avoid a division by zero.
continue
bom_line_moves = self.filtered(lambda m: m.bom_line_id == bom_line)
if bom_line_moves:
if float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding):
# As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those
# to avoid a division by zero.
continue
# We compute the quantities needed of each components to make one kit.
# Then, we collect every relevant moves related to a specific component
# to know how many are considered delivered.
@@ -9,7 +9,7 @@
"node":"*"
},
"author":{
"name":"FlectraHQ, Inc., Odoo S.A. - Hitesh Trivedi",
"name":"FlectraHQ Inc., Odoo S.A. - Hitesh Trivedi",
"email":"thiteshm155@gmail.com"
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
'description': """
Auto-complete partner companies' data
""",
'author': "FlectraHQ Inc..",
'author': "FlectraHQ Inc.",
'category': 'Hidden/Tools',
'version': '1.0',
'depends': [
@@ -3,3 +3,5 @@ data_dir = /var/run/odoo
log_level = error
logfile = /var/log/odoo/odoo-server.log
pidfile = /var/run/odoo/odoo.pid
limit_time_cpu = 600
limit_time_real = 1200
@@ -32,7 +32,7 @@ function _start() {
while ! test -a /run/systemd/timesync/synchronized && ! ip route | grep 10.11.12.1 && ((cnt--)); do
sleep 1
done
start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=$MODULES --limit-time-cpu=600 --limit-time-real=1200
start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=$MODULES
}
function _stop() {
+2 -8
View File
@@ -53,14 +53,8 @@ class SaleReport(models.Model):
partner.country_id AS country_id,
partner.industry_id AS industry_id,
partner.commercial_partner_id AS commercial_partner_id,
(select sum(t.weight*l.qty/u.factor) from pos_order_line l
join product_product p on (l.product_id=p.id)
left join product_template t on (p.product_tmpl_id=t.id)
left join uom_uom u on (u.id=t.uom_id)) AS weight,
(select sum(t.volume*l.qty/u.factor) from pos_order_line l
join product_product p on (l.product_id=p.id)
left join product_template t on (p.product_tmpl_id=t.id)
left join uom_uom u on (u.id=t.uom_id)) AS volume,
(sum(t.weight) * l.qty / u.factor) AS weight,
(sum(t.volume) * l.qty / u.factor) AS volume,
l.discount as discount,
sum((l.price_unit * l.discount * l.qty / 100.0 / CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END)) as discount_amount,
NULL as order_id
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import test_pos_sale_report
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
import flectra
from flectra.addons.point_of_sale.tests.common import TestPoSCommon
@flectra.tests.tagged('post_install', '-at_install')
class TestPoSSaleReport(TestPoSCommon):
def setUp(self):
super(TestPoSSaleReport, self).setUp()
self.config = self.basic_config
self.product0 = self.create_product('Product 0', self.categ_basic, 0.0, 0.0)
def test_weight_and_volume(self):
self.product0.product_tmpl_id.weight = 3
self.product0.product_tmpl_id.volume = 4
self.open_new_session()
session = self.pos_session
orders = []
# Process two orders
orders.append(self.create_ui_order_data([(self.product0, 3)]))
orders.append(self.create_ui_order_data([(self.product0, 1)]))
self.env['pos.order'].create_from_ui(orders)
# Duplicate the first line of the first order
session.order_ids[0].lines.copy()
session.action_pos_session_closing_control()
# Orders are reversed, so reports[0] will correspond to the second order
reports = self.env['sale.report'].sudo().search([('product_id', '=', self.product0.id)], order='id desc', limit=2)
self.assertEqual(reports[0].weight, 3)
self.assertEqual(reports[0].volume, 4)
self.assertEqual(reports[1].weight, 18)
self.assertEqual(reports[1].volume, 24)
+20 -1
View File
@@ -198,6 +198,13 @@ class PurchaseOrder(models.Model):
if self.date_planned:
self.order_line.filtered(lambda line: not line.display_type).date_planned = self.date_planned
def write(self, vals):
vals, partner_vals = self._write_partner_values(vals)
res = super().write(vals)
if partner_vals:
self.partner_id.sudo().write(partner_vals) # Because the purchase user doesn't have write on `res.partner`
return res
@api.model
def create(self, vals):
company_id = vals.get('company_id', self.default_get(['company_id'])['company_id'])
@@ -208,7 +215,11 @@ class PurchaseOrder(models.Model):
if 'date_order' in vals:
seq_date = fields.Datetime.context_timestamp(self, fields.Datetime.to_datetime(vals['date_order']))
vals['name'] = self_comp.env['ir.sequence'].next_by_code('purchase.order', sequence_date=seq_date) or '/'
return super(PurchaseOrder, self_comp).create(vals)
vals, partner_vals = self._write_partner_values(vals)
res = super(PurchaseOrder, self_comp).create(vals)
if partner_vals:
res.sudo().write(partner_vals) # Because the purchase user doesn't have write on `res.partner`
return res
def unlink(self):
for order in self:
@@ -765,6 +776,14 @@ class PurchaseOrder(models.Model):
for line, date in updated_dates:
activity.note += _('<p> &nbsp; - %s from %s to %s </p>') % (line.product_id.display_name, line.date_planned.date(), date.date())
def _write_partner_values(self, vals):
partner_values = {}
if 'receipt_reminder_email' in vals:
partner_values['receipt_reminder_email'] = vals.pop('receipt_reminder_email')
if 'reminder_date_before_receipt' in vals:
partner_values['reminder_date_before_receipt'] = vals.pop('reminder_date_before_receipt')
return vals, partner_values
class PurchaseOrderLine(models.Model):
_name = 'purchase.order.line'
+5 -5
View File
@@ -144,9 +144,9 @@
<thead class="bg-100">
<tr>
<th>Products</th>
<th class="text-right d-none d-sm-table-cell">Unit Price</th>
<th class="text-right d-none d-sm-table-cell" t-if="order.state in ['purchase', 'done']">Unit Price</th>
<th class="text-right">Quantity</th>
<th class="text-right">Subtotal</th>
<th class="text-right" t-if="order.state in ['purchase', 'done']">Subtotal</th>
</tr>
</thead>
<tbody>
@@ -157,13 +157,13 @@
<img t-att-src="image_data_uri(resize_to_48(ol.product_id.image_1024))" alt="Product" class="d-none d-lg-inline"/>
<span t-esc="ol.name"/>
</td>
<td class="text-right d-none d-sm-table-cell">
<td class="text-right d-none d-sm-table-cell" t-if="order.state in ['purchase', 'done']">
<span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
<td class="text-right">
<span t-esc="ol.product_qty"/>
</td>
<td class="text-right">
<td class="text-right" t-if="order.state in ['purchase', 'done']">
<span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
</td>
</t>
@@ -183,7 +183,7 @@
</t>
</tbody>
</table>
<div class="row">
<div class="row" t-if="order.state in ['purchase', 'done']">
<div class="col-sm-7 col-md-5 ml-auto">
<table class="table table-sm">
<tbody>
@@ -48,7 +48,7 @@ class PurchaseOrder(models.Model):
for line in requisition.line_ids:
# Compute name
product_lang = line.product_id.with_context(
lang=partner.lang,
lang=partner.lang or self.env.user.lang,
partner_id=partner.id
)
name = product_lang.display_name
-1
View File
@@ -470,7 +470,6 @@ class PurchaseOrderLine(models.Model):
'origin': self.order_id.name,
'description_picking': description_picking,
'propagate_cancel': self.propagate_cancel,
'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids])] or [],
'warehouse_id': self.order_id.picking_type_id.warehouse_id.id,
'product_uom_qty': product_uom_qty,
'product_uom': product_uom.id,
+3 -2
View File
@@ -310,9 +310,10 @@ class StockRule(models.Model):
)
if values.get('orderpoint_id'):
procurement_date = fields.Date.to_date(values['date_planned']) - relativedelta(days=int(values['supplier'].delay) + company_id.po_lead)
delta_days = int(self.env['ir.config_parameter'].get_param('purchase_stock.delta_days_merge') or 0)
domain += (
('date_order', '<=', datetime.combine(procurement_date, datetime.max.time())),
('date_order', '>=', datetime.combine(procurement_date, datetime.min.time()))
('date_order', '<=', datetime.combine(procurement_date + relativedelta(days=delta_days), datetime.max.time())),
('date_order', '>=', datetime.combine(procurement_date - relativedelta(days=delta_days), datetime.min.time()))
)
if group:
domain += (('group_id', '=', group.id),)
+1 -1
View File
@@ -461,7 +461,7 @@ class SaleOrder(models.Model):
}
}
@api.onchange('pricelist_id')
@api.onchange('pricelist_id', 'order_line')
def _onchange_pricelist_id(self):
if self.order_line and self.pricelist_id and self._origin.pricelist_id != self.pricelist_id:
self.show_update_pricelist = True
+1 -1
View File
@@ -41,7 +41,7 @@ class CouponProgram(models.Model):
)}
elif self.promo_code and self.promo_code == order.promo_code:
message = {'error': _('The promo code is already applied on this order')}
elif not self.promo_code and self in order.no_code_promo_program_ids:
elif self in order.no_code_promo_program_ids:
message = {'error': _('The promotional offer is already applied on this order')}
elif not self.active:
message = {'error': _('Promo code is invalid')}
@@ -302,3 +302,44 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
self.assertEqual(len(order_bis.order_line), 2, "You should get 1 regular product_B and 1 free product_B")
order_bis.recompute_coupon_lines()
self.assertEqual(len(order_bis.order_line), 2, "Free product from a coupon generated from a promotion program on next order should not dissapear")
def test_edit_and_reapply_promotion_program(self):
# The flow:
# 1. Create a program auto applied, giving a fixed amount discount
# 2. Create a SO and apply the program
# 3. Change the program, requiring a mandatory code
# 4. Reapply the program on the same SO via code
# 1.
self.p1 = self.env['coupon.program'].create({
'name': 'Promo fixed amount',
'promo_code_usage': 'no_code_needed',
'discount_type': 'fixed_amount',
'discount_fixed_amount': 10.0,
'program_type': 'promotion_program',
})
# 2.
order = self.empty_order.copy()
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line), 2, "You should get a discount line")
# 3.
self.p1.write({
'promo_code_usage': 'code_needed',
'promo_code': 'test',
})
order.recompute_coupon_lines()
# 4.
with self.assertRaises(UserError):
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': 'test'
}).process_coupon()
self.assertEqual(len(order.order_line), 2, "You should get a discount line")
+1 -1
View File
@@ -250,7 +250,7 @@ class ProjectTask(models.Model):
if timesheet.so_line == timesheet.task_id.sale_line_id:
delta -= timesheet.unit_amount
if delta:
mapped_remaining_hours[timesheet.task_id._origin.id] += timesheet.so_line.product_uom._compute_quantity(delta, uom_hour)
mapped_remaining_hours[timesheet.task_id._origin.id] += timesheet.product_uom_id._compute_quantity(delta, uom_hour)
for task in self:
task.remaining_hours_so = mapped_remaining_hours[task._origin.id]
+1 -1
View File
@@ -8,7 +8,7 @@
The purpose of this technical module is to provide a front for
social media configuration for any other module that might need it.
""",
'author': "FlectraHQ Inc.., Odoo S.A.",
'author': "FlectraHQ Inc., Odoo S.A.",
'category': 'Hidden',
'version': '0.1',
'depends': ['base'],
+2 -2
View File
@@ -433,7 +433,7 @@ Résultat de recherche d'images pour "dessin la princesse au petit pois"
--
Soup
FlectraHQ, Inc., Odoo S.A.
FlectraHQ Inc., Odoo S.A.
Chaussée de Namur, 40
B-1367 Grand Rosière
Web: http://www.flectrahq.com
@@ -456,7 +456,7 @@ src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQjCNAadd3NDM8g9w0P_-g
<pre class="moz-signature" cols="72">--
Soup
FlectraHQ, Inc., Odoo S.A.
FlectraHQ Inc., Odoo S.A.
Chaussée de Namur, 40
B-1367 Grand Rosière
Web: <a class="moz-txt-link-freetext" href="http://www.flectrahq.com">http://www.flectrahq.com</a> </pre>
+5 -2
View File
@@ -337,15 +337,17 @@ class TestActivityMixin(TestActivityCommon):
@mute_logger('flectra.addons.mail.models.mail_mail')
def test_my_activity_flow_employee(self):
Activity = self.env['mail.activity']
date_today = date.today()
activity_1 = Activity.create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
'date_deadline': date_today,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': self.test_record.id,
'user_id': self.user_admin.id,
})
activity_2 = Activity.create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_call').id,
'date_deadline': date.today() + relativedelta(days=1),
'date_deadline': date_today + relativedelta(days=1),
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': self.test_record.id,
'user_id': self.user_employee.id,
@@ -354,10 +356,11 @@ class TestActivityMixin(TestActivityCommon):
test_record_1 = self.env['mail.test.activity'].with_context(self._test_context).create({'name': 'Test 1'})
activity_3 = Activity.create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id,
'date_deadline': date_today,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': test_record_1.id,
'user_id': self.user_employee.id,
})
with self.with_user('employee'):
record = self.env['mail.test.activity'].search([('my_activity_date_deadline', '=', datetime.today())])
record = self.env['mail.test.activity'].search([('my_activity_date_deadline', '=', date_today)])
self.assertEqual(test_record_1, record)
@@ -12,6 +12,7 @@ from flectra.api import call_kw
from flectra.exceptions import AccessError
from flectra.tests import tagged
from flectra.tools import mute_logger, formataddr
from flectra.tests.common import users
@tagged('mail_post')
@@ -35,6 +36,43 @@ class TestMessagePost(TestMailCommon, TestRecipients):
messageId = call_kw(self.env['mail.channel'], 'message_post', [test_channel.id], {'body': 'test'})
self.assertTrue(isinstance(messageId, int))
@users('employee')
def test_notify_prepare_template_context_company_value(self):
""" Verify that the template context company value is right
after switching the env company or if a company_id is set
on mail record.
"""
current_user = self.env.user
main_company = current_user.company_id
other_company = self.env['res.company'].with_user(self.user_admin).create({'name': 'Company B'})
current_user.sudo().write({'company_ids': [(4, other_company.id)]})
test_record = self.env['mail.test.multi.company'].with_user(self.user_admin).create({
'name': 'Multi Company Record',
'company_id': False,
})
# self.env.company.id = Main Company AND test_record.company_id = False
self.assertEqual(self.env.company.id, main_company.id)
self.assertEqual(test_record.company_id.id, False)
template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
self.assertEqual(template_values.get('company').id, self.env.company.id)
# self.env.company.id = Other Company AND test_record.company_id = False
current_user.company_id = other_company
test_record = self.env['mail.test.multi.company'].browse(test_record.id)
self.assertEqual(self.env.company.id, other_company.id)
self.assertEqual(test_record.company_id.id, False)
template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
self.assertEqual(template_values.get('company').id, self.env.company.id)
# self.env.company.id = Other Company AND test_record.company_id = Main Company
test_record.company_id = main_company
test_record = self.env['mail.test.multi.company'].browse(test_record.id)
self.assertEqual(self.env.company.id, other_company.id)
self.assertEqual(test_record.company_id.id, main_company.id)
template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
self.assertEqual(template_values.get('company').id, main_company.id)
def test_notify_recipients_internals(self):
pdata = self._generate_notify_recipients(self.partner_1 | self.partner_employee)
msg_vals = {
@@ -119,6 +119,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
on_attach_callback: function () {
ActionMixin.on_attach_callback.call(this);
this.searchModel.on('search', this, this._onSearch);
this.searchModel.trigger('focus-control-panel');
if (this.withControlPanel) {
this.searchModel.on('get-controller-query-params', this, this._onGetOwnedQueryParams);
}
@@ -89,6 +89,15 @@ return mvc.Renderer.extend({
*/
giveFocus: function () {
},
/**
* Resets state that renderer keeps, state may contains scroll position,
* the currently active tab page, ...
*
* @see getLocalState
* @see setLocalState
*/
resetLocalState() {
},
/**
* This is the reverse operation from getLocalState. With this method, we
* expect the renderer to restore all DOM state, if it is relevant.
@@ -63,6 +63,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
this.$el.toggleClass('o_cannot_create', !this.activeActions.create);
await this._super(...arguments);
},
/**
* Called each time the controller is dettached into the DOM
*/
on_detach_callback() {
this._super.apply(this, arguments);
this.renderer.resetLocalState();
},
//--------------------------------------------------------------------------
// Public
@@ -227,6 +227,21 @@ var FormRenderer = BasicRenderer.extend({
resetLastActivatedField: function () {
this.lastActivatedFieldIndex = -1;
},
/**
* Resets state which stores information like scroll position, curently
* active page, ...
*
* @override
*/
resetLocalState() {
for (const notebook of this.el.querySelectorAll(':scope div.o_notebook')) {
[...notebook.querySelectorAll(':scope .o_notebook_headers .nav-item .nav-link')]
.map(nav => nav.classList.remove('active'));
[...notebook.querySelectorAll(':scope .tab-content > .tab-pane')]
.map(tab => tab.classList.remove('active'));
}
},
/**
* Restore active tab pages for each notebook. It relies on the implicit fact
* that each nav header corresponds to a tab page.
@@ -390,7 +390,7 @@ var ListController = BasicController.extend({
*/
_getExportDialogWidget() {
let state = this.model.get(this.handle);
let defaultExportFields = this.renderer.columns.filter(field => field.tag === 'field').map(field => field.attrs.name);
let defaultExportFields = this.renderer.columns.filter(field => field.tag === 'field' && state.fields[field.attrs.name].exportable !== false).map(field => field.attrs.name);
let groupedBy = this.renderer.state.groupedBy;
const domain = this.isDomainSelected && state.getDomain();
return new DataExport(this, state, defaultExportFields, groupedBy,
@@ -7,6 +7,7 @@ flectra.define('web.RendererWrapper', function (require) {
getLocalState() { }
setLocalState() { }
giveFocus() { }
resetLocalState() { }
}
return RendererWrapper;
-1
View File
@@ -114,7 +114,6 @@
flex: 0 0 auto;
border: 0;
font-size: 12px;
cursor: pointer;
user-select: none;
display: flex;
max-width: 100%;
+1 -1
View File
@@ -622,7 +622,7 @@ $o-cw-filter-avatar-size: 20px;
}
.o_cw_filter_title {
line-height: 1;
line-height: $o-line-height-base;
flex-grow: 1;
}
@@ -666,5 +666,37 @@ flectra.define('web.search_bar_tests', function (require) {
actionManager.destroy();
});
QUnit.test('focus should be on search bar when switching between views', async function (assert) {
assert.expect(4);
this.actions[0].views = [[false, 'list'], [false, 'form']];
this.archs['partner,false,form'] = `
<form>
<group>
<field name="display_name"/>
</group>
</form>`;
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(1);
assert.containsOnce(actionManager, '.o_list_view');
assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'),
"searchview should have focus");
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_cell:first'));
assert.containsOnce(actionManager, '.o_form_view');
await testUtils.dom.click(actionManager.$('.o_back_button'));
assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'),
"searchview should have focus");
actionManager.destroy();
});
});
});
@@ -1025,6 +1025,55 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('reset local state when switching to another view', async function (assert) {
assert.expect(3);
const actionManager = await createActionManager({
data: this.data,
archs: {
'partner,false,form': `<form>
<sheet>
<field name="product_id"/>
<notebook>
<page string="Foo">
<field name="foo"/>
</page>
<page string="Bar">
<field name="bar"/>
</page>
</notebook>
</sheet>
</form>`,
'partner,false,list': '<tree><field name="foo"/></tree>',
'partner,false,search': '<search></search>',
},
actions: [{
id: 1,
name: 'Partner',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
}],
});
await actionManager.doAction(1);
await testUtils.dom.click(actionManager.$('.o_list_button_add'));
assert.containsOnce(actionManager, '.o_form_view');
// click on second page tab
await testUtils.dom.click(actionManager.$('.o_notebook .nav-link:eq(1)'));
await testUtils.dom.click('.o_control_panel .o_form_button_cancel');
assert.containsNone(actionManager, '.o_form_view');
await testUtils.dom.click(actionManager.$('.o_list_button_add'));
// check notebook active page is 0th page
assert.hasClass(actionManager.$('.o_notebook .nav-link:eq(0)'), 'active');
actionManager.destroy();
});
QUnit.test('rendering stat buttons', async function (assert) {
assert.expect(3);
@@ -16,6 +16,7 @@ QUnit.module('widgets', {
fields: {
foo: {string: "Foo", type: "char"},
bar: {string: "Bar", type: "char"},
unexportable: {string: "Unexportable", type: "boolean", exportable: false},
},
records: [
{
@@ -193,6 +194,35 @@ QUnit.module('widgets', {
list.destroy();
});
QUnit.test('exporting view with non-exportable field', async function (assert) {
assert.expect(0);
var list = await createView({
View: ListView,
model: 'partner',
data: this.data,
arch: '<tree><field name="unexportable"/></tree>',
viewOptions: {
hasActionMenus: true,
},
mockRPC: this.mockDataExportRPCs,
session: {
...this.mockSession,
get_file: function (params) {
assert.step(params.url);
params.complete();
},
},
});
await testUtils.dom.click(list.$('thead th.o_list_record_selector input'));
await cpHelpers.toggleActionMenu(list);
await cpHelpers.toggleMenuItem(list, 'Export');
list.destroy();
});
QUnit.test('saving fields list when exporting data', async function (assert) {
assert.expect(4);
+1 -1
View File
@@ -163,7 +163,7 @@
</div>
</div>
<div class="form-group row">
<label for="demo" class="col-md-4 col-form-label">Demo data</label>
<label for="load_demo_checkbox" class="col-md-4 col-form-label">Demo data</label>
<div class="col-md-8">
<input type="checkbox" id="load_demo_checkbox" class="form-control-sm" name="demo" value="1">
</div>
@@ -286,6 +286,7 @@ eventHandler.modules.popover.button.update = function ($container, oStyle) {
$container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x"));
$container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x"));
$container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length);
$container.find('button[data-event="cropImage"]').addClass('d-none');
$container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin"));
$container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));
@@ -293,6 +294,7 @@ eventHandler.modules.popover.button.update = function ($container, oStyle) {
} else {
$container.find('.d-none:not(.only_fa, .note-recent-color)').removeClass('d-none');
$container.find('button[data-event="cropImage"]').removeClass('d-none');
$container.find('.only_fa').addClass('d-none');
var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/);
if (width) {
@@ -1050,7 +1050,7 @@ var SnippetsMenu = Widget.extend({
// Active snippet editor on click in the page
var lastElement;
this.$document.on('click.snippets_menu', '*', ev => {
const onClick = ev => {
var srcElement = ev.target || (ev.originalEvent && (ev.originalEvent.target || ev.originalEvent.originalTarget)) || ev.srcElement;
if (!srcElement || lastElement === srcElement) {
return;
@@ -1078,7 +1078,11 @@ var SnippetsMenu = Widget.extend({
return;
}
this._activateSnippet($target);
});
};
this.$document.on('click.snippets_menu', '*', onClick);
// Needed as bootstrap stop the propagation of click events for dropdowns
this.$document.on('mouseup.snippets_menu', '.dropdown-toggle', onClick);
core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet);
@@ -1123,7 +1127,7 @@ var SnippetsMenu = Widget.extend({
editor.cover();
}
}, 250);
}, 50)
}, 50);
// We use addEventListener instead of jQuery because we need 'capture'.
// Setting capture to true allows to take advantage of event bubbling
// for events that otherwise don’t support it. (e.g. useful when
@@ -1766,7 +1770,8 @@ var SnippetsMenu = Widget.extend({
.addClass('oe_snippet')
.each((i, el) => {
const $snippet = $(el);
const name = el.getAttribute('name');
const name = _.escape(el.getAttribute('name'));
const thumbnailSrc = _.escape(el.dataset.oeThumbnail);
const $sbody = $snippet.children().addClass('oe_snippet_body');
const isCustomSnippet = !!el.closest('#snippet_custom');
@@ -1783,7 +1788,7 @@ var SnippetsMenu = Widget.extend({
// Create the thumbnail
const $thumbnail = $(`
<div class="oe_snippet_thumbnail">
<div class="oe_snippet_thumbnail_img" style="background-image: url(${el.dataset.oeThumbnail});"/>
<div class="oe_snippet_thumbnail_img" style="background-image: url(${thumbnailSrc});"/>
<span class="oe_snippet_thumbnail_title">${name}</span>
</div>
`);
@@ -1447,10 +1447,11 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
*/
_onClick: function (ev) {
// Do not close the colorpalette on colorpicker click
if (!ev.originalEvent.__isColorpickerClick) {
this._super(...arguments);
if (ev.originalEvent.__isColorpickerClick) {
ev.stopPropagation();
return;
}
ev.stopPropagation();
return this._super(...arguments);
},
});
+2 -2
View File
@@ -282,8 +282,8 @@ class Website(Home):
suggested_controllers = []
for name, url, mod in current_website.get_suggested_controllers():
if needle.lower() in name.lower() or needle.lower() in url.lower():
module = mod and request.env.ref('base.module_%s' % mod, False)
icon = mod and "<img src='%s' width='24px' class='mr-2 rounded' /> " % (module and module.icon or mod) or ''
module_sudo = mod and request.env.ref('base.module_%s' % mod, False).sudo()
icon = mod and "<img src='%s' width='24px' class='mr-2 rounded' /> " % (module_sudo and module_sudo.icon or mod) or ''
suggested_controllers.append({
'value': url,
'label': '%s%s (%s)' % (icon, url, name),
+2 -1
View File
@@ -305,7 +305,8 @@ class Http(models.AbstractModel):
req_page = request.httprequest.path
domain = [
('redirect_type', 'in', ('301', '302')),
('url_from', '=', req_page)
# trailing / could have been removed by server_page
'|', ('url_from', '=', req_page.rstrip('/')), ('url_from', '=', req_page + '/')
]
domain += request.website.website_domain()
return request.env['website.rewrite'].sudo().search(domain, limit=1)
@@ -20,6 +20,14 @@ const ShareWidget = publicWidget.Widget.extend({
return href.replace(urlRegex, function (match, a, b, c) {
return a + url + c;
}).replace(titleRegex, function (match, a, b, c) {
if ($a.hasClass('s_share_whatsapp')) {
// WhatsApp does not support the "url" GET parameter.
// Instead we need to include the url within the passed "text" parameter, merging everything together.
// e.g of output:
// https://wa.me/?text=%20OpenWood%20Collection%20Online%20Reveal%20%7C%20My%20Website%20http%3A%2F%2Flocalhost%3A8888%2Fevent%2Fopenwood-collection-online-reveal-2021-06-21-2021-06-23-8%2Fregister
// see https://faq.whatsapp.com/general/chats/how-to-use-click-to-chat/ for more details
return a + title + url + c;
}
return a + title + c;
});
});
+1 -1
View File
@@ -13,7 +13,7 @@
<a href="http://www.linkedin.com/sharing/share-offsite/?url={url}" t-attf-class="s_share_linkedin #{_link_classes}" target="_blank">
<i t-attf-class="fa fa-linkedin #{not _link_classes and 'rounded shadow-sm'}"/>
</a>
<a href="whatsapp://send?text={title}&amp;url={url}" t-attf-class="s_share_whatsapp #{_link_classes}" target="_blank">
<a href="https://wa.me/?text={title}" t-attf-class="s_share_whatsapp #{_link_classes}" target="_blank">
<i t-attf-class="fa fa-whatsapp #{not _link_classes and 'rounded shadow-sm'}"/>
</a>
<a href="http://pinterest.com/pin/create/button/?url={url}&amp;description={title}" t-attf-class="s_share_pinterest #{_link_classes}" target="_blank">
@@ -21,7 +21,7 @@ flectra.define('website_crm.tour', function(require) {
}, {
content: "Complete Company",
trigger: "input[name=partner_name]",
run: "text FlectraHQ, Inc., Odoo S.A."
run: "text FlectraHQ Inc., Odoo S.A."
}, {
content: "Complete Subject",
trigger: "input[name=name]",
+1 -1
View File
@@ -15,4 +15,4 @@ class TestWebsiteCrm(flectra.tests.HttpCase):
self.assertEqual(len(record), 1)
self.assertEqual(record.contact_name, 'John Smith')
self.assertEqual(record.email_from, 'john@smith.com')
self.assertEqual(record.partner_name, 'FlectraHQ, Inc., Odoo S.A.')
self.assertEqual(record.partner_name, 'FlectraHQ Inc., Odoo S.A.')
@@ -138,7 +138,7 @@ Camptocamp has developed a strong expertise in the following areas:
<li>PrestaShop Flectra connector;</li></ul></li>
<li>development and implementation of business applications 100% specific leveraging the power of the Flectra framework (OpenObject).</li></ul>
<br />
In addition, thanks to its heavy involvement in the ecosystem Flectra for several years, has been promoted by Camptocamp Editor FlectraHQ, Inc. status Flectra best contributor in 2012 and 2013.<br/>
In addition, thanks to its heavy involvement in the ecosystem Flectra for several years, has been promoted by Camptocamp Editor FlectraHQ Inc. status Flectra best contributor in 2012 and 2013.<br/>
]]></field>
</record>
+1 -1
View File
@@ -291,7 +291,7 @@ class ProductTemplate(models.Model):
product = self.env['product.product'].browse(combination_info['product_id']) or self
tax_display = self.user_has_groups('account.group_show_line_subtotals_tax_excluded') and 'total_excluded' or 'total_included'
fpos = self.env['account.fiscal.position'].get_fiscal_position(partner.id).sudo()
fpos = self.env['account.fiscal.position'].sudo().get_fiscal_position(partner.id)
taxes = fpos.map_tax(product.sudo().taxes_id.filtered(lambda x: x.company_id == company_id), product, partner)
# The list_price is always the price of one.
@@ -10,7 +10,7 @@ To configure product attributes, activate *Attributes & Variants* in the Website
Finally, the module comes with an option to display an attribute summary table in product web pages (available in Customize menu).
""",
'author': 'FlectraHQ Inc..',
'author': 'FlectraHQ Inc.',
'category': 'Website/Website',
'version': '1.0',
'depends': ['website_sale'],
+1 -1
View File
@@ -6,7 +6,7 @@
'description': """
Allow shoppers of your eCommerce store to create personalized collections of products they want to buy and save them for future reference.
""",
'author': 'FlectraHQ Inc..',
'author': 'FlectraHQ Inc.',
'category': 'Website/Website',
'version': '1.0',
'depends': ['website_sale'],