[MERGE] forward port branch saas-12.4 up to 9ed4872ea0
closes odoo/odoo#37372 Signed-off-by: Christophe Simonis <chs@odoo.com>
This commit is contained in:
@@ -1220,10 +1220,15 @@ class AccountMove(models.Model):
|
||||
lang_env = move.with_context(lang=move.partner_id.lang).env
|
||||
tax_lines = move.line_ids.filtered(lambda line: line.tax_line_id)
|
||||
res = {}
|
||||
# There are as many tax line as there are repartition lines
|
||||
done_taxes = set()
|
||||
for line in tax_lines:
|
||||
res.setdefault(line.tax_line_id.tax_group_id, {'base': 0.0, 'amount': 0.0})
|
||||
res[line.tax_line_id.tax_group_id]['amount'] += line.price_subtotal
|
||||
res[line.tax_line_id.tax_group_id]['base'] += line.tax_base_amount
|
||||
if line.tax_line_id.id not in done_taxes:
|
||||
# The base should be added ONCE
|
||||
res[line.tax_line_id.tax_group_id]['base'] += line.tax_base_amount
|
||||
done_taxes.add(line.tax_line_id.id)
|
||||
res = sorted(res.items(), key=lambda l: l[0].sequence)
|
||||
move.amount_by_group = [(
|
||||
group.name, amounts['amount'],
|
||||
@@ -1315,7 +1320,7 @@ class AccountMove(models.Model):
|
||||
self.env['account.move.line'].flush(['debit', 'credit', 'move_id'])
|
||||
self.env['account.move'].flush(['journal_id'])
|
||||
self._cr.execute('''
|
||||
SELECT line.move_id
|
||||
SELECT line.move_id, ROUND(SUM(debit - credit), currency.decimal_places)
|
||||
FROM account_move_line line
|
||||
JOIN account_move move ON move.id = line.move_id
|
||||
JOIN account_journal journal ON journal.id = move.journal_id
|
||||
@@ -1329,7 +1334,8 @@ class AccountMove(models.Model):
|
||||
query_res = self._cr.fetchall()
|
||||
if query_res:
|
||||
ids = [res[0] for res in query_res]
|
||||
raise UserError(_("Cannot create unbalanced journal entry. Ids: %s") % str(ids))
|
||||
sums = [res[1] for res in query_res]
|
||||
raise UserError(_("Cannot create unbalanced journal entry. Ids: %s\nDifferences debit - credit: %s") % (ids, sums))
|
||||
|
||||
def _check_fiscalyear_lock_date(self):
|
||||
for move in self:
|
||||
@@ -2060,7 +2066,6 @@ class AccountMove(models.Model):
|
||||
"""
|
||||
if any(not move.is_invoice(include_receipts=True) for move in self):
|
||||
raise UserError(_("Only invoices could be printed."))
|
||||
self._check_tax_lock_date()
|
||||
|
||||
self.filtered(lambda inv: not inv.invoice_sent).write({'invoice_sent': True})
|
||||
if self.user_has_groups('account.group_account_invoice'):
|
||||
@@ -2309,7 +2314,7 @@ class AccountMoveLine(models.Model):
|
||||
),
|
||||
(
|
||||
'check_accountable_required_fields',
|
||||
"CHECK(display_type IN ('line_section', 'line_note') OR account_id IS NOT NULL)",
|
||||
"CHECK(COALESCE(display_type IN ('line_section', 'line_note'), 'f') OR account_id IS NOT NULL)",
|
||||
"Missing required account on accountable invoice line."
|
||||
),
|
||||
(
|
||||
@@ -2794,7 +2799,7 @@ class AccountMoveLine(models.Model):
|
||||
currency = record.company_id.currency_id
|
||||
audit_str = ''
|
||||
for tag in record.tag_ids:
|
||||
tag_amount = (tag.tax_negate and -1 or 1) * (record.move_id.is_outbound() and -1 or 1) * record.balance
|
||||
tag_amount = (tag.tax_negate and -1 or 1) * (record.move_id.is_inbound() and -1 or 1) * record.balance
|
||||
|
||||
if tag.tax_report_line_ids:
|
||||
#Then, the tag comes from a report line, and hence has a + or - sign (also in its name)
|
||||
|
||||
@@ -265,6 +265,7 @@ class AccountReconcileModel(models.Model):
|
||||
'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
|
||||
'debit': line_balance > 0 and line_balance or 0,
|
||||
'credit': line_balance < 0 and -line_balance or 0,
|
||||
'reconcile_model_id': self.id,
|
||||
}
|
||||
new_aml_dicts.append(writeoff_line)
|
||||
|
||||
@@ -298,6 +299,7 @@ class AccountReconcileModel(models.Model):
|
||||
'analytic_tag_ids': [(6, 0, self.second_analytic_tag_ids.ids)],
|
||||
'debit': line_balance > 0 and line_balance or 0,
|
||||
'credit': line_balance < 0 and -line_balance or 0,
|
||||
'reconcile_model_id': self.id,
|
||||
}
|
||||
new_aml_dicts.append(second_writeoff_line)
|
||||
|
||||
|
||||
@@ -556,10 +556,8 @@ class AccountReconciliation(models.AbstractModel):
|
||||
])
|
||||
|
||||
domain = expression.OR([domain_reconciliation, domain_matching])
|
||||
partner_domain = []
|
||||
if partner_id:
|
||||
partner_domain = [('partner_id', '=', partner_id)]
|
||||
domain = expression.AND([domain, partner_domain])
|
||||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
|
||||
if mode == 'rp':
|
||||
domain = expression.AND([domain,
|
||||
[('account_id.internal_type', 'in', ['receivable', 'payable', 'liquidity'])]
|
||||
@@ -572,14 +570,10 @@ class AccountReconciliation(models.AbstractModel):
|
||||
# Domain factorized for all reconciliation use cases
|
||||
if search_str:
|
||||
str_domain = self._domain_move_lines(search_str=search_str)
|
||||
if not partner_id:
|
||||
partner_domain = [('partner_id.name', 'ilike', search_str)]
|
||||
|
||||
str_domain = expression.OR([
|
||||
str_domain,
|
||||
partner_domain,
|
||||
[('partner_id.name', 'ilike', search_str)]
|
||||
])
|
||||
|
||||
domain = expression.AND([
|
||||
domain,
|
||||
str_domain
|
||||
@@ -596,7 +590,6 @@ class AccountReconciliation(models.AbstractModel):
|
||||
|
||||
if st_line.company_id.account_bank_reconciliation_start:
|
||||
domain = expression.AND([domain, [('date', '>=', st_line.company_id.account_bank_reconciliation_start)]])
|
||||
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -12,8 +12,12 @@ class Users(models.Model):
|
||||
def _check_one_user_type(self):
|
||||
super(Users, self)._check_one_user_type()
|
||||
|
||||
g1 = self.env.ref('account.group_show_line_subtotals_tax_included')
|
||||
g2 = self.env.ref('account.group_show_line_subtotals_tax_excluded')
|
||||
g1 = self.env.ref('account.group_show_line_subtotals_tax_included', False)
|
||||
g2 = self.env.ref('account.group_show_line_subtotals_tax_excluded', False)
|
||||
|
||||
if not g1 or not g2:
|
||||
# A user cannot be in a non-existant group
|
||||
return
|
||||
|
||||
if self._has_multiple_groups([g1.id, g2.id]):
|
||||
raise ValidationError(_("A user cannot have both Tax B2B and Tax B2C.\n"
|
||||
|
||||
@@ -83,12 +83,16 @@ class TestAccountNoChartCommon(SavepointCase):
|
||||
'code': 'AJ-PURC',
|
||||
'type': 'purchase',
|
||||
'company_id': cls.env.user.company_id.id,
|
||||
'default_debit_account_id': cls.account_expense.id,
|
||||
'default_credit_account_id': cls.account_expense.id,
|
||||
})
|
||||
cls.journal_sale = cls.env['account.journal'].create({
|
||||
'name': 'Sale Journal - Test',
|
||||
'code': 'AJ-SALE',
|
||||
'type': 'sale',
|
||||
'company_id': cls.env.user.company_id.id,
|
||||
'default_debit_account_id': cls.account_income.id,
|
||||
'default_credit_account_id': cls.account_income.id,
|
||||
})
|
||||
cls.journal_general = cls.env['account.journal'].create({
|
||||
'name': 'General Journal - Test',
|
||||
|
||||
@@ -60,6 +60,7 @@ class TestAccountMove(InvoiceTestCommon):
|
||||
self.env['account.move'].create(self.test_move.copy_data())
|
||||
|
||||
def test_misc_tax_lock_date_1(self):
|
||||
self.test_move.post()
|
||||
# Set the tax lock date after the journal entry date.
|
||||
self.test_move.company_id.tax_lock_date = fields.Date.from_string('2017-01-01')
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMovePartnerCount(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -14,6 +16,13 @@ class TestAccountMovePartnerCount(TransactionCase):
|
||||
with self.registry.cursor() as cr:
|
||||
env = self.env(cr=cr)
|
||||
self.partner = self.env['res.partner'].with_env(env).create({'name': 'Lucien'})
|
||||
user_type_income = env.ref('account.data_account_type_direct_costs')
|
||||
self.account_income = self.env['account.account'].with_env(env).create({
|
||||
'code': 'TESTCOUNT42',
|
||||
'name': 'Sale - Test Account',
|
||||
'user_type_id': user_type_income.id
|
||||
})
|
||||
|
||||
self.journal = self.env['account.journal'].with_env(env).create({
|
||||
'name': 'Journal',
|
||||
'code': '7775',
|
||||
@@ -28,6 +37,7 @@ class TestAccountMovePartnerCount(TransactionCase):
|
||||
self.line = self.env['account.move.line'].with_env(env).create({
|
||||
'move_id': self.move.id,
|
||||
'name': 'account move line test',
|
||||
'account_id': self.account_income.id,
|
||||
})
|
||||
|
||||
def tearDown(self):
|
||||
@@ -38,6 +48,7 @@ class TestAccountMovePartnerCount(TransactionCase):
|
||||
move.state = 'draft'
|
||||
move.unlink()
|
||||
self.journal.with_env(env).unlink()
|
||||
self.account_income.with_env(env).unlink()
|
||||
self.partner.with_env(env).unlink()
|
||||
|
||||
def test_account_move_count(self):
|
||||
|
||||
@@ -57,6 +57,7 @@ class TestReconciliationWidget(TestReconciliation):
|
||||
def test_filter_partner1(self):
|
||||
inv1 = self.create_invoice(currency_id=self.currency_euro_id)
|
||||
inv2 = self.create_invoice(currency_id=self.currency_euro_id)
|
||||
partner = inv1.partner_id
|
||||
|
||||
receivable1 = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
receivable2 = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
@@ -75,7 +76,33 @@ class TestReconciliationWidget(TestReconciliation):
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
})
|
||||
|
||||
# This is like opening the widget, and type "deco" in the filter
|
||||
# This is like input a partner in the widget
|
||||
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
|
||||
bank_stmt_line.id,
|
||||
partner_id=partner.id,
|
||||
excluded_ids=[],
|
||||
search_str=False,
|
||||
mode="rp",
|
||||
)
|
||||
mv_lines_ids = [l['id'] for l in mv_lines_rec]
|
||||
|
||||
self.assertIn(receivable1.id, mv_lines_ids)
|
||||
self.assertIn(receivable2.id, mv_lines_ids)
|
||||
|
||||
# With a partner set, type the invoice reference in the filter
|
||||
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
|
||||
bank_stmt_line.id,
|
||||
partner_id=partner.id,
|
||||
excluded_ids=[],
|
||||
search_str=inv1.invoice_payment_ref,
|
||||
mode="rp",
|
||||
)
|
||||
mv_lines_ids = [l['id'] for l in mv_lines_rec]
|
||||
|
||||
self.assertIn(receivable1.id, mv_lines_ids)
|
||||
self.assertNotIn(receivable2.id, mv_lines_ids)
|
||||
|
||||
# Without a partner set, type "deco" in the filter
|
||||
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
|
||||
bank_stmt_line.id,
|
||||
partner_id=False,
|
||||
@@ -88,8 +115,7 @@ class TestReconciliationWidget(TestReconciliation):
|
||||
self.assertIn(receivable1.id, mv_lines_ids)
|
||||
self.assertIn(receivable2.id, mv_lines_ids)
|
||||
|
||||
# This is like clicking on the first receivable
|
||||
partner = inv1.partner_id
|
||||
# With a partner set, type "deco" in the filter and click on the first receivable
|
||||
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
|
||||
bank_stmt_line.id,
|
||||
partner_id=partner.id,
|
||||
|
||||
@@ -1265,7 +1265,7 @@ action = model.setting_init_bank_account_action()
|
||||
<field name="include_base_amount" attrs="{'invisible':[('amount_type','=', 'group')]}" />
|
||||
<field name="hide_tax_exigibility" invisible="1"/>
|
||||
<field name="tax_exigibility" widget="radio" attrs="{'invisible':['|', ('amount_type','=', 'group'), ('hide_tax_exigibility', '=', False)]}" groups="account.group_account_user"/>
|
||||
<field name="cash_basis_base_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}" groups="account.group_account_user"/>
|
||||
<field name="cash_basis_base_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')]}" groups="account.group_account_user"/>
|
||||
<field name="cash_basis_transition_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}" groups="account.group_account_user"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -109,7 +109,7 @@ class ResPartner(models.Model):
|
||||
|
||||
allow_signup = self.env['res.users']._get_signup_invitation_scope() == 'b2c'
|
||||
for partner in self:
|
||||
if allow_signup and not partner.user_ids:
|
||||
if allow_signup and not partner.sudo().user_ids:
|
||||
partner = partner.sudo()
|
||||
partner.signup_prepare()
|
||||
res[partner.id]['auth_signup_token'] = partner.signup_token
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
<record id="default_emails_digest" model="ir.config_parameter">
|
||||
<odoo noupdate="1">
|
||||
<record id="default_emails_digest" model="ir.config_parameter" forcecreate="0">
|
||||
<field name="key">digest.default_digest_emails</field>
|
||||
<field name="value">True</field>
|
||||
</record>
|
||||
<record id="default_digest" model="ir.config_parameter">
|
||||
<record id="default_digest" model="ir.config_parameter" forcecreate="0">
|
||||
<field name="key">digest.default_digest_id</field>
|
||||
<field name="value" ref="digest.digest_digest_default"/>
|
||||
</record>
|
||||
|
||||
@@ -10,8 +10,6 @@ class EventController(Controller):
|
||||
|
||||
@route(['''/event/<model("event.event", "[('state', 'in', ('confirm', 'done'))]"):event>/ics'''], type='http', auth="public")
|
||||
def event_ics_file(self, event, **kwargs):
|
||||
if not event or not event.registration_ids:
|
||||
return request.not_found()
|
||||
files = event._get_ics_file()
|
||||
if not event.id in files:
|
||||
return NotFound()
|
||||
|
||||
@@ -14,8 +14,8 @@ class HrOrgChartController(http.Controller):
|
||||
return None
|
||||
employee_id = int(employee_id)
|
||||
|
||||
if ('context' in kw and 'allowed_company_ids' in kw['context']):
|
||||
cids = kw['context']['allowed_company_ids']
|
||||
if 'allowed_company_ids' in request.env.context:
|
||||
cids = request.env.context['allowed_company_ids']
|
||||
else:
|
||||
cids = [request.env.company.id]
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
<field name="tax_src_id" ref="tva_normale_ttc" />
|
||||
<field name="tax_dest_id" ref="tva_intra_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_vt_intermediaire_encaissement" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_intermediaire_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_intra_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_vt_normale_encaissement" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_normale_encaissement" />
|
||||
@@ -64,6 +69,11 @@
|
||||
<field name="tax_src_id" ref="tva_normale_encaissement_ttc" />
|
||||
<field name="tax_dest_id" ref="tva_intra_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_vt_intermediaire_encaissement_ttc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_intermediaire_encaissement_ttc" />
|
||||
<field name="tax_dest_id" ref="tva_intra_0" />
|
||||
</record>
|
||||
<!-- Taux DOM-TOM -->
|
||||
<record id="fp_tax_template_intraeub2b_vt_specifique" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
@@ -146,11 +156,31 @@
|
||||
<field name="tax_src_id" ref="tva_acq_specifique" />
|
||||
<field name="tax_dest_id" ref="tva_intra_specifique" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_encaissement_deduc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_intra_normale" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_specifique_acq" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_specifique" />
|
||||
<field name="tax_dest_id" ref="tva_acq_intra_specifique" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_encaissement_acq" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_acq_intra_normale" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_encaissement_deduc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_intermediaire_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_intra_intermediaire" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_encaissement_acq" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_intermediaire_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_acq_intra_intermediaire" />
|
||||
</record>
|
||||
<!-- Taux Intermédiaire -->
|
||||
<record id="fp_tax_template_intraeub2b_ha_intermediaire_deduc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
@@ -168,22 +198,42 @@
|
||||
<field name="tax_src_id" ref="tva_acq_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_intra_reduite" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_encaissement_reduite_deduc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_intra_reduite" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_reduite_acq" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_acq_intra_reduite" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_encaissement_reduite_acq" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_acq_intra_reduite" />
|
||||
</record>
|
||||
<!-- Taux super réduit -->
|
||||
<record id="fp_tax_template_intraeub2b_ha_super_reduite_deduc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_super_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_intra_super_reduite" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_encaissement_super_reduite_deduc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement_super_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_intra_super_reduite" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_super_reduite_acq" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_super_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_acq_intra_super_reduite" />
|
||||
</record>
|
||||
<record id="fp_tax_template_intraeub2b_ha_encaissement_super_reduite_acq" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_intraeub2b" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement_super_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_acq_intra_super_reduite" />
|
||||
</record>
|
||||
|
||||
<!-- Import/Export + DOM/TOM -->
|
||||
<!-- ventes -->
|
||||
@@ -203,11 +253,21 @@
|
||||
<field name="tax_src_id" ref="tva_normale_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_export_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_impexp_vt_intermediaire_encaissement" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_intermediaire_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_export_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_impexp_vt_normale_encaissement_ttc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_normale_encaissement_ttc" />
|
||||
<field name="tax_dest_id" ref="tva_export_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_impexp_vt_intermediaire_encaissement_ttc" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_intermediaire_encaissement_ttc" />
|
||||
<field name="tax_dest_id" ref="tva_export_0" />
|
||||
</record>
|
||||
<!-- Taux DOM-TOM -->
|
||||
<record id="fp_tax_template_impexp_vt_specifique" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
@@ -280,6 +340,11 @@
|
||||
<field name="tax_dest_id" ref="tva_import_0" />
|
||||
</record>
|
||||
<!-- Taux DOM-TOM -->
|
||||
<record id="fp_tax_template_impexp_ha_encaissement" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_import_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_impexp_ha_specifique" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_acq_specifique" />
|
||||
@@ -291,12 +356,27 @@
|
||||
<field name="tax_src_id" ref="tva_acq_intermediaire" />
|
||||
<field name="tax_dest_id" ref="tva_import_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_impexp_ha_intermediaire" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_acq_intermediaire_encaissement" />
|
||||
<field name="tax_dest_id" ref="tva_import_0" />
|
||||
</record>
|
||||
<!-- Taux Réduit -->
|
||||
<record id="fp_tax_template_impexp_ha_reduite" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_acq_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_import_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_impexp_ha_encaissement_super_reduite" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement_super_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_import_0" />
|
||||
</record>
|
||||
<record id="fp_tax_template_impexp_ha_encaissement_reduite" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
<field name="tax_src_id" ref="tva_acq_encaissement_reduite" />
|
||||
<field name="tax_dest_id" ref="tva_import_0" />
|
||||
</record>
|
||||
<!-- Taux super réduit -->
|
||||
<record id="fp_tax_template_impexp_ha_super_reduite" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import_export" />
|
||||
|
||||
@@ -46,6 +46,47 @@
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_intermediaire_encaissement">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (vente) 10,0%</field>
|
||||
<field name="description">TVA 10%</field>
|
||||
<field name="amount" eval="10.0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_445712" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
|
||||
'account_id': ref('pcg_445712'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_normale_encaissement">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (vente) 20,0%</field>
|
||||
@@ -367,6 +408,48 @@
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_intermediaire_encaissement_ttc">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (vente) 10,0% TTC</field>
|
||||
<field name="description">TVA 10% TTC</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="10"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_445712" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
|
||||
'account_id': ref('pcg_445712'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_normale_encaissement_ttc">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (vente) 20,0% TTC</field>
|
||||
@@ -693,6 +776,88 @@
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_encaissement">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (achat) 20,0%</field>
|
||||
<field name="description">TVA 20%</field>
|
||||
<field name="amount" eval="20.0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_44566" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_20"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_20')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_20')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_20')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_20')],
|
||||
'account_id': ref('pcg_44566'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_intermediaire_encaissement">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (achat) 10,0%</field>
|
||||
<field name="description">TVA 10%</field>
|
||||
<field name="amount" eval="10.0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_44566" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
|
||||
'account_id': ref('pcg_44566'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_specifique">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA déductible (achat) 8,5%</field>
|
||||
@@ -810,6 +975,47 @@
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_encaissement_reduite">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (achat) 5,5%</field>
|
||||
<field name="description">TVA 5,5%</field>
|
||||
<field name="amount" eval="5.5"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_44566" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_55"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_5_5')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_5_5')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_5_5')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_5_5')],
|
||||
'account_id': ref('pcg_44566'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_super_reduite">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA déductible (achat) 2,1%</field>
|
||||
@@ -851,6 +1057,47 @@
|
||||
|
||||
|
||||
<!-- ACHATS Tax included in price -->
|
||||
<record model="account.tax.template" id="tva_acq_encaissement_super_reduite">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (achat) 2,1% </field>
|
||||
<field name="description">TVA 2,1%</field>
|
||||
<field name="amount" eval="2.1"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_44566" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_21"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_2_1')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_2_1')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_2_1')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_2_1')],
|
||||
'account_id': ref('pcg_44566'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_normale_TTC">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA déductible (achat) 20,0% TTC</field>
|
||||
@@ -891,6 +1138,48 @@
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_encaissement_TTC">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (achat) 20,0% TTC</field>
|
||||
<field name="description">TVA 20% TTC</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="20.0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_44566" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_20"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_20')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_20')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_20')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_20')],
|
||||
'account_id': ref('pcg_44566'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_specifique_TTC">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA déductible (achat) 8,5% TTC</field>
|
||||
@@ -931,6 +1220,48 @@
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_intermediaire_encaissement_TTC">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (achat) 10,0% TTC</field>
|
||||
<field name="description">TVA 10% TTC</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="10.0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_44566" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_10')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_10')],
|
||||
'account_id': ref('pcg_44566'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_intermediaire_TTC">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA déductible (achat) 10,0% TTC</field>
|
||||
@@ -1011,6 +1342,48 @@
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_encaissement_reduite_TTC">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (achat) 5,5% TTC</field>
|
||||
<field name="description">TVA 5,5% TTC</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="5.5"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_44566" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_55"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_5_5')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_5_5')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_5_5')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_5_5')],
|
||||
'account_id': ref('pcg_44566'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_super_reduite_TTC">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA déductible (achat) 2,1% TTC</field>
|
||||
@@ -1051,6 +1424,48 @@
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="tva_acq_encaissement_super_reduite_TTC">
|
||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||
<field name="name">TVA à l'encaissement (achat) 2,1% TTC</field>
|
||||
<field name="description">TVA 2,1% TTC</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="2.1"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_transition_account_id" ref="pcg_44566" />
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_tva_21"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_2_1')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'plus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_2_1')],
|
||||
'account_id': ref('pcg_445800'),
|
||||
}),
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_base_coll_2_1')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
'minus_report_line_ids': [ref('l10n_fr.tax_report_tva_coll_2_1')],
|
||||
'account_id': ref('pcg_44566'),
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
<!-- ImmoBILISATIONS (achats) -->
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Account Tax Group -->
|
||||
<record id="sgst_group" model="account.tax.group">
|
||||
<field name="name">SGST</field>
|
||||
</record>
|
||||
<record id="cgst_group" model="account.tax.group">
|
||||
<field name="name">CGST</field>
|
||||
</record>
|
||||
<record id="igst_group" model="account.tax.group">
|
||||
<field name="name">IGST</field>
|
||||
</record>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -109,7 +109,7 @@ class AccountMove(models.Model):
|
||||
raise UserError(_("%s must have a country.") % (seller.display_name))
|
||||
|
||||
# <1.4.1>
|
||||
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code != 'IT':
|
||||
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
|
||||
raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name))
|
||||
|
||||
# <1.4.2>
|
||||
@@ -270,11 +270,12 @@ class AccountMove(models.Model):
|
||||
'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_id.name, self.env.company.l10n_it_address_recipient_fatturapa),
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'email_from': self.env.company.l10n_it_address_send_fatturapa,
|
||||
'reply_to': self.env.company.l10n_it_address_send_fatturapa,
|
||||
'mail_server_id': self.env.company.l10n_it_mail_pec_server_id.id,
|
||||
'attachment_ids': [(6, 0, self.l10n_it_einvoice_id.ids)],
|
||||
})
|
||||
|
||||
mail_fattura = self.env['mail.mail'].create({
|
||||
mail_fattura = self.env['mail.mail'].with_context(wo_return_path=True).create({
|
||||
'mail_message_id': message.id,
|
||||
'email_to': self.env.company.l10n_it_address_recipient_fatturapa,
|
||||
})
|
||||
|
||||
@@ -360,3 +360,17 @@ class FetchmailServer(models.Model):
|
||||
if descrizione:
|
||||
output_str += "<li>Errore %s: %s</li>" % (element[0].text, descrizione)
|
||||
return output_str + "</ul>"
|
||||
|
||||
class IrMailServer(models.Model):
|
||||
_name = "ir.mail_server"
|
||||
_inherit = "ir.mail_server"
|
||||
|
||||
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
|
||||
attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None,
|
||||
body_alternative=None, subtype_alternative='plain'):
|
||||
|
||||
if self.env.context.get('wo_return_path') and headers:
|
||||
headers.pop('Return-Path', False)
|
||||
return super(IrMailServer, self).build_email(email_from, email_to, subject, body, email_cc=email_cc, email_bcc=email_bcc, reply_to=reply_to,
|
||||
attachments=attachments, message_id=message_id, references=references, object_id=object_id, subtype=subtype, headers=headers,
|
||||
body_alternative=body_alternative, subtype_alternative=subtype_alternative)
|
||||
|
||||
@@ -280,6 +280,8 @@ var Discuss = AbstractAction.extend({
|
||||
this.options.channelQuickSearchThreshold = 20;
|
||||
}
|
||||
|
||||
this._isMessagingReady = this.call('mail_service', 'isReady');
|
||||
this._isStarted = false;
|
||||
this._threadsScrolltop = {};
|
||||
this._composerStates = {};
|
||||
this._defaultThreadID = this.options.active_id ||
|
||||
@@ -291,68 +293,20 @@ var Discuss = AbstractAction.extend({
|
||||
this._updateThreads.bind(this), 100, { leading: false });
|
||||
|
||||
this.controlPanelParams.modelName = 'mail.message';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
return Promise.all([this._super(), this.call('mail_service', 'isReady')]);
|
||||
this.call('mail_service', 'getMailBus').on('messaging_ready', this, this._onMessagingReady);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this._basicComposer = new BasicComposer(this, {
|
||||
mentionPartnersRestricted: true,
|
||||
showTyping: true,
|
||||
this._isStarted = true;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
if (!self._isMessagingReady) {
|
||||
return;
|
||||
}
|
||||
return self._initRender();
|
||||
});
|
||||
this._extendedComposer = new ExtendedComposer(this, {
|
||||
mentionPartnersRestricted: true,
|
||||
showTyping: true,
|
||||
});
|
||||
this._basicComposer
|
||||
.on('post_message', this, this._onPostMessage)
|
||||
.on('input_focused', this, this._onComposerFocused);
|
||||
this._extendedComposer
|
||||
.on('post_message', this, this._onPostMessage)
|
||||
.on('input_focused', this, this._onComposerFocused);
|
||||
this._renderButtons();
|
||||
|
||||
var defs = [];
|
||||
defs.push(
|
||||
this._renderThread()
|
||||
);
|
||||
defs.push(
|
||||
this._basicComposer.appendTo(this.$('.o_mail_discuss_content')));
|
||||
defs.push(
|
||||
this._extendedComposer.appendTo(this.$('.o_mail_discuss_content')));
|
||||
defs.push(this._super.apply(this, arguments));
|
||||
|
||||
return Promise.all(defs)
|
||||
.then(function () {
|
||||
return self._setThread(self._defaultThreadID);
|
||||
})
|
||||
.then(function () {
|
||||
self._initThreads();
|
||||
self._startListening();
|
||||
self._threadWidget.$el.on('scroll', null, _.debounce(function () {
|
||||
if (
|
||||
self._threadWidget.getScrolltop() < 20 &&
|
||||
!self._threadWidget.$('.o_mail_no_content').length &&
|
||||
!self._thread.isAllHistoryLoaded(self.domain)
|
||||
) {
|
||||
self._loadMoreMessages();
|
||||
}
|
||||
if (
|
||||
self._threadWidget.isAtBottom() &&
|
||||
self._thread.getType() !== 'mailbox'
|
||||
) {
|
||||
self._thread.markAsRead();
|
||||
}
|
||||
}, 100));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -381,8 +335,8 @@ var Discuss = AbstractAction.extend({
|
||||
this.call('mail_service', 'getMailBus').trigger('discuss_open', true);
|
||||
if (this._thread) {
|
||||
this._threadWidget.scrollToPosition(this._threadsScrolltop[this._thread.getID()]);
|
||||
this._loadEnoughMessages();
|
||||
}
|
||||
this._loadEnoughMessages();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -546,6 +500,60 @@ var Discuss = AbstractAction.extend({
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_initRender: function () {
|
||||
var self = this;
|
||||
this._basicComposer = new BasicComposer(this, {
|
||||
mentionPartnersRestricted: true,
|
||||
showTyping: true,
|
||||
});
|
||||
this._extendedComposer = new ExtendedComposer(this, {
|
||||
mentionPartnersRestricted: true,
|
||||
showTyping: true,
|
||||
});
|
||||
this._basicComposer
|
||||
.on('post_message', this, this._onPostMessage)
|
||||
.on('input_focused', this, this._onComposerFocused);
|
||||
this._extendedComposer
|
||||
.on('post_message', this, this._onPostMessage)
|
||||
.on('input_focused', this, this._onComposerFocused);
|
||||
this._renderButtons();
|
||||
|
||||
var defs = [];
|
||||
defs.push(
|
||||
this._renderThread()
|
||||
);
|
||||
defs.push(
|
||||
this._basicComposer.appendTo(this.$('.o_mail_discuss_content')));
|
||||
defs.push(
|
||||
this._extendedComposer.appendTo(this.$('.o_mail_discuss_content')));
|
||||
|
||||
return Promise.all(defs)
|
||||
.then(function () {
|
||||
return self._setThread(self._defaultThreadID);
|
||||
})
|
||||
.then(function () {
|
||||
self._initThreads();
|
||||
self._startListening();
|
||||
self._threadWidget.$el.on('scroll', null, _.debounce(function () {
|
||||
if (
|
||||
self._threadWidget.getScrolltop() < 20 &&
|
||||
!self._threadWidget.$('.o_mail_no_content').length &&
|
||||
!self._thread.isAllHistoryLoaded(self.domain)
|
||||
) {
|
||||
self._loadMoreMessages();
|
||||
}
|
||||
if (
|
||||
self._threadWidget.isAtBottom() &&
|
||||
self._thread.getType() !== 'mailbox'
|
||||
) {
|
||||
self._thread.markAsRead();
|
||||
}
|
||||
}, 100));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Renders the mainside bar with current threads
|
||||
*
|
||||
@@ -1400,6 +1408,20 @@ var Discuss = AbstractAction.extend({
|
||||
this._fetchAndRenderThread();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onMessagingReady: function () {
|
||||
if (this._isMessagingReady) {
|
||||
return;
|
||||
}
|
||||
this._isMessagingReady = true;
|
||||
if (!this._isStarted) {
|
||||
return;
|
||||
}
|
||||
this.$('.o_mail_discuss_loading').remove();
|
||||
this._initRender();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
|
||||
@@ -34,7 +34,7 @@ MailManager.include({
|
||||
this.call('local_storage', 'getItem', key);
|
||||
}
|
||||
}
|
||||
this.isReady().then(this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates));
|
||||
this._mailBus.on('messaging_ready', this, this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates));
|
||||
// listen to localStorage changes to synchronize DocumentThread's
|
||||
// windows between tabs
|
||||
this.call('local_storage', 'onStorage', this, this._onStorage);
|
||||
|
||||
@@ -46,6 +46,7 @@ var PREVIEW_MSG_MAX_SIZE = 350; // optimal for native english speakers
|
||||
|
||||
var MailManager = AbstractService.extend({
|
||||
dependencies: ['ajax', 'bus_service', 'local_storage'],
|
||||
_isReady: false,
|
||||
_ODOOBOT_ID: ["ODOOBOT", "ODOOBOT"], // authorID for transient messages
|
||||
|
||||
/**
|
||||
@@ -294,7 +295,7 @@ var MailManager = AbstractService.extend({
|
||||
* This is the case when it has fetched the initial state from the server,
|
||||
* by means of the route 'mail/init_messaging'
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReady: function () {
|
||||
return this._isReady;
|
||||
@@ -636,7 +637,7 @@ var MailManager = AbstractService.extend({
|
||||
*/
|
||||
_fetchMailStateFromServer: function () {
|
||||
var self = this;
|
||||
this._isReady = session.is_bound.then(function () {
|
||||
session.is_bound.then(function () {
|
||||
var context = _.extend(
|
||||
{ isMobile: config.device.isMobile },
|
||||
session.user_context
|
||||
@@ -648,6 +649,8 @@ var MailManager = AbstractService.extend({
|
||||
}).then(function (result) {
|
||||
self._updateInternalStateFromServer(result);
|
||||
self.call('bus_service', 'startPolling');
|
||||
self._isReady = true;
|
||||
self._mailBus.trigger('messaging_ready');
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -19,9 +19,6 @@ var ActivityMenu = Widget.extend({
|
||||
'click .o_mail_preview': '_onActivityFilterClick',
|
||||
'show.bs.dropdown': '_onActivityMenuShow',
|
||||
},
|
||||
willStart: function () {
|
||||
return Promise.resolve(this.call('mail_service', 'isReady'));
|
||||
},
|
||||
start: function () {
|
||||
this._$activitiesPreview = this.$('.o_mail_systray_dropdown_items');
|
||||
this.call('mail_service', 'getMailBus').on('activity_updated', this, this._updateCounter);
|
||||
|
||||
@@ -27,12 +27,6 @@ var MessagingMenu = Widget.extend({
|
||||
'click .o_thread_window_expand': '_onClickExpand',
|
||||
'show.bs.dropdown': '_onShowDropdown',
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
return Promise.all([this._super.apply(this, arguments), this.call('mail_service', 'isReady')]);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -40,8 +34,10 @@ var MessagingMenu = Widget.extend({
|
||||
this._$filterButtons = this.$('.o_filter_button');
|
||||
this._$previews = this.$('.o_mail_systray_dropdown_items');
|
||||
this._filter = false;
|
||||
this._isMessagingReady = this.call('mail_service', 'isReady');
|
||||
this._updateCounter();
|
||||
var mailBus = this.call('mail_service', 'getMailBus');
|
||||
mailBus.on('messaging_ready', this, this._onMessagingReady);
|
||||
mailBus.on('update_needaction', this, this._updateCounter);
|
||||
mailBus.on('new_channel', this, this._updateCounter);
|
||||
mailBus.on('update_thread_unread_counter', this, this._updateCounter);
|
||||
@@ -235,6 +231,9 @@ var MessagingMenu = Widget.extend({
|
||||
_updatePreviews: function () {
|
||||
// Display spinner while waiting for conversations preview
|
||||
this._$previews.html(QWeb.render('Spinner'));
|
||||
if (!this._isMessagingReady) {
|
||||
return;
|
||||
}
|
||||
this._getPreviews()
|
||||
.then(this._renderPreviews.bind(this));
|
||||
},
|
||||
@@ -257,7 +256,11 @@ var MessagingMenu = Widget.extend({
|
||||
* @private
|
||||
*/
|
||||
_updateCounter: function () {
|
||||
if (!this._isMessagingReady) {
|
||||
return;
|
||||
}
|
||||
var counter = this._computeCounter();
|
||||
this.$('.o_mail_messaging_menu_icon').removeClass('fa-spinner fa-spin');
|
||||
this.$('.o_notification_counter').text(counter);
|
||||
this.$el.toggleClass('o_no_notification', !counter);
|
||||
if (this._isShown()) {
|
||||
@@ -346,6 +349,16 @@ var MessagingMenu = Widget.extend({
|
||||
var $preview = $(ev.currentTarget).closest('.o_mail_preview');
|
||||
this._markAsRead($preview);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onMessagingReady: function () {
|
||||
if (this._isMessagingReady) {
|
||||
return;
|
||||
}
|
||||
this._isMessagingReady = true;
|
||||
this._updateCounter();
|
||||
},
|
||||
});
|
||||
|
||||
// Systray menu items display order matches order in the list
|
||||
|
||||
@@ -20,23 +20,21 @@ var ThreadField = AbstractField.extend({
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._isMessagingReady = this.call('mail_service', 'isReady');
|
||||
this._isStarted = false;
|
||||
this.call('mail_service', 'getMailBus').on('messaging_ready', this, this._onMessagingReady);
|
||||
// Used to automatically mark document thread as read at the moment we
|
||||
// access the document and render the thread.
|
||||
this._markAsReadOnNextRender = false;
|
||||
this._setDocumentThread();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
return this.alive(this.call('mail_service', 'isReady'));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this._isStarted = true;
|
||||
this.dp = new concurrency.DropPrevious();
|
||||
|
||||
this._threadWidget = new ThreadWidget(this, {
|
||||
@@ -160,6 +158,9 @@ var ThreadField = AbstractField.extend({
|
||||
* If it is a new document in create mode, unset the document thread.
|
||||
*/
|
||||
_setDocumentThread: function () {
|
||||
if (!this._isMessagingReady) {
|
||||
return;
|
||||
}
|
||||
var params = {
|
||||
messageIDs: this.value.res_ids,
|
||||
name: this.recordData.display_name,
|
||||
@@ -186,6 +187,19 @@ var ThreadField = AbstractField.extend({
|
||||
_onLoadMoreMessages: function () {
|
||||
this._fetchAndRenderThread({ forceFetch: true });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onMessagingReady: function () {
|
||||
if (this._isMessagingReady) {
|
||||
return;
|
||||
}
|
||||
this._isMessagingReady = true;
|
||||
if (!this._isStarted) {
|
||||
return;
|
||||
}
|
||||
this._setDocumentThread();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {mail.model.Message}
|
||||
|
||||
@@ -7,6 +7,18 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.o_mail_discuss_loading {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
|
||||
> i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_discuss_sidebar {
|
||||
width: $o-mail-chat-sidebar-width;
|
||||
height: 100%;
|
||||
|
||||
@@ -8,8 +8,15 @@
|
||||
-->
|
||||
<t t-name="mail.discuss">
|
||||
<div class="o_mail_discuss">
|
||||
<div class="o_mail_discuss_sidebar"/>
|
||||
<div class="o_mail_discuss_sidebar">
|
||||
<div class="o_mail_discuss_loading">
|
||||
<i class="fa fa-spinner fa-spin"/> Please wait...
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_mail_discuss_content">
|
||||
<div class="o_mail_discuss_loading">
|
||||
<i class="fa fa-spinner fa-spin"/> Please wait...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<t t-name="mail.systray.MessagingMenu">
|
||||
<li class="o_mail_systray_item">
|
||||
<a class="dropdown-toggle o-no-caret" data-toggle="dropdown" data-display="static" aria-expanded="false" title="Conversations" href="#" role="button">
|
||||
<i class="fa fa-comments" role="img" aria-label="Messages"/> <span class="o_notification_counter badge badge-pill"/>
|
||||
<i class="o_mail_messaging_menu_icon fa fa-comments fa-spinner fa-spin" role="img" aria-label="Messages"/> <span class="o_notification_counter badge badge-pill"/>
|
||||
</a>
|
||||
<div class="o_mail_systray_dropdown dropdown-menu dropdown-menu-right" role="menu">
|
||||
<t t-if="widget.isMobile()">
|
||||
|
||||
@@ -141,6 +141,56 @@ QUnit.test('basic rendering', async function (assert) {
|
||||
discuss.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('messaging not ready', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const messagingReadyProm = testUtils.makeTestPromise();
|
||||
const discuss = await createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
async mockRPC(route, args) {
|
||||
if (route === '/mail/init_messaging') {
|
||||
const _super = this._super.bind(this, ...arguments); // limitation of class.js
|
||||
assert.step('/mail/init_messaging:pending');
|
||||
await messagingReadyProm;
|
||||
assert.step('/mail/init_messaging:resolved');
|
||||
return _super();
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(['/mail/init_messaging:pending']);
|
||||
assert.ok(
|
||||
discuss.el,
|
||||
"discuss should be rendered");
|
||||
assert.strictEqual(
|
||||
$('.o_action')[0],
|
||||
discuss.el,
|
||||
"should display discuss even when messaging is not ready");
|
||||
assert.containsOnce(
|
||||
discuss,
|
||||
'.o_mail_discuss_sidebar .o_mail_discuss_loading',
|
||||
"should display sidebar is loading (messaging not yet ready)");
|
||||
assert.containsOnce(
|
||||
discuss,
|
||||
'.o_mail_discuss_content .o_mail_discuss_loading',
|
||||
"should display content is loading (messaging not yet ready)");
|
||||
|
||||
messagingReadyProm.resolve();
|
||||
await testUtils.nextTick();
|
||||
assert.verifySteps(['/mail/init_messaging:resolved']);
|
||||
assert.containsNone(
|
||||
discuss,
|
||||
'.o_mail_discuss_loading',
|
||||
"should no longer display sidebar or content is loading (messaging is ready)");
|
||||
|
||||
discuss.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('searchview options visibility', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
|
||||
@@ -120,6 +120,53 @@ QUnit.test('messaging menu widget: menu with no records', async function (assert
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('messaging menu widget: messaging not ready', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const messagingReadyProm = testUtils.makeTestPromise();
|
||||
|
||||
const messagingMenu = new MessagingMenu();
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
async mockRPC(route, args) {
|
||||
if (route === '/mail/init_messaging') {
|
||||
const _super = this._super.bind(this, ...arguments); // limitation of class.js
|
||||
assert.step('/mail/init_messaging:pending');
|
||||
await messagingReadyProm;
|
||||
assert.step('/mail/init_messaging:resolved');
|
||||
return _super();
|
||||
}
|
||||
if (args.method === 'message_fetch') {
|
||||
return [];
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
await messagingMenu.appendTo($('#qunit-fixture'));
|
||||
assert.verifySteps(['/mail/init_messaging:pending']);
|
||||
assert.ok(
|
||||
messagingMenu.el,
|
||||
"messaging menu should be rendered");
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_mail_systray_item'),
|
||||
messagingMenu.el,
|
||||
"should display messaging menu even when messaging is not ready");
|
||||
assert.hasClass(
|
||||
messagingMenu.$('.o_mail_messaging_menu_icon'),
|
||||
'fa-spinner',
|
||||
"Messaging menu icon should be a spinner when messaging menu is not ready");
|
||||
|
||||
messagingReadyProm.resolve();
|
||||
await testUtils.nextTick();
|
||||
assert.verifySteps(['/mail/init_messaging:resolved']);
|
||||
assert.doesNotHaveClass(
|
||||
messagingMenu.$('.o_mail_messaging_menu_icon'),
|
||||
'fa-spinner',
|
||||
"Messaging menu icon should no longer be a spinner when messaging menu is ready");
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('messaging menu widget: messaging menu with 1 record', async function (assert) {
|
||||
assert.expect(3);
|
||||
var messagingMenu = new MessagingMenu();
|
||||
|
||||
@@ -422,6 +422,7 @@ class MrpProduction(models.Model):
|
||||
self.product_qty = self.bom_id.product_qty
|
||||
self.product_uom_id = self.bom_id.product_uom_id.id
|
||||
self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)]
|
||||
self.picking_type_id = self.bom_id.picking_type_id or self.picking_type_id
|
||||
|
||||
@api.onchange('date_planned_start')
|
||||
def _onchange_date_planned_start(self):
|
||||
|
||||
@@ -13,6 +13,7 @@ from odoo.addons.base.models import ir_module
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.tools.misc import formatLang
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -221,6 +222,19 @@ class PaymentAcquirer(models.Model):
|
||||
if field_names:
|
||||
raise ValidationError(_("Required fields not filled: %s") % ", ".join(field_names))
|
||||
|
||||
def get_base_url(self):
|
||||
self.ensure_one()
|
||||
# priority is always given to url_root
|
||||
# from the request
|
||||
url = ''
|
||||
if request:
|
||||
url = request.httprequest.url_root
|
||||
|
||||
if not url and 'website_id' in self and self.website_id:
|
||||
url = self.website_id._get_http_domain()
|
||||
|
||||
return url or self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
|
||||
def _get_feature_support(self):
|
||||
"""Get advanced feature support by provider.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class PaymentAcquirerStripe(models.Model):
|
||||
def stripe_form_generate_values(self, tx_values):
|
||||
self.ensure_one()
|
||||
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
base_url = self.get_base_url()
|
||||
stripe_session_data = {
|
||||
'payment_method_types[]': 'card',
|
||||
'line_items[][amount]': int(tx_values['amount'] if tx_values['currency'].name in INT_CURRENCIES else float_round(tx_values['amount'] * 100, 2)),
|
||||
@@ -147,6 +147,10 @@ class PaymentTransactionStripe(models.Model):
|
||||
return super(PaymentTransactionStripe, self).form_feedback(data, acquirer_name)
|
||||
|
||||
def _stripe_create_payment_intent(self, acquirer_ref=None, email=None):
|
||||
if not self.payment_token_id.stripe_payment_method:
|
||||
# old token before using sca, need to fetch data from the api
|
||||
self.payment_token_id._stripe_sca_migrate_customer()
|
||||
|
||||
charge_params = {
|
||||
'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else float_round(self.amount * 100, 2)),
|
||||
'currency': self.currency_id.name.lower(),
|
||||
@@ -298,3 +302,39 @@ class PaymentTokenStripe(models.Model):
|
||||
'acquirer_ref': cust_resp['id'],
|
||||
}
|
||||
return values
|
||||
|
||||
def _stripe_sca_migrate_customer(self):
|
||||
"""Migrate a token from the old implementation of Stripe to the SCA one.
|
||||
|
||||
In the old implementation, it was possible to create a valid charge just by
|
||||
giving the customer ref to ask Stripe to use the default source (= default
|
||||
card). Since we have a one-to-one matching between a saved card, this used to
|
||||
work well - but now we need to specify the payment method for each call and so
|
||||
we have to contact stripe to get the default source for the customer and save it
|
||||
in the payment token.
|
||||
This conversion will happen once per token, the first time it gets used following
|
||||
the installation of the module."""
|
||||
self.ensure_one()
|
||||
url = "customers/%s" % (self.acquirer_ref)
|
||||
data = self.acquirer_id._stripe_request(url, method="GET")
|
||||
sources = data.get('sources', {}).get('data', [])
|
||||
pm_ref = False
|
||||
if sources:
|
||||
if len(sources) > 1:
|
||||
_logger.warning('stripe sca customer conversion: there should be a single saved source per customer!')
|
||||
pm_ref = sources[0].get('id')
|
||||
else:
|
||||
url = 'payment_methods'
|
||||
params = {
|
||||
'type': 'card',
|
||||
'customer': self.acquirer_ref,
|
||||
}
|
||||
payment_methods = self.acquirer_id._stripe_request(url, params, method='GET')
|
||||
cards = payment_methods.get('data', [])
|
||||
if len(cards) > 1:
|
||||
_logger.warning('stripe sca customer conversion: there should be a single saved source per customer!')
|
||||
pm_ref = cards and cards[0].get('id')
|
||||
if not pm_ref:
|
||||
raise ValidationError(_('Unable to convert Stripe customer for SCA compatibility. Is there at least one card for this customer in the Stripe backend?'))
|
||||
self.stripe_payment_method = pm_ref
|
||||
_logger.info('converted old customer ref to sca-compatible record for payment token %s', self.id)
|
||||
|
||||
@@ -121,7 +121,7 @@ class Pricelist(models.Model):
|
||||
""" Low-level method - Mono pricelist, multi products
|
||||
Returns: dict{product_id: (price, suitable_rule) for the given pricelist}
|
||||
|
||||
If date in context: Date of the pricelist (%Y-%m-%d)
|
||||
Date in context can be a date, datetime, ...
|
||||
|
||||
:param products_qty_partner: list of typles products, quantity, partner
|
||||
:param datetime date: validity date
|
||||
@@ -129,8 +129,8 @@ class Pricelist(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not date:
|
||||
date = self._context.get('date') or fields.Date.context_today(self)
|
||||
date = fields.Date.to_date(date) # handwritten query below does not work with non-dates (e.g. datetimes)
|
||||
date = self._context.get('date') or fields.Date.today()
|
||||
date = fields.Date.to_date(date) # boundary conditions differ if we have a datetime
|
||||
if not uom_id and self._context.get('uom'):
|
||||
uom_id = self._context['uom']
|
||||
if uom_id:
|
||||
|
||||
@@ -406,7 +406,7 @@ class ProductTemplate(models.Model):
|
||||
|
||||
def name_get(self):
|
||||
# Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
|
||||
self.read(['name', 'default_code'])
|
||||
self.browse(self.ids).read(['name', 'default_code'])
|
||||
return [(template.id, '%s%s' % (template.default_code and '[%s] ' % template.default_code or '', template.name))
|
||||
for template in self]
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class TestPurchaseOrderReport(common.TransactionCase):
|
||||
})
|
||||
po.button_confirm()
|
||||
|
||||
f = Form(self.env['account.move'])
|
||||
f = Form(self.env['account.move'].with_context(default_type='in_invoice'))
|
||||
f.partner_id = po.partner_id
|
||||
f.purchase_id = po
|
||||
invoice = f.save()
|
||||
|
||||
@@ -216,17 +216,14 @@ class TestReInvoice(TestCommonSaleNoChart):
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
# create invoice lines and validate it
|
||||
invoice_a = self.AccountMove.create({
|
||||
'partner_id': self.partner_customer_usd.id,
|
||||
'journal_id': self.journal_purchase.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'product_id': self.product_no_expense.id,
|
||||
'quantity': 3,
|
||||
'analytic_account_id': self.analytic_account.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
move_form = Form(self.AccountMove)
|
||||
move_form.partner_id = self.partner_customer_usd
|
||||
move_form.journal_id = self.journal_purchase
|
||||
with move_form.line_ids.new() as line_form:
|
||||
line_form.product_id = self.product_no_expense
|
||||
line_form.quantity = 3.0
|
||||
line_form.analytic_account_id = self.analytic_account
|
||||
invoice_a = move_form.save()
|
||||
invoice_a.post()
|
||||
|
||||
self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) when validating vendor bill")
|
||||
|
||||
@@ -196,7 +196,6 @@ class TestSaleOrder(TestCommonSaleNoChart):
|
||||
so._create_analytic_account()
|
||||
|
||||
inv = self.env['account.move'].with_context(default_type='in_invoice').create({
|
||||
'type': 'in_invoice',
|
||||
'partner_id': self.partner_customer_usd.id,
|
||||
'invoice_line_ids': [
|
||||
(0, 0, {
|
||||
|
||||
@@ -1078,6 +1078,7 @@ class StockMove(models.Model):
|
||||
# cache invalidation when actually reserving the move.
|
||||
reserved_availability = {move: move.reserved_availability for move in self}
|
||||
roundings = {move: move.product_id.uom_id.rounding for move in self}
|
||||
move_line_vals_list = []
|
||||
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
|
||||
rounding = roundings[move]
|
||||
missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[move]
|
||||
@@ -1086,7 +1087,7 @@ class StockMove(models.Model):
|
||||
# create the move line(s) but do not impact quants
|
||||
if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
|
||||
for i in range(0, int(missing_reserved_quantity)):
|
||||
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
|
||||
move_line_vals_list.append(move._prepare_move_line_vals(quantity=1))
|
||||
else:
|
||||
to_update = move.move_line_ids.filtered(lambda ml: ml.product_uom_id == move.product_uom and
|
||||
ml.location_id == move.location_id and
|
||||
@@ -1098,7 +1099,7 @@ class StockMove(models.Model):
|
||||
if to_update:
|
||||
to_update[0].product_uom_qty += missing_reserved_uom_quantity
|
||||
else:
|
||||
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=missing_reserved_quantity))
|
||||
move_line_vals_list.append(move._prepare_move_line_vals(quantity=missing_reserved_quantity))
|
||||
assigned_moves |= move
|
||||
else:
|
||||
if not move.move_orig_ids:
|
||||
@@ -1191,6 +1192,8 @@ class StockMove(models.Model):
|
||||
partially_available_moves |= move
|
||||
if move.product_id.tracking == 'serial':
|
||||
move._update_next_serial_count()
|
||||
|
||||
self.env['stock.move.line'].create(move_line_vals_list)
|
||||
partially_available_moves.write({'state': 'partially_available'})
|
||||
assigned_moves.write({'state': 'assigned'})
|
||||
self.mapped('picking_id')._check_entire_pack()
|
||||
|
||||
@@ -715,21 +715,31 @@ class Picking(models.Model):
|
||||
@api.depends('state', 'move_lines', 'move_lines.state', 'move_lines.package_level_id', 'move_lines.move_line_ids.package_level_id')
|
||||
def _compute_move_without_package(self):
|
||||
for picking in self:
|
||||
move_ids_without_package = self.env['stock.move']
|
||||
if picking.picking_type_entire_packs == False:
|
||||
move_ids_without_package = picking.move_lines
|
||||
else:
|
||||
for move in picking.move_lines:
|
||||
if not move.package_level_id:
|
||||
if move.state in ('assigned', 'done'):
|
||||
if any(not ml.package_level_id for ml in move.move_line_ids):
|
||||
move_ids_without_package |= move
|
||||
else:
|
||||
move_ids_without_package |= move
|
||||
picking.move_ids_without_package = move_ids_without_package.filtered(lambda move: not move.scrap_ids)
|
||||
picking.move_ids_without_package = picking._get_move_ids_without_package()
|
||||
|
||||
def _set_move_without_package(self):
|
||||
self.move_lines |= self.move_ids_without_package
|
||||
new_mwp = self[0].move_ids_without_package
|
||||
for picking in self:
|
||||
old_mwp = picking._get_move_ids_without_package()
|
||||
picking.move_lines = (picking.move_lines - old_mwp) | new_mwp
|
||||
moves_to_unlink = old_mwp - new_mwp
|
||||
if moves_to_unlink:
|
||||
moves_to_unlink.unlink()
|
||||
|
||||
def _get_move_ids_without_package(self):
|
||||
self.ensure_one()
|
||||
move_ids_without_package = self.env['stock.move']
|
||||
if not self.picking_type_entire_packs:
|
||||
move_ids_without_package = self.move_lines
|
||||
else:
|
||||
for move in self.move_lines:
|
||||
if not move.package_level_id:
|
||||
if move.state in ('assigned', 'done'):
|
||||
if any(not ml.package_level_id for ml in move.move_line_ids):
|
||||
move_ids_without_package |= move
|
||||
else:
|
||||
move_ids_without_package |= move
|
||||
return move_ids_without_package.filtered(lambda move: not move.scrap_ids)
|
||||
|
||||
def _check_move_lines_map_quant_package(self, package):
|
||||
""" This method checks that all product of the package (quant) are well present in the move_line_ids of the picking. """
|
||||
|
||||
@@ -1815,6 +1815,20 @@ class TestSinglePicking(TestStockCommon):
|
||||
back_order = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)])
|
||||
self.assertFalse(back_order, 'There should be no back order')
|
||||
|
||||
def test_unlink_move_1(self):
|
||||
picking = Form(self.env['stock.picking'])
|
||||
ptout = self.env['stock.picking.type'].browse(self.picking_type_out)
|
||||
picking.picking_type_id = ptout
|
||||
with picking.move_ids_without_package.new() as move:
|
||||
move.product_id = self.productA
|
||||
move.product_uom_qty = 10
|
||||
picking = picking.save()
|
||||
|
||||
picking = Form(picking)
|
||||
picking.move_ids_without_package.remove(0)
|
||||
picking = picking.save()
|
||||
self.assertEqual(len(picking.move_ids_without_package), 0)
|
||||
|
||||
def test_owner_1(self):
|
||||
"""Make a receipt, set an owner and validate"""
|
||||
owner1 = self.env['res.partner'].create({'name': 'owner'})
|
||||
|
||||
@@ -411,10 +411,11 @@ class Survey(http.Controller):
|
||||
answer_tag = "%s_%s" % (survey_sudo.id, question.id)
|
||||
request.env['survey.user_input_line'].sudo().save_lines(answer_sudo.id, question, post, answer_tag)
|
||||
|
||||
vals = {}
|
||||
if answer_sudo.is_time_limit_reached or survey_sudo.questions_layout == 'one_page':
|
||||
go_back = False
|
||||
answer_sudo._mark_done()
|
||||
else:
|
||||
elif 'button_submit' in post:
|
||||
go_back = post['button_submit'] == 'previous'
|
||||
next_page, last = request.env['survey.survey'].next_page_or_question(answer_sudo, page_or_question_id, go_back=go_back)
|
||||
vals = {'last_displayed_page_id': page_or_question_id}
|
||||
@@ -423,11 +424,16 @@ class Survey(http.Controller):
|
||||
answer_sudo._mark_done()
|
||||
else:
|
||||
vals.update({'state': 'skip'})
|
||||
answer_sudo.write(vals)
|
||||
|
||||
ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token)
|
||||
if go_back:
|
||||
ret['redirect'] += '?prev=prev'
|
||||
if 'breadcrumb_redirect' in post:
|
||||
ret['redirect'] = post['breadcrumb_redirect']
|
||||
else:
|
||||
if vals:
|
||||
answer_sudo.write(vals)
|
||||
|
||||
ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token)
|
||||
if go_back:
|
||||
ret['redirect'] += '?prev=prev'
|
||||
|
||||
return json.dumps(ret)
|
||||
|
||||
|
||||
@@ -129,6 +129,15 @@ if(!the_form.length) {
|
||||
}
|
||||
}
|
||||
|
||||
$('.o_survey_header .breadcrumb-item a').each(function() {
|
||||
var $valObj = $(this);
|
||||
$valObj.click(function(event) {
|
||||
event.preventDefault();
|
||||
$('<input>').attr({type: 'hidden', name: 'breadcrumb_redirect', value: $valObj.attr('href')}).appendTo('.js_surveyform');
|
||||
$('.js_surveyform').submit();
|
||||
});
|
||||
});
|
||||
|
||||
// Parameters for form submission
|
||||
$('.js_surveyform').ajaxForm({
|
||||
url: submit_controller,
|
||||
@@ -232,5 +241,4 @@ if(!the_form.length) {
|
||||
display_scores();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
-4
@@ -55,10 +55,6 @@ ul ul {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
|
||||
@@ -537,7 +537,7 @@ var dom = {
|
||||
menuItemsWidth += computeFloatOuterWidthWithMargins($extraItemsToggle[0]);
|
||||
do {
|
||||
menuItemsWidth -= computeFloatOuterWidthWithMargins($items.eq(--nbItems)[0]);
|
||||
} while (!(maxWidth - menuItemsWidth >= -0.001));
|
||||
} while (!(maxWidth - menuItemsWidth >= -0.001) && (nbItems > 0));
|
||||
|
||||
var $extraItems = $items.slice(nbItems).detach();
|
||||
$extraItems.removeClass('nav-item');
|
||||
|
||||
@@ -415,7 +415,7 @@ function parseDate(value, field, options) {
|
||||
if (options && options.isUTC) {
|
||||
date = moment.utc(value);
|
||||
} else {
|
||||
date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true);
|
||||
date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601]);
|
||||
}
|
||||
if (date.isValid()) {
|
||||
if (date.year() === 0) {
|
||||
@@ -459,7 +459,7 @@ function parseDateTime(value, field, options) {
|
||||
// phatomjs crash if we don't use this format
|
||||
datetime = moment.utc(value.replace(' ', 'T') + 'Z');
|
||||
} else {
|
||||
datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601], true);
|
||||
datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601]);
|
||||
if (options && options.timezone) {
|
||||
datetime.add(-session.getTZOffset(datetime), 'minutes');
|
||||
}
|
||||
|
||||
@@ -828,6 +828,8 @@ var ListFieldMany2One = FieldMany2One.extend({
|
||||
}),
|
||||
|
||||
/**
|
||||
* Should never be allowed to be opened while in readonly mode in a list
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
|
||||
@@ -54,6 +54,19 @@ var ListController = BasicController.extend({
|
||||
this.selectedRecords = params.selectedRecords || [];
|
||||
this.multipleRecordsSavingPromise = null;
|
||||
this.fieldChangedPrevented = false;
|
||||
this._onMouseupWindowDiscard = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Overriden to properly unbind any mouseup listeners still bound to the window
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
if (this._onMouseupWindowDiscard) {
|
||||
window.removeEventListener('mouseup', this._onMouseupWindowDiscard, true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -91,6 +91,13 @@ var DateWidget = Widget.extend({
|
||||
* set datetime value
|
||||
*/
|
||||
changeDatetime: function () {
|
||||
if (this.__libInput > 0) {
|
||||
if (this.options.warn_future) {
|
||||
this._warnFuture(this.getValue());
|
||||
}
|
||||
this.trigger("datetime_changed");
|
||||
return;
|
||||
}
|
||||
var oldValue = this.getValue();
|
||||
if (this.isValid()) {
|
||||
this._setValueFromUi();
|
||||
@@ -253,6 +260,7 @@ var DateWidget = Widget.extend({
|
||||
*/
|
||||
_onDateTimePickerHide: function () {
|
||||
this.__isOpen = false;
|
||||
this.changeDatetime();
|
||||
if (this._onScroll) {
|
||||
window.removeEventListener('scroll', this._onScroll, true);
|
||||
}
|
||||
|
||||
@@ -511,6 +511,42 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('float field in list view no widget', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="qux" digits="[5,3]"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty',
|
||||
'Float field should be considered set for value 0.');
|
||||
assert.strictEqual(form.$('.o_field_widget').first().text(), '0.000',
|
||||
'The value should be displayed properly.');
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
assert.strictEqual(form.$('input[name=qux]').val(), '0.000',
|
||||
'The value should be rendered with correct precision.');
|
||||
|
||||
await testUtils.fields.editInput(form.$('input[name=qux]'), '108.2458938598598');
|
||||
assert.strictEqual(form.$('input[name=qux]').val(), '108.2458938598598',
|
||||
'The value should not be formated yet.');
|
||||
|
||||
await testUtils.fields.editInput(form.$('input[name=qux]'), '18.8958938598598');
|
||||
await testUtils.form.clickSave(form);
|
||||
assert.strictEqual(form.$('.o_field_widget').first().text(), '18.896',
|
||||
'The new value should be rounded properly.');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('float field in form view', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
|
||||
@@ -353,5 +353,39 @@ QUnit.test('parse datetime', function (assert) {
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
|
||||
QUnit.test('parse date without separator', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
|
||||
_.extend(core._t.database.parameters, {date_format: '%d.%m/%Y'});
|
||||
var dateFormat = "DD.MM/YYYY";
|
||||
|
||||
assert.throws(function () {fieldUtils.parse.date("1197")}, /is not a correct/, "Wrongly formated dates should be invalid");
|
||||
assert.throws(function () {fieldUtils.parse.date("0131")}, /is not a correct/, "Wrongly formated dates should be invalid");
|
||||
assert.throws(function () {fieldUtils.parse.date("970131")}, /is not a correct/, "Wrongly formated dates should be invalid");
|
||||
assert.equal(fieldUtils.parse.date("3101").format(dateFormat), "31.01/" + moment.utc().year());
|
||||
assert.equal(fieldUtils.parse.date("31.01").format(dateFormat), "31.01/" + moment.utc().year());
|
||||
assert.equal(fieldUtils.parse.date("310197").format(dateFormat), "31.01/1997");
|
||||
assert.equal(fieldUtils.parse.date("310117").format(dateFormat), "31.01/2017");
|
||||
assert.equal(fieldUtils.parse.date("31011985").format(dateFormat), "31.01/1985");
|
||||
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
|
||||
QUnit.test('parse datetime without separator', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var originalParameters = _.clone(core._t.database.parameters);
|
||||
|
||||
_.extend(core._t.database.parameters, {date_format: '%d.%m/%Y', time_format: '%H:%M/%S'});
|
||||
var dateTimeFormat = "DD.MM/YYYY HH:mm/ss";
|
||||
|
||||
assert.equal(fieldUtils.parse.datetime("3101198508").format(dateTimeFormat), "31.01/1985 08:00/00");
|
||||
assert.equal(fieldUtils.parse.datetime("310119850833").format(dateTimeFormat), "31.01/1985 08:33/00");
|
||||
assert.equal(fieldUtils.parse.datetime("31/01/1985 08").format(dateTimeFormat), "31.01/1985 08:00/00");
|
||||
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -701,6 +701,7 @@ QUnit.module('Search View', {
|
||||
// DateTime case
|
||||
$autocomplete = $('.o_searchview_input');
|
||||
await stringToEvent($autocomplete, '07/15/1983 00:00:00');
|
||||
await testUtils.fields.triggerKey('down', $autocomplete, 'down');
|
||||
await testUtils.fields.triggerKey('up', $autocomplete, 'enter');
|
||||
|
||||
assert.equal($('.o_searchview_facet .o_facet_values').text().trim(), '07/15/1983 00:00:00',
|
||||
|
||||
@@ -362,9 +362,11 @@ table.table_desc tr td {
|
||||
// Title
|
||||
.s_title {
|
||||
.s_title_boxed {
|
||||
display: inline-block;
|
||||
padding: $grid-gutter-width;
|
||||
border: 1px solid;
|
||||
> * {
|
||||
display: inline-block;
|
||||
padding: $grid-gutter-width;
|
||||
border: 1px solid;
|
||||
}
|
||||
}
|
||||
.s_title_lines {
|
||||
overflow: hidden;
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
<filter string="Event" name="group_by_event" context="{'group_by': 'event_id'}" />
|
||||
<filter string="Question" name="group_by_question" context="{'group_by': 'question_id'}"/>
|
||||
<filter string="Answer" name="group_by_answer" context="{'group_by': 'answer_id'}" />
|
||||
<filter name="exclude_cancelled"
|
||||
string="Exclude cancelled registrations"
|
||||
domain="[('attendee_id.state', '!=', 'cancel')]" />
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<h1 style="color:#A9A9A9;">THERE'S SOMETHING IN YOUR CART.</h1>
|
||||
Would you like to complete your purchase?<br/><br/>
|
||||
% if object.order_line:
|
||||
% for line in object.order_line:
|
||||
% for line in object.website_order_line:
|
||||
<hr/>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
|
||||
@@ -55,5 +55,5 @@ class WebsiteSaleWishlist(WebsiteSale):
|
||||
request.session.modified = True
|
||||
wish.sudo().unlink()
|
||||
else:
|
||||
wish.active = False
|
||||
wish.unlink()
|
||||
return True
|
||||
|
||||
@@ -28,7 +28,7 @@ new file mode 100644
|
||||
+ """,
|
||||
+
|
||||
+ 'author': "My Company",
|
||||
+ 'website': "http://www.yourcompany.com",
|
||||
+ 'website': "https://www.yourcompany.com",
|
||||
+
|
||||
+ # Categories can be used to filter modules in modules listing
|
||||
+ # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
|
||||
+141
-152
@@ -18,19 +18,23 @@ e.g. button methods. All actions share two mandatory attributes:
|
||||
``name``
|
||||
short user-readable description of the action, may be displayed in the
|
||||
client's interface
|
||||
``binding_model_id``
|
||||
if set, the action is available in the action bar for the given model
|
||||
|
||||
.. note:: For Server Actions, use ``model_id``.
|
||||
|
||||
A client can get actions in 4 forms:
|
||||
|
||||
``False``
|
||||
if any action dialog is currently open, close it
|
||||
A string
|
||||
if a :ref:`client action <reference/actions/client>` matches, interpret as
|
||||
a client action's tag, otherwise treat as a number
|
||||
A number
|
||||
read the corresponding action record from the database, may be a database
|
||||
identifier or an :term:`external id`
|
||||
A dictionary
|
||||
treat as a client action descriptor and execute
|
||||
* ``False``
|
||||
if any action dialog is currently open, close it
|
||||
* A string
|
||||
if a :ref:`client action <reference/actions/client>` matches, interpret as
|
||||
a client action's tag, otherwise treat as a number
|
||||
* A number
|
||||
read the corresponding action record from the database, may be a database
|
||||
identifier or an :term:`external id`
|
||||
* A dictionary
|
||||
treat as a client action descriptor and execute
|
||||
|
||||
Bindings
|
||||
========
|
||||
@@ -122,12 +126,27 @@ dialog::
|
||||
In-database window actions have a few different fields which should be ignored
|
||||
by clients, mostly to use in composing the ``views`` list:
|
||||
|
||||
``view_mode``
|
||||
comma-separated list of view types as a string. All of these types will be
|
||||
``view_mode`` (default= ``tree,form`` )
|
||||
comma-separated list of view types as a string (/!\\ No spaces /!\\). All of these types will be
|
||||
present in the generated ``views`` list (with at least a ``False`` view_id)
|
||||
``view_ids``
|
||||
M2M\ [#notquitem2m]_ to view objects, defines the initial content of
|
||||
``views``
|
||||
|
||||
.. note:: Act_window views can also be defined cleanly through ``ir.actions.act_window.view``.
|
||||
|
||||
If you plan to allow multiple views for your model, prefer using
|
||||
ir.actions.act_window.view instead of the action ``view_ids``
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.actions.act_window.view" id="test_action_tree">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="view_test_tree"/>
|
||||
<field name="act_window_id" ref="test_action"/>
|
||||
</record>
|
||||
|
||||
``view_id``
|
||||
specific view added to the ``views`` list in case its type is part of the
|
||||
``view_mode`` list and not already filled by one of the views in
|
||||
@@ -160,6 +179,9 @@ The server-side composition of the ``views`` sequence is the following:
|
||||
* ``groups_id``?
|
||||
* ``filter``?
|
||||
|
||||
.. [#notquitem2m] technically not an M2M: adds a sequence field and may be
|
||||
composed of just a view type, without a view id.
|
||||
|
||||
.. _reference/actions/url:
|
||||
|
||||
URL Actions (``ir.actions.act_url``)
|
||||
@@ -178,7 +200,7 @@ via two fields:
|
||||
|
||||
{
|
||||
"type": "ir.actions.act_url",
|
||||
"url": "http://odoo.com",
|
||||
"url": "https://odoo.com",
|
||||
"target": "self",
|
||||
}
|
||||
|
||||
@@ -189,6 +211,8 @@ will replace the current content section by the Odoo home page.
|
||||
Server Actions (``ir.actions.server``)
|
||||
======================================
|
||||
|
||||
.. autoclass:: odoo.addons.base.models.ir_actions.IrActionsServer
|
||||
|
||||
Allow triggering complex server code from any valid action location. Only
|
||||
two fields are relevant to clients:
|
||||
|
||||
@@ -202,151 +226,90 @@ specific or generic actions based on their ``state``. Some fields (and
|
||||
corresponding behaviors) are shared between states:
|
||||
|
||||
``model_id``
|
||||
Odoo model linked to the action, made available in
|
||||
:ref:`evaluation contexts <reference/actions/server/context>`
|
||||
``condition`` (optional)
|
||||
evaluated as Python code using the server action's
|
||||
:ref:`evaluation context <reference/actions/server/context>`. If
|
||||
``False``, prevents the action from running. Default: ``True``
|
||||
Odoo model linked to the action.
|
||||
|
||||
Valid action types (``state`` field) are extensible, the default types are:
|
||||
``state``
|
||||
|
||||
``code``
|
||||
--------
|
||||
* ``code``: Executes python code given through the ``code`` argument.
|
||||
|
||||
The default and most flexible server action type, executes arbitrary Python
|
||||
code with the action's :ref:`evaluation context
|
||||
<reference/actions/server/context>`. Only uses one specific type-specific
|
||||
field:
|
||||
* ``object_create``: Creates a new record of model ``crud_model_id`` following ``fields_lines`` specifications.
|
||||
|
||||
``code``
|
||||
a piece of Python code to execute when the action is called
|
||||
* ``object_write``: Updates the current record(s) following ``fields_lines`` specifications
|
||||
|
||||
.. code-block:: xml
|
||||
* ``multi``: Executes serveral actions given through the ``child_ids`` argument.
|
||||
|
||||
<record model="ir.actions.server" id="print_instance">
|
||||
<field name="name">Res Partner Server Action</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="code">
|
||||
raise Warning(object.name)
|
||||
</field>
|
||||
</record>
|
||||
State fields
|
||||
------------
|
||||
|
||||
.. note::
|
||||
Depending on its state, the behavior is defined through different fields.
|
||||
The concerned state is given after each field.
|
||||
|
||||
The code segment can define a variable called ``action``, which will be
|
||||
returned to the client as the next action to execute:
|
||||
``code`` (code)
|
||||
Specify a piece of Python code to execute when the action is called
|
||||
|
||||
.. code-block:: xml
|
||||
.. code-block:: xml
|
||||
|
||||
<record model="ir.actions.server" id="print_instance">
|
||||
<field name="name">Res Partner Server Action</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="code">
|
||||
if object.some_condition():
|
||||
action = {
|
||||
"type": "ir.actions.act_window",
|
||||
"view_mode": "form",
|
||||
"res_model": object._name,
|
||||
"res_id": object.id,
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.server" id="print_instance">
|
||||
<field name="name">Res Partner Server Action</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="code">
|
||||
raise Warning(record.name)
|
||||
</field>
|
||||
</record>
|
||||
|
||||
will ask the client to open a form for the record if it fulfills some
|
||||
condition
|
||||
.. note::
|
||||
|
||||
This tends to be the only action type created from :ref:`data files
|
||||
<reference/data>`, other types aside from
|
||||
:ref:`reference/actions/server/multi` are simpler than Python code to define
|
||||
from the UI, but not from :ref:`data files <reference/data>`.
|
||||
The code segment can define a variable called ``action``, which will be
|
||||
returned to the client as the next action to execute:
|
||||
|
||||
.. _reference/actions/server/object_create:
|
||||
.. code-block:: xml
|
||||
|
||||
``object_create``
|
||||
-----------------
|
||||
<record model="ir.actions.server" id="print_instance">
|
||||
<field name="name">Res Partner Server Action</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="code">
|
||||
if record.some_condition():
|
||||
action = {
|
||||
"type": "ir.actions.act_window",
|
||||
"view_mode": "form",
|
||||
"res_model": record._name,
|
||||
"res_id": record.id,
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
Creates a new record, from scratch (via :meth:`~odoo.models.Model.create`)
|
||||
or by copying an existing record (via :meth:`~odoo.models.Model.copy`)
|
||||
will ask the client to open a form for the record if it fulfills some
|
||||
condition
|
||||
|
||||
``use_create``
|
||||
the creation policy, one of:
|
||||
.. This tends to be the only action type created from :ref:`data files
|
||||
<reference/data>`, other types aside from
|
||||
:ref:`reference/actions/server/multi` are simpler than Python code to define
|
||||
from the UI, but not from :ref:`data files <reference/data>`.
|
||||
|
||||
``new``
|
||||
creates a record in the model specified by ``model_id``
|
||||
``new_other``
|
||||
creates a record in the model specified by ``crud_model_id``
|
||||
``copy_current``
|
||||
copies the record on which the action was invoked
|
||||
``copy_other``
|
||||
copies an other record, obtained via ``ref_object``
|
||||
``fields_lines``
|
||||
``crud_model_id`` (create)(required)
|
||||
model in which to create a new record
|
||||
``link_field_id`` (create)
|
||||
many2one to ``ir.model.fields``, specifies the current record's m2o field
|
||||
on which the newly created record should be set (models should match)
|
||||
|
||||
``fields_lines`` (create/write)
|
||||
fields to override when creating or copying the record.
|
||||
:class:`~odoo.fields.One2many` with the fields:
|
||||
|
||||
``col1``
|
||||
``ir.model.fields`` to set in the model implied by ``use_create``
|
||||
``ir.model.fields`` to set in the concerned model
|
||||
(``crud_model_id`` for creates, ``model_id`` for updates)
|
||||
``value``
|
||||
value for the field, interpreted via ``type``
|
||||
``type``
|
||||
``type`` (value|reference|equation)
|
||||
If ``value``, the ``value`` field is interpreted as a literal value
|
||||
(possibly converted), if ``equation`` the ``value`` field is
|
||||
interpreted as a Python expression and evaluated
|
||||
``crud_model_id``
|
||||
model in which to create a new record, if ``use_create`` is set to
|
||||
``new_other``
|
||||
``ref_object``
|
||||
:class:`~odoo.fields.Reference` to an arbitrary record to copy, used if
|
||||
``use_create`` is set to ``copy_other``
|
||||
``link_new_record``
|
||||
boolean flag linking the newly created record to the current one via a
|
||||
many2one field specified through ``link_field_id``, defaults to ``False``
|
||||
``link_field_id``
|
||||
many2one to ``ir.model.fields``, specifies the current record's m2o field
|
||||
on which the newly created record should be set (models should match)
|
||||
|
||||
``object_write``
|
||||
----------------
|
||||
|
||||
Similar to :ref:`reference/actions/server/object_create` but alters an
|
||||
existing records instead of creating one
|
||||
|
||||
``use_write``
|
||||
write policy, one of:
|
||||
|
||||
``current``
|
||||
write to the current record
|
||||
``other``
|
||||
write to an other record selected via ``crud_model_id`` and
|
||||
``ref_object``
|
||||
``expression``
|
||||
write to an other record whose model is selected via ``crud_model_id``
|
||||
and whose id is selected by evaluating ``write_expression``
|
||||
``write_expression``
|
||||
Python expression returning a record or an object id, used when
|
||||
``use_write`` is set to ``expression`` in order to decide which record
|
||||
should be modified
|
||||
``fields_lines``
|
||||
see :ref:`reference/actions/server/object_create`
|
||||
``crud_model_id``
|
||||
see :ref:`reference/actions/server/object_create`
|
||||
``ref_object``
|
||||
see :ref:`reference/actions/server/object_create`
|
||||
|
||||
.. _reference/actions/server/multi:
|
||||
|
||||
``multi``
|
||||
---------
|
||||
|
||||
Executes multiple actions one after the other. Actions to execute are defined
|
||||
via the ``child_ids`` m2m. If sub-actions themselves return actions, the last
|
||||
one will be returned to the client as the multi's own next action
|
||||
|
||||
``client_action``
|
||||
-----------------
|
||||
|
||||
Indirection for directly returning an other action defined using
|
||||
``action_id``. Simply returns that action to the client for execution.
|
||||
``child_ids`` (multi)
|
||||
Specify the multiple sub-actions (``ir.actions.server``) to enact in state multi.
|
||||
If sub-actions themselves return actions, the last
|
||||
one will be returned to the client as the multi's own next action
|
||||
|
||||
.. _reference/actions/server/context:
|
||||
|
||||
@@ -356,23 +319,12 @@ Evaluation context
|
||||
A number of keys are available in the evaluation context of or surrounding
|
||||
server actions:
|
||||
|
||||
``model``
|
||||
the model object linked to the action via ``model_id``
|
||||
``object``, ``obj``
|
||||
only available if ``active_model`` and ``active_id`` are provided (via
|
||||
context) otherwise ``None``. The actual record selected by ``active_id``
|
||||
``pool``
|
||||
the current database registry
|
||||
``datetime``, ``dateutil``, ``time``
|
||||
corresponding Python modules
|
||||
``cr``
|
||||
the current cursor
|
||||
``user``
|
||||
the current user record
|
||||
``context``
|
||||
execution context
|
||||
``Warning``
|
||||
constructor for the ``Warning`` exception
|
||||
* ``model`` model object linked to the action via ``model_id``
|
||||
* ``record``/``records`` record/recorset on which the action is triggered, can be void.
|
||||
* ``env`` Odoo Environment
|
||||
* ``datetime``, ``dateutil``, ``time``, ``timezone`` corresponding Python modules
|
||||
* ``log: log(message, level='info')`` logging function to record debug information in ir.logging table
|
||||
* ``Warning`` constructor for the ``Warning`` exception
|
||||
|
||||
.. _reference/actions/report:
|
||||
|
||||
@@ -386,13 +338,15 @@ Triggers the printing of a report
|
||||
in a list of some sort
|
||||
``model`` (mandatory)
|
||||
the model your report will be about
|
||||
``report_type`` (mandatory)
|
||||
``report_type`` (default=qweb-pdf)
|
||||
either ``qweb-pdf`` for PDF reports or ``qweb-html`` for HTML
|
||||
``report_name``
|
||||
``report_name`` (mandatory)
|
||||
the name of your report (which will be the name of the PDF output)
|
||||
``groups_id``
|
||||
:class:`~odoo.fields.Many2many` field to the groups allowed to view/use
|
||||
the current report
|
||||
``multi``
|
||||
if set to ``True``, the action will not be displayed on a form view.
|
||||
``paperformat_id``
|
||||
:class:`~odoo.fields.Many2one` field to the paper format you wish to
|
||||
use for this report (if not specified, the company format will be used)
|
||||
@@ -436,5 +390,40 @@ Triggers an action implemented entirely in the client.
|
||||
tells the client to start the Point of Sale interface, the server has no idea
|
||||
how the POS interface works.
|
||||
|
||||
.. [#notquitem2m] technically not an M2M: adds a sequence field and may be
|
||||
composed of just a view type, without a view id.
|
||||
.. _reference/actions/cron:
|
||||
|
||||
Automated Actions (``ir.cron``)
|
||||
======================================
|
||||
|
||||
Actions triggered automatically on a predefined frequency.
|
||||
|
||||
``name``
|
||||
Name of the automated action (Mainly used in log display)
|
||||
|
||||
``interval_number``
|
||||
Number of *interval_type* uom between two executions of the action
|
||||
|
||||
``interval_type``
|
||||
Unit of measure of frequency interval (``minutes``, ``hours``, ``days``, ``weeks``, ``months``,
|
||||
|
||||
``numbercall``
|
||||
Number of times this action has to be run.
|
||||
If the action is expected to run indefinitely, set to ``-1``.
|
||||
|
||||
``doall``
|
||||
Boolean precising whether the missed actions have to be executed in case of
|
||||
server restarts.
|
||||
|
||||
``model_id``
|
||||
Model on which this action will be called
|
||||
|
||||
``code``
|
||||
Code content of the action.
|
||||
Can be a simple call to the model's method :
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
model.<method_name>()
|
||||
|
||||
``nextcall``
|
||||
Next planned execution date of this action (date/time format)
|
||||
|
||||
+192
-141
@@ -19,6 +19,8 @@ Running the server
|
||||
Providing a comma-separated list restrict access to databases provided in
|
||||
list.
|
||||
|
||||
For advanced database options, take a look :ref:`below <reference/cmdline/server/database>`.
|
||||
|
||||
.. option:: -i <modules>, --init <modules>
|
||||
|
||||
comma-separated list of modules to install before running the server
|
||||
@@ -32,82 +34,24 @@ Running the server
|
||||
.. option:: --addons-path <directories>
|
||||
|
||||
comma-separated list of directories in which modules are stored. These
|
||||
directories are scanned for modules (nb: when and why?)
|
||||
directories are scanned for modules.
|
||||
|
||||
.. option:: --workers <count>
|
||||
|
||||
if ``count`` is not 0 (the default), enables multiprocessing and sets up
|
||||
the specified number of HTTP workers (sub-processes processing HTTP
|
||||
and RPC requests).
|
||||
|
||||
.. note:: multiprocessing mode is only available on Unix-based systems
|
||||
|
||||
A number of options allow limiting and recycling workers:
|
||||
|
||||
.. option:: --limit-request <limit>
|
||||
|
||||
Number of requests a worker will process before being recycled and
|
||||
restarted.
|
||||
|
||||
Defaults to 8196.
|
||||
|
||||
.. option:: --limit-memory-soft <limit>
|
||||
|
||||
Maximum allowed virtual memory per worker. If the limit is exceeded,
|
||||
the worker is killed and recycled at the end of the current request.
|
||||
|
||||
Defaults to 2048MiB.
|
||||
|
||||
.. option:: --limit-memory-hard <limit>
|
||||
|
||||
Hard limit on virtual memory, any worker exceeding the limit will be
|
||||
immediately killed without waiting for the end of the current request
|
||||
processing.
|
||||
|
||||
Defaults to 2560MiB.
|
||||
|
||||
.. option:: --limit-time-cpu <limit>
|
||||
|
||||
Prevents the worker from using more than <limit> CPU seconds for each
|
||||
request. If the limit is exceeded, the worker is killed.
|
||||
|
||||
Defaults to 60.
|
||||
|
||||
.. option:: --limit-time-real <limit>
|
||||
|
||||
Prevents the worker from taking longer than <limit> seconds to process
|
||||
a request. If the limit is exceeded, the worker is killed.
|
||||
|
||||
Differs from :option:`--limit-time-cpu` in that this is a "wall time"
|
||||
limit including e.g. SQL queries.
|
||||
|
||||
Defaults to 120.
|
||||
|
||||
.. option:: --max-cron-threads <count>
|
||||
|
||||
number of workers dedicated to cron jobs. Defaults to 2. The workers are
|
||||
threads in multi-threading mode and processes in multi-processing mode.
|
||||
|
||||
For multi-processing mode, this is in addition to the HTTP worker
|
||||
processes.
|
||||
.. (nb: when and why?)
|
||||
|
||||
.. option:: -c <config>, --config <config>
|
||||
|
||||
provide an alternate configuration file
|
||||
provide an alternate :ref:`configuration file <reference/cmdline/config>`
|
||||
|
||||
.. option:: -s, --save
|
||||
|
||||
saves the server configuration to the current configuration file
|
||||
(:file:`{$HOME}/.odoorc` by default, and can be overridden using
|
||||
:option:`-c`)
|
||||
:option:`-c`).
|
||||
|
||||
.. option:: --proxy-mode
|
||||
.. option:: --without-demo
|
||||
|
||||
enables the use of ``X-Forwarded-*`` headers through `Werkzeug's proxy
|
||||
support`_.
|
||||
|
||||
.. warning:: proxy mode *must not* be enabled outside of a reverse proxy
|
||||
scenario
|
||||
disables demo data loading for modules installed
|
||||
comma-separated, use ``all`` for all modules.
|
||||
|
||||
.. option:: --test-enable
|
||||
|
||||
@@ -131,25 +75,9 @@ Running the server
|
||||
``1``, ``t`` or ``true`` can be used to use the same directory as the above
|
||||
mentioned ``--screenshots`` option.
|
||||
|
||||
.. option:: --dev <feature,feature,...,feature>
|
||||
|
||||
* ``all``: all the features below are activated
|
||||
|
||||
* ``xml``: read template qweb from xml file directly instead of database.
|
||||
Once a template has been modified in database, it will be not be read from
|
||||
the xml file until the next update/init.
|
||||
|
||||
* ``reload``: restart server when python file are updated (may not be detected
|
||||
depending on the text editor used)
|
||||
|
||||
* ``qweb``: break in the evaluation of qweb template when a node contains ``t-debug='debugger'``
|
||||
|
||||
* ``(i)p(u)db``: start the chosen python debugger in the code when an
|
||||
unexpected error is raised before logging and returning the error.
|
||||
|
||||
.. _reference/cmdline/server/database:
|
||||
|
||||
database
|
||||
Database
|
||||
--------
|
||||
|
||||
.. option:: -r <user>, --db_user <user>
|
||||
@@ -189,40 +117,40 @@ database
|
||||
listen by using the --database parameter and specifying a comma-separated
|
||||
list of databases
|
||||
|
||||
When combining the two parameters, db-filter superseed the comma-separated
|
||||
When combining the two parameters, db-filter supersedes the comma-separated
|
||||
database list for restricting database list, while the comma-separated list
|
||||
is used for performing requested operations like upgrade of modules.
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
odoo-bin --db-filter ^11.*$
|
||||
$ odoo-bin --db-filter ^11.*$
|
||||
|
||||
Restrict access to databases whose name starts with 11
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
odoo-bin --database 11firstdatabase,11seconddatabase
|
||||
$ odoo-bin --database 11firstdatabase,11seconddatabase
|
||||
|
||||
Restrict access to only two databases, 11firstdatabase and 11seconddatabase
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
odoo-bin --database 11firstdatabase,11seconddatabase -u base
|
||||
$ odoo-bin --database 11firstdatabase,11seconddatabase -u base
|
||||
|
||||
Restrict access to only two databases, 11firstdatabase and 11seconddatabase,
|
||||
and update base module on one database: 11firstdatabase
|
||||
and update base module on one database: 11firstdatabase.
|
||||
If database 11seconddatabase doesn't exist, the database is created and base modules
|
||||
is installed
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
odoo-bin --db-filter ^11.*$ --database 11firstdatabase,11seconddatabase -u base
|
||||
|
||||
$ odoo-bin --db-filter ^11.*$ --database 11firstdatabase,11seconddatabase -u base
|
||||
|
||||
Restrict access to databases whose name starts with 11,
|
||||
and update base module on one database: 11firstdatabase
|
||||
and update base module on one database: 11firstdatabase.
|
||||
If database 11seconddatabase doesn't exist, the database is created and base modules
|
||||
is installed
|
||||
|
||||
|
||||
.. option:: --db-template <template>
|
||||
|
||||
when creating new databases from the database-management screens, use the
|
||||
@@ -237,7 +165,7 @@ database
|
||||
.. option:: --no-database-list
|
||||
|
||||
Suppresses the ability to list databases available on the system
|
||||
|
||||
|
||||
.. option:: --db_sslmode
|
||||
|
||||
Control the SSL security of the connection between Odoo and PostgreSQL.
|
||||
@@ -245,6 +173,33 @@ database
|
||||
'verify-ca' or 'verify-full'
|
||||
Default value is 'prefer'
|
||||
|
||||
.. _reference/cmdline/server/emails:
|
||||
|
||||
Emails
|
||||
------
|
||||
|
||||
.. option:: --email-from <address>
|
||||
|
||||
Email address used as <FROM> when Odoo needs to send mails
|
||||
|
||||
.. option:: --smtp <server>
|
||||
|
||||
Address of the SMTP server to connect to in order to send mails
|
||||
|
||||
.. option:: --smtp-port <port>
|
||||
|
||||
.. option:: --smtp-ssl
|
||||
|
||||
If set, odoo should use SSL/STARTSSL SMTP connections
|
||||
|
||||
.. option:: --smtp-user <name>
|
||||
|
||||
Username to connect to the SMTP server
|
||||
|
||||
.. option:: --smtp-password <password>
|
||||
|
||||
Password to connect to the SMTP server
|
||||
|
||||
.. _reference/cmdline/server/internationalisation:
|
||||
|
||||
Internationalisation
|
||||
@@ -283,13 +238,41 @@ of importation
|
||||
|
||||
specify modules to export. Use in combination with --i18n-export
|
||||
|
||||
.. _reference/cmdline/advanced:
|
||||
|
||||
built-in HTTP
|
||||
-------------
|
||||
Advanced Options
|
||||
----------------
|
||||
|
||||
.. _reference/cmdline/dev:
|
||||
|
||||
Developer features
|
||||
''''''''''''''''''
|
||||
|
||||
.. option:: --dev <feature,feature,...,feature>
|
||||
|
||||
* ``all``: all the features below are activated
|
||||
|
||||
* ``xml``: read template qweb from xml file directly instead of database.
|
||||
Once a template has been modified in database, it will be not be read from
|
||||
the xml file until the next update/init.
|
||||
|
||||
* ``reload``: restart server when python file are updated (may not be detected
|
||||
depending on the text editor used)
|
||||
|
||||
* ``qweb``: break in the evaluation of qweb template when a node contains ``t-debug='debugger'``
|
||||
|
||||
* ``(i)p(u)db``: start the chosen python debugger in the code when an
|
||||
unexpected error is raised before logging and returning the error.
|
||||
|
||||
|
||||
.. _reference/cmdline/server/http:
|
||||
|
||||
HTTP
|
||||
''''
|
||||
|
||||
.. option:: --no-http
|
||||
|
||||
do not start the HTTP or long-polling workers (may still start cron
|
||||
do not start the HTTP or long-polling workers (may still start :ref:`cron <reference/actions/cron>`
|
||||
workers)
|
||||
|
||||
.. warning:: has no effect if :option:`--test-enable` is set, as tests
|
||||
@@ -309,25 +292,35 @@ built-in HTTP
|
||||
TCP port for long-polling connections in multiprocessing or gevent mode,
|
||||
defaults to 8072. Not used in default (threaded) mode.
|
||||
|
||||
logging
|
||||
-------
|
||||
.. option:: --proxy-mode
|
||||
|
||||
enables the use of ``X-Forwarded-*`` headers through `Werkzeug's proxy
|
||||
support`_.
|
||||
|
||||
.. warning:: proxy mode *must not* be enabled outside of a reverse proxy
|
||||
scenario
|
||||
|
||||
.. _reference/cmdline/server/logging:
|
||||
|
||||
Logging
|
||||
'''''''
|
||||
|
||||
By default, Odoo displays all logging of level_ ``info`` except for workflow
|
||||
logging (``warning`` only), and log output is sent to ``stdout``. Various
|
||||
options are available to redirect logging to other destinations and to
|
||||
customize the amount of logging output
|
||||
customize the amount of logging output.
|
||||
|
||||
.. option:: --logfile <file>
|
||||
|
||||
sends logging output to the specified file instead of stdout. On Unix, the
|
||||
file `can be managed by external log rotation programs
|
||||
<https://docs.python.org/2/library/logging.handlers.html#watchedfilehandler>`_
|
||||
<https://docs.python.org/3/library/logging.handlers.html#watchedfilehandler>`_
|
||||
and will automatically be reopened when replaced
|
||||
|
||||
.. option:: --syslog
|
||||
|
||||
logs to the system's event logger: `syslog on unices <https://docs.python.org/2/library/logging.handlers.html#sysloghandler>`_
|
||||
and `the Event Log on Windows <https://docs.python.org/2/library/logging.handlers.html#nteventloghandler>`_.
|
||||
logs to the system's event logger: `syslog on unices <https://docs.python.org/3/library/logging.handlers.html#sysloghandler>`_
|
||||
and `the Event Log on Windows <https://docs.python.org/3/library/logging.handlers.html#nteventloghandler>`_.
|
||||
|
||||
Neither is configurable
|
||||
|
||||
@@ -335,7 +328,7 @@ customize the amount of logging output
|
||||
|
||||
logs to the ``ir.logging`` model (``ir_logging`` table) of the specified
|
||||
database. The database can be the name of a database in the "current"
|
||||
PostgreSQL, or `a PostgreSQL URI`_ for e.g. log aggregation
|
||||
PostgreSQL, or `a PostgreSQL URI`_ for e.g. log aggregation.
|
||||
|
||||
.. option:: --log-handler <handler-spec>
|
||||
|
||||
@@ -402,60 +395,66 @@ customize the amount of logging output
|
||||
In case of conflict between :option:`--log-level` and
|
||||
:option:`--log-handler`, the latter is used
|
||||
|
||||
emails
|
||||
------
|
||||
.. _reference/cdmline/workers:
|
||||
|
||||
.. option:: --email-from <address>
|
||||
Multiprocessing
|
||||
'''''''''''''''
|
||||
|
||||
Email address used as <FROM> when Odoo needs to send mails
|
||||
.. option:: --workers <count>
|
||||
|
||||
.. option:: --smtp <server>
|
||||
if ``count`` is not 0 (the default), enables multiprocessing and sets up
|
||||
the specified number of HTTP workers (sub-processes processing HTTP
|
||||
and RPC requests).
|
||||
|
||||
Address of the SMTP server to connect to in order to send mails
|
||||
.. note:: multiprocessing mode is only available on Unix-based systems
|
||||
|
||||
.. option:: --smtp-port <port>
|
||||
A number of options allow limiting and recycling workers:
|
||||
|
||||
.. option:: --smtp-ssl
|
||||
.. option:: --limit-request <limit>
|
||||
|
||||
If set, odoo should use SSL/STARTSSL SMTP connections
|
||||
Number of requests a worker will process before being recycled and
|
||||
restarted.
|
||||
|
||||
.. option:: --smtp-user <name>
|
||||
Defaults to *8196*.
|
||||
|
||||
Username to connect to the SMTP server
|
||||
.. option:: --limit-memory-soft <limit>
|
||||
|
||||
.. option:: --smtp-password <password>
|
||||
Maximum allowed virtual memory per worker. If the limit is exceeded,
|
||||
the worker is killed and recycled at the end of the current request.
|
||||
|
||||
Password to connect to the SMTP server
|
||||
Defaults to *2048MiB*.
|
||||
|
||||
.. option:: --limit-memory-hard <limit>
|
||||
|
||||
.. _reference/cmdline/scaffold:
|
||||
Hard limit on virtual memory, any worker exceeding the limit will be
|
||||
immediately killed without waiting for the end of the current request
|
||||
processing.
|
||||
|
||||
Scaffolding
|
||||
===========
|
||||
Defaults to *2560MiB*.
|
||||
|
||||
.. program:: odoo-bin scaffold
|
||||
.. option:: --limit-time-cpu <limit>
|
||||
|
||||
Scaffolding is the automated creation of a skeleton structure to simplify
|
||||
bootstrapping (of new modules, in the case of Odoo). While not necessary it
|
||||
avoids the tedium of setting up basic structures and looking up what all
|
||||
starting requirements are.
|
||||
Prevents the worker from using more than <limit> CPU seconds for each
|
||||
request. If the limit is exceeded, the worker is killed.
|
||||
|
||||
Scaffolding is available via the :command:`odoo-bin scaffold` subcommand.
|
||||
Defaults to *60*.
|
||||
|
||||
.. option:: -t <template>
|
||||
.. option:: --limit-time-real <limit>
|
||||
|
||||
a template directory, files are passed through jinja2_ then copied to
|
||||
the ``destination`` directory
|
||||
Prevents the worker from taking longer than <limit> seconds to process
|
||||
a request. If the limit is exceeded, the worker is killed.
|
||||
|
||||
.. option:: name
|
||||
Differs from :option:`--limit-time-cpu` in that this is a "wall time"
|
||||
limit including e.g. SQL queries.
|
||||
|
||||
the name of the module to create, may munged in various manners to
|
||||
generate programmatic names (e.g. module directory name, model names, …)
|
||||
Defaults to *120*.
|
||||
|
||||
.. option:: destination
|
||||
.. option:: --max-cron-threads <count>
|
||||
|
||||
directory in which to create the new module, defaults to the current
|
||||
directory
|
||||
number of workers dedicated to :ref:`cron <reference/actions/cron>` jobs. Defaults to *2*.
|
||||
The workers are threads in multi-threading mode and processes in multi-processing mode.
|
||||
|
||||
For multi-processing mode, this is in addition to the HTTP worker processes.
|
||||
|
||||
.. _reference/cmdline/config:
|
||||
|
||||
@@ -487,15 +486,67 @@ can be overridden using :option:`--config <odoo-bin -c>`. Specifying
|
||||
to that file.
|
||||
|
||||
.. _jinja2: http://jinja.pocoo.org
|
||||
.. _regular expression: https://docs.python.org/2/library/re.html
|
||||
.. _regular expression: https://docs.python.org/3/library/re.html
|
||||
.. _password authentication:
|
||||
http://www.postgresql.org/docs/9.3/static/auth-methods.html#AUTH-PASSWORD
|
||||
https://www.postgresql.org/docs/9.3/static/auth-methods.html#AUTH-PASSWORD
|
||||
.. _template database:
|
||||
http://www.postgresql.org/docs/9.3/static/manage-ag-templatedbs.html
|
||||
https://www.postgresql.org/docs/9.3/static/manage-ag-templatedbs.html
|
||||
.. _level:
|
||||
https://docs.python.org/2/library/logging.html#logging.Logger.setLevel
|
||||
https://docs.python.org/3/library/logging.html#logging.Logger.setLevel
|
||||
.. _a PostgreSQL URI:
|
||||
http://www.postgresql.org/docs/9.2/static/libpq-connect.html#AEN38208
|
||||
https://www.postgresql.org/docs/9.2/static/libpq-connect.html#AEN38208
|
||||
.. _Werkzeug's proxy support:
|
||||
http://werkzeug.pocoo.org/docs/contrib/fixers/#werkzeug.contrib.fixers.ProxyFix
|
||||
.. _pyinotify: https://github.com/seb-m/pyinotify/wiki
|
||||
|
||||
|
||||
Shell
|
||||
=====
|
||||
|
||||
Odoo command-line also allows to launch odoo as a python console environment.
|
||||
This enables direct interaction with the :ref:`orm <reference/orm>` and its functionalities.
|
||||
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo_bin shell
|
||||
|
||||
.. option:: --shell-interface (ipython|ptpython|bpython|python)
|
||||
|
||||
Specify a preferred REPL to use in shell mode.
|
||||
|
||||
|
||||
.. _reference/cmdline/scaffold:
|
||||
|
||||
Scaffolding
|
||||
===========
|
||||
|
||||
.. program:: odoo-bin scaffold
|
||||
|
||||
Scaffolding is the automated creation of a skeleton structure to simplify
|
||||
bootstrapping (of new modules, in the case of Odoo). While not necessary it
|
||||
avoids the tedium of setting up basic structures and looking up what all
|
||||
starting requirements are.
|
||||
|
||||
Scaffolding is available via the :command:`odoo-bin scaffold` subcommand.
|
||||
|
||||
.. option:: name (required)
|
||||
|
||||
the name of the module to create, may munged in various manners to
|
||||
generate programmatic names (e.g. module directory name, model names, …)
|
||||
|
||||
.. option:: destination (default=current directory)
|
||||
|
||||
directory in which to create the new module, defaults to the current
|
||||
directory
|
||||
|
||||
.. option:: -t <template>
|
||||
|
||||
a template directory, files are passed through jinja2_ then copied to
|
||||
the ``destination`` directory
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ odoo_bin scaffold my_module /addons/
|
||||
|
||||
This will create module *my_module* in directory */addons/*.
|
||||
+49
-6
@@ -22,6 +22,7 @@ of an XML data file is the following:
|
||||
.. code-block:: xml
|
||||
|
||||
<!-- the root elements of the data file -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<operation/>
|
||||
...
|
||||
@@ -30,6 +31,25 @@ of an XML data file is the following:
|
||||
Data files are executed sequentially, operations can only refer to the result
|
||||
of operations defined previously
|
||||
|
||||
.. note::
|
||||
|
||||
If the content of the data file is expected to be applied only once, you
|
||||
can specify the odoo flag ``noupdate`` set to 1. If part of
|
||||
the data in the file is expected to be applied once, you can place this part
|
||||
of the file in a <data noupdate="1"> domain.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Only loaded when installing the module (odoo-bin -i module) -->
|
||||
<operation/>
|
||||
</data>
|
||||
|
||||
<!-- (Re)Loaded at install and update (odoo-bin -i/-u) -->
|
||||
<operation/>
|
||||
</odoo>
|
||||
|
||||
Core operations
|
||||
===============
|
||||
|
||||
@@ -152,8 +172,31 @@ Parameters can be provided using ``eval`` (should evaluate to a sequence of
|
||||
parameters to call the method with) or ``value`` elements (see ``list``
|
||||
values).
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record name="partner_1" model="res.partner">
|
||||
<field name="name">Odude</field>
|
||||
</record>
|
||||
|
||||
<function model="res.partner" name="send_inscription_notice"
|
||||
eval="[[ref('partner_1'), ref('partner_2')]]"/>
|
||||
|
||||
<function model="res.users" name="send_vip_inscription_notice">
|
||||
<function eval="[[('vip','=',True)]]" model="res.partner" name="search"/>
|
||||
</function>
|
||||
</data>
|
||||
|
||||
<record id="model_form_view" model="ir.ui.view">
|
||||
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
.. ignored assert
|
||||
|
||||
.. _reference/data/shortcuts:
|
||||
|
||||
Shortcuts
|
||||
=========
|
||||
|
||||
@@ -166,7 +209,7 @@ data files provide shorter alternatives to defining them using
|
||||
|
||||
Defines an ``ir.ui.menu`` record with a number of defaults and fallbacks:
|
||||
|
||||
Parent menu
|
||||
``parent``
|
||||
* If a ``parent`` attribute is set, it should be the :term:`external id`
|
||||
of an other menu item, used as the new item's parent
|
||||
* If no ``parent`` is provided, tries to interpret the ``name`` attribute
|
||||
@@ -175,10 +218,10 @@ Parent menu
|
||||
created
|
||||
* Otherwise the menu is defined as a "top-level" menu item (*not* a menu
|
||||
with no parent)
|
||||
Menu name
|
||||
``name``
|
||||
If no ``name`` attribute is specified, tries to get the menu name from
|
||||
a linked action if any. Otherwise uses the record's ``id``
|
||||
Groups
|
||||
``groups``
|
||||
A ``groups`` attribute is interpreted as a comma-separated sequence of
|
||||
:term:`external identifiers` for ``res.groups`` models. If an
|
||||
:term:`external identifier` is prefixed with a minus (``-``), the group
|
||||
@@ -218,7 +261,7 @@ section of the view, and allowing a few *optional* attributes:
|
||||
``report``
|
||||
----------
|
||||
|
||||
Creates a ``ir.actions.report`` record with a few default values.
|
||||
Creates a :ref:`IrActionsReport <reference/actions/report>` record with a few default values.
|
||||
|
||||
Mostly just proxies attributes to the corresponding fields on
|
||||
``ir.actions.report``, but also automatically creates the item in the
|
||||
@@ -263,5 +306,5 @@ For each row (record):
|
||||
* the third column is the ``name`` field for ``res.country.state``
|
||||
* the fourth column is the ``code`` field for ``res.country.state``
|
||||
|
||||
.. _base64: http://tools.ietf.org/html/rfc3548.html#section-3
|
||||
.. _csv: http://en.wikipedia.org/wiki/Comma-separated_values
|
||||
.. _base64: https://tools.ietf.org/html/rfc3548.html#section-3
|
||||
.. _csv: https://en.wikipedia.org/wiki/Comma-separated_values
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
:orphan:
|
||||
:banner: banners/javascript.jpg
|
||||
|
||||
.. highlight:: javascript
|
||||
|
||||
.. default-domain:: js
|
||||
|
||||
.. _api/js:
|
||||
|
||||
==============
|
||||
Javascript API
|
||||
==============
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
:banner: banners/javascript.jpg
|
||||
|
||||
.. _reference/jscs:
|
||||
|
||||
=====================
|
||||
Javascript Cheatsheet
|
||||
=====================
|
||||
@@ -280,6 +282,7 @@ using the view in the kanban arch (a specific example is the helpdesk dashboard)
|
||||
|
||||
Promises and asynchronous code
|
||||
===============================
|
||||
|
||||
For a very good and complete introduction to promises, please read this excellent article https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md
|
||||
|
||||
Creating new Promises
|
||||
@@ -418,6 +421,7 @@ Waiting for Promises
|
||||
|
||||
Error handling
|
||||
--------------
|
||||
|
||||
- in general in promises
|
||||
The general idea is that a promise should not be rejected for control flow, but should only be rejected for errors.
|
||||
When that is the case, you would have multiple resolutions of your promise with, for instance status codes that you would have to check in the `then` handlers and a single `catch` handler at the end of the promise chain.
|
||||
@@ -540,4 +544,4 @@ Testing asynchronous code
|
||||
});
|
||||
});
|
||||
|
||||
as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write.
|
||||
as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write.
|
||||
|
||||
@@ -15,13 +15,8 @@ description into a live application, able to interact with every model and
|
||||
records in the database. It is even possible to use the web client to modify
|
||||
the interface of the web client.
|
||||
|
||||
.. note:: An html version of all docstrings in Odoo is available at:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
javascript_api
|
||||
|
||||
.. note:: An html version of all js docstrings in Odoo is available at:
|
||||
:ref:`JS API <api/js>`
|
||||
|
||||
Overview
|
||||
=========
|
||||
@@ -1421,7 +1416,7 @@ the AbstractView, AbstractController, AbstractRenderer and AbstractModel classes
|
||||
.. raw:: html
|
||||
|
||||
<svg width="550" height="173">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<!-- Created with Method Draw - https://github.com/duopixel/Method-Draw/ -->
|
||||
<path id="svg_1" d="m147.42498,79.79206c0.09944,-8.18859 -0.06363,-16.38812 0.81774,-24.5623c21.65679,2.68895 43.05815,7.08874 64.35,11.04543c1.14304,-4.01519 0.60504,-7.34585 1.59817,-11.05817c13.67878,7.81176 27.23421,15.73476 40.23409,24.03505c-12.47212,9.41539 -26.77809,17.592 -40.82272,25.96494c-0.4548,-3.89916 -0.90967,-7.79828 -1.36448,-11.69744c-20.69972,3.77225 -42.59036,7.6724 -63.42391,11.12096c-1.41678,-7.95741 -1.37514,-16.62327 -1.38888,-24.84846z" stroke-width="1.5" stroke="#000" fill="#fff"/>
|
||||
<rect id="svg_3" height="41" width="110" y="57.5" x="7" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" stroke="#000" fill="#fff"/>
|
||||
<rect stroke="#000" id="svg_5" height="41" width="135" y="20.5" x="328" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="#fff"/>
|
||||
@@ -1467,6 +1462,8 @@ the AbstractView, AbstractController, AbstractRenderer and AbstractModel classes
|
||||
context of a view manager/action manager. They could be used in a client action,
|
||||
or, they could be displayed in the public website (with some work on the assets).
|
||||
|
||||
.. _reference/js/widgets:
|
||||
|
||||
Field Widgets
|
||||
=============
|
||||
|
||||
@@ -1927,7 +1924,6 @@ order.
|
||||
|
||||
Options:
|
||||
|
||||
- title: title of the bar, displayed on top of the bar options
|
||||
- editable: boolean if value is editable
|
||||
- current_value: get the current_value from the field that must be present in the view
|
||||
- max_value: get the max_value from the field that must be present in the view
|
||||
@@ -2278,18 +2274,18 @@ do that, several steps should be done.
|
||||
For more information, look into the *control_panel.js* file.
|
||||
|
||||
.. _.appendTo():
|
||||
http://api.jquery.com/appendTo/
|
||||
https://api.jquery.com/appendTo/
|
||||
|
||||
.. _.prependTo():
|
||||
http://api.jquery.com/prependTo/
|
||||
https://api.jquery.com/prependTo/
|
||||
|
||||
.. _.insertAfter():
|
||||
http://api.jquery.com/insertAfter/
|
||||
https://api.jquery.com/insertAfter/
|
||||
|
||||
.. _.insertBefore():
|
||||
http://api.jquery.com/insertBefore/
|
||||
https://api.jquery.com/insertBefore/
|
||||
|
||||
.. _event delegation:
|
||||
http://api.jquery.com/delegate/
|
||||
https://api.jquery.com/delegate/
|
||||
|
||||
.. _datepicker: https://github.com/Eonasdan/bootstrap-datetimepicker
|
||||
|
||||
+77
-78
@@ -39,11 +39,11 @@ widgets) to your form view will get you up and running in no time.
|
||||
_name = 'business.trip'
|
||||
_inherit = ['mail.thread']
|
||||
_description = 'Business Trip'
|
||||
|
||||
|
||||
name = fields.Char()
|
||||
partner_id = fields.Many2one('res.partner', 'Responsible')
|
||||
guest_ids = fields.Many2many('res.partner', 'Participants')
|
||||
|
||||
|
||||
In the form view:
|
||||
|
||||
.. code-block:: xml
|
||||
@@ -78,10 +78,10 @@ to manage followers on your record:
|
||||
.. rubric:: Posting messages
|
||||
|
||||
.. method:: message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, **kwargs)
|
||||
|
||||
|
||||
Post a new message in an existing thread, returning the new
|
||||
mail.message ID.
|
||||
|
||||
|
||||
:param str body: body of the message, usually raw HTML that will
|
||||
be sanitized
|
||||
:param str message_type: see mail_message.message_type field
|
||||
@@ -95,20 +95,20 @@ to manage followers on your record:
|
||||
:rtype: int
|
||||
|
||||
.. method:: message_post_with_view(views_or_xmlid, **kwargs):
|
||||
|
||||
|
||||
Helper method to send a mail / post a message using a view_id to
|
||||
render using the ir.qweb engine. This method is stand alone, because
|
||||
there is nothing in template and composer that allows to handle
|
||||
views in batch. This method will probably disappear when templates
|
||||
handle ir ui views.
|
||||
|
||||
|
||||
:param str or ``ir.ui.view`` record: external id or record of the view that
|
||||
should be sent
|
||||
|
||||
.. method:: message_post_with_template(template_id, **kwargs)
|
||||
|
||||
|
||||
Helper method to send a mail with a template
|
||||
|
||||
|
||||
:param template_id: the id of the template to render to create the body of the message
|
||||
:param `\**kwargs`: parameter to create a mail.compose.message wizzard (which inherit from mail.message)
|
||||
|
||||
@@ -121,7 +121,7 @@ on the thread's record depending on some values from the email itself (i.e. upda
|
||||
a date or an e-mail address, add CC's addresses as followers, etc.).
|
||||
|
||||
.. method:: message_new(msg_dict, custom_values=None)
|
||||
|
||||
|
||||
Called by ``message_process`` when a new message is received
|
||||
for a given thread model, if the message did not belong to
|
||||
an existing thread.
|
||||
@@ -129,7 +129,7 @@ a date or an e-mail address, add CC's addresses as followers, etc.).
|
||||
The default behavior is to create a new record of the corresponding
|
||||
model (based on some very basic info extracted from the message).
|
||||
Additional behavior may be implemented by overriding this method.
|
||||
|
||||
|
||||
:param dict msg_dict: a map containing the email details and
|
||||
attachments. See ``message_process`` and ``mail.message.parse`` for details
|
||||
:param dict custom_values: optional dictionary of additional
|
||||
@@ -140,14 +140,14 @@ a date or an e-mail address, add CC's addresses as followers, etc.).
|
||||
:return: the id of the newly created thread object
|
||||
|
||||
.. method:: message_update(msg_dict, update_vals=None)
|
||||
|
||||
|
||||
Called by ``message_process`` when a new message is received
|
||||
for an existing thread. The default behavior is to update the record
|
||||
with ``update_vals`` taken from the incoming email.
|
||||
|
||||
|
||||
Additional behavior may be implemented by overriding this
|
||||
method.
|
||||
|
||||
|
||||
:param dict msg_dict: a map containing the email details and attachments;
|
||||
see ``message_process`` and ``mail.message.parse()`` for details.
|
||||
:param dict update_vals: a dict containing values to update records given
|
||||
@@ -159,7 +159,7 @@ a date or an e-mail address, add CC's addresses as followers, etc.).
|
||||
.. method:: message_subscribe(partner_ids=None, channel_ids=None, subtype_ids=None, force=True)
|
||||
|
||||
Add partners to the records followers.
|
||||
|
||||
|
||||
:param list(int) partner_ids: IDs of the partners that will be subscribed
|
||||
to the record
|
||||
:param list(int) channel_ids: IDs of the channels that will be subscribed
|
||||
@@ -175,7 +175,7 @@ a date or an e-mail address, add CC's addresses as followers, etc.).
|
||||
.. method:: message_unsubscribe(partner_ids=None, channel_ids=None)
|
||||
|
||||
Remove partners from the record's followers.
|
||||
|
||||
|
||||
:param list(int) partner_ids: IDs of the partners that will be subscribed
|
||||
to the record
|
||||
:param list(int) channel_ids: IDs of the channels that will be subscribed
|
||||
@@ -211,12 +211,12 @@ to a field, simple set the tracking attribute to True.
|
||||
_name = 'business.trip'
|
||||
_inherit = ['mail.thread']
|
||||
_description = 'Business Trip'
|
||||
|
||||
|
||||
name = fields.Char(tracking=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Responsible',
|
||||
tracking=True)
|
||||
guest_ids = fields.Many2many('res.partner', 'Participants')
|
||||
|
||||
|
||||
From now on, every change to a trip's name or responsible will log a note
|
||||
on the record. The ``name`` field will be displayed in the notification as
|
||||
well to give more context about the notification (even if the name did not
|
||||
@@ -232,32 +232,32 @@ subtype of notifications they wish to receive.
|
||||
|
||||
Subtypes are created as data in your module; the model has the following fields:
|
||||
|
||||
``name`` (mandatory) - :class:`~odoo.fields.Char`
|
||||
``name`` (mandatory) - :class:`~odoo.fields.Char`
|
||||
name of the subtype, will be displayed in the notification customization
|
||||
popup
|
||||
``description`` - :class:`~odoo.fields.Char`
|
||||
``description`` - :class:`~odoo.fields.Char`
|
||||
description that will be added in the message posted for this
|
||||
subtype. If void, the name will be added instead
|
||||
``internal`` - :class:`~odoo.fields.Boolean`
|
||||
``internal`` - :class:`~odoo.fields.Boolean`
|
||||
messages with internal subtypes will be visible only by employees,
|
||||
aka members of the ``base.group_user`` group
|
||||
``parent_id`` - :class:`~odoo.fields.Many2one`
|
||||
``parent_id`` - :class:`~odoo.fields.Many2one`
|
||||
link subtypes for automatic subscription; for example project subtypes are
|
||||
linked to task subtypes through this link. When someone is subscribed to
|
||||
a project, he will be subscribed to all tasks of this project with
|
||||
subtypes found using the parent subtype
|
||||
``relation_field`` - :class:`~odoo.fields.Char`
|
||||
``relation_field`` - :class:`~odoo.fields.Char`
|
||||
as an example, when linking project and tasks subtypes, the relation
|
||||
field is the project_id field of tasks
|
||||
``res_model`` - :class:`~odoo.fields.Char`
|
||||
``res_model`` - :class:`~odoo.fields.Char`
|
||||
model the subtype applies to; if False, this subtype applies to all models
|
||||
``default`` - :class:`~odoo.fields.Boolean`
|
||||
``default`` - :class:`~odoo.fields.Boolean`
|
||||
wether the subtype is activated by default when subscribing
|
||||
``sequence`` - :class:`~odoo.fields.Integer`
|
||||
``sequence`` - :class:`~odoo.fields.Integer`
|
||||
used to order subtypes in the notification customization popup
|
||||
``hidden`` - :class:`~odoo.fields.Boolean`
|
||||
``hidden`` - :class:`~odoo.fields.Boolean`
|
||||
wether the subtype is hidden in the notification customization popup
|
||||
|
||||
|
||||
|
||||
Interfacing subtypes with field tracking allows to subscribe to different kind
|
||||
of notifications depending on what might interest users. To do this, you
|
||||
@@ -301,7 +301,7 @@ can override the ``_track_subtype()`` function:
|
||||
_name = 'business.trip'
|
||||
_inherit = ['mail.thread']
|
||||
_description = 'Business Trip'
|
||||
|
||||
|
||||
name = fields.Char(tracking=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Responsible',
|
||||
tracking=True)
|
||||
@@ -392,7 +392,7 @@ yourself by overriding the function ``_notification_recipients``.
|
||||
:returns: a subtype's full external id or False if no subtype is triggered
|
||||
|
||||
|
||||
The urls in the actions list can be generated automatically by calling the
|
||||
The urls in the actions list can be generated automatically by calling the
|
||||
``_notification_link_helper()`` function:
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ The urls in the actions list can be generated automatically by calling the
|
||||
record if the kwargs ``model`` and ``res_id`` are set).
|
||||
|
||||
:param str link_type: link type to be generated; can be any of these values:
|
||||
|
||||
|
||||
``view``
|
||||
link to form view of the record
|
||||
``assign``
|
||||
@@ -418,8 +418,8 @@ The urls in the actions list can be generated automatically by calling the
|
||||
``new``
|
||||
open an empty form view for a new record; you can specify
|
||||
a specific action by providing its id (database id or fully resolved
|
||||
external id) in the kwarg ``action_id``
|
||||
|
||||
external id) in the kwarg ``action_id``
|
||||
|
||||
:returns: link of the type selected for the record
|
||||
:rtype: str
|
||||
|
||||
@@ -428,7 +428,7 @@ The urls in the actions list can be generated automatically by calling the
|
||||
Let's add a custom button to the Business Trip state change notification;
|
||||
this button will reset the state to Draft and will be only visible to a member
|
||||
of the (imaginary) group Travel Manager (``business.group_trip_manager``)
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class BusinessTrip(models.Model):
|
||||
@@ -437,10 +437,10 @@ The urls in the actions list can be generated automatically by calling the
|
||||
_description = 'Business Trip'
|
||||
|
||||
# Pevious code goes here
|
||||
|
||||
|
||||
def action_cancel(self):
|
||||
self.write({'state': 'draft'})
|
||||
|
||||
|
||||
def _notification_recipients(self, message, groups):
|
||||
""" Handle Trip Manager recipients that can cancel the trip at the last
|
||||
minute and kill all the fun. """
|
||||
@@ -451,7 +451,7 @@ The urls in the actions list can be generated automatically by calling the
|
||||
app_action = self._notification_link_helper('method',
|
||||
method='action_cancel')
|
||||
trip_actions = [{'url': app_action, 'title': _('Cancel')}]
|
||||
|
||||
|
||||
new_group = (
|
||||
'group_trip_manager',
|
||||
lambda partner: bool(partner.user_ids) and
|
||||
@@ -462,11 +462,11 @@ The urls in the actions list can be generated automatically by calling the
|
||||
})
|
||||
|
||||
return [new_group] + groups
|
||||
|
||||
|
||||
Note that that I could have defined my evaluation function outside of this
|
||||
|
||||
Note that that I could have defined my evaluation function outside of this
|
||||
method and define a global function to do it instead of a lambda, but for
|
||||
the sake of being more brief and less verbose in these documentation files
|
||||
the sake of being more brief and less verbose in these documentation files
|
||||
that can sometimes be boring, I choose the former instead of the latter.
|
||||
|
||||
Overriding defaults
|
||||
@@ -483,7 +483,7 @@ Context keys:
|
||||
These context keys can be used to somewhat control ``mail.thread`` features
|
||||
like auto-subscription or field tracking during calls to ``create()`` or
|
||||
``write()`` (or any other method where it may be useful).
|
||||
|
||||
|
||||
- ``mail_create_nosubscribe``: at create or message_post, do not subscribe
|
||||
the current user to the record thread
|
||||
- ``mail_create_nolog``: at create, do not log the automatic '<Document>
|
||||
@@ -496,7 +496,7 @@ Context keys:
|
||||
- ``mail_notify_force_send``: if less than 50 email notifications to send,
|
||||
send them directly instead of using the queue; True by default
|
||||
- ``mail_notify_user_signature``: add the current user signature in
|
||||
email notifications; True by default
|
||||
email notifications; True by default
|
||||
|
||||
|
||||
.. _reference/mixins/mail/alias:
|
||||
@@ -540,14 +540,14 @@ or issues, Sales Team have aliases to generate Leads.
|
||||
|
||||
.. note:: The model that will be created by the alias **must** inherit the
|
||||
``mail_thread`` model.
|
||||
|
||||
|
||||
Alias support is added by inheriting ``mail.alias.mixin``; this mixin will
|
||||
generate a new ``mail.alias`` record for each record of the parent class that
|
||||
gets created (for example, every ``project.project`` record having its ``mail.alias``
|
||||
record initialized on creation).
|
||||
|
||||
.. note:: Aliases can also be created manually and supported by a simple
|
||||
:class:`~odoo.fields.Many2one` field. This guide assumes you wish a
|
||||
:class:`~odoo.fields.Many2one` field. This guide assumes you wish a
|
||||
more complete integration with automatic creation of the alias, record-specific
|
||||
default values, etc.
|
||||
|
||||
@@ -557,25 +557,25 @@ of the created alias, like the kind of record it must create and possibly
|
||||
some default values these records may have depending on the parent object:
|
||||
|
||||
.. method:: get_alias_model_name(vals)
|
||||
|
||||
|
||||
Return the model name for the alias. Incoming emails that are not
|
||||
replies to existing records will cause the creation of a new record
|
||||
of this alias model. The value may depend on ``vals``, the dict of
|
||||
values passed to ``create`` when a record of this model is created.
|
||||
|
||||
|
||||
:param vals dict: values of the newly created record that will holding
|
||||
the alias
|
||||
:return: model name
|
||||
:rtype: str
|
||||
|
||||
.. method:: get_alias_values()
|
||||
|
||||
|
||||
Return values to create an alias, or to write on the alias after its
|
||||
creation. While not completely mandatory, it is usually required to make
|
||||
sure that newly created records will be linked to the alias' parent (i.e.
|
||||
tasks getting created in the right project) by setting a dictionary of
|
||||
default values in the alias' ``alias_defaults`` field.
|
||||
|
||||
|
||||
:return: dictionnary of values that will be written to the new alias
|
||||
:rtype: dict
|
||||
|
||||
@@ -583,24 +583,24 @@ The ``get_alias_values()`` override is particularly interesting as it allows you
|
||||
to modify the behaviour of your aliases easily. Among the fields that can be set
|
||||
on the alias, the following are of particular interest:
|
||||
|
||||
``alias_name`` - :class:`~odoo.fields.Char`
|
||||
``alias_name`` - :class:`~odoo.fields.Char`
|
||||
name of the email alias, e.g. 'jobs' if you want to catch emails for
|
||||
<jobs@example.odoo.com>
|
||||
``alias_user_id`` - :class:`~odoo.fields.Many2one` (``res.users``)
|
||||
``alias_user_id`` - :class:`~odoo.fields.Many2one` (``res.users``)
|
||||
owner of records created upon receiving emails on this alias;
|
||||
if this field is not set the system will attempt to find the right owner
|
||||
based on the sender (From) address, or will use the Administrator account
|
||||
if no system user is found for that address
|
||||
``alias_defaults`` - :class:`~odoo.fields.Text`
|
||||
``alias_defaults`` - :class:`~odoo.fields.Text`
|
||||
Python dictionary that will be evaluated to provide
|
||||
default values when creating new records for this alias
|
||||
``alias_force_thread_id`` - :class:`~odoo.fields.Integer`
|
||||
``alias_force_thread_id`` - :class:`~odoo.fields.Integer`
|
||||
optional ID of a thread (record) to which all incoming messages will be
|
||||
attached, even if they did not reply to it; if set, this will disable the
|
||||
creation of new records completely
|
||||
``alias_contact`` - :class:`~odoo.fields.Selection`
|
||||
``alias_contact`` - :class:`~odoo.fields.Selection`
|
||||
Policy to post a message on the document using the mailgateway
|
||||
|
||||
|
||||
- *everyone*: everyone can post
|
||||
- *partners*: only authenticated partners
|
||||
- *followers*: only followers of the related document or members of following channels
|
||||
@@ -631,7 +631,7 @@ you to make your alias easily configurable from the record's form view.
|
||||
expense_ids = fields.One2many('business.expense', 'trip_id', 'Expenses')
|
||||
alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict",
|
||||
required=True)
|
||||
|
||||
|
||||
def get_alias_model_name(self, vals):
|
||||
""" Specify the model that will get created when the alias receives a message """
|
||||
return 'business.expense'
|
||||
@@ -641,7 +641,7 @@ you to make your alias easily configurable from the record's form view.
|
||||
values = super(BusinessTrip, self).get_alias_values()
|
||||
# alias_defaults holds a dictionnary that will be written
|
||||
# to all records created by this alias
|
||||
#
|
||||
#
|
||||
# in this case, we want all expense records sent to a trip alias
|
||||
# to be linked to the corresponding business trip
|
||||
values['alias_defaults'] = {'trip_id': self.id}
|
||||
@@ -649,7 +649,7 @@ you to make your alias easily configurable from the record's form view.
|
||||
# by default
|
||||
values['alias_contact'] = 'followers'
|
||||
return values
|
||||
|
||||
|
||||
class BusinessExpense(models.Model):
|
||||
_name = 'business.expense'
|
||||
_inherit = ['mail.thread']
|
||||
@@ -662,9 +662,9 @@ you to make your alias easily configurable from the record's form view.
|
||||
|
||||
We would like our alias to be easily configurable from the form view of our
|
||||
business trips, so let's add the following to our form view:
|
||||
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
|
||||
<page string="Emails">
|
||||
<group name="group_alias">
|
||||
<label for="alias_name" string="Email Alias"/>
|
||||
@@ -684,7 +684,7 @@ you to make your alias easily configurable from the record's form view.
|
||||
string="Accept Emails From"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
|
||||
Now we can change the alias address directly from the form view and change
|
||||
who can send e-mails to the alias.
|
||||
|
||||
@@ -696,12 +696,12 @@ you to make your alias easily configurable from the record's form view.
|
||||
class BusinessExpense(models.Model):
|
||||
# Previous code goes here
|
||||
# ...
|
||||
|
||||
|
||||
def message_new(self, msg, custom_values=None):
|
||||
""" Override to set values according to the email.
|
||||
|
||||
""" Override to set values according to the email.
|
||||
|
||||
In this simple example, we simply use the email title as the name
|
||||
of the expense, try to find a partner with this email address and
|
||||
of the expense, try to find a partner with this email address and
|
||||
do a regex match to find the amount of the expense."""
|
||||
name = msg_dict.get('subject', 'New Expense')
|
||||
# Match the last occurence of a float in the string
|
||||
@@ -728,7 +728,7 @@ Activities tracking
|
||||
-------------------
|
||||
|
||||
Activities are actions users have to take on a document like making a phone call
|
||||
or organizing a meeting. Activities come with the mail module as they are
|
||||
or organizing a meeting. Activities come with the mail module as they are
|
||||
integrated in the Chatter but are *not bundled with mail.thread*. Activities
|
||||
are records of the ``mail.activity`` class, which have a type (``mail.activity.type``),
|
||||
name, description, scheduled time (among others). Pending activities are visible
|
||||
@@ -798,16 +798,16 @@ campaigns through arguments in links to specified resources. The mixin adds
|
||||
|
||||
* ``campaign_id``: :class:`~odoo.fields.Many2one` field to a ``utm.campaign``
|
||||
object (i.e. Christmas_Special, Fall_Collection, etc.)
|
||||
* ``source_id``: :class:`~odoo.fields.Many2one` field to a ``utm.source``
|
||||
* ``source_id``: :class:`~odoo.fields.Many2one` field to a ``utm.source``
|
||||
object (i.e. Search Engine, mailing list, etc.)
|
||||
* ``medium_id``: :class:`~odoo.fields.Many2one` field to a ``utm.medium``
|
||||
* ``medium_id``: :class:`~odoo.fields.Many2one` field to a ``utm.medium``
|
||||
object (i.e. Snail Mail, e-Mail, social network update, etc.)
|
||||
|
||||
These models have a single field ``name`` (i.e. they are simply there to
|
||||
distinguish campaigns but don't have any specific behaviour).
|
||||
|
||||
Once a customer visits your website with these parameters set in the url
|
||||
(i.e. http://www.odoo.com/?campaign_id=mixin_talk&source_id=www.odoo.com&medium_id=website),
|
||||
(i.e. https://www.odoo.com/?campaign_id=mixin_talk&source_id=www.odoo.com&medium_id=website),
|
||||
three cookies are set in the visitor's website for these parameters.
|
||||
Once a object that inherits the utm.mixin is created from the website (i.e. lead
|
||||
form, job application, etc.), the utm.mixin code kicks in and fetches the values
|
||||
@@ -834,7 +834,7 @@ model should support the *quick create* (i.e. call to ``create()`` with a single
|
||||
_description = 'My Tracked Object'
|
||||
|
||||
my_field = fields.Many2one('my_module.my_track', 'My Field')
|
||||
|
||||
|
||||
@api.model
|
||||
def tracking_fields(self):
|
||||
result = super(MyModel, self).tracking_fields()
|
||||
@@ -884,7 +884,7 @@ This mixin adds 2 fields on your model:
|
||||
the status of the publication
|
||||
* ``website_url``: :class:`~odoo.fields.Char` field which represents
|
||||
the URL through which the object is accessed
|
||||
|
||||
|
||||
Note that this last field is a computed field and must be implemented for your class:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -918,13 +918,13 @@ buttons to website visitors:
|
||||
</t>
|
||||
</div>
|
||||
|
||||
Note that you must pass your object as the variable ``object`` to the template;
|
||||
Note that you must pass your object as the variable ``object`` to the template;
|
||||
in this example, the ``blog.post`` record was passed as the ``blog_post`` variable
|
||||
to the ``qweb`` rendering engine, it is necessary to specify this to the publish
|
||||
management template. The ``publish_edit`` variable allow the frontend
|
||||
button to link to the backend (allowing you to switch from frontend to backend
|
||||
and vice-versa easily); if set, you must specify the full external id of the action
|
||||
you want to call in the backend in the ``action`` variable (note that a Form View
|
||||
you want to call in the backend in the ``action`` variable (note that a Form View
|
||||
must exist for the model).
|
||||
|
||||
The action ``website_publish_button`` is defined in the mixin and adapts its
|
||||
@@ -987,7 +987,7 @@ To add rating support, simply inherit the ``rating.mixin`` model:
|
||||
class MyModel(models.Models):
|
||||
_name = 'my_module.my_model'
|
||||
_inherit = ['rating.mixin', 'mail.thread']
|
||||
|
||||
|
||||
user_id = fields.Many2one('res.users', 'Responsible')
|
||||
partner_id = fields.Many2one('res.partner', 'Customer')
|
||||
|
||||
@@ -995,18 +995,18 @@ The behaviour of the mixin adapts to your model:
|
||||
|
||||
* The ``rating.rating`` record will be linked to the ``partner_id`` field of your
|
||||
model (if the field is present).
|
||||
|
||||
|
||||
- this behaviour can be overridden with the function ``rating_get_partner_id()``
|
||||
if you use another field than ``partner_id``
|
||||
|
||||
|
||||
* The ``rating.rating`` record will be linked to the partner of the ``user_id``
|
||||
field of your model (if the field is present) (i.e. the partner who is rated)
|
||||
|
||||
|
||||
- this behaviour can be overridden with the function ``rating_get_rated_partner_id()``
|
||||
if you use another field than ``user_id`` (note that the function must return a
|
||||
``res.partner``, for ``user_id`` the system automatically fetches the partner
|
||||
of the user)
|
||||
|
||||
|
||||
* The chatter history will display the rating event (if your model inherits from
|
||||
``mail.thread``)
|
||||
|
||||
@@ -1036,7 +1036,7 @@ links to the rating object. A very basic email template could look like this:
|
||||
</ul>
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
|
||||
Your customer will then receive an e-mail with links to a simple webpage allowing
|
||||
them to provide a feedback on their interaction with your users (including a free-text
|
||||
feedback message).
|
||||
@@ -1074,4 +1074,3 @@ You can find concrete examples of integration in the following models:
|
||||
|
||||
* ``project.task`` in the Project (*rating_project*) Application
|
||||
* ``helpdesk.ticket`` in the Helpdesk (*helpdesk* - Odoo Enterprise only) Application
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ Available manifest fields are:
|
||||
located in ``static/src/css`` inside the module.
|
||||
``images`` (``list(str)``)
|
||||
Specify image files to be used by the module.
|
||||
``installable`` (``bool`` default: ``False``)
|
||||
``installable`` (``bool`` default: ``True``)
|
||||
Whether a user should be able to install the module from the Web UI or not.
|
||||
``maintainer`` (``str``)
|
||||
Person or entity in charge of the maintenance of this module, by default
|
||||
@@ -128,6 +128,6 @@ Available manifest fields are:
|
||||
These hooks should only be used when setup/cleanup required for this module
|
||||
is either extremely difficult or impossible through the api.
|
||||
|
||||
.. _semantic versioning: http://semver.org
|
||||
.. _semantic versioning: https://semver.org
|
||||
.. _existing categories:
|
||||
https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
|
||||
+14
-12
@@ -42,7 +42,7 @@ will result in::
|
||||
|
||||
.. _reference/qweb/output:
|
||||
|
||||
data output
|
||||
Data output
|
||||
===========
|
||||
|
||||
QWeb has a primary output directive which automatically HTML-escape its
|
||||
@@ -63,7 +63,7 @@ sanitized user-provided markup.
|
||||
|
||||
.. _reference/qweb/conditionals:
|
||||
|
||||
conditionals
|
||||
Conditionals
|
||||
============
|
||||
|
||||
QWeb has a conditional directive ``if``, which evaluates an expression given
|
||||
@@ -107,7 +107,7 @@ available::
|
||||
|
||||
.. _reference/qweb/loops:
|
||||
|
||||
loops
|
||||
Loops
|
||||
=====
|
||||
|
||||
QWeb has an iteration directive ``foreach`` which take an expression returning
|
||||
@@ -335,7 +335,7 @@ Python
|
||||
Exclusive directives
|
||||
--------------------
|
||||
|
||||
asset bundles
|
||||
Asset bundles
|
||||
'''''''''''''
|
||||
|
||||
.. todo:: have fme write these up because I've no idea how they work
|
||||
@@ -351,7 +351,7 @@ website's rich text edition.
|
||||
``t-options`` can be used to customize fields, the most common option
|
||||
is ``widget``, other options are field- or widget-dependent.
|
||||
|
||||
debugging
|
||||
Debugging
|
||||
---------
|
||||
|
||||
``t-debug``
|
||||
@@ -552,6 +552,8 @@ Helpers
|
||||
:js:func:`core.qweb.render <QWeb2.Engine.render>` can be used to
|
||||
easily render basic module templates
|
||||
|
||||
.. _reference/qweb/api:
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
@@ -638,13 +640,13 @@ API
|
||||
use case. Odoo 9.0 still depends on Jinja_ and Mako_.
|
||||
|
||||
.. _templating:
|
||||
http://en.wikipedia.org/wiki/Template_processor
|
||||
https://en.wikipedia.org/wiki/Template_processor
|
||||
|
||||
.. _Jinja: http://jinja.pocoo.org
|
||||
.. _Mako: http://www.makotemplates.org
|
||||
.. _Genshi: http://genshi.edgewall.org
|
||||
.. _XML namespaces: http://en.wikipedia.org/wiki/XML_namespace
|
||||
.. _HTML: http://en.wikipedia.org/wiki/HTML
|
||||
.. _XSS: http://en.wikipedia.org/wiki/Cross-site_scripting
|
||||
.. _Mako: https://www.makotemplates.org
|
||||
.. _Genshi: https://genshi.edgewall.org
|
||||
.. _XML namespaces: https://en.wikipedia.org/wiki/XML_namespace
|
||||
.. _HTML: https://en.wikipedia.org/wiki/HTML
|
||||
.. _XSS: https://en.wikipedia.org/wiki/Cross-site_scripting
|
||||
.. _JSON: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON
|
||||
.. _CSS selector: http://api.jquery.com/category/selectors/
|
||||
.. _CSS selector: https://api.jquery.com/category/selectors/
|
||||
|
||||
@@ -129,13 +129,13 @@ you need to define two templates:
|
||||
* The main report template
|
||||
* The translatable document
|
||||
|
||||
You can then call the translatable document from your main template with the attribute
|
||||
You can then call the translatable document from your main template with the attribute
|
||||
``t-lang`` set to a language code (for example ``fr`` or ``en_US``) or to a record field.
|
||||
You will also need to re-browse the related records with the proper context if you use
|
||||
fields that are translatable (like country names, sales conditions, etc.)
|
||||
|
||||
.. warning::
|
||||
|
||||
|
||||
If your report template does not use translatable record fields, re-browsing the record
|
||||
in another language is *not* necessary and will impact performances.
|
||||
|
||||
@@ -169,10 +169,10 @@ For example, let's look at the Sale Order report from the Sale module::
|
||||
</template>
|
||||
|
||||
|
||||
The main template calls the translatable template with ``doc.partner_id.lang`` as a
|
||||
``t-lang`` parameter, so it will be rendered in the language of the partner. This way,
|
||||
each Sale Order will be printed in the language of the corresponding customer. If you wish
|
||||
to translate only the body of the document, but keep the header and footer in a default
|
||||
The main template calls the translatable template with ``doc.partner_id.lang`` as a
|
||||
``t-lang`` parameter, so it will be rendered in the language of the partner. This way,
|
||||
each Sale Order will be printed in the language of the corresponding customer. If you wish
|
||||
to translate only the body of the document, but keep the header and footer in a default
|
||||
language, you could call the report's external layout this way::
|
||||
|
||||
<t t-call="web.external_layout" t-lang="en_US">
|
||||
@@ -180,7 +180,7 @@ language, you could call the report's external layout this way::
|
||||
.. tip::
|
||||
|
||||
Please take note that this works only when calling external templates, you will not be
|
||||
able to translate part of a document by setting a ``t-lang`` attribute on an xml node other
|
||||
able to translate part of a document by setting a ``t-lang`` attribute on an xml node other
|
||||
than ``t-call``. If you wish to translate part of a template, you can create an external
|
||||
template with this partial template and call it from the main one with the ``t-lang``
|
||||
attribute.
|
||||
@@ -345,4 +345,4 @@ For example, you can access a Sale Order report in html mode by going to
|
||||
Or you can access the pdf version at
|
||||
\http://<server-address>/report/pdf/sale.report_saleorder/38
|
||||
|
||||
.. _wkhtmltopdf: http://wkhtmltopdf.org
|
||||
.. _wkhtmltopdf: https://wkhtmltopdf.org
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
test_empty_find (odoo.addons.web.tests.test_dataset.TestDataSetController) ... ok
|
||||
test_ids_shortcut (odoo.addons.web.tests.test_dataset.TestDataSetController) ... ok
|
||||
test_regular_find (odoo.addons.web.tests.test_dataset.TestDataSetController) ... ok
|
||||
web.testing.stack: direct, value, success ... ok
|
||||
web.testing.stack: direct, deferred, success ... ok
|
||||
web.testing.stack: direct, value, error ... ok
|
||||
web.testing.stack: direct, deferred, failure ... ok
|
||||
web.testing.stack: successful setup ... ok
|
||||
web.testing.stack: successful teardown ... ok
|
||||
web.testing.stack: successful setup and teardown ... ok
|
||||
|
||||
[snip ~150 lines]
|
||||
|
||||
test_convert_complex_context (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
|
||||
test_convert_complex_domain (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
|
||||
test_convert_literal_context (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
|
||||
test_convert_literal_domain (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
|
||||
test_retrieve_nonliteral_context (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
|
||||
test_retrieve_nonliteral_domain (odoo.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 181 tests in 15.706s
|
||||
|
||||
OK
|
||||
|
||||
@@ -43,12 +43,6 @@ and ``__init__.py`` contains::
|
||||
test modules which are not imported from ``tests/__init__.py`` will not be
|
||||
run
|
||||
|
||||
.. versionchanged:: 8.0
|
||||
|
||||
previously, the test runner would only run modules added to two lists
|
||||
``fast_suite`` and ``checks`` in ``tests/__init__.py``. In 8.0 it will
|
||||
run all imported modules
|
||||
|
||||
The test runner will simply run any test case, as described in the official
|
||||
`unittest documentation`_, but Odoo provides a number of utilities and helpers
|
||||
related to testing Odoo content (modules, mainly):
|
||||
@@ -108,10 +102,7 @@ Tests are automatically run when installing or updating modules if
|
||||
:option:`--test-enable <odoo-bin --test-enable>` was enabled when starting the
|
||||
Odoo server.
|
||||
|
||||
As of Odoo 8, running tests outside of the install/update cycle is not
|
||||
supported.
|
||||
|
||||
.. _unittest documentation: https://docs.python.org/2/library/unittest.html
|
||||
.. _unittest documentation: https://docs.python.org/3/library/unittest.html
|
||||
|
||||
Test selection
|
||||
--------------
|
||||
@@ -528,7 +519,7 @@ Two new command line arguments were added since Odoo 13.0 to control this behavi
|
||||
:option:`--screenshots <odoo-bin --screenshots>` and :option:`--screencasts <odoo-bin --screencasts>`
|
||||
|
||||
|
||||
.. _qunit: http://qunitjs.com/
|
||||
.. _qunit: https://qunitjs.com/
|
||||
.. _qunit_config.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_config.js#L49
|
||||
.. _web.js_tests_assets: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L427
|
||||
.. _web.qunit_suite: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L509
|
||||
@@ -537,4 +528,4 @@ Two new command line arguments were added since Odoo 13.0 to control this behavi
|
||||
.. _test_utils.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils.js
|
||||
.. _mock server: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/mock_server.js
|
||||
.. _qunit assertions: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_asserts.js
|
||||
.. _createView: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils_create.js#L267
|
||||
.. _createView: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils_create.js#L267
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
Translating Modules
|
||||
===================
|
||||
|
||||
This section explains how to provide translation abilities to your module.
|
||||
|
||||
.. note:: If you want to contribute to the translation of Odoo itself, please refer to the
|
||||
`Odoo Wiki page <https://github.com/odoo/odoo/wiki/Translations>`_.
|
||||
|
||||
Exporting translatable term
|
||||
===========================
|
||||
|
||||
@@ -203,6 +208,6 @@ or **do** evaluate dynamically the translatable content::
|
||||
};
|
||||
|
||||
|
||||
.. _PO File: http://en.wikipedia.org/wiki/Gettext#Translating
|
||||
.. _msginit: http://www.gnu.org/software/gettext/manual/gettext.html#Creating
|
||||
.. _POEdit: http://poedit.net/
|
||||
.. _PO File: https://en.wikipedia.org/wiki/Gettext#Translating
|
||||
.. _msginit: https://www.gnu.org/software/gettext/manual/gettext.html#Creating
|
||||
.. _POEdit: https://poedit.net/
|
||||
|
||||
@@ -624,16 +624,16 @@ of the supported browsers is the following:
|
||||
environment than over the internet.
|
||||
|
||||
.. _regular expression: https://docs.python.org/3/library/re.html
|
||||
.. _ARP spoofing: http://en.wikipedia.org/wiki/ARP_spoofing
|
||||
.. _ARP spoofing: https://en.wikipedia.org/wiki/ARP_spoofing
|
||||
.. _Nginx termination example:
|
||||
http://nginx.com/resources/admin-guide/nginx-ssl-termination/
|
||||
https://nginx.com/resources/admin-guide/nginx-ssl-termination/
|
||||
.. _Nginx proxying example:
|
||||
http://nginx.com/resources/admin-guide/reverse-proxy/
|
||||
https://nginx.com/resources/admin-guide/reverse-proxy/
|
||||
.. _socat: http://www.dest-unreach.org/socat/
|
||||
.. _PostgreSQL connection settings:
|
||||
.. _listen to network interfaces:
|
||||
http://www.postgresql.org/docs/9.6/static/runtime-config-connection.html
|
||||
https://www.postgresql.org/docs/9.6/static/runtime-config-connection.html
|
||||
.. _use an SSH tunnel:
|
||||
http://www.postgresql.org/docs/9.6/static/ssh-tunnels.html
|
||||
.. _WSGI: http://wsgi.readthedocs.org/
|
||||
https://www.postgresql.org/docs/9.6/static/ssh-tunnels.html
|
||||
.. _WSGI: https://wsgi.readthedocs.org/
|
||||
.. _POSBox: https://www.odoo.com/page/point-of-sale-hardware#part_2
|
||||
|
||||
+407
-254
@@ -1,5 +1,6 @@
|
||||
:banner: banners/installing_odoo.jpg
|
||||
|
||||
|
||||
.. _setup/install:
|
||||
|
||||
===============
|
||||
@@ -27,6 +28,7 @@ This documents attempts to describe most of the installation options.
|
||||
If you usually use docker_ for development or deployment, an official
|
||||
docker_ base image is available.
|
||||
|
||||
|
||||
.. _setup/install/editions:
|
||||
|
||||
Editions
|
||||
@@ -40,6 +42,7 @@ available to anyone.
|
||||
If you already use the Community version and wish to upgrade to Enterprise, please
|
||||
refer to :ref:`setup/enterprise` (except for :ref:`setup/install/source`).
|
||||
|
||||
|
||||
.. _setup/install/online:
|
||||
|
||||
Online
|
||||
@@ -67,6 +70,7 @@ Can be used for both testing Odoo and long-term production use.
|
||||
Like demo_ instances, SaaS_ instances require no local installation, a web
|
||||
browser is sufficient.
|
||||
|
||||
|
||||
.. _setup/install/packaged:
|
||||
|
||||
Packaged installers
|
||||
@@ -81,14 +85,14 @@ but may be difficult to keep up-to-date.
|
||||
|
||||
Official Community packages with all relevant dependency requirements are
|
||||
available on our nightly_ server. Both Communtiy and Enterprise packages can
|
||||
be downloaded from our Download_ page (you must to be logged in as a paying
|
||||
be downloaded from our download_ page (you must to be logged in as a paying
|
||||
customer or partner to download the Enterprise packages).
|
||||
|
||||
Windows
|
||||
-------
|
||||
|
||||
* Download the installer from our nightly_ server (Community only)
|
||||
or the Windows installer from the Download_ page (any edition)
|
||||
or the Windows installer from the download_ page (any edition)
|
||||
* Run the downloaded file
|
||||
|
||||
.. warning:: on Windows 8, you may see a warning titled "Windows protected
|
||||
@@ -252,325 +256,475 @@ Once downloaded, the package can be installed using the 'dnf' package manager:
|
||||
$ sudo systemctl enable odoo
|
||||
$ sudo systemctl start odoo
|
||||
|
||||
|
||||
.. _setup/install/source:
|
||||
|
||||
Source Install
|
||||
==============
|
||||
|
||||
The source "installation" really is about not installing Odoo, and running
|
||||
it directly from source instead.
|
||||
The source "installation" is really about not installing Odoo, and running it directly from source
|
||||
instead.
|
||||
|
||||
This can be more convenient for module developers as the Odoo source is
|
||||
more easily accessible than using packaged installation (for information or
|
||||
to build this documentation and have it available offline).
|
||||
This can be more convenient for module developers as the Odoo source is more easily accessible
|
||||
than using packaged installation (for information or to build this documentation and have it
|
||||
available offline).
|
||||
|
||||
It also makes starting and stopping Odoo more flexible and explicit than the
|
||||
services set up by the packaged installations, and allows overriding settings
|
||||
using :ref:`command-line parameters <reference/cmdline>` without needing to
|
||||
edit a configuration file.
|
||||
It also makes starting and stopping Odoo more flexible and explicit than the services set up by the
|
||||
packaged installations, and allows overriding settings using
|
||||
:ref:`command-line parameters <reference/cmdline>` without needing to edit a configuration file.
|
||||
|
||||
Finally it provides greater control over the system's set up, and allows more
|
||||
easily keeping (and running) multiple versions of Odoo side-by-side.
|
||||
Finally it provides greater control over the system's set up, and allows to more easily keep
|
||||
(and run) multiple versions of Odoo side-by-side.
|
||||
|
||||
Prepare
|
||||
Windows
|
||||
-------
|
||||
|
||||
Source installation requires manually installing dependencies:
|
||||
|
||||
* Python 3.5+.
|
||||
|
||||
- on Linux and OS X, using your package manager if not installed by default
|
||||
|
||||
.. note:: on some system, ``python`` command refers to Python 2 (outdated)
|
||||
or to Python 3 (supported). Make sure you are using the right
|
||||
version and that the alias ``python3`` is present in your
|
||||
:envvar:`PATH`
|
||||
|
||||
- on Windows, use `the official Python 3 installer
|
||||
<https://www.python.org/downloads/windows/>`_.
|
||||
|
||||
.. warning:: select "add python.exe to Path" during installation, and
|
||||
reboot afterwards to ensure the :envvar:`PATH` is updated
|
||||
|
||||
.. note:: if Python is already installed, make sure it is 3.5 or above,
|
||||
previous versions are not compatible with Odoo.
|
||||
|
||||
* PostgreSQL, to use a local database
|
||||
|
||||
After installation you will need to create a postgres user: by default the
|
||||
only user is ``postgres``, and Odoo forbids connecting as ``postgres``.
|
||||
|
||||
- on Linux, use your distribution's package, then create a postgres user
|
||||
named like your login:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo su - postgres -c "createuser -s $USER"
|
||||
|
||||
Because the role login is the same as your unix login unix sockets can be
|
||||
use without a password.
|
||||
|
||||
- on OS X, `postgres.app <http://postgresapp.com>`_ is the simplest way to
|
||||
get started, then create a postgres user as on Linux
|
||||
|
||||
- on Windows, use `PostgreSQL for windows`_ then
|
||||
|
||||
- add PostgreSQL's ``bin`` directory (default:
|
||||
``C:\Program Files\PostgreSQL\9.4\bin``) to your :envvar:`PATH`
|
||||
- create a postgres user with a password using the pg admin gui: open
|
||||
pgAdminIII, double-click the server to create a connection, select
|
||||
:menuselection:`Edit --> New Object --> New Login Role`, enter the
|
||||
usename in the :guilabel:`Role Name` field (e.g. ``odoo``), then open
|
||||
the :guilabel:`Definition` tab and enter the password (e.g. ``odoo``),
|
||||
then click :guilabel:`OK`.
|
||||
|
||||
The user and password must be passed to Odoo using either the
|
||||
:option:`-w <odoo-bin -w>` and :option:`-r <odoo-bin -r>` options or
|
||||
:ref:`the configuration file <reference/cmdline/config>`
|
||||
|
||||
* Python dependencies listed in the :file:`requirements.txt` file.
|
||||
|
||||
- on Linux, python dependencies may be installable with the system's package
|
||||
manager or using pip.
|
||||
|
||||
For libraries using native code (Pillow, lxml, greenlet, gevent, psycopg2,
|
||||
ldap) it may be necessary to install development tools and native
|
||||
dependencies before pip is able to install the dependencies themselves.
|
||||
These are available in ``-dev`` or ``-devel`` packages for Python,
|
||||
Postgres, libxml2, libxslt, libevent, libsasl2 and libldap2. Then the Python
|
||||
dependecies can themselves be installed:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip3 install -r requirements.txt
|
||||
|
||||
- on OS X, you will need to install the Command Line Tools
|
||||
(``xcode-select --install``) then download and install a package manager
|
||||
of your choice (homebrew_, macports_) to install non-Python dependencies.
|
||||
pip can then be used to install the Python dependencies as on Linux:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip3 install -r requirements.txt
|
||||
|
||||
- on Windows you need to install some of the dependencies manually, tweak the
|
||||
requirements.txt file, then run pip to install the remaning ones.
|
||||
|
||||
Install ``psycopg`` using the installer here
|
||||
http://www.stickpeople.com/projects/python/win-psycopg/
|
||||
|
||||
Then use pip to install the dependencies using the following
|
||||
command from a cmd.exe prompt (replace ``\YourOdooPath`` by the actual
|
||||
path where you downloaded Odoo):
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\> cd \YourOdooPath
|
||||
C:\YourOdooPath> C:\Python35\Scripts\pip.exe install -r requirements.txt
|
||||
|
||||
* *RTLCSS* via nodejs
|
||||
|
||||
For languages with right-to-left interface (such as Arabic or Hebrew), the
|
||||
package ``rtlcss`` is needed.
|
||||
|
||||
- on Linux, use your distribution's package manager to install nodejs and
|
||||
npm.
|
||||
Once npm is installed, use it to install rtlcss:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo npm install -g rtlcss
|
||||
|
||||
- on OS X, install nodejs via your preferred package manager (homebrew_,
|
||||
macports_) then install less:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo npm install -g rtlcss
|
||||
|
||||
- on Windows, `install nodejs <https://nodejs.org/en/download/>`_, reboot (to
|
||||
update the :envvar:`PATH`) and install rtlcss:
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\> npm install -g rtlcss
|
||||
|
||||
It is then necessary to edit the System Environment's variable
|
||||
:envvar:`PATH` and add the folder where `rtlcss.cmd` is located. Typically:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
C:\Users\<user>\AppData\Roaming\npm\
|
||||
|
||||
|
||||
Fetch the sources
|
||||
-----------------
|
||||
|
||||
There are two ways to obtain the Odoo source code: zip or git.
|
||||
|
||||
* Odoo zip can be downloaded from our nightly_ server or our Download_ page,
|
||||
the zip file then needs to be uncompressed to use its content
|
||||
|
||||
* git allows simpler update and easier switching between different versions
|
||||
of Odoo. It also simplifies maintaining non-module patches and
|
||||
contributions. The primary drawback of git is that it is significantly
|
||||
larger than a tarball as it contains the entire history of the Odoo project.
|
||||
|
||||
Community Edition
|
||||
'''''''''''''''''
|
||||
|
||||
The git repository is https://github.com/odoo/odoo.git for the Community
|
||||
edition.
|
||||
There are two ways to obtain the source code of Odoo: as a zip **archive** or through **git**.
|
||||
|
||||
Downloading it requires a `git client <http://git-scm.com/download/>`_
|
||||
(which may be available via your distribution on linux) and can be performed
|
||||
using the following command:
|
||||
Archive
|
||||
^^^^^^^
|
||||
|
||||
Community Edition:
|
||||
|
||||
* `Official download page <download_>`_
|
||||
* `GitHub repository <community-repository_>`_
|
||||
* `Nightly server <nightly_>`_
|
||||
|
||||
Enterprise Edition:
|
||||
|
||||
* `Official download page <download_>`_
|
||||
* `GitHub repository <enterprise-repository_>`_
|
||||
|
||||
Git
|
||||
^^^
|
||||
|
||||
The following requires git_ to be installed on your machine and that you have basic knowledge of
|
||||
git commands.
|
||||
|
||||
Community Edition:
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\> git clone https://github.com/odoo/odoo.git
|
||||
|
||||
|
||||
Enterprise Edition: (see :ref:`setup/install/editions` to get access)
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\> git clone https://github.com/odoo/enterprise.git
|
||||
|
||||
.. note:: **The Enterprise git repository does not contain the full Odoo source code**. It is only
|
||||
a collection of extra add-ons. The main server code is in the Community version. Running
|
||||
the Enterprise version actually means running the server from the Community version with
|
||||
the addons-path option set to the folder with the Enterprise version. You need to clone
|
||||
both the Community and Enterprise repository to have a working Odoo Enterprise
|
||||
installation.
|
||||
|
||||
Prepare
|
||||
'''''''
|
||||
|
||||
Python
|
||||
^^^^^^
|
||||
|
||||
Odoo requires Python 3.5 or later to run. Use the the official `Python 3 installer
|
||||
<https://www.python.org/downloads/windows/>`_ to download and install Python 3 on your machine.
|
||||
|
||||
During installation, check **Add Python 3 to PATH**, then click **Customize Installation** and make
|
||||
sure that **pip** is checked.
|
||||
|
||||
.. note:: If Python 3 is already installed, make sure that it is 3.5 or above, as previous versions
|
||||
are not compatible with Odoo.
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\> python3 --version
|
||||
|
||||
Verify also that pip_ is installed for this version.
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\> pip3 --version
|
||||
|
||||
PostgreSQL
|
||||
^^^^^^^^^^
|
||||
|
||||
Odoo uses PostgreSQL as database management system. Download and install the `latest version of
|
||||
PostgreSQL <https://www.postgresql.org/download/windows/>`_.
|
||||
|
||||
By default, the only user is `postgres` but Odoo forbids connecting as `postgres`, so you need to
|
||||
create a new PostgreSQL user:
|
||||
|
||||
#. Add PostgreSQL's `bin` directory (by default: `C:\\Program Files\\PostgreSQL\\<version>\\bin`) to
|
||||
your `PATH`.
|
||||
#. Create a postgres user with a password using the pg admin gui:
|
||||
|
||||
* Open **pgAdminIII**.
|
||||
* Double-click the server to create a connection.
|
||||
* Select :menuselection:`Edit --> New Object --> New Login Role`.
|
||||
* Enter the username in the **Role Name** field (e.g. `odoo`).
|
||||
* Open the **Definition** tab and enter the password (e.g. ``odoo``), then click **OK**.
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Odoo dependencies are listed in the `requirements.txt` file located at the root of the Odoo
|
||||
community directory. Most of them can be installed with **pip**.
|
||||
|
||||
.. tip:: It can be preferable to not mix python modules packages between different instances of Odoo
|
||||
or with your system. You can use virtualenv_ to create isolated Python environments.
|
||||
|
||||
Navigate to the path of your Odoo Community installation (`YourOdooCommunityPath`) and run **pip**
|
||||
on the requirements file:
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\> cd \YourOdooCommunityPath
|
||||
C:\YourOdooCommunityPath> C:\Python35\Scripts\pip.exe install -r requirements.txt
|
||||
|
||||
.. warning:: Some dependencies cannot be installed through pip and require to be installed manually.
|
||||
In particular:
|
||||
|
||||
* `psycopg` must be installed with
|
||||
`this installer <http://www.stickpeople.com/projects/python/win-psycopg/>`_.
|
||||
* `wkhtmltopdf` must be installed in version `0.12.5 <the wkhtmltopdf download page_>`_
|
||||
for it to support headers and footers. See our
|
||||
`wiki <https://github.com/odoo/odoo/wiki/Wkhtmltopdf>`_ for more details on the
|
||||
various versions.
|
||||
|
||||
For languages with right-to-left interface (such as Arabic or Hebrew), the package `rtlcss` is
|
||||
needed:
|
||||
|
||||
#. Download and install `nodejs <https://nodejs.org/en/download/>`_.
|
||||
#. Install `rtlcss`:
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\> npm install -g rtlcss
|
||||
|
||||
#. Edit the System Environment's variable `PATH` to add the folder where `rtlcss.cmd` is located
|
||||
(typically: `C:\\Users\\<user>\\AppData\\Roaming\\npm\\`).
|
||||
|
||||
Running Odoo
|
||||
''''''''''''
|
||||
|
||||
Once all dependencies are set up, Odoo can be launched by running `odoo-bin`, the
|
||||
command-line interface of the server. It is located at the root of the Odoo Community directory.
|
||||
|
||||
To configure the server, you can either specify :ref:`command-line arguments <reference/cmdline/server>` or a
|
||||
:ref:`configuration file <reference/cmdline/config>`.
|
||||
|
||||
.. tip:: For the Enterprise edition, you must add the path to the `enterprise` addons to the
|
||||
`addons-path` argument. Note that it must come before the other paths in `addons-path` for
|
||||
addons to be loaded correctly.
|
||||
|
||||
Common necessary configurations are:
|
||||
|
||||
* PostgreSQL user and password.
|
||||
* Custom addon paths beyond the defaults, to load your own modules.
|
||||
|
||||
A typical way to run the server would be:
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\YourOdooCommunityPath> python3 odoo-bin -r dbuser -w dbpassword --addons-path=addons,../mymodules --db-filter=mydb$
|
||||
|
||||
Where `YourOdooCommunityPath` is the path of the Odoo Community installation, `dbuser` is the
|
||||
PostgreSQL login, `dbpassword` is the PostgreSQL password, `../mymodules` is a directory with
|
||||
additional addons and `mydb` is the default database to serve on `localhost:8069`.
|
||||
|
||||
Linux
|
||||
-----
|
||||
|
||||
Fetch the sources
|
||||
'''''''''''''''''
|
||||
|
||||
There are two ways to obtain the source code of Odoo: as a zip **archive** or through **git**.
|
||||
|
||||
Archive
|
||||
^^^^^^^
|
||||
|
||||
Community Edition:
|
||||
|
||||
* `Official download page <download_>`_
|
||||
* `GitHub repository <community-repository_>`_
|
||||
* `Nightly server <nightly_>`_
|
||||
|
||||
Enterprise Edition:
|
||||
|
||||
* `Official download page <download_>`_
|
||||
* `GitHub repository <enterprise-repository_>`_
|
||||
|
||||
Git
|
||||
^^^
|
||||
|
||||
The following requires git_ to be installed on your machine and that you have basic knowledge of
|
||||
git commands.
|
||||
|
||||
Community Edition:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ git clone https://github.com/odoo/odoo.git
|
||||
|
||||
Enterprise Edition
|
||||
''''''''''''''''''
|
||||
|
||||
If you have access to the Enterprise repository (see :ref:`setup/install/editions`
|
||||
if you wish to get access), you can use this command to fetch the addons:
|
||||
Enterprise Edition: (see :ref:`setup/install/editions` to get access)
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ git clone https://github.com/odoo/enterprise.git
|
||||
|
||||
.. note:: The Enterprise git repository **does not contain the full Odoo
|
||||
source code**. It is only a collection of extra add-ons. The main server
|
||||
code is in the Community version. Running the Enterprise version actually
|
||||
means running the server from the Community version with the addons-path option
|
||||
set to the folder with the Enterprise version.
|
||||
.. note:: **The Enterprise git repository does not contain the full Odoo source code**. It is only
|
||||
a collection of extra add-ons. The main server code is in the Community version. Running
|
||||
the Enterprise version actually means running the server from the Community version with
|
||||
the addons-path option set to the folder with the Enterprise version. You need to clone
|
||||
both the Community and Enterprise repository to have a working Odoo Enterprise
|
||||
installation.
|
||||
|
||||
You need to clone both the Community and Enterprise repository to have a working
|
||||
Odoo installation
|
||||
Prepare
|
||||
'''''''
|
||||
|
||||
Python
|
||||
^^^^^^
|
||||
|
||||
Odoo requires Python 3.5 or later to run. Use your package manager to download and install Python 3
|
||||
on your machine if it is not already done.
|
||||
|
||||
.. note:: If Python 3 is already installed, make sure that it is 3.5 or above, as previous versions
|
||||
are not compatible with Odoo.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python3 --version
|
||||
|
||||
Verify also that pip_ is installed for this version.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip3 --version
|
||||
|
||||
PostgreSQL
|
||||
^^^^^^^^^^
|
||||
|
||||
Odoo uses PostgreSQL as database management system. Use your package manager to download and install
|
||||
the latest version of PostgreSQL.
|
||||
|
||||
By default, the only user is `postgres` but Odoo forbids connecting as `postgres`, so you need to
|
||||
create a new PostgreSQL user:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo -u postgres createuser -s $USER
|
||||
$ createdb $USER
|
||||
|
||||
.. note:: Because your PostgreSQL user has the same name as your Unix login, you will be able to
|
||||
connect to the database without password.
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Odoo dependencies are listed in the `requirements.txt` file located at the root of the Odoo
|
||||
community directory. Most of them can be installed with **pip**.
|
||||
|
||||
.. tip:: It can be preferable to not mix python modules packages between different instances of Odoo
|
||||
or with your system. You can use virtualenv_ to create isolated Python environments.
|
||||
|
||||
Navigate to the path of your Odoo Community installation (`YourOdooCommunityPath`) and run **pip**
|
||||
on the requirements file:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd /YourOdooCommunityPath
|
||||
/YourOdooCommunityPath$ pip3 install -r requirements.txt
|
||||
|
||||
.. warning:: For libraries using native code (Pillow, lxml, greenlet, gevent, psycopg2, ldap), it
|
||||
may be necessary to install development tools and native dependencies before pip is
|
||||
able to install the dependencies themselves. These are available in `-dev` or `-devel`
|
||||
packages for Python, PostgreSQL, libxml2, libxslt, libevent, libsasl2 and libldap2.
|
||||
|
||||
.. warning:: Some dependencies cannot be installed through pip and require to be installed manually.
|
||||
In particular:
|
||||
|
||||
* `wkhtmltopdf` must be installed in version `0.12.5 <the wkhtmltopdf download page_>`_
|
||||
for it to support headers and footers. See our
|
||||
`wiki <https://github.com/odoo/odoo/wiki/Wkhtmltopdf>`_ for more details on the
|
||||
various versions.
|
||||
|
||||
For languages with right-to-left interface (such as Arabic or Hebrew), the package `rtlcss` is
|
||||
needed:
|
||||
|
||||
#. Download and install **nodejs** and **npm** with your package manager.
|
||||
#. Install `rtlcss`:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo npm install -g rtlcss
|
||||
|
||||
Running Odoo
|
||||
------------
|
||||
''''''''''''
|
||||
|
||||
Once all dependencies are set up, Odoo can be launched by running ``odoo-bin``.
|
||||
Once all dependencies are set up, Odoo can be launched by running `odoo-bin`, the
|
||||
command-line interface of the server. It is located at the root of the Odoo Community directory.
|
||||
|
||||
.. tip:: For the Enterprise edition, you must specify the :file:`enterprise`
|
||||
addons folder when starting your server. You can do so by providing the path
|
||||
to your :file:`enterprise` folder in the ``addons-path`` parameter. Please
|
||||
note that the :file:`enterprise` folder must come before the default
|
||||
:file:`addons` folder in the list for the addons to be loaded correctly.
|
||||
|
||||
:ref:`Configuration <reference/cmdline>` can be provided either through
|
||||
:ref:`command-line arguments <reference/cmdline>` or through a
|
||||
To configure the server, you can either specify :ref:`command-line arguments <reference/cmdline/server>` or a
|
||||
:ref:`configuration file <reference/cmdline/config>`.
|
||||
|
||||
.. tip:: For the Enterprise edition, you must add the path to the `enterprise` addons to the
|
||||
`addons-path` argument. Note that it must come before the other paths in `addons-path` for
|
||||
addons to be loaded correctly.
|
||||
|
||||
Common necessary configurations are:
|
||||
|
||||
* PostgreSQL host, port, user and password.
|
||||
* PostgreSQL user and password. Odoo has no defaults beyond
|
||||
`psycopg2's defaults <http://initd.org/psycopg/docs/module.html>`_: connects over a UNIX socket on
|
||||
port `5432` with the current user and no password.
|
||||
* Custom addon paths beyond the defaults, to load your own modules.
|
||||
|
||||
Odoo has no defaults beyond
|
||||
`psycopg2's defaults <http://initd.org/psycopg/docs/module.html>`_: connects
|
||||
over a UNIX socket on port 5432 with the current user and no password. By
|
||||
default this should work on Linux and OS X, but it *will not work* on
|
||||
windows as it does not support UNIX sockets.
|
||||
|
||||
* Custom addons path beyond the defaults, to load your own modules
|
||||
|
||||
Under Windows a typical way to execute odoo would be:
|
||||
|
||||
.. code-block:: doscon
|
||||
|
||||
C:\YourOdooPath> python3 odoo-bin -w odoo -r odoo --addons-path=addons,../mymodules --db-filter=mydb$
|
||||
|
||||
Where ``odoo``, ``odoo`` are the postgresql login and password,
|
||||
``../mymodules`` a directory with additional addons and ``mydb`` the default
|
||||
db to serve on localhost:8069
|
||||
|
||||
Under Unix a typical way to execute odoo would be:
|
||||
A typical way to run the server would be:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ ./odoo-bin --addons-path=addons,../mymodules --db-filter=mydb$
|
||||
/YourOdooCommunityPath$ python3 odoo-bin --addons-path=addons,../mymodules --db-filter=mydb$
|
||||
|
||||
Where ``../mymodules`` is a directory with additional addons and ``mydb`` the
|
||||
default db to serve on localhost:8069
|
||||
Where `YourOdooCommunityPath` is the path of the Odoo Community installation, `../mymodules` is a
|
||||
directory with additional addons and `mydb` is the default database to serve on `localhost:8069`.
|
||||
|
||||
Virtualenv
|
||||
----------
|
||||
Mac OS
|
||||
------
|
||||
|
||||
Virtualenv_ is a tool to create Python isolated environments because it's
|
||||
sometimes preferable to not mix your distribution python modules packages
|
||||
with globally installed python modules with pip.
|
||||
Fetch the sources
|
||||
'''''''''''''''''
|
||||
|
||||
This section will explain how to run Odoo in a such isolated Python environment.
|
||||
There are two ways to obtain the source code of Odoo: as a zip **archive** or through **git**.
|
||||
|
||||
Here we are going to use virtualenvwrapper_ which is a set of shell scripts that
|
||||
makes the use of virtualenv easier.
|
||||
Archive
|
||||
^^^^^^^
|
||||
|
||||
The examples below are based on a Debian 9 distribution but could be adapted on
|
||||
any platform where virtualenvwrapper_ and virtualenv_ are able to run.
|
||||
Community Edition:
|
||||
|
||||
This section assumes that you obtained the Odoo sources from the zip file or the
|
||||
git repository as explained above. The same apply for postgresql installation
|
||||
and configuration.
|
||||
* `Official download page <download_>`_
|
||||
* `GitHub repository <community-repository_>`_
|
||||
* `Nightly server <nightly_>`_
|
||||
|
||||
Install virtualenvwrapper
|
||||
'''''''''''''''''''''''''
|
||||
Enterprise Edition:
|
||||
|
||||
* `Official download page <download_>`_
|
||||
* `GitHub repository <enterprise-repository_>`_
|
||||
|
||||
Git
|
||||
^^^
|
||||
|
||||
The following requires git_ to be installed on your machine and that you have basic knowledge of
|
||||
git commands.
|
||||
|
||||
Community Edition:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo apt install virtualenvwrapper
|
||||
$ source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
|
||||
$ git clone https://github.com/odoo/odoo.git
|
||||
|
||||
This will install virtualenvwrapper_ and activate it immediately.
|
||||
Now, let's install the tools required to build Odoo dependencies if needed:
|
||||
|
||||
Enterprise Edition: (see :ref:`setup/install/editions` to get access)
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo apt install build-essential python3-dev libxslt-dev libzip-dev libldap2-dev libsasl2-dev
|
||||
$ git clone https://github.com/odoo/enterprise.git
|
||||
|
||||
Create an isolated environment
|
||||
''''''''''''''''''''''''''''''
|
||||
.. note:: **The Enterprise git repository does not contain the full Odoo source code**. It is only
|
||||
a collection of extra add-ons. The main server code is in the Community version. Running
|
||||
the Enterprise version actually means running the server from the Community version with
|
||||
the addons-path option set to the folder with the Enterprise version. You need to clone
|
||||
both the Community and Enterprise repository to have a working Odoo Enterprise
|
||||
installation.
|
||||
|
||||
Now we can create a virtual environment for Odoo like this:
|
||||
Prepare
|
||||
'''''''
|
||||
|
||||
Python
|
||||
^^^^^^
|
||||
|
||||
Odoo requires Python 3.5 or later to run. Use your preferred package manager (homebrew_, macports_)
|
||||
to download and install Python 3 on your machine if it is not already done.
|
||||
|
||||
.. note:: If Python 3 is already installed, make sure that it is 3.5 or above, as previous versions
|
||||
are not compatible with Odoo.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python3 --version
|
||||
|
||||
Verify also that pip_ is installed for this version.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip3 --version
|
||||
|
||||
PostgreSQL
|
||||
^^^^^^^^^^
|
||||
|
||||
Odoo uses PostgreSQL as database management system. Use `postgres.app <https://postgresapp.com>`_
|
||||
to download and install the latest version of PostgreSQL.
|
||||
|
||||
By default, the only user is `postgres` but Odoo forbids connecting as `postgres`, so you need to
|
||||
create a new PostgreSQL user:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ mkvirtualenv -p /usr/bin/python3 odoo-venv
|
||||
$ sudo -u postgres createuser -s $USER
|
||||
$ createdb $USER
|
||||
|
||||
With this command, we ask for an isolated Python3 environment that will be named
|
||||
"odoo-env". If the command works as expected, your shell is now using this
|
||||
environment. Your prompt should have changed to remind you that you are using
|
||||
an isolated environment. You can verify with this command:
|
||||
.. note:: Because your PostgreSQL user has the same name as your Unix login, you will be able to
|
||||
connect to the database without password.
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Odoo dependencies are listed in the `requirements.txt` file located at the root of the Odoo
|
||||
community directory. Most of them can be installed with **pip**.
|
||||
|
||||
.. tip:: It can be preferable to not mix python modules packages between different instances of Odoo
|
||||
or with your system. You can use virtualenv_ to create isolated Python environments.
|
||||
|
||||
Navigate to the path of your Odoo Community installation (`YourOdooCommunityPath`) and run **pip**
|
||||
on the requirements file:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ which python3
|
||||
$ cd /YourOdooCommunityPath
|
||||
/YourOdooCommunityPath$ pip3 install -r requirements.txt
|
||||
|
||||
This command should show you the path to the Python interpreter located in the
|
||||
isolated environment directory.
|
||||
.. warning:: Non-Python dependencies need to be installed with a package manager:
|
||||
|
||||
Now let's install the Odoo required python packages:
|
||||
#. Download and install the **Command Line Tools**:
|
||||
|
||||
.. code-block:: console
|
||||
.. code-block:: console
|
||||
|
||||
$ cd your_odoo_sources_path
|
||||
$ pip install -r requirements.txt
|
||||
$ xcode-select --install
|
||||
|
||||
After a little while, you should be ready to run odoo from the command line as
|
||||
explained above.
|
||||
#. Download and install the package manager of your choice (homebrew_, macports_).
|
||||
#. Install non-python dependencies.
|
||||
|
||||
When you you want to leave the virtual environment, just issue this command:
|
||||
.. warning:: Some dependencies cannot be installed through pip and require to be installed manually.
|
||||
In particular:
|
||||
|
||||
.. code-block:: console
|
||||
* `wkhtmltopdf` must be installed in version `0.12.5 <the wkhtmltopdf download page_>`_
|
||||
for it to support headers and footers. See our
|
||||
`wiki <https://github.com/odoo/odoo/wiki/Wkhtmltopdf>`_ for more details on the
|
||||
various versions.
|
||||
|
||||
$ deactivate
|
||||
For languages with right-to-left interface (such as Arabic or Hebrew), the package `rtlcss` is
|
||||
needed:
|
||||
|
||||
Whenever you want to work again with your 'odoo-venv' environment:
|
||||
#. Download and install **nodejs** with your preferred package manager (homebrew_, macports_).
|
||||
#. Install `rtlcss`:
|
||||
|
||||
.. code-block:: console
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo npm install -g rtlcss
|
||||
|
||||
$ workon odoo-venv
|
||||
|
||||
.. _setup/install/docker:
|
||||
|
||||
@@ -580,18 +734,16 @@ Docker
|
||||
The full documentation on how to use Odoo with Docker can be found on the
|
||||
official Odoo `docker image <https://registry.hub.docker.com/_/odoo/>`_ page.
|
||||
|
||||
.. _Debian Stretch: https://www.debian.org/releases/stretch/
|
||||
.. _demo: https://demo.odoo.com
|
||||
.. _docker: https://www.docker.com
|
||||
.. _Download: https://www.odoo.com/page/download
|
||||
.. _Debian Stretch: https://www.debian.org/releases/stretch/
|
||||
.. _download: https://www.odoo.com/page/download
|
||||
.. _Ubuntu 18.04: http://releases.ubuntu.com/18.04/
|
||||
.. _EPEL: https://fedoraproject.org/wiki/EPEL
|
||||
.. _PostgreSQL: http://www.postgresql.org
|
||||
.. _the official installer:
|
||||
.. _install pip:
|
||||
https://pip.pypa.io/en/latest/installing.html#install-pip
|
||||
.. _PostgreSQL for windows:
|
||||
http://www.enterprisedb.com/products-services-training/pgdownload
|
||||
.. _Quilt: http://en.wikipedia.org/wiki/Quilt_(software)
|
||||
.. _saas: https://www.odoo.com/page/start
|
||||
.. _the wkhtmltopdf download page: https://github.com/wkhtmltopdf/wkhtmltopdf/releases/tag/0.12.5
|
||||
@@ -604,8 +756,9 @@ official Odoo `docker image <https://registry.hub.docker.com/_/odoo/>`_ page.
|
||||
.. _virtualenv: https://pypi.python.org/pypi/virtualenv
|
||||
.. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.io/en/latest/
|
||||
.. _pywin32: http://sourceforge.net/projects/pywin32/files/pywin32/
|
||||
.. _the repository: https://github.com/odoo/odoo
|
||||
.. _git: http://git-scm.com
|
||||
.. _community-repository: https://github.com/odoo/odoo
|
||||
.. _enterprise-repository: https://github.com/odoo/enterprise
|
||||
.. _git: https://git-scm.com/
|
||||
.. _Editions: https://www.odoo.com/pricing#pricing_table_features
|
||||
.. _nightly: https://nightly.odoo.com/12.0/nightly/
|
||||
.. _nightly: https://nightly.odoo.com/
|
||||
.. _extra: https://nightly.odoo.com/extra/
|
||||
|
||||
+15
-13
@@ -75,7 +75,7 @@ Overview
|
||||
service directly, or you can delegate the actual service acting as a
|
||||
bridge/translator between an Odoo system and the actual service.
|
||||
|
||||
|
||||
|
||||
.. figure:: images/credits.jpg
|
||||
:align: center
|
||||
|
||||
@@ -94,7 +94,7 @@ Overview
|
||||
* for a postage service: 1 credit = 1 post stamp.
|
||||
|
||||
A credit can also simply be associated with a fixed amount of money
|
||||
to palliate the variations of price (e.g. the prices of sms and stamps
|
||||
to palliate the variations of price (e.g. the prices of sms and stamps
|
||||
may vary following the countries).
|
||||
|
||||
The value of the credits is fixed with the help of prepaid credit packs
|
||||
@@ -158,15 +158,15 @@ Register the service on Odoo
|
||||
|
||||
.. todo:: complete this part with screenshots
|
||||
|
||||
The first step is to register your service on the IAP endpoint (production
|
||||
The first step is to register your service on the IAP endpoint (production
|
||||
and/or test) before you can actually query user accounts. To create a service,
|
||||
go to your *Portal Account* on the IAP endpoint (https://iap.odoo.com for
|
||||
production, https://iap-sandbox.odoo.com for testing, the endpoints are
|
||||
*independent* and *not synchronized*). Alternatively, you can go to your portal
|
||||
on Odoo (https://iap.odoo.com/my/home) and select *In-App Services*.
|
||||
|
||||
.. note::
|
||||
|
||||
.. note::
|
||||
|
||||
On production, there is a manual validation step before the service
|
||||
can be used to manage real transactions. This step is automatically passed when
|
||||
on sandbox to ease the tests.
|
||||
@@ -187,7 +187,7 @@ The service has *seven* important fields:
|
||||
|
||||
|
||||
.. warning::
|
||||
Both the :class:`ServiceName` and :class:`Label` are unique. As good practice, the
|
||||
Both the :class:`ServiceName` and :class:`Label` are unique. As good practice, the
|
||||
:class:`ServiceName` should usually match the name of your Odoo Client App.
|
||||
|
||||
* :samp:`icon` - :class:`Icon`: A generic icon that will serve as default for your
|
||||
@@ -241,8 +241,8 @@ A credit pack is essentially a product with five characteristics:
|
||||
* Amount: amount of credits the client is entitled to when buying the pack,
|
||||
* Price: price in EUR (for the time being, USD support is planned).
|
||||
|
||||
.. note::
|
||||
|
||||
.. note::
|
||||
|
||||
Odoo takes a 25% commission on all pack sales. Adjust your selling price accordingly.
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ In that call, we will need to provide:
|
||||
* the :class:`token <UserToken>` of the current client that is provided by
|
||||
the ``iap.account`` model's ``account_token`` field. You can retrieve the
|
||||
account for your service by calling :samp:`env['iap.account'].get({service_name})`
|
||||
where :class:`service_name <ServiceName>` is the name of the service registered
|
||||
where :class:`service_name <ServiceName>` is the name of the service registered
|
||||
on IAP endpoint.
|
||||
|
||||
.. patch::
|
||||
@@ -465,7 +465,7 @@ Capture
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 8
|
||||
|
||||
|
||||
r2 = requests.post(ODOO + '/iap/1/capture', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': None,
|
||||
@@ -550,6 +550,7 @@ care how they are implemented.
|
||||
pending transactions/existing holds).
|
||||
|
||||
.. exception:: odoo.exceptions.AccessError
|
||||
:noindex:
|
||||
|
||||
Raised by:
|
||||
|
||||
@@ -557,8 +558,9 @@ care how they are implemented.
|
||||
* any failure in an inter-server call. (typically, in :func:`~odoo.addons.iap.jsonrpc`).
|
||||
|
||||
.. exception:: odoo.exceptions.UserError
|
||||
:noindex:
|
||||
|
||||
Raised by any unexpeted behaviour at the discretion of the App developer (*you*).
|
||||
Raised by any unexpected behaviour at the discretion of the App developer (*you*).
|
||||
|
||||
|
||||
Test the API
|
||||
@@ -687,7 +689,7 @@ Cancel
|
||||
------
|
||||
|
||||
.. class:: odoo.addons.iap.models.iap.cancel(env, transaction_token, key)
|
||||
|
||||
|
||||
Will cancel an authorized transaction.
|
||||
|
||||
:param odoo.api.Environment env: used to retrieve the ``iap.endpoint``
|
||||
@@ -761,5 +763,5 @@ Capture
|
||||
capture(env,transaction_token, key, min(credits, 2))
|
||||
|
||||
|
||||
.. _JSON-RPC2: http://www.jsonrpc.org/specification
|
||||
.. _JSON-RPC2: https://www.jsonrpc.org/specification
|
||||
.. _Odoo App: https://www.odoo.com/apps
|
||||
|
||||
+39
-39
@@ -184,13 +184,13 @@ database:
|
||||
These examples use the `Ripcord <https://code.google.com/p/ripcord/>`_
|
||||
library, which provides a simple XML-RPC API. Ripcord requires that
|
||||
`XML-RPC support be enabled
|
||||
<http://php.net/manual/en/xmlrpc.installation.php>`_ in your PHP
|
||||
<https://php.net/manual/en/xmlrpc.installation.php>`_ in your PHP
|
||||
installation.
|
||||
|
||||
Since calls are performed over
|
||||
`HTTPS <http://en.wikipedia.org/wiki/HTTP_Secure>`_, it also requires that
|
||||
`HTTPS <https://en.wikipedia.org/wiki/HTTP_Secure>`_, it also requires that
|
||||
the `OpenSSL extension
|
||||
<http://php.net/manual/en/openssl.installation.php>`_ be enabled.
|
||||
<https://php.net/manual/en/openssl.installation.php>`_ be enabled.
|
||||
|
||||
.. case:: Java
|
||||
|
||||
@@ -219,7 +219,7 @@ database:
|
||||
Logging in
|
||||
----------
|
||||
|
||||
Odoo requires users of the API to be authenticated before they can query most
|
||||
Odoo requires users of the API to be authenticated before they can query most
|
||||
data.
|
||||
|
||||
The ``xmlrpc/2/common`` endpoint provides meta-calls which don't require
|
||||
@@ -414,31 +414,31 @@ available to only retrieve a subset of all matched records.
|
||||
.. container:: doc-aside
|
||||
|
||||
.. switcher::
|
||||
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
|
||||
models.execute_kw(db, uid, password,
|
||||
'res.partner', 'search',
|
||||
[[['is_company', '=', True], ['customer', '=', True]]],
|
||||
{'offset': 10, 'limit': 5})
|
||||
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
|
||||
models.execute_kw(db, uid, password,
|
||||
'res.partner', 'search',
|
||||
[[['is_company', '=', true], ['customer', '=', true]]],
|
||||
{offset: 10, limit: 5})
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
||||
$models->execute_kw($db, $uid, $password,
|
||||
'res.partner', 'search',
|
||||
array(array(array('is_company', '=', true),
|
||||
array('customer', '=', true))),
|
||||
array('offset'=>10, 'limit'=>5));
|
||||
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
|
||||
asList((Object[])models.execute("execute_kw", asList(
|
||||
db, uid, password,
|
||||
"res.partner", "search",
|
||||
@@ -447,9 +447,9 @@ available to only retrieve a subset of all matched records.
|
||||
asList("customer", "=", true))),
|
||||
new HashMap() {{ put("offset", 10); put("limit", 5); }}
|
||||
)));
|
||||
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
|
||||
[13, 20, 30, 22, 29]
|
||||
|
||||
Count records
|
||||
@@ -464,28 +464,28 @@ only the number of records matching the query. It takes the same
|
||||
.. container:: doc-aside
|
||||
|
||||
.. switcher::
|
||||
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
|
||||
models.execute_kw(db, uid, password,
|
||||
'res.partner', 'search_count',
|
||||
[[['is_company', '=', True], ['customer', '=', True]]])
|
||||
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
|
||||
models.execute_kw(db, uid, password,
|
||||
'res.partner', 'search_count',
|
||||
[[['is_company', '=', true], ['customer', '=', true]]])
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
||||
$models->execute_kw($db, $uid, $password,
|
||||
'res.partner', 'search_count',
|
||||
array(array(array('is_company', '=', true),
|
||||
array('customer', '=', true))));
|
||||
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
|
||||
(Integer)models.execute("execute_kw", asList(
|
||||
db, uid, password,
|
||||
"res.partner", "search_count",
|
||||
@@ -493,9 +493,9 @@ only the number of records matching the query. It takes the same
|
||||
asList("is_company", "=", true),
|
||||
asList("customer", "=", true)))
|
||||
));
|
||||
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
|
||||
19
|
||||
|
||||
.. warning::
|
||||
@@ -514,11 +514,11 @@ fetch. By default, it will fetch all the fields the current user can read,
|
||||
which tends to be a huge amount.
|
||||
|
||||
.. container:: doc-aside
|
||||
|
||||
|
||||
.. switcher::
|
||||
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
|
||||
ids = models.execute_kw(db, uid, password,
|
||||
'res.partner', 'search',
|
||||
[[['is_company', '=', True], ['customer', '=', True]]],
|
||||
@@ -527,9 +527,9 @@ which tends to be a huge amount.
|
||||
'res.partner', 'read', [ids])
|
||||
# count the number of fields fetched by default
|
||||
len(record)
|
||||
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
|
||||
ids = models.execute_kw(db, uid, password,
|
||||
'res.partner', 'search',
|
||||
[[['is_company', '=', true], ['customer', '=', true]]],
|
||||
@@ -538,9 +538,9 @@ which tends to be a huge amount.
|
||||
'res.partner', 'read', [ids]).first
|
||||
# count the number of fields fetched by default
|
||||
record.length
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
||||
$ids = $models->execute_kw($db, $uid, $password,
|
||||
'res.partner', 'search',
|
||||
array(array(array('is_company', '=', true),
|
||||
@@ -550,9 +550,9 @@ which tends to be a huge amount.
|
||||
'res.partner', 'read', array($ids));
|
||||
// count the number of fields fetched by default
|
||||
count($records[0]);
|
||||
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
|
||||
final List ids = asList((Object[])models.execute(
|
||||
"execute_kw", asList(
|
||||
db, uid, password,
|
||||
@@ -570,9 +570,9 @@ which tends to be a huge amount.
|
||||
))[0];
|
||||
// count the number of fields fetched by default
|
||||
record.size();
|
||||
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
|
||||
121
|
||||
|
||||
Conversedly, picking only three fields deemed interesting.
|
||||
@@ -910,7 +910,7 @@ a record).
|
||||
Delete records
|
||||
--------------
|
||||
|
||||
Records can be deleted in bulk by providing their ids to
|
||||
Records can be deleted in bulk by providing their ids to
|
||||
:meth:`~odoo.models.Model.unlink`.
|
||||
|
||||
.. container:: doc-aside
|
||||
@@ -1296,6 +1296,6 @@ activated as actual fields on the model.
|
||||
]
|
||||
|
||||
|
||||
.. _PostgreSQL: http://www.postgresql.org
|
||||
.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
|
||||
.. _base64: http://en.wikipedia.org/wiki/Base64
|
||||
.. _PostgreSQL: https://www.postgresql.org
|
||||
.. _XML-RPC: https://en.wikipedia.org/wiki/XML-RPC
|
||||
.. _base64: https://en.wikipedia.org/wiki/Base64
|
||||
|
||||
@@ -135,7 +135,7 @@ Sample script
|
||||
Here are 2 examples of database upgrade request creation using:
|
||||
|
||||
* one in the python programming language using the requests library
|
||||
* one in the bash programming language using `curl <http://curl.haxx.se>`_ (tool
|
||||
* one in the bash programming language using `curl <https://curl.haxx.se>`_ (tool
|
||||
for transfering data using http) and `jq <https://stedolan.github.io/jq>`_ (JSON processor):
|
||||
|
||||
.. rst-class:: setup doc-aside
|
||||
|
||||
@@ -869,7 +869,7 @@
|
||||
<record id="KWD" model="res.currency">
|
||||
<field name="name">KWD</field>
|
||||
<field name="symbol"> د.ك </field>
|
||||
<field name="rounding">0.01</field>
|
||||
<field name="rounding">0.001</field>
|
||||
<field name="active" eval="False"/>
|
||||
<field name="currency_unit_label">Dinar</field>
|
||||
<field name="currency_subunit_label">Fils</field>
|
||||
|
||||
@@ -483,13 +483,15 @@ class AssetsBundle(object):
|
||||
compiled = compiled.strip()
|
||||
|
||||
# Post process the produced css to add required vendor prefixes here
|
||||
compiled = re.sub(r'(appearance: (\w+);)', r'-webkit-appearance: \2; -moz-appearance: \2; \1', compiled);
|
||||
compiled = re.sub(r'(appearance: (\w+);)', r'-webkit-appearance: \2; -moz-appearance: \2; \1', compiled)
|
||||
|
||||
compiled = re.sub(r'(display: ((?:inline-)?)flex((?: ?!important)?);)', r'display: -webkit-\2box\3; display: -webkit-\2flex\3; \1', compiled) # For PhantomJS tests and wkhtmltopdf
|
||||
compiled = re.sub(r'(justify-content: flex-(\w+)((?: ?!important)?);)', r'-webkit-box-pack: \2\3; \1', compiled) # For wkhtmltopdf
|
||||
compiled = re.sub(r'(flex-flow: (\w+ \w+);)', r'-webkit-flex-flow: \2; \1', compiled) # For PhantomJS tests
|
||||
compiled = re.sub(r'(flex-direction: (column);)', r'-webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: \2; \1', compiled) # For wkhtmltopdf
|
||||
compiled = re.sub(r'(flex: ((\d)+ \d+ (?:\d+|auto));)', r'-webkit-box-flex: \3; -webkit-flex: \2; \1', compiled) # For PhantomJS tests and wkhtmltopdf
|
||||
# Most of those are only useful for wkhtmltopdf (some for old PhantomJS)
|
||||
compiled = re.sub(r'(display: ((?:inline-)?)flex((?: ?!important)?);)', r'display: -webkit-\2box\3; display: -webkit-\2flex\3; \1', compiled)
|
||||
compiled = re.sub(r'(justify-content: flex-(\w+)((?: ?!important)?);)', r'-webkit-box-pack: \2\3; \1', compiled)
|
||||
compiled = re.sub(r'(flex-flow: (\w+ \w+);)', r'-webkit-flex-flow: \2; \1', compiled)
|
||||
compiled = re.sub(r'(flex-direction: (column);)', r'-webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: \2; \1', compiled)
|
||||
compiled = re.sub(r'(flex-wrap: (\w+);)', r'-webkit-flex-wrap: \2; \1', compiled)
|
||||
compiled = re.sub(r'(flex: ((\d)+ \d+ (?:\d+|auto));)', r'-webkit-box-flex: \3; -webkit-flex: \2; \1', compiled)
|
||||
|
||||
return compiled
|
||||
|
||||
|
||||
@@ -318,9 +318,7 @@ class IrActionsServer(models.Model):
|
||||
The available actions are :
|
||||
|
||||
- 'Execute Python Code': a block of python code that will be executed
|
||||
- 'Run a Client Action': choose a client action to launch
|
||||
- 'Create or Copy a new Record': create a new record with new values, or
|
||||
copy an existing record in your database
|
||||
- 'Create a new Record': create a new record with new values
|
||||
- 'Write on a Record': update the values of a record
|
||||
- 'Execute several actions': define an action that triggers several other
|
||||
server actions
|
||||
@@ -361,11 +359,12 @@ class IrActionsServer(models.Model):
|
||||
default='object_write', required=True,
|
||||
help="Type of server action. The following values are available:\n"
|
||||
"- 'Execute Python Code': a block of python code that will be executed\n"
|
||||
"- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
|
||||
"- 'Write on a Record': update the values of a record\n"
|
||||
"- 'Create': create a new record with new values\n"
|
||||
"- 'Update a Record': update the values of a record\n"
|
||||
"- 'Execute several actions': define an action that triggers several other server actions\n"
|
||||
"- 'Add Followers': add followers to a record (available in Discuss)\n"
|
||||
"- 'Send Email': automatically send an email (available in email_template)")
|
||||
"- 'Send Email': automatically send an email (Discuss)\n"
|
||||
"- 'Add Followers': add followers to a record (Discuss)\n"
|
||||
"- 'Create Next Activity': create an activity (Discuss)")
|
||||
# Generic
|
||||
sequence = fields.Integer(default=5,
|
||||
help="When dealing with multiple actions, the execution order is "
|
||||
@@ -443,15 +442,7 @@ class IrActionsServer(models.Model):
|
||||
|
||||
@api.model
|
||||
def run_action_object_write(self, action, eval_context=None):
|
||||
""" Write server action.
|
||||
|
||||
- 1. evaluate the value mapping
|
||||
- 2. depending on the write configuration:
|
||||
|
||||
- `current`: id = active_id
|
||||
- `other`: id = from reference object
|
||||
- `expression`: id = from expression evaluation
|
||||
"""
|
||||
"""Apply specified write changes to active_id."""
|
||||
res = {}
|
||||
for exp in action.fields_lines:
|
||||
res[exp.col1.name] = exp.eval_value(eval_context=eval_context)[exp.id]
|
||||
@@ -465,16 +456,9 @@ class IrActionsServer(models.Model):
|
||||
|
||||
@api.model
|
||||
def run_action_object_create(self, action, eval_context=None):
|
||||
""" Create and Copy server action.
|
||||
"""Create specified model object with specified values.
|
||||
|
||||
- 1. evaluate the value mapping
|
||||
- 2. depending on the write configuration:
|
||||
|
||||
- `new`: new record in the base model
|
||||
- `copy_current`: copy the current record (id = active_id) + gives custom values
|
||||
- `new_other`: new record in target model
|
||||
- `copy_other`: copy the current record (id from reference object)
|
||||
+ gives custom values
|
||||
If applicable, link active_id.<self.link_field_id> to the new record.
|
||||
"""
|
||||
res = {}
|
||||
for exp in action.fields_lines:
|
||||
|
||||
@@ -228,6 +228,7 @@ class ir_cron(models.Model):
|
||||
try:
|
||||
registry = odoo.registry(db_name)
|
||||
registry[cls._name]._process_job(job_cr, job, lock_cr)
|
||||
_logger.info('Job `%s` done.', job['cron_name'])
|
||||
except Exception:
|
||||
_logger.exception('Unexpected exception while processing cron job %r', job)
|
||||
finally:
|
||||
|
||||
@@ -517,19 +517,19 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
|
||||
# Special case when value is the id of a deleted record, we do not want to
|
||||
# block the settings screen
|
||||
value = self.env[field.comodel_name].browse(int(value)).exists().id
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
_logger.warning(WARNING_MESSAGE, value, field, icp)
|
||||
value = False
|
||||
elif field.type == 'integer':
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
_logger.warning(WARNING_MESSAGE, value, field, icp)
|
||||
value = 0
|
||||
elif field.type == 'float':
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
_logger.warning(WARNING_MESSAGE, value, field, icp)
|
||||
value = 0.0
|
||||
elif field.type == 'boolean':
|
||||
|
||||
@@ -984,22 +984,19 @@ class UsersImplied(models.Model):
|
||||
# complete 'groups_id' with implied groups
|
||||
user = self.new(values)
|
||||
gs = user.groups_id._origin
|
||||
group_public = self.env.ref('base.group_public', raise_if_not_found=False)
|
||||
group_portal = self.env.ref('base.group_portal', raise_if_not_found=False)
|
||||
if group_public and group_public in gs:
|
||||
gs = group_public
|
||||
elif group_portal and group_portal in gs:
|
||||
gs = group_portal
|
||||
gs = gs | gs.trans_implied_ids
|
||||
values['groups_id'] = type(self).groups_id.convert_to_write(gs, user)
|
||||
return super(UsersImplied, self).create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
users_before = self.filtered(lambda u: u.has_group('base.group_user'))
|
||||
res = super(UsersImplied, self).write(values)
|
||||
if values.get('groups_id'):
|
||||
# add implied groups for all users
|
||||
for user in self:
|
||||
if not user.has_group('base.group_user'):
|
||||
if not user.has_group('base.group_user') and user in users_before:
|
||||
# if we demoted a user, we strip him of all its previous privileges
|
||||
# (but we should not do it if we are simply adding a technical group to a portal user)
|
||||
vals = {'groups_id': [(5, 0, 0)] + values['groups_id']}
|
||||
super(UsersImplied, user).write(vals)
|
||||
gs = set(concat(g.trans_implied_ids for g in user.groups_id))
|
||||
|
||||
@@ -37,5 +37,4 @@ from . import test_res_partner
|
||||
from . import test_res_partner_bank
|
||||
from . import test_reports
|
||||
from . import test_tests_tags
|
||||
from . import test_non_regression
|
||||
from . import test_base_document_layout
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Non-Regression Tests
|
||||
"""
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestNR(TransactionCase):
|
||||
def test_issue26036(self):
|
||||
# Coming from https://github.com/odoo/odoo/pull/26036
|
||||
U = self.env["res.users"]
|
||||
G = self.env["res.groups"]
|
||||
group_user = self.env.ref('base.group_user')
|
||||
group_no_one = self.env.ref('base.group_no_one')
|
||||
|
||||
group_A = G.create({"name": "A"})
|
||||
group_AA = G.create({"name": "AA", "implied_ids": [(6, 0, [group_A.id])]})
|
||||
group_B = G.create({"name": "B"})
|
||||
group_BB = G.create({"name": "BB", "implied_ids": [(6, 0, [group_B.id])]})
|
||||
group_C = G.create({"name": "C"})
|
||||
|
||||
user_a = U.create({"name": "a", "login": "a", "groups_id": [(6, 0, [group_AA.id, group_user.id])]})
|
||||
user_b = U.create({"name": "b", "login": "b", "groups_id": [(6, 0, [group_BB.id])]})
|
||||
|
||||
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_user + group_no_one))
|
||||
|
||||
(user_a + user_b).write({"groups_id": [(4, group_C.id)]})
|
||||
|
||||
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_C + group_user + group_no_one))
|
||||
# As user_b is not an internal user, all its groups are removed
|
||||
self.assertEqual(user_b.groups_id, group_C)
|
||||
@@ -28,6 +28,8 @@ class TestHasGroup(TransactionCase):
|
||||
self.grp_internal = self.env.ref(self.grp_internal_xml_id)
|
||||
self.grp_portal_xml_id = 'base.group_portal'
|
||||
self.grp_portal = self.env.ref(self.grp_portal_xml_id)
|
||||
self.grp_public_xml_id = 'base.group_public'
|
||||
self.grp_public = self.env.ref(self.grp_public_xml_id)
|
||||
|
||||
def test_env_uid(self):
|
||||
Users = self.env['res.users'].with_user(self.test_user)
|
||||
@@ -51,6 +53,10 @@ class TestHasGroup(TransactionCase):
|
||||
)
|
||||
|
||||
def test_portal_creation(self):
|
||||
"""Here we check that portal user creation fails if it tries to create a user
|
||||
who would also have group_user by implied_group.
|
||||
Otherwise, it succeeds with the groups we asked for.
|
||||
"""
|
||||
grp_public = self.env.ref('base.group_public')
|
||||
grp_test_portal_xml_id = 'test_user_has_group.portal_implied_group'
|
||||
grp_test_portal = self.env['res.groups']._load_records([
|
||||
@@ -64,14 +70,16 @@ class TestHasGroup(TransactionCase):
|
||||
dict(xml_id=grp_test_internal2_xml_id, values={'name': 'Test Group Internal 2'})
|
||||
])
|
||||
self.grp_portal.implied_ids = grp_test_portal
|
||||
grp_test_internal1.implied_ids = self.grp_internal
|
||||
grp_test_internal2.implied_ids = self.grp_internal
|
||||
|
||||
grp_test_internal1.implied_ids = False
|
||||
grp_test_internal2.implied_ids = False
|
||||
|
||||
portal_user = self.env['res.users'].create({
|
||||
'login': 'portalTest',
|
||||
'name': 'Portal test',
|
||||
'sel_groups_%s_%s_%s' % (self.grp_internal.id, self.grp_portal.id, grp_public.id): self.grp_portal.id,
|
||||
'sel_groups_%s_%s' % (grp_test_internal1.id, grp_test_internal2.id): grp_test_internal2.id,
|
||||
})
|
||||
})
|
||||
|
||||
self.assertTrue(
|
||||
portal_user.has_group(self.grp_portal_xml_id),
|
||||
@@ -81,45 +89,52 @@ class TestHasGroup(TransactionCase):
|
||||
portal_user.has_group(grp_test_portal_xml_id),
|
||||
"The portal user should belong to '%s'" % grp_test_portal_xml_id,
|
||||
)
|
||||
self.assertFalse(
|
||||
self.assertTrue(
|
||||
portal_user.has_group(grp_test_internal2_xml_id),
|
||||
"The portal user should not belong to '%s'" % grp_test_internal2_xml_id
|
||||
"The portal user should belong to '%s'" % grp_test_internal2_xml_id
|
||||
)
|
||||
self.assertFalse(
|
||||
portal_user.has_group(self.grp_internal_xml_id),
|
||||
"The portal user should not belong to '%s'" % self.grp_internal_xml_id
|
||||
)
|
||||
|
||||
portal_user.unlink() # otherwise, badly modifying the implication would raise
|
||||
|
||||
grp_test_internal1.implied_ids = self.grp_internal
|
||||
grp_test_internal2.implied_ids = self.grp_internal
|
||||
|
||||
with self.assertRaises(ValidationError): # current group implications forbid to create a portal user
|
||||
portal_user = self.env['res.users'].create({
|
||||
'login': 'portalFail',
|
||||
'name': 'Portal fail',
|
||||
'sel_groups_%s_%s_%s' % (self.grp_internal.id, self.grp_portal.id, grp_public.id): self.grp_portal.id,
|
||||
'sel_groups_%s_%s' % (grp_test_internal1.id, grp_test_internal2.id): grp_test_internal2.id,
|
||||
})
|
||||
|
||||
def test_portal_write(self):
|
||||
grp_remove_xml_id = 'test_portal_write.group_to_remove'
|
||||
grp_remove = self.env['res.groups']._load_records([
|
||||
dict(xml_id=grp_remove_xml_id, values={'name': 'Group to remove'})
|
||||
])
|
||||
"""Check that adding a new group to a portal user works as expected,
|
||||
except if it implies group_user/public, in chich case it should raise.
|
||||
"""
|
||||
grp_test_portal = self.env["res.groups"].create({"name": "implied by portal"})
|
||||
self.grp_portal.implied_ids = grp_test_portal
|
||||
|
||||
portal_user = self.env['res.users'].create({
|
||||
'login': 'portalTest2',
|
||||
'name': 'Portal test 2',
|
||||
'groups_id': [(6, 0, [grp_remove.id])],
|
||||
'groups_id': [(6, 0, [self.grp_portal.id])],
|
||||
})
|
||||
grp_test_portal_xml_id = 'test_portal_write.portal_implied_group'
|
||||
grp_test_portal = self.env['res.groups']._load_records([
|
||||
dict(xml_id=grp_test_portal_xml_id, values={'name': 'Test Group Portal'})
|
||||
])
|
||||
self.grp_portal.implied_ids = grp_test_portal
|
||||
portal_user.write({'groups_id': [(4, self.grp_portal.id, 0)]})
|
||||
|
||||
self.assertFalse(
|
||||
portal_user.has_group(grp_remove_xml_id),
|
||||
"The portal user should not belong to '%s'" % grp_remove_xml_id
|
||||
)
|
||||
self.assertTrue(
|
||||
portal_user.has_group(self.grp_portal_xml_id),
|
||||
"The portal user should belong to '%s'" % self.grp_portal_xml_id,
|
||||
)
|
||||
self.assertTrue(
|
||||
portal_user.has_group(grp_test_portal_xml_id),
|
||||
"The portal user should belong to '%s'" % grp_test_portal_xml_id,
|
||||
self.assertEqual(
|
||||
portal_user.groups_id, (self.grp_portal + grp_test_portal),
|
||||
"The portal user should have the implied group.",
|
||||
)
|
||||
|
||||
grp_fail = self.env["res.groups"].create(
|
||||
{"name": "fail", "implied_ids": [(6, 0, [self.grp_internal.id])]})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
portal_user.write({'groups_id': [(4, grp_fail.id)]})
|
||||
|
||||
def test_two_user_types(self):
|
||||
#Create a user with two groups of user types kind (Internal and Portal)
|
||||
grp_test = self.env['res.groups']._load_records([
|
||||
@@ -161,3 +176,76 @@ class TestHasGroup(TransactionCase):
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
grp_test.write({'implied_ids': [(4, self.grp_portal.id)]})
|
||||
|
||||
def test_demote_user(self):
|
||||
"""When a user is demoted to the status of portal/public,
|
||||
we should strip him of all his (previous) rights
|
||||
"""
|
||||
group_0 = self.env.ref(self.group0) # the group to which test_user already belongs
|
||||
group_U = self.env["res.groups"].create({"name": "U", "implied_ids": [(6, 0, [self.grp_internal.id])]})
|
||||
self.grp_internal.implied_ids = False # only there to simplify the test by not having to care about its trans_implied_ids
|
||||
|
||||
self.test_user.write({'groups_id': [(4, group_U.id)]})
|
||||
self.assertEqual(
|
||||
self.test_user.groups_id, (group_0 + group_U + self.grp_internal),
|
||||
"We should have our 2 groups and the implied user group",
|
||||
)
|
||||
|
||||
# Now we demote him. The JS framework sends 3 and 4 commands,
|
||||
# which is what we write here, but it should work even with a 5 command or whatever.
|
||||
self.test_user.write({'groups_id': [
|
||||
(3, self.grp_internal.id),
|
||||
(3, self.grp_public.id),
|
||||
(4, self.grp_portal.id),
|
||||
]})
|
||||
|
||||
# if we screw up the removing groups/adding the implied ids, we could end up in two situations:
|
||||
# 1. we have a portal user with way too much rights (e.g. 'Contact Creation', which does not imply any other group)
|
||||
# 2. because a group may be (transitively) implying group_user, then it would raise an exception
|
||||
# so as a compromise we remove all groups when demoting a user
|
||||
# (even technical display groups, e.g. TaxB2B, which could be re-added later)
|
||||
self.assertEqual(
|
||||
self.test_user.groups_id, (self.grp_portal),
|
||||
"Here the portal group does not imply any other group, so we should only have this group.",
|
||||
)
|
||||
|
||||
def test_implied_groups(self):
|
||||
""" We check that the adding of implied ids works correctly for normal users and portal users.
|
||||
In the second case, working normally means raising if a group implies to give 'group_user'
|
||||
rights to a portal user.
|
||||
"""
|
||||
U = self.env["res.users"]
|
||||
G = self.env["res.groups"]
|
||||
group_user = self.env.ref('base.group_user')
|
||||
group_portal = self.env.ref('base.group_portal')
|
||||
group_no_one = self.env.ref('base.group_no_one')
|
||||
|
||||
group_A = G.create({"name": "A"})
|
||||
group_AA = G.create({"name": "AA", "implied_ids": [(6, 0, [group_A.id])]})
|
||||
group_B = G.create({"name": "B"})
|
||||
group_BB = G.create({"name": "BB", "implied_ids": [(6, 0, [group_B.id])]})
|
||||
|
||||
# user_a is a normal user, so we expect groups to be added when we add them,
|
||||
# as well as 'implied_groups'; otherwise nothing else should happen.
|
||||
# By contrast, for a portal user we want implied groups not to be added
|
||||
# if and only if it would not give group_user (or group_public) privileges
|
||||
user_a = U.create({"name": "a", "login": "a", "groups_id": [(6, 0, [group_AA.id, group_user.id])]})
|
||||
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_user + group_no_one))
|
||||
|
||||
user_b = U.create({"name": "b", "login": "b", "groups_id": [(6, 0, [group_portal.id, group_AA.id])]})
|
||||
self.assertEqual(user_b.groups_id, (group_AA + group_A + group_portal))
|
||||
|
||||
# user_b is not an internal user, but giving it a new group just added a new group
|
||||
(user_a + user_b).write({"groups_id": [(4, group_BB.id)]})
|
||||
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_BB + group_B + group_user + group_no_one))
|
||||
self.assertEqual(user_b.groups_id, (group_AA + group_A + group_BB + group_B + group_portal))
|
||||
|
||||
# now we create a group that implies the group_user
|
||||
# adding it to a user should work normally, whereas adding it to a portal user should raise
|
||||
group_C = G.create({"name": "C", "implied_ids": [(6, 0, [group_user.id])]})
|
||||
|
||||
user_a.write({"groups_id": [(4, group_C.id)]})
|
||||
self.assertEqual(user_a.groups_id, (group_AA + group_A + group_BB + group_B + group_C + group_user + group_no_one))
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
user_b.write({"groups_id": [(4, group_C.id)]})
|
||||
|
||||
@@ -157,6 +157,7 @@ def check(f):
|
||||
return wrapper
|
||||
|
||||
def execute_cr(cr, uid, obj, method, *args, **kw):
|
||||
odoo.api.Environment.reset() # clean cache etc if we retry the same transaction
|
||||
recs = odoo.api.Environment(cr, uid, {}).get(obj)
|
||||
if recs is None:
|
||||
raise UserError(_("Object %s doesn't exist") % obj)
|
||||
|
||||
@@ -507,6 +507,8 @@ class ThreadedServer(CommonServer):
|
||||
# We wait there is no processing requests
|
||||
# other than the ones exceeding the limits, up to 1 min,
|
||||
# before asking for a reload.
|
||||
_logger.info('Dumping stacktrace of limit exceeding threads before reloading')
|
||||
dumpstacks(thread_idents=[thread.ident for thread in self.limits_reached_threads])
|
||||
self.reload()
|
||||
# `reload` increments `self.quit_signals_received`
|
||||
# and the loop will end after this iteration,
|
||||
@@ -709,7 +711,7 @@ class PreforkServer(CommonServer):
|
||||
raise KeyboardInterrupt
|
||||
elif sig == signal.SIGQUIT:
|
||||
# dump stacks on kill -3
|
||||
self.dumpstacks()
|
||||
dumpstacks()
|
||||
elif sig == signal.SIGUSR1:
|
||||
# log ormcache stats on kill -SIGUSR1
|
||||
log_ormcache_stats()
|
||||
|
||||
+13
-10
@@ -926,8 +926,10 @@ class ConstantMapping(Mapping):
|
||||
return self._value
|
||||
|
||||
|
||||
def dumpstacks(sig=None, frame=None):
|
||||
""" Signal handler: dump a stack trace for each existing thread."""
|
||||
def dumpstacks(sig=None, frame=None, thread_idents=None):
|
||||
""" Signal handler: dump a stack trace for each existing thread or given
|
||||
thread(s) specified through the ``thread_idents`` sequence.
|
||||
"""
|
||||
code = []
|
||||
|
||||
def extract_stack(stack):
|
||||
@@ -944,14 +946,15 @@ def dumpstacks(sig=None, frame=None):
|
||||
'url': getattr(th, 'url', 'n/a')}
|
||||
for th in threading.enumerate()}
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
thread_info = threads_info.get(threadId, {})
|
||||
code.append("\n# Thread: %s (db:%s) (uid:%s) (url:%s)" %
|
||||
(thread_info.get('repr', threadId),
|
||||
thread_info.get('dbname', 'n/a'),
|
||||
thread_info.get('uid', 'n/a'),
|
||||
thread_info.get('url', 'n/a')))
|
||||
for line in extract_stack(stack):
|
||||
code.append(line)
|
||||
if not thread_idents or threadId in thread_idents:
|
||||
thread_info = threads_info.get(threadId, {})
|
||||
code.append("\n# Thread: %s (db:%s) (uid:%s) (url:%s)" %
|
||||
(thread_info.get('repr', threadId),
|
||||
thread_info.get('dbname', 'n/a'),
|
||||
thread_info.get('uid', 'n/a'),
|
||||
thread_info.get('url', 'n/a')))
|
||||
for line in extract_stack(stack):
|
||||
code.append(line)
|
||||
|
||||
if odoo.evented:
|
||||
# code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets
|
||||
|
||||
Reference in New Issue
Block a user