mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-10-24 23:17:39 -05:00
Master upstream patch
This commit is contained in:
1 parent
5b378526e9
commit
ed061991dd
74 files changed
+2637
-122
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -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');")
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
===============================================================
|
||||
|
||||
@@ -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':
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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> - %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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
flex: 0 0 auto;
|
||||
border: 0;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}&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}&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]",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in new issue
Block a user