summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-08-30 17:20:28 +0200
committerChristophe Simonis <chs@odoo.com>2019-08-30 17:20:28 +0200
commit545e6d20344f37081ccd6c6e788ea1c5383717af (patch)
tree7786f6001919ea18792c35c4f769336ef991fd3a
parent513545cdbdf95ae61c068270ab61eec8b81c9bd8 (diff)
parent52f6e38cea8ff9d16ab3432b3ccf032472e5a666 (diff)
[MERGE] forward port branch 12.0 up to 52f6e38cea8
-rw-r--r--addons/account/models/account.py2
-rw-r--r--addons/account/models/account_invoice.py2
-rw-r--r--addons/account/models/account_move.py8
-rw-r--r--addons/account/models/account_payment.py3
-rw-r--r--addons/account/models/account_reconcile_model.py2
-rw-r--r--addons/account/models/reconciliation_widget.py18
-rw-r--r--addons/account/tests/test_reconciliation.py104
-rw-r--r--addons/account/tests/test_reconciliation_widget.py80
-rw-r--r--addons/account/views/report_invoice.xml2
-rw-r--r--addons/account_check_printing/models/account_payment.py5
-rw-r--r--addons/account_check_printing/views/account_payment_views.xml1
-rw-r--r--addons/account_tax_python/models/account_tax.py4
-rw-r--r--addons/auth_oauth/controllers/main.py17
-rw-r--r--addons/base_address_city/models/res_partner.py1
-rw-r--r--addons/base_import/models/base_import.py6
-rw-r--r--addons/crm_iap_lead_website/models/crm_reveal_rule.py5
-rw-r--r--addons/gamification/views/goal.xml5
-rw-r--r--addons/hr_recruitment/views/hr_recruitment_views.xml1
-rw-r--r--addons/im_livechat/controllers/main.py7
-rw-r--r--addons/l10n_lu/__init__.py7
-rw-r--r--addons/l10n_lu/__manifest__.py2
-rw-r--r--addons/l10n_multilang/models/l10n_multilang.py13
-rw-r--r--addons/l10n_uk/__manifest__.py2
-rw-r--r--addons/mail/static/src/js/thread_windows/thread_window.js44
-rw-r--r--addons/mail/static/src/js/utils.js7
-rw-r--r--addons/mail/static/src/scss/composer.scss7
-rw-r--r--addons/mail/static/tests/discuss_mobile_tests.js67
-rw-r--r--addons/maintenance/views/maintenance_views.xml8
-rw-r--r--addons/mrp/models/stock_move.py6
-rw-r--r--addons/mrp/tests/test_warehouse_multistep_manufacturing.py94
-rw-r--r--addons/payment/views/payment_portal_templates.xml2
-rw-r--r--addons/point_of_sale/static/src/css/customer_facing_display.css21
-rw-r--r--addons/point_of_sale/static/src/scss/customer_facing_display.scss36
-rw-r--r--addons/product/models/product_template.py9
-rw-r--r--addons/product/tests/test_variants.py22
-rw-r--r--addons/sale/models/sale.py57
-rw-r--r--addons/stock/models/stock_move.py15
-rw-r--r--addons/stock/models/stock_picking.py2
-rw-r--r--addons/stock/models/stock_warehouse.py6
-rw-r--r--addons/stock/tests/test_packing.py47
-rw-r--r--addons/stock_account/models/stock.py13
-rw-r--r--addons/stock_account/tests/test_stockvaluation.py4
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js2
-rw-r--r--addons/web/static/src/js/views/form/form_view.js4
-rw-r--r--addons/web/static/src/js/views/graph/graph_view.js3
-rw-r--r--addons/web/static/src/js/widgets/auto_complete.js12
-rw-r--r--addons/web/static/src/scss/ui.scss8
-rw-r--r--addons/web/static/tests/helpers/mock_server.js15
-rw-r--r--addons/web/static/tests/views/form_tests.js35
-rw-r--r--addons/web/static/tests/views/graph_tests.js21
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js6
-rw-r--r--addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js2
-rw-r--r--addons/website/models/website.py7
-rw-r--r--addons/website/static/src/js/content/menu.js3
-rw-r--r--addons/website/static/src/scss/website.scss10
-rw-r--r--addons/website/views/website_templates.xml2
-rw-r--r--addons/website_blog/controllers/main.py2
-rw-r--r--addons/website_event_sale/models/sale_order.py5
-rw-r--r--addons/website_event_track/models/event_track.py2
-rw-r--r--addons/website_sale/models/website.py2
-rw-r--r--addons/website_sale/views/templates.xml2
-rw-r--r--addons/website_sale_comparison/models/website_sale_comparison.py2
-rw-r--r--addons/website_sale_stock/models/product_template.py4
-rw-r--r--doc/cla/corporate/arxilead.md15
-rw-r--r--doc/cla/corporate/tvtmarine-automation.md7
-rw-r--r--doc/cla/individual/hari4274.md11
-rw-r--r--doc/reference/javascript_reference.rst4
-rw-r--r--doc/reference/orm.rst6
-rw-r--r--odoo/addons/base/data/ir_module_module.xml2
-rw-r--r--odoo/addons/base/models/ir_model.py2
-rw-r--r--odoo/addons/base/models/res_bank.py7
-rw-r--r--odoo/addons/base/models/res_partner.py6
-rw-r--r--odoo/addons/base/tests/test_base.py8
-rw-r--r--odoo/addons/base/views/ir_qweb_widget_templates.xml6
-rw-r--r--odoo/tools/mail.py2
75 files changed, 792 insertions, 197 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 703a999a826..4abf984c719 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -956,7 +956,7 @@ class AccountJournal(models.Model):
@api.depends('company_id')
def _belong_to_company(self):
for journal in self:
- journal.belong_to_company = (journal.company_id.id == self.env.user.company_id.id)
+ journal.belongs_to_company = (journal.company_id.id == self.env.user.company_id.id)
@api.multi
def _search_company_journals(self, operator, value):
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index d20913a5f20..b5f0c64a523 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -327,7 +327,7 @@ class AccountInvoice(models.Model):
"means direct payment.")
partner_id = fields.Many2one('res.partner', string='Partner', change_default=True,
readonly=True, states={'draft': [('readonly', False)]},
- tracking=True, help="You can find a contact by its Name, TIN, Email or Internal Reference.")
+ tracking=True, ondelete="restrict", help="You can find a contact by its Name, TIN, Email or Internal Reference.")
vendor_bill_id = fields.Many2one('account.invoice', string='Vendor Bill',
help="Auto-complete from a past bill.")
payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term',
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 0e1085ffcce..895defe1de7 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -162,7 +162,7 @@ class AccountMove(models.Model):
help="Technical field used to keep track of the tax cash basis reconciliation. "
"This is needed when cancelling the source: it will post the inverse journal entry to cancel that part too.")
auto_post = fields.Boolean(string='Post Automatically', default=False, help='If this checkbox is ticked, this entry will be automatically posted at its date.')
- reverse_entry_id = fields.Many2one('account.move', String="Reverse entry", store=True, readonly=True)
+ reverse_entry_id = fields.Many2one('account.move', String="Reverse entry", store=True, readonly=True, copy=False)
to_check = fields.Boolean(string='To Check', default=False, help='If this checkbox is ticked, it means that the user was not sure of all the related informations at the time of the creation of the move and that the move needs to be checked again.')
tax_type_domain = fields.Char(store=False, help='Technical field used to have a dynamic taxes domain on the form view.')
@@ -588,7 +588,7 @@ class AccountMoveLine(models.Model):
else:
date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date
rate = line.currency_id.with_context(date=date).rate
- amount_residual_currency += sign_partial_line * partial_line.amount * rate
+ amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate)
#computing the `reconciled` field.
reconciled = False
@@ -720,6 +720,10 @@ class AccountMoveLine(models.Model):
balance = 0
for line in move.line_ids:
balance += line.debit - line.credit
+ # if we are here, line_ids is in context, so journal_id should also be.
+ currency = self._context.get('journal_id') and self.env["account.journal"].browse(self._context['journal_id']).company_id.currency_id
+ if currency:
+ balance = currency.round(balance)
if balance < 0:
rec.update({'debit': -balance})
if balance > 0:
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 511abadd872..45a3970552d 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -452,7 +452,8 @@ class account_payment(models.Model):
for move in rec.move_line_ids.mapped('move_id'):
if rec.reconciled_invoice_ids:
move.line_ids.remove_move_reconcile()
- move.button_cancel()
+ if move.state != 'draft':
+ move.button_cancel()
move.unlink()
rec.write({
'state': 'cancelled',
diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py
index 8b6054dc878..1c2a11373f7 100644
--- a/addons/account/models/account_reconcile_model.py
+++ b/addons/account/models/account_reconcile_model.py
@@ -354,7 +354,7 @@ class AccountReconcileModel(models.Model):
query += ' AND st_line.name NOT ILIKE %s'
params += ['%%%s%%' % rule.match_label_param]
elif rule.match_label == 'match_regex':
- query += ' AND st_line.name ~ %s'
+ query += ' AND st_line.name ~* %s'
params += [rule.match_label_param]
# Filter on partners.
diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py
index 05edf105763..3d2edd4d224 100644
--- a/addons/account/models/reconciliation_widget.py
+++ b/addons/account/models/reconciliation_widget.py
@@ -116,7 +116,8 @@ class AccountReconciliation(models.AbstractModel):
query += 'LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number %s\n' % (where_bank)
query += 'LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n'
query += 'LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n'
- query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s\n' % (where_partner)
+ # By definition the commercial partner_id doesn't have a parent_id set
+ query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s AND p3.parent_id is NULL \n' % (where_partner)
query += 'WHERE st_line.id IN %s'
params += [tuple(st_lines.ids)]
@@ -520,17 +521,22 @@ class AccountReconciliation(models.AbstractModel):
])
domain = expression.OR([domain_reconciliation, domain_matching])
+ partner_domain = []
if partner_id:
- domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
+ partner_domain = [('partner_id', '=', partner_id)]
+ domain = expression.AND([domain, partner_domain])
# Domain factorized for all reconciliation use cases
if search_str:
str_domain = self._domain_move_lines(search_str=search_str)
if not partner_id:
- str_domain = expression.OR([
- str_domain,
- [('partner_id.name', 'ilike', search_str)]
- ])
+ partner_domain = [('partner_id.name', 'ilike', search_str)]
+
+ str_domain = expression.OR([
+ str_domain,
+ partner_domain,
+ ])
+
domain = expression.AND([
domain,
str_domain
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index dcc8f0e470c..38734622ab8 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -153,6 +153,27 @@ class TestReconciliation(AccountingTestCase):
invoice.action_invoice_open()
return invoice
+ def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False):
+ #we create an invoice in given currency
+ invoice = self.account_invoice_model.create({'partner_id': partner_id,
+ 'currency_id': currency_id,
+ 'name': type == 'out_invoice' and 'invoice to client' or 'invoice to vendor',
+ 'account_id': self.account_rcv.id,
+ 'type': type,
+ 'date_invoice': time.strftime('%Y') + '-07-01',
+ })
+ self.account_invoice_line_model.create({'product_id': self.product.id,
+ 'quantity': 1,
+ 'price_unit': invoice_amount,
+ 'invoice_id': invoice.id,
+ 'name': 'product that cost ' + str(invoice_amount),
+ 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id,
+ })
+
+ #validate invoice
+ invoice.action_invoice_open()
+ return invoice
+
def make_payment(self, invoice_record, bank_journal, amount=0.0, amount_currency=0.0, currency_id=None):
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': bank_journal.id,
@@ -914,27 +935,6 @@ class TestReconciliationExec(TestReconciliation):
self.assertEqual(reversed_bank_line.full_reconcile_id.id, bank_line.full_reconcile_id.id)
self.assertEqual(reversed_customer_line.full_reconcile_id.id, customer_line.full_reconcile_id.id)
- def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False):
- #we create an invoice in given currency
- invoice = self.account_invoice_model.create({'partner_id': partner_id,
- 'currency_id': currency_id,
- 'name': type == 'out_invoice' and 'invoice to client' or 'invoice to vendor',
- 'account_id': self.account_rcv.id,
- 'type': type,
- 'date_invoice': time.strftime('%Y') + '-07-01',
- })
- self.account_invoice_line_model.create({'product_id': self.product.id,
- 'quantity': 1,
- 'price_unit': invoice_amount,
- 'invoice_id': invoice.id,
- 'name': 'product that cost ' + str(invoice_amount),
- 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id,
- })
-
- #validate invoice
- invoice.action_invoice_open()
- return invoice
-
def test_aged_report(self):
AgedReport = self.env['report.account.report_agedpartnerbalance'].with_context(include_nullified_amount=True)
account_type = ['receivable']
@@ -1205,6 +1205,68 @@ class TestReconciliationExec(TestReconciliation):
self.assertEqual(invoice_cust_1.state, 'open',
'Invoice is in status %s' % invoice_cust_1.state)
+ def test_inv_refund_foreign_payment_writeoff_domestic(self):
+ company = self.env.ref('base.main_company')
+ self.env['res.currency.rate'].search([]).unlink()
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.0,
+ 'currency_id': self.currency_euro_id,
+ 'company_id': company.id
+ })
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y') + '-07-01',
+ 'rate': 1.113900, # Don't change this !
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.env.ref('base.main_company').id
+ })
+ inv1 = self.create_invoice(invoice_amount=480, currency_id=self.currency_usd_id)
+ inv2 = self.create_invoice(type="out_refund", invoice_amount=140, currency_id=self.currency_usd_id)
+
+ payment = self.env['account.payment'].create({
+ 'payment_method_id': self.inbound_payment_method.id,
+ 'payment_type': 'inbound',
+ 'partner_type': 'customer',
+ 'partner_id': inv1.partner_id.id,
+ 'amount': 287.20,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': company.id,
+ })
+ payment.post()
+
+ inv1_receivable = inv1.move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ inv2_receivable = inv2.move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ pay_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ data_for_reconciliation = [
+ {
+ 'type': 'partner',
+ 'id': inv1.partner_id.id,
+ 'mv_line_ids': (inv1_receivable + inv2_receivable + pay_receivable).ids,
+ 'new_mv_line_dicts': [
+ {
+ 'credit': 18.04,
+ 'debit': 0.00,
+ 'journal_id': self.bank_journal_euro.id,
+ 'name': 'Total WriteOff (Fees)',
+ 'account_id': self.diff_expense_account.id
+ }
+ ]
+ }
+ ]
+
+ self.env["account.reconciliation.widget"].process_move_lines(data_for_reconciliation)
+
+ self.assertTrue(inv1_receivable.full_reconcile_id.exists())
+ self.assertEquals(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
+ self.assertEquals(inv1_receivable.full_reconcile_id, pay_receivable.full_reconcile_id)
+
+ self.assertTrue(inv1.reconciled)
+ self.assertTrue(inv2.reconciled)
+
+ self.assertEquals(inv1.state, 'paid')
+ self.assertEquals(inv2.state, 'paid')
+
def test_multiple_term_reconciliation_opw_1906665(self):
'''Test that when registering a payment to an invoice with multiple
payment term lines the reconciliation happens against the line
diff --git a/addons/account/tests/test_reconciliation_widget.py b/addons/account/tests/test_reconciliation_widget.py
index a24cec75ca5..2bee0773251 100644
--- a/addons/account/tests/test_reconciliation_widget.py
+++ b/addons/account/tests/test_reconciliation_widget.py
@@ -53,4 +53,82 @@ class TestReconciliationWidget(TestReconciliation):
})
result = self.env['account.reconciliation.widget'].get_bank_statement_line_data(bank_stmt_line.ids)
- self.assertEqual(result['lines'][0]['reconciliation_proposition'][0]['amount_str'], '$ 50.00') \ No newline at end of file
+ self.assertEqual(result['lines'][0]['reconciliation_proposition'][0]['amount_str'], '$ 50.00')
+
+ 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)
+
+ receivable1 = inv1.move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+ receivable2 = inv2.move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
+
+ bank_stmt = self.acc_bank_stmt_model.create({
+ 'company_id': self.env.ref('base.main_company').id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'date': time.strftime('%Y-07-15'),
+ 'name': 'test',
+ })
+
+ bank_stmt_line = self.acc_bank_stmt_line_model.create({
+ 'name': 'testLine',
+ 'statement_id': bank_stmt.id,
+ 'amount': 100,
+ 'date': time.strftime('%Y-07-15'),
+ })
+
+ # This is like opening the widget, and 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,
+ excluded_ids=[],
+ search_str='deco'
+ )
+ mv_lines_ids = [l['id'] for l in mv_lines_rec]
+
+ self.assertTrue(receivable1.id in mv_lines_ids)
+ self.assertTrue(receivable2.id in mv_lines_ids)
+
+ # This is like clicking on the first receivable
+ partner = inv1.partner_id
+ 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=[receivable1.id],
+ search_str='deco'
+ )
+ mv_lines_ids = [l['id'] for l in mv_lines_rec]
+
+ self.assertFalse(receivable1.id in mv_lines_ids)
+ self.assertTrue(receivable2.id in mv_lines_ids)
+
+ def test_partner_name_with_parent(self):
+ parent_partner = self.env['res.partner'].create({
+ 'name': 'test',
+ })
+ child_partner = self.env['res.partner'].create({
+ 'name': 'test',
+ 'parent_id': parent_partner.id,
+ 'type': 'delivery',
+ })
+ inv1 = self.create_invoice_partner(currency_id=self.currency_euro_id, partner_id=child_partner.id)
+ self.assertEqual(inv1.move_id.partner_id, parent_partner)
+
+ bank_stmt = self.acc_bank_stmt_model.create({
+ 'company_id': self.env.ref('base.main_company').id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'date': time.strftime('%Y-07-15'),
+ 'name': 'test',
+ })
+
+ bank_stmt_line = self.acc_bank_stmt_line_model.create({
+ 'name': 'testLine',
+ 'statement_id': bank_stmt.id,
+ 'amount': 100,
+ 'date': time.strftime('%Y-07-15'),
+ 'partner_name': 'test',
+ })
+
+ bkstmt_data = self.env['account.reconciliation.widget'].get_bank_statement_line_data(bank_stmt_line.ids)
+
+ self.assertEqual(len(bkstmt_data['lines']), 1)
+ self.assertEqual(bkstmt_data['lines'][0]['partner_id'], parent_partner.id)
diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml
index 8c3da3ae101..ac1bc1db544 100644
--- a/addons/account/views/report_invoice.xml
+++ b/addons/account/views/report_invoice.xml
@@ -181,7 +181,7 @@
<p t-if="o.fiscal_position_id.note" name="note">
<span t-field="o.fiscal_position_id.note"/>
</p>
- <div id="qrcode" t-if="(o.partner_id.country_id.code in ['BE', 'AT', 'DE', 'FI', 'NL']) and (o.company_id.qr_code) and (o.currency_id.name == 'EUR') and (o.partner_bank_id.acc_number != False)">
+ <div id="qrcode" t-if="(o.company_id.qr_code) and (o.currency_id.name == 'EUR') and (o.partner_bank_id.acc_number != False)">
<p t-if="(o.partner_bank_id.qr_code_valid)">
<strong class="text-center">Scan me with your banking app.</strong><br /><br />
<img class="border border-dark rounded" t-att-src="o.partner_bank_id.build_qr_code_url(o.residual,(o.reference) if (o.reference) else o.number)"/>
diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py
index 69bec9ea956..f69782178ef 100644
--- a/addons/account_check_printing/models/account_payment.py
+++ b/addons/account_check_printing/models/account_payment.py
@@ -104,6 +104,11 @@ class AccountPayment(models.Model):
""" This method is a hook for l10n_xx_check_printing modules to implement actual check printing capabilities """
raise UserError(_("You have to choose a check layout. For this, go in Apps, search for 'Checks layout' and install one."))
+
+ def set_check_amount_in_words(self):
+ for payment in self:
+ payment.check_amount_in_words = payment.currency_id.amount_to_text(payment.amount) if payment.currency_id else ''
+
#######################
#CHECK PRINTING METHODS
#######################
diff --git a/addons/account_check_printing/views/account_payment_views.xml b/addons/account_check_printing/views/account_payment_views.xml
index 82a97e89edc..78429cd7bf5 100644
--- a/addons/account_check_printing/views/account_payment_views.xml
+++ b/addons/account_check_printing/views/account_payment_views.xml
@@ -8,6 +8,7 @@
<xpath expr="//button[@name='post']" position="before">
<button name="print_checks" class="oe_highlight" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('state', '!=', 'posted')]}" string="Print Check" type="object"/>
<button name="unmark_sent" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('state', '!=', 'sent')]}" string="Unmark Sent" type="object"/>
+ <button name="set_check_amount_in_words" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('check_amount_in_words', '!=', False)]}" string="Reset Amount in Words" type="object" groups="base.group_no_one"/>
</xpath>
<xpath expr="//div[@name='amount_div']" position="after">
<field name="check_amount_in_words" attrs="{'invisible': [('payment_method_code', '!=', 'check_printing')], 'readonly': [('state', '!=', 'draft')]}" groups="base.group_no_one"/>
diff --git a/addons/account_tax_python/models/account_tax.py b/addons/account_tax_python/models/account_tax.py
index a3899375d78..3029292e898 100644
--- a/addons/account_tax_python/models/account_tax.py
+++ b/addons/account_tax_python/models/account_tax.py
@@ -28,6 +28,8 @@ class AccountTaxPython(models.Model):
def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None):
self.ensure_one()
+ if product and product._name == 'product.template':
+ product = product.product_variant_id
if self.amount_type == 'code':
company = self.env.user.company_id
localdict = {'base_amount': base_amount, 'price_unit':price_unit, 'quantity': quantity, 'product':product, 'partner':partner, 'company': company}
@@ -39,6 +41,8 @@ class AccountTaxPython(models.Model):
def compute_all(self, price_unit, currency=None, quantity=1.0, product=None, partner=None, is_refund=False):
taxes = self.filtered(lambda r: r.amount_type != 'code')
company = self.env.user.company_id
+ if product and product._name == 'product.template':
+ product = product.product_variant_id
for tax in self.filtered(lambda r: r.amount_type == 'code'):
localdict = self._context.get('tax_computation_context', {})
localdict.update({'price_unit': price_unit, 'quantity': quantity, 'product': product, 'partner': partner, 'company': company})
diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py
index 3b300070929..00eea3268b6 100644
--- a/addons/auth_oauth/controllers/main.py
+++ b/addons/auth_oauth/controllers/main.py
@@ -109,19 +109,10 @@ class OAuthLogin(Home):
return response
- @http.route()
- def web_auth_signup(self, *args, **kw):
- providers = self.list_providers()
- response = super(OAuthLogin, self).web_auth_signup(*args, **kw)
- response.qcontext.update(providers=providers)
- return response
-
- @http.route()
- def web_auth_reset_password(self, *args, **kw):
- providers = self.list_providers()
- response = super(OAuthLogin, self).web_auth_reset_password(*args, **kw)
- response.qcontext.update(providers=providers)
- return response
+ def get_auth_signup_qcontext(self):
+ result = super(OAuthLogin, self).get_auth_signup_qcontext()
+ result["providers"] = self.list_providers()
+ return result
class OAuthController(http.Controller):
diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py
index c9ee67f2c7b..8540d916a5e 100644
--- a/addons/base_address_city/models/res_partner.py
+++ b/addons/base_address_city/models/res_partner.py
@@ -31,6 +31,7 @@ class Partner(models.Model):
replacement_xml = """
<div>
<field name="country_enforce_cities" invisible="1"/>
+ <field name="parent_id" invisible="1"/>
<field name='city' placeholder="%(placeholder)s" class="o_address_city"
attrs="{
'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)],
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index 04012e07138..dae5b383ca7 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -448,7 +448,11 @@ class Import(models.TransientModel):
'date_format') else []
user_date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format
if user_date_format:
- date_patterns.append(user_date_format)
+ try:
+ to_re(user_date_format)
+ date_patterns.append(user_date_format)
+ except KeyError:
+ pass
date_patterns.extend(DATE_PATTERNS)
match = check_patterns(date_patterns, preview_values)
if match:
diff --git a/addons/crm_iap_lead_website/models/crm_reveal_rule.py b/addons/crm_iap_lead_website/models/crm_reveal_rule.py
index ac1dde52157..1f12f4cee08 100644
--- a/addons/crm_iap_lead_website/models/crm_reveal_rule.py
+++ b/addons/crm_iap_lead_website/models/crm_reveal_rule.py
@@ -7,7 +7,8 @@ import logging
import re
from dateutil.relativedelta import relativedelta
-from odoo import api, fields, models, tools, _, _geoip_resolver
+import odoo
+from odoo import api, fields, models, tools, _
from odoo.addons.iap import jsonrpc
from odoo.addons.crm.models import crm_stage
from odoo.exceptions import ValidationError
@@ -74,7 +75,7 @@ class CRMRevealRule(models.Model):
@api.model
def _assert_geoip(self):
- if not _geoip_resolver:
+ if not odoo._geoip_resolver:
message = _('Lead Generation requires a GeoIP resolver which could not be found on your system. Please consult https://pypi.org/project/GeoIP/.')
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
diff --git a/addons/gamification/views/goal.xml b/addons/gamification/views/goal.xml
index 106c8967c38..cd17738869d 100644
--- a/addons/gamification/views/goal.xml
+++ b/addons/gamification/views/goal.xml
@@ -135,6 +135,7 @@
<field name="completeness"/>
<field name="state"/>
<field name="target_goal"/>
+ <field name="definition_description"/>
<field name="definition_condition"/>
<field name="definition_suffix"/>
<field name="definition_display"/>
@@ -143,7 +144,9 @@
<field name="last_update"/>
<templates>
<t t-name="kanban-tooltip">
- <field name="definition_description"/>
+ <ul class="oe_kanban_tooltip">
+ <li><t t-esc="record.definition_description.value" /></li>
+ </ul>
</t>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click o_kanban_gamification #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml
index 4a053c45f64..c01a6472fa0 100644
--- a/addons/hr_recruitment/views/hr_recruitment_views.xml
+++ b/addons/hr_recruitment/views/hr_recruitment_views.xml
@@ -286,6 +286,7 @@
<field name="user_email"/>
<field name="partner_name"/>
<field name="type_id"/>
+ <field name="partner_id"/>
<field name="job_id"/>
<field name="department_id"/>
<field name="message_needaction_counter"/>
diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py
index 5703563e95d..1dc620edd49 100644
--- a/addons/im_livechat/controllers/main.py
+++ b/addons/im_livechat/controllers/main.py
@@ -103,7 +103,6 @@ class LivechatController(http.Controller):
@http.route('/im_livechat/feedback', type='json', auth='public', cors="*")
def feedback(self, uuid, rate, reason=None, **kwargs):
Channel = request.env['mail.channel']
- Rating = request.env['rating.rating']
channel = Channel.sudo().search([('uuid', '=', uuid)], limit=1)
if channel:
# limit the creation : only ONE rating per session
@@ -124,7 +123,11 @@ class LivechatController(http.Controller):
# if logged in user, set its partner on rating
values['partner_id'] = request.env.user.partner_id.id if request.session.uid else False
# create the rating
- rating = Rating.sudo().create(values)
+
+ channel.write({
+ 'rating_ids': [(0, False, values)]
+ })
+ rating = channel.rating_ids[0]
else:
rating = channel.rating_ids[0]
rating.write(values)
diff --git a/addons/l10n_lu/__init__.py b/addons/l10n_lu/__init__.py
index 1947891c8f5..79508eb4f80 100644
--- a/addons/l10n_lu/__init__.py
+++ b/addons/l10n_lu/__init__.py
@@ -1,8 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import api, SUPERUSER_ID
+
from . import models
+def _post_init_hook(cr, registry):
+ _preserve_tag_on_taxes(cr, registry)
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ env.ref('l10n_lu.lu_2011_chart_1').process_coa_translations()
+
def _preserve_tag_on_taxes(cr, registry):
from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes
preserve_existing_tags_on_taxes(cr, registry, 'l10n_lu')
diff --git a/addons/l10n_lu/__manifest__.py b/addons/l10n_lu/__manifest__.py
index c5457fd82d4..9eabf73f08d 100644
--- a/addons/l10n_lu/__manifest__.py
+++ b/addons/l10n_lu/__manifest__.py
@@ -43,5 +43,5 @@ Notes:
'data/account.chart.template.csv',
'data/account_chart_template_data.xml',
],
- 'post_init_hook': '_preserve_tag_on_taxes',
+ 'post_init_hook': '_post_init_hook',
}
diff --git a/addons/l10n_multilang/models/l10n_multilang.py b/addons/l10n_multilang/models/l10n_multilang.py
index 2f6e3731faa..51ddb70c94c 100644
--- a/addons/l10n_multilang/models/l10n_multilang.py
+++ b/addons/l10n_multilang/models/l10n_multilang.py
@@ -12,6 +12,19 @@ _logger = logging.getLogger(__name__)
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
+ def load_for_current_company(self, sale_tax_rate, purchase_tax_rate):
+ res = super(AccountChartTemplate, self).load_for_current_company(sale_tax_rate, purchase_tax_rate)
+ # Copy chart of account translations when loading chart of account
+ for chart_template in self.filtered('spoken_languages'):
+ external_id = self.env['ir.model.data'].search([
+ ('model', '=', 'account.chart.template'),
+ ('res_id', '=', chart_template.id),
+ ], order='id', limit=1)
+ module = external_id and self.env.ref('base.module_' + external_id.module)
+ if module and module.state == 'installed':
+ chart_template.process_coa_translations()
+ return res
+
@api.multi
def process_translations(self, langs, in_field, in_ids, out_ids):
"""
diff --git a/addons/l10n_uk/__manifest__.py b/addons/l10n_uk/__manifest__.py
index 55168a9887c..fdd0323faf4 100644
--- a/addons/l10n_uk/__manifest__.py
+++ b/addons/l10n_uk/__manifest__.py
@@ -15,7 +15,7 @@ This is the latest UK Odoo localisation necessary to run Odoo accounting for UK
- InfoLogic UK counties listing
- a few other adaptations""",
'author': 'SmartMode LTD',
- 'website': 'http://www.smartmode.co.uk',
+ 'website': 'https://www.odoo.com/page/accounting',
'depends': [
'account',
'base_iban',
diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js
index 16d5cd31c86..08671354a3e 100644
--- a/addons/mail/static/src/js/thread_windows/thread_window.js
+++ b/addons/mail/static/src/js/thread_windows/thread_window.js
@@ -3,7 +3,9 @@ odoo.define('mail.ThreadWindow', function (require) {
var AbstractThreadWindow = require('mail.AbstractThreadWindow');
var BasicComposer = require('mail.composer.Basic');
+var ExtendedComposer = require('mail.composer.Extended');
+var config = require('web.config');
var core = require('web.core');
var QWeb = core.qweb;
@@ -65,19 +67,27 @@ var ThreadWindow = AbstractThreadWindow.extend({
if (!this.hasThread()) {
this._startWithoutThread();
} else if (this.needsComposer()) {
- var basicComposer = new BasicComposer(this, {
+ var composer;
+ var composerParams = {
mentionPartnersRestricted: this._thread.getType() !== 'document_thread',
- isMini: true,
thread: this._thread,
- });
- basicComposer.on('post_message', this, this._postMessage);
- basicComposer.once('input_focused', this, function () {
+ };
+ if (
+ config.device.isMobile &&
+ this._thread.isMassMailing()
+ ) {
+ composer = new ExtendedComposer(this, composerParams);
+ } else {
+ composer = new BasicComposer(this, _.extend({ isMini: true }, composerParams));
+ }
+ composer.on('post_message', this, this._postMessage);
+ composer.once('input_focused', this, function () {
var commands = this._thread.getCommands();
var partners = this._thread.getMentionPartnerSuggestions();
- basicComposer.mentionSetCommands(commands);
- basicComposer.mentionSetPrefetchedPartners(partners);
+ composer.mentionSetCommands(commands);
+ composer.mentionSetPrefetchedPartners(partners);
});
- composerDef = basicComposer.replace(this.$('.o_thread_composer'));
+ composerDef = composer.replace(this.$('.o_thread_composer'));
composerDef.then(function () {
self.$input = self.$('.o_composer_text_field');
});
@@ -130,24 +140,6 @@ var ThreadWindow = AbstractThreadWindow.extend({
return this._passive;
},
/**
- * States whether the input of the thread window should be displayed or not.
- * This is based on the type of the thread:
- *
- * Do not display the input in the following cases:
- *
- * - no thread related to this window
- * - window of a mailbox (temp: let us have mailboxes in window mode)
- * - window of a thread with mass mailing
- *
- * Any other threads show the input in the window.
- *
- * @override
- * @returns {boolean}
- */
- needsComposer: function () {
- return this._super() && !this._thread.isMassMailing();
- },
- /**
* Turn the thread window in active mode, so that when the bottom of the
* thread is visible, it is automatically marked as read.
*/
diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js
index 14ac97def76..25e327b0428 100644
--- a/addons/mail/static/src/js/utils.js
+++ b/addons/mail/static/src/js/utils.js
@@ -8,7 +8,12 @@ var _t = core._t;
function parseAndTransform(htmlString, transformFunction) {
var openToken = "OPEN" + Date.now();
var string = htmlString.replace(/&lt;/g, openToken);
- var children = $('<div>').html(string).contents();
+ var children;
+ try {
+ children = $('<div>').html(string).contents();
+ } catch (e) {
+ children = $('<div>').html('<pre>' + string + '</pre>').contents();
+ }
return _parseAndTransform(children, transformFunction)
.replace(new RegExp(openToken, "g"), "&lt;");
}
diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss
index b1e78345d1f..0a6a49a703b 100644
--- a/addons/mail/static/src/scss/composer.scss
+++ b/addons/mail/static/src/scss/composer.scss
@@ -251,6 +251,9 @@
.o_composer_button_send, .o_chatter_avatar {
display: none !important;
}
+ .o_attachment {
+ width: 50%;
+ }
}
.o_file_drop_zone_container {
@@ -373,6 +376,10 @@ $o-mail-attachment-margin: 5px;
}
&.o_attachment_uploading {
+ .o_attachment_wrap {
+ padding-right: 0px;
+ }
+
.o_attachment_progress_bar {
display: inline-block;
margin: 0 0 0 8px;
diff --git a/addons/mail/static/tests/discuss_mobile_tests.js b/addons/mail/static/tests/discuss_mobile_tests.js
index 84fd155522c..8f8bc88ca6c 100644
--- a/addons/mail/static/tests/discuss_mobile_tests.js
+++ b/addons/mail/static/tests/discuss_mobile_tests.js
@@ -13,6 +13,9 @@ QUnit.module('Discuss in mobile', {
beforeEach: function () {
this.services = mailTestUtils.getMailServices();
this.data = {
+ 'mail.channel': {
+ fields: {},
+ },
'mail.message': {
fields: {},
},
@@ -80,27 +83,65 @@ QUnit.test('mobile basic rendering', async function (assert) {
discuss.destroy();
});
-QUnit.test('on_{attach/detach}_callback', function (assert) {
+QUnit.test('on_{attach/detach}_callback', async function (assert) {
assert.expect(2);
- var done = assert.async();
- createDiscuss({
+ var discuss = await createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
- }).then(function (discuss) {
- try {
- discuss.on_attach_callback();
- assert.ok(true, 'should not crash on attach callback');
- discuss.on_detach_callback();
- assert.ok(true, 'should not crash on detach callback');
- } finally {
- discuss.destroy();
- done();
- }
});
+
+ discuss.on_attach_callback();
+ assert.ok(true, 'should not crash on attach callback');
+ discuss.on_detach_callback();
+ assert.ok(true, 'should not crash on detach callback');
+ discuss.destroy();
+});
+
+QUnit.test('extended composer in mass mailing channel', async function (assert) {
+ assert.expect(4);
+
+ this.data.initMessaging = {
+ channel_slots: {
+ channel_channel: [{
+ id: 1,
+ channel_type: "channel",
+ name: "General",
+ mass_mailing: true,
+ }],
+ },
+ };
+
+ var discuss = await createDiscuss({
+ id: 1,
+ context: {},
+ params: {},
+ data: this.data,
+ services: this.services,
+ });
+
+ testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type="public"]'));
+ testUtils.dom.click(discuss.$('.o_mail_preview[data-preview-id="1"]'));
+ assert.containsOnce(
+ $,
+ '.o_thread_window',
+ "should display a chat window");
+ assert.strictEqual(
+ $('.o_thread_window').data('thread-id'),
+ 1,
+ "chat window should be from channel General");
+ assert.containsOnce(
+ $('.o_thread_window'),
+ '.o_thread_composer',
+ "chat window should have a composer");
+ assert.ok(
+ $('.o_thread_window .o_thread_composer').hasClass('o_thread_composer_extended'),
+ "chat window composer should be extended composer");
+
+ discuss.destroy();
});
});
diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml
index 5dc67817fea..e821b918f85 100644
--- a/addons/maintenance/views/maintenance_views.xml
+++ b/addons/maintenance/views/maintenance_views.xml
@@ -128,8 +128,8 @@
<templates>
<t t-name="kanban-tooltip">
<ul class="oe_kanban_tooltip">
- <li><b>Category:</b> <field name="category_id"/></li>
- <li><b>Request to:</b> <field name="user_id"/></li>
+ <li t-if="record.category_id.raw_value"><b>Category:</b> <t t-esc="record.category_id.value"/></li>
+ <li t-if="record.user_id.raw_value"><b>Request to:</b> <t t-esc="record.user_id.value"/></li>
</ul>
</t>
<t t-name="kanban-box">
@@ -427,8 +427,8 @@
<templates>
<t t-name="kanban-tooltip">
<ul class="oe_kanban_tooltip">
- <li><b>Serial Number:</b> <field name="serial_no"/></li>
- <li><b>Model Number:</b> <field name="model"/></li>
+ <li t-if="record.serial_no.raw_value"><b>Serial Number:</b> <t t-esc="record.serial_no.value"/></li>
+ <li t-if="record.model.raw_value"><b>Model Number:</b> <t t-esc="record.model.value"/></li>
</ul>
</t>
<t t-name="kanban-box">
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 2912532c8e5..d0ef5bf3637 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -233,6 +233,12 @@ class StockMove(models.Model):
res = super(StockMove, self)._should_be_assigned()
return bool(res and not (self.production_id or self.raw_material_production_id))
+ def _search_picking_for_assignation_domain(self):
+ res = super(StockMove, self)._search_picking_for_assignation_domain()
+ if self.created_production_id:
+ res.append(('move_lines.created_production_id', '=', self.created_production_id.id))
+ return res
+
def _compute_kit_quantities(self, product_id, kit_qty, kit_bom, filters):
""" Computes the quantity delivered or received when a kit is sold or purchased.
A ratio 'qty_processed/qty_needed' is computed for each component, and the lowest one is kept
diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py
index a0d78d3cc78..7367d818bd2 100644
--- a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py
+++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py
@@ -122,6 +122,100 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
self.assertFalse(sam_move.move_dest_ids)
+ def test_manufacturing_complex_product_3_steps(self):
+ """ Test MO/picking after manufacturing a complex product which uses
+ manufactured components. Ensure that everything is created and picked
+ correctly.
+ """
+ # Create manifactured product which uses another manifactured
+
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Arrow'
+ product_form.type = 'product'
+ product_form.route_ids.clear()
+ product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
+ product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
+ self.complex_product = product_form.save()
+
+ ## Create raw product for manufactured product
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Raw Iron'
+ product_form.type = 'product'
+ product_form.uom_id = self.uom_unit
+ product_form.uom_po_id = self.uom_unit
+ self.raw_product_2 = product_form.save()
+
+ ## Create bom for manufactured product
+ bom_product_form = Form(self.env['mrp.bom'])
+ bom_product_form.product_id = self.complex_product
+ bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id
+ with bom_product_form.bom_line_ids.new() as line:
+ line.product_id = self.finished_product
+ line.product_qty = 1.0
+ with bom_product_form.bom_line_ids.new() as line:
+ line.product_id = self.raw_product_2
+ line.product_qty = 1.0
+
+ self.complex_bom = bom_product_form.save()
+
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm_sam'
+
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.complex_product
+ production_form.picking_type_id = self.warehouse.manu_type_id
+ production = production_form.save()
+
+ move_raw_ids = production.move_raw_ids
+ self.assertEqual(len(move_raw_ids), 2)
+ sfp_move_raw_id, raw_move_raw_id = move_raw_ids
+ self.assertEqual(sfp_move_raw_id.product_id, self.finished_product)
+ self.assertEqual(raw_move_raw_id.product_id, self.raw_product_2)
+
+ for move_raw_id in move_raw_ids:
+ self.assertEqual(move_raw_id.picking_type_id, self.warehouse.manu_type_id)
+
+ pbm_move = move_raw_id.move_orig_ids
+ self.assertEqual(len(pbm_move), 1)
+ self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
+ self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
+ self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
+
+ move_finished_ids = production.move_finished_ids
+ self.assertEqual(len(move_finished_ids), 1)
+ self.assertEqual(move_finished_ids.product_id, self.complex_product)
+ self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
+ sam_move = move_finished_ids.move_dest_ids
+ self.assertEqual(len(sam_move), 1)
+ self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
+ self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
+ self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
+ self.assertFalse(sam_move.move_dest_ids)
+
+ pickings = production.picking_ids.sorted('id', reverse=True)
+ self.assertEqual(len(pickings), 3)
+
+ # Picking: SFP PostProcessing -> Stock
+ ## Stick from Subprocess
+ picking = pickings[1]
+ self.assertEqual(len(picking.move_lines), 1)
+ picking.product_id = self.finished_product
+
+ # Picking: PC Stock -> Preprocessing
+ ## Stick
+ ## Raw Iron
+ picking = pickings[0]
+ self.assertEqual(len(picking.move_lines), 2)
+ picking.move_lines[0].product_id = self.finished_product
+ picking.move_lines[1].product_id = self.raw_product_2
+
+ # Picking: SFP PostProcessing -> Stock
+ ## Arrow from this process
+ picking = pickings[2]
+ self.assertEqual(len(picking.move_lines), 1)
+ picking.product_id = self.complex_product
+
+
def test_manufacturing_flow(self):
""" Simulate a pick pack ship delivery combined with a picking before
manufacturing and store after manufacturing. Also ensure that the MO and
diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml
index 8328fac20c8..17ac429ec9f 100644
--- a/addons/payment/views/payment_portal_templates.xml
+++ b/addons/payment/views/payment_portal_templates.xml
@@ -139,7 +139,7 @@
<div t-if="thanks_msg and payment_tx_id.acquirer_id.provider == 'transfer' and reference">
<b>Communication: </b><span t-esc='reference'/>
</div>
- <div t-if="(payment_tx_id.partner_id.country_id.code in ['BE', 'AT', 'DE', 'FI', 'NL']) and (payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.currency_id.name == 'EUR')">
+ <div t-if="(payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.currency_id.name == 'EUR')">
<div t-if="payment_tx_id.acquirer_id.journal_id.bank_account_id.qr_code_valid">
<h3>Or scan me with your banking app.</h3>
<img class="border border-dark rounded" t-att-src="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(payment_tx_id.amount,payment_tx_id.reference)"/>
diff --git a/addons/point_of_sale/static/src/css/customer_facing_display.css b/addons/point_of_sale/static/src/css/customer_facing_display.css
index d9be0c89c35..bb699672ad8 100644
--- a/addons/point_of_sale/static/src/css/customer_facing_display.css
+++ b/addons/point_of_sale/static/src/css/customer_facing_display.css
@@ -122,8 +122,7 @@ body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_order
box-shadow: none;
animation: none;
}
-body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div,
-body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div:last-child {
+body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div, body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div:last-child {
border-left-width: 0;
text-align: center;
font-size: 70%;
@@ -423,7 +422,7 @@ body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .p
text-align: center;
}
body .pos-customer_facing_display .pos-payment_info .pos-company_logo {
- display: none!important;
+ display: none !important;
}
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details {
-webkit-box-flex: 1 0 100%;
@@ -439,6 +438,7 @@ body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .p
font-size: 6.5vw;
}
}
+
body .pos-hidden {
opacity: 0;
}
@@ -460,7 +460,7 @@ body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-gro
}
body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group > .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu > a {
position: relative;
- padding: 14px 0!important;
+ padding: 14px 0 !important;
margin-bottom: 1px;
}
body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group > .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu > a:before {
@@ -470,6 +470,7 @@ body.editor_enable #oe_manipulators .no_parent_options .snippet-option-pos_palet
body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
display: none;
}
+
.pos-palette_01 .pos-payment_info {
background: #3E3E3E;
color: #f6f6f6;
@@ -506,6 +507,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_01:before {
text-shadow: 0 0 1px #090909;
}
+
.pos-palette_02 .pos-payment_info {
background: #364152;
color: #e6e7e8;
@@ -542,6 +544,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_02:before {
text-shadow: 0 0 1px #130d09;
}
+
.pos-palette_03 .pos-payment_info {
background: #1BA39C;
color: #f6f6f6;
@@ -578,6 +581,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_03:before {
text-shadow: 0 0 1px #131313;
}
+
.pos-palette_04 .pos-payment_info {
background: #0b7b6c;
color: #f6f6f6;
@@ -614,6 +618,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_04:before {
text-shadow: 0 0 1px #101113;
}
+
.pos-palette_05 .pos-payment_info {
background: #E26868;
color: #f6f6f6;
@@ -650,6 +655,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_05:before {
text-shadow: 0 0 1px #131313;
}
+
.pos-palette_06 .pos-payment_info {
background: #9E373B;
color: #f6f6f6;
@@ -686,6 +692,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_06:before {
text-shadow: 0 0 1px #090909;
}
+
.pos-palette_07 .pos-payment_info {
background: #ce9934;
color: white;
@@ -722,6 +729,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_07:before {
text-shadow: 0 0 1px #131313;
}
+
.pos-palette_08 .pos-payment_info {
background: #a48c77;
color: #f6f6f6;
@@ -758,6 +766,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_08:before {
text-shadow: 0 0 1px #131313;
}
+
.pos-palette_09 .pos-payment_info {
background: linear-gradient(30deg, #014d43, #127e71);
color: #f6f6f6;
@@ -794,6 +803,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_09:before {
text-shadow: 0 0 1px #131313;
}
+
.pos-palette_10 .pos-payment_info {
background: linear-gradient(30deg, #e2316c, #ea4c89);
color: white;
@@ -830,6 +840,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_10:before {
text-shadow: 0 0 1px #131313;
}
+
.pos-palette_11 .pos-payment_info {
background: linear-gradient(30deg, #362b3d, #5b4a63);
color: white;
@@ -866,6 +877,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_11:before {
text-shadow: 0 0 1px #131313;
}
+
.pos-palette_12 .pos-payment_info {
background: #434343;
color: #e6e6e6;
@@ -902,6 +914,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_12:before {
text-shadow: 0 0 1px #a4a4a4;
}
+
.pos-palette_13 .pos-payment_info {
background: linear-gradient(30deg, #1a1b1f, #3d3f45);
color: white;
diff --git a/addons/point_of_sale/static/src/scss/customer_facing_display.scss b/addons/point_of_sale/static/src/scss/customer_facing_display.scss
index 25e0a0bc324..6677b6a572e 100644
--- a/addons/point_of_sale/static/src/scss/customer_facing_display.scss
+++ b/addons/point_of_sale/static/src/scss/customer_facing_display.scss
@@ -99,7 +99,8 @@ body {
@include flex-display;
@include flex-direction(row);
- .pos-customer_products, .pos-payment_info {
+ .pos-customer_products,
+ .pos-payment_info {
height: 100%;
padding: 2%;
@include flex-display;
@@ -139,11 +140,14 @@ body {
box-shadow: none;
animation: none;
- > div, > div:last-child {
- border-left-width: 0;
- text-align: center;
- font-size: 70%;
- font-weight: normal;
+ > div {
+ &,
+ &:last-child {
+ border-left-width: 0;
+ text-align: center;
+ font-size: 70%;
+ font-weight: normal;
+ }
}
> div:last-child {
@@ -194,7 +198,8 @@ body {
@include flex-direction(column);
@include justify-content(space-between);
- .pos-adv, .pos-company_logo {
+ .pos-adv,
+ .pos-company_logo {
@include pos-bg;
}
@@ -212,7 +217,8 @@ body {
}
.pos-payment_info_details{
- .pos-total, .pos-paymentlines {
+ .pos-total,
+ .pos-paymentlines {
@include flex-direction(row);
@include flex-display;
@include flex-wrap(wrap);
@@ -221,6 +227,7 @@ body {
> div {
@include flex(1 0 48%);
+
&:nth-child(even) {
font-weight: bold;
font-size: 120%;
@@ -433,7 +440,7 @@ body {
}
}
- #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group> .dropdown.oe_options.btn-group.snippet-option-pos_palette > ul {
+ #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group> .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu {
min-width: 100px;
max-width: 100px;
@@ -441,7 +448,7 @@ body {
display: none;
}
- > li > a {
+ > a {
position: relative;
padding: 14px 0!important;
margin-bottom: 1px;
@@ -453,7 +460,8 @@ body {
}
#oe_manipulators .no_parent_options {
- .snippet-option-pos_palette, .o_parent_editor_header {
+ .snippet-option-pos_palette,
+ .o_parent_editor_header {
display: none;
}
}
@@ -462,7 +470,7 @@ body {
// =========== PALETTE GENERATOR ===========
-.palette_variant (
+@mixin palette-variant (
$bg-info: $color-gray-dark,
$text-info: $color-gray-lighter,
$bg-products: $color-gray-lighter,
@@ -497,7 +505,7 @@ body {
}
// Editor dropdown buttons
- #oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > ul > li > & {
+ #oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > & {
background: $bg-products!important;
&:after {
@@ -511,7 +519,7 @@ body {
}
&:before {
- text-shadow: 0 0 1px (#fff - $bg-products);
+ text-shadow: 0 0 1px invert($bg-products);
}
}
};
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 4be76675244..c5a7aab49b8 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -413,13 +413,20 @@ class ProductTemplate(models.Model):
Product = self.env['product.product']
templates = self.browse([])
+ domain_no_variant = [('product_variant_ids', '=', False)]
while True:
domain = templates and [('product_tmpl_id', 'not in', templates.ids)] or []
args = args if args is not None else []
products_ns = Product._name_search(name, args+domain, operator=operator, name_get_uid=name_get_uid)
products = Product.browse([x[0] for x in products_ns])
templates |= products.mapped('product_tmpl_id')
- if (not products) or (limit and (len(templates) > limit)):
+ current_round_templates = self.browse([])
+ if not products:
+ domain_template = args + domain_no_variant + (templates and [('id', 'not in', templates.ids)] or [])
+ template_ns = super(ProductTemplate, self)._name_search(name=name, args=domain_template, operator=operator, limit=limit, name_get_uid=name_get_uid)
+ current_round_templates |= self.browse([ns[0] for ns in template_ns])
+ templates |= current_round_templates
+ if (not products and not current_round_templates) or (limit and (len(templates) > limit)):
break
searched_ids = set(templates.ids)
diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py
index 40d95171449..0959bfb242b 100644
--- a/addons/product/tests/test_variants.py
+++ b/addons/product/tests/test_variants.py
@@ -817,3 +817,25 @@ class TestVariantsArchive(common.TestProductCommon):
"We should have re-activated one variant only.")
Product._revert_method('unlink')
+
+ def test_name_search_dynamic_attributes(self):
+ dynamic_attr = self.env['product.attribute'].create({
+ 'name': 'Dynamic',
+ 'create_variant': 'dynamic',
+ 'value_ids': [(0, False, {'name': 'ValueDynamic'})],
+ })
+ template = self.env['product.template'].create({
+ 'name': 'cimanyd'
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+
+ template.write({
+ 'attribute_line_ids': [(0, False, {
+ 'attribute_id': dynamic_attr.id,
+ 'value_ids': [(4, dynamic_attr.value_ids[0].id, False)],
+ })]
+ })
+ self.assertEqual(len(template.product_variant_ids), 0)
+
+ name_searched = self.env['product.template'].name_search(name='cima')
+ self.assertIn(template.id, [ng[0] for ng in name_searched])
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index e951e03c15c..11042c8894e 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -490,6 +490,37 @@ class SaleOrder(models.Model):
action = {'type': 'ir.actions.act_window_close'}
return action
+ def _finalize_invoices(self, invoices, references):
+ """
+ Invoked after creating invoices at the end of action_invoice_create.
+ :param invoices: {group_key: invoice}
+ :param references: {invoice: order}
+ """
+ for invoice in invoices.values():
+ invoice.compute_taxes()
+ if not invoice.invoice_line_ids:
+ raise UserError(_(
+ 'There is no invoiceable line. If a product has a Delivered quantities invoicing policy, please make sure that a quantity has been delivered.'))
+ # If invoice is negative, do a refund invoice instead
+ if invoice.amount_total < 0:
+ invoice.type = 'out_refund'
+ for line in invoice.invoice_line_ids:
+ line.quantity = -line.quantity
+ # Use additional field helper function (for account extensions)
+ for line in invoice.invoice_line_ids:
+ line._set_additional_fields(invoice)
+ # Necessary to force computation of taxes. In account_invoice, they are triggered
+ # by onchanges, which are not triggered when doing a create.
+ invoice.compute_taxes()
+ # Idem for partner
+ so_payment_term_id = invoice.payment_term_id.id
+ invoice._onchange_partner_id()
+ # To keep the payment terms set on the SO
+ invoice.payment_term_id = so_payment_term_id
+ invoice.message_post_with_view('mail.message_origin_link',
+ values={'self': invoice, 'origin': references[invoice]},
+ subtype_id=self.env.ref('mail.mt_note').id)
+
@api.multi
def _create_invoices(self, grouped=False, final=False):
"""
@@ -571,31 +602,7 @@ class SaleOrder(models.Model):
if not invoices:
raise UserError(_('There is no invoiceable line. If a product has a Delivered quantities invoicing policy, please make sure that a quantity has been delivered.'))
- for invoice in invoices.values():
- invoice.compute_taxes()
- if not invoice.invoice_line_ids:
- raise UserError(_('There is no invoiceable line. If a product has a Delivered quantities invoicing policy, please make sure that a quantity has been delivered.'))
- # If invoice is negative, do a refund invoice instead
- if invoice.amount_total < 0:
- invoice.type = 'out_refund'
- for line in invoice.invoice_line_ids:
- line.quantity = -line.quantity
- # Use additional field helper function (for account extensions)
- for line in invoice.invoice_line_ids:
- line._set_additional_fields(invoice)
- # Necessary to force computation of taxes. In account_invoice, they are triggered
- # by onchanges, which are not triggered when doing a create.
- invoice.compute_taxes()
- # Idem for partner
- so_payment_term_id = invoice.payment_term_id.id
- fp_invoice = invoice.fiscal_position_id
- invoice._onchange_partner_id()
- invoice.fiscal_position_id = fp_invoice
- # To keep the payment terms set on the SO
- invoice.payment_term_id = so_payment_term_id
- invoice.message_post_with_view('mail.message_origin_link',
- values={'self': invoice, 'origin': references[invoice]},
- subtype_id=self.env.ref('mail.mt_note').id)
+ self._finalize_invoices(invoices, references)
return [inv.id for inv in invoices.values()]
@api.multi
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index fdcce6710b2..3ebc3dbc864 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -682,15 +682,20 @@ class StockMove(models.Model):
self.ensure_one()
return self.group_id, self.location_id, self.location_dest_id, self.picking_type_id
- def _search_picking_for_assignation(self):
- self.ensure_one()
- picking = self.env['stock.picking'].search([
+ def _search_picking_for_assignation_domain(self):
+ return [
('group_id', '=', self.group_id.id),
('location_id', '=', self.location_id.id),
('location_dest_id', '=', self.location_dest_id.id),
('picking_type_id', '=', self.picking_type_id.id),
('printed', '=', False),
- ('state', 'in', ['draft', 'confirmed', 'waiting', 'partially_available', 'assigned'])], limit=1)
+ ('state', 'in', ['draft', 'confirmed', 'waiting',
+ 'partially_available', 'assigned'])
+ ]
+
+ def _search_picking_for_assignation(self):
+ self.ensure_one()
+ picking = self.env['stock.picking'].search(self._search_picking_for_assignation_domain(), limit=1)
return picking
def _assign_picking(self):
@@ -1159,7 +1164,7 @@ class StockMove(models.Model):
for result_package in moves_todo\
.mapped('move_line_ids.result_package_id')\
.filtered(lambda p: p.quant_ids and len(p.quant_ids) > 1):
- if len(result_package.quant_ids.mapped('location_id')) > 1:
+ if len(result_package.quant_ids.filtered(lambda q: float_is_zero(abs(q.quantity) + abs(q.reserved_quantity), precision_rounding=q.product_uom_id.rounding)).mapped('location_id')) > 1:
raise UserError(_('You cannot move the same package content more than once in the same transfer or split the same package into two location.'))
picking = moves_todo.mapped('picking_id')
moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index c2e5bf0e203..1398d26188c 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -494,7 +494,7 @@ class Picking(models.Model):
# TDE FIXME: what ?
# As the on_change in one2many list is WIP, we will overwrite the locations on the stock moves here
# As it is a create the format will be a list of (0, 0, dict)
- moves = vals.get('move_lines') or vals.get('move_ids_without_package')
+ moves = vals.get('move_lines', []) + vals.get('move_ids_without_package', [])
if moves and vals.get('location_id') and vals.get('location_dest_id'):
for move in moves:
if len(move) == 3 and move[0] == 0:
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index 55d30bc0663..2221edac80a 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -499,7 +499,7 @@ class Warehouse(models.Model):
delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
code = vals.get('code') or code
code = code.replace(' ', '').upper()
- company_id = vals.get('company_id', self.company_id.id)
+ company_id = vals.get('company_id', self.default_get(['company_id'])['company_id'])
sub_locations = {
'lot_stock_id': {
'name': _('Stock'),
@@ -548,12 +548,14 @@ class Warehouse(models.Model):
avoid mistakes during picking types and rules creation.
"""
for warehouse in self:
+ company_id = vals.get('company_id', warehouse.company_id.id)
+ vals.update(company_id=company_id)
sub_locations = warehouse._get_locations_values(vals, warehouse.code)
missing_location = {}
for location, location_values in sub_locations.items():
if not warehouse[location] and location not in vals:
location_values['location_id'] = vals.get('view_location_id', warehouse.view_location_id.id)
- location_values['company_id'] = vals.get('company_id', warehouse.company_id.id)
+ location_values['company_id'] = company_id
missing_location[location] = self.env['stock.location'].create(location_values).id
if missing_location:
warehouse.write(missing_location)
diff --git a/addons/stock/tests/test_packing.py b/addons/stock/tests/test_packing.py
index 02b35707e17..d9c89d7a077 100644
--- a/addons/stock/tests/test_packing.py
+++ b/addons/stock/tests/test_packing.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import SavepointCase
+from odoo.tools import float_round
class TestPacking(SavepointCase):
@@ -260,3 +261,49 @@ class TestPacking(SavepointCase):
qp1 = pack2.quant_ids[0]
qp2 = pack2.quant_ids[1]
self.assertEqual(qp1.quantity + qp2.quantity, 12, 'The quant has not the good quantity')
+
+ def test_move_picking_with_package(self):
+ """
+ 355.4 rounded with 0.001 precision is 355.40000000000003.
+ check that nonetheless, moving a picking is accepted
+ """
+ self.assertEqual(self.productA.uom_id.rounding, 0.001)
+ self.assertEqual(
+ float_round(355.4, precision_rounding=self.productA.uom_id.rounding),
+ 355.40000000000003,
+ )
+ location_dict = {
+ 'location_id': self.stock_location.id,
+ }
+ quant = self.env['stock.quant'].create({
+ **location_dict,
+ **{'product_id': self.productA.id, 'quantity': 355.4}, # important number
+ })
+ package = self.env['stock.quant.package'].create({
+ **location_dict, **{'quant_ids': [(6, 0, [quant.id])]},
+ })
+ location_dict.update({
+ 'state': 'draft',
+ 'location_dest_id': self.ship_location.id,
+ })
+ move = self.env['stock.move'].create({
+ **location_dict,
+ **{
+ 'name': "XXX",
+ 'product_id': self.productA.id,
+ 'product_uom': self.productA.uom_id.id,
+ 'product_uom_qty': 355.40000000000003, # other number
+ }})
+ picking = self.env['stock.picking'].create({
+ **location_dict,
+ **{
+ 'picking_type_id': self.warehouse.in_type_id.id,
+ 'move_lines': [(6, 0, [move.id])],
+ }})
+
+ picking.action_confirm()
+ picking.action_assign()
+ move.quantity_done = move.reserved_availability
+ picking.action_done()
+ # if we managed to get there, there was not any exception
+ # complaining that 355.4 is not 355.40000000000003. Good job!
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index f7945dffa10..37eccbf2a4e 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -271,6 +271,7 @@ class StockMove(models.Model):
qty_to_take_on_candidates = quantity or valued_quantity
candidates = move.product_id._get_fifo_candidates_in_move_with_company(move.company_id.id)
new_standard_price = 0
+ tmp_qty = 0
tmp_value = 0 # to accumulate the value taken on the candidates
for candidate in candidates:
new_standard_price = candidate.price_unit
@@ -291,6 +292,7 @@ class StockMove(models.Model):
candidate.write(candidate_vals)
qty_to_take_on_candidates -= qty_taken_on_candidate
+ tmp_qty += qty_taken_on_candidate
tmp_value += value_taken_on_candidate
if qty_to_take_on_candidates == 0:
break
@@ -304,9 +306,18 @@ class StockMove(models.Model):
# negative stock use case. We chose to value the out move at the price of the
# last out and a correction entry will be made once `_fifo_vacuum` is called.
if qty_to_take_on_candidates == 0:
+ # If the move is not valued yet we compute the price_unit based on the value taken on
+ # the candidates.
+ # If the move has already been valued, it means that we editing the qty_done on the
+ # move. In this case, the price_unit computation should take into account the quantity
+ # already valued and the new quantity taken.
+ if not move.value:
+ price_unit = -tmp_value / (move.product_qty or quantity)
+ else:
+ price_unit = (-(tmp_value) + move.value) / (tmp_qty + move.product_qty)
move.write({
'value': -tmp_value if not quantity else move.value or -tmp_value, # outgoing move are valued negatively
- 'price_unit': -tmp_value / (move.product_qty or quantity),
+ 'price_unit': price_unit,
})
elif qty_to_take_on_candidates > 0:
last_fifo_price = new_standard_price or move.product_id.standard_price
diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py
index 0ea9fa57a6e..243338f84fa 100644
--- a/addons/stock_account/tests/test_stockvaluation.py
+++ b/addons/stock_account/tests/test_stockvaluation.py
@@ -2153,6 +2153,7 @@ class TestStockValuation(SavepointCase):
# stock values for move3
self.assertEqual(move3.value, -80.0)
+ self.assertAlmostEqual(move3.price_unit, -10.0)
self.assertEqual(move3.remaining_qty, 0.0)
self.assertEqual(move3.remaining_value, 0.0)
@@ -2184,6 +2185,7 @@ class TestStockValuation(SavepointCase):
# old value: -80 -(8@10)
# new value: -148 => -(10@10 + 4@12)
self.assertEqual(move3.value, -148)
+ self.assertAlmostEqual(move3.price_unit, -148.0/14.0)
# older move
self.assertEqual(move1.remaining_value, 0) # before, 20
@@ -2272,6 +2274,7 @@ class TestStockValuation(SavepointCase):
# stock values for move2
self.assertEqual(move2.value, -100.0)
+ self.assertAlmostEqual(move2.price_unit, -10.0)
self.assertEqual(move2.remaining_qty, 0.0)
self.assertEqual(move2.remaining_value, 0.0)
@@ -2281,6 +2284,7 @@ class TestStockValuation(SavepointCase):
move2.quantity_done = 8
self.assertEqual(move2.value, -80.0) # the move actually sent 8@10
+ self.assertAlmostEqual(move2.price_unit, -10.0)
self.assertEqual(move2.remaining_qty, 0.0)
self.assertEqual(move2.remaining_value, 0.0)
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 13e58095d40..8155f9d56f1 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -2418,7 +2418,7 @@ var FieldStatus = AbstractField.extend({
var FieldSelection = AbstractField.extend({
template: 'FieldSelection',
specialData: "_fetchSpecialRelation",
- supportedFieldTypes: ['selection', 'many2one'],
+ supportedFieldTypes: ['selection'],
events: _.extend({}, AbstractField.prototype.events, {
'change': '_onChange',
}),
diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js
index 0703d9713d5..4889a27f9e5 100644
--- a/addons/web/static/src/js/views/form/form_view.js
+++ b/addons/web/static/src/js/views/form/form_view.js
@@ -127,6 +127,10 @@ var FormView = BasicView.extend({
var refinedContext = _.pick(self.loadParams.context, function (value, key) {
return key.indexOf('_view_ref') === -1;
});
+ // Specify the main model to prevent access rights defined in the context
+ // (e.g. create: 0) to apply to subviews. We use here the same logic as
+ // the one applied by the server for inline views.
+ refinedContext.base_model_name = self.controllerParams.modelName;
defs.push(parent.loadViews(
field.relation,
new Context(context, self.userContext, refinedContext).eval(),
diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js
index f953c488a09..2e622d9a47a 100644
--- a/addons/web/static/src/js/views/graph/graph_view.js
+++ b/addons/web/static/src/js/views/graph/graph_view.js
@@ -48,6 +48,9 @@ var GraphView = AbstractView.extend({
this.arch.children.forEach(function (field) {
var fieldName = field.attrs.name;
+ if (fieldName === "id") {
+ return;
+ }
var interval = field.attrs.interval;
if (interval) {
fieldName = fieldName + ':' + interval;
diff --git a/addons/web/static/src/js/widgets/auto_complete.js b/addons/web/static/src/js/widgets/auto_complete.js
index 4da54cf8b5c..1bb00be8d38 100644
--- a/addons/web/static/src/js/widgets/auto_complete.js
+++ b/addons/web/static/src/js/widgets/auto_complete.js
@@ -140,12 +140,22 @@ return Widget.extend({
var $item = self.make_list_item(result).appendTo($list);
result.$el = $item;
});
+ // IE9 doesn't support addEventListener with option { once: true }
+ this.el.onmousemove = function (ev) {
+ self.$('li').each(function (index, li) {
+ li.onmouseenter = self.focus_element.bind(self, $(li));
+ });
+ var targetFocus = ev.target.tagName === 'LI' ?
+ ev.target :
+ ev.target.closest('li');
+ self.focus_element($(targetFocus));
+ self.el.onmousemove = null;
+ };
this.show();
},
make_list_item: function (result) {
var self = this;
var $li = $('<li>')
- .hover(function () {self.focus_element($li);})
.mousedown(function (ev) {
if (ev.button === 0) { // left button
self.select(ev, {item: {facet: result.facet}});
diff --git a/addons/web/static/src/scss/ui.scss b/addons/web/static/src/scss/ui.scss
index 44d639805be..9ab1729a4ac 100644
--- a/addons/web/static/src/scss/ui.scss
+++ b/addons/web/static/src/scss/ui.scss
@@ -83,3 +83,11 @@
.o_debounce_disabled {
pointer-events: none;
}
+
+span.o_force_ltr {
+ display: inline-block;
+}
+.o_force_ltr, .o_field_phone {
+ /*rtl:ignore*/
+ direction: ltr;
+}
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index e276b5da5b6..f373cd710e5 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -85,7 +85,7 @@ var MockServer = Class.extend({
throw new Error('Model ' + model + ' was not defined in mock server data');
}
var fields = $.extend(true, {}, this.data[model].fields);
- var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context);
+ var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context || {});
if (toolbar) {
fvg.toolbar = toolbar;
}
@@ -214,6 +214,17 @@ var MockServer = Class.extend({
var inTreeView = (doc.tagName === 'tree');
+ // mock _postprocess_access_rights
+ const isBaseModel = !context.base_model_name || (model === context.base_model_name);
+ var views = ['kanban', 'tree', 'form', 'gantt', 'activity'];
+ if (isBaseModel && views.indexOf(doc.tagName) !== -1) {
+ for (let action of ['create', 'delete', 'edit', 'write']) {
+ if (!doc.getAttribute(action) && action in context && !context[action]) {
+ doc.setAttribute(action, 'false');
+ }
+ }
+ }
+
this._traverse(doc, function (node) {
if (node.nodeType === Node.TEXT_NODE) {
return false;
@@ -318,7 +329,7 @@ var MockServer = Class.extend({
relModel = field.relation;
relFields = $.extend(true, {}, self.data[relModel].fields);
field.views[children.tagName] = self._fieldsViewGet(children, relModel,
- relFields, context);
+ relFields, _.extend({}, context, {base_model_name: model}));
});
}
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 2c5eaa3e627..d89bffac31b 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -459,8 +459,10 @@ QUnit.module('Views', {
'The correct _view_ref should have been sent to the server, first time');
}
if (args.model === 'partner_type') {
- assert.deepEqual(context, {tree_view_ref: 'some_other_tree_view'},
- 'The correct _view_ref should have been sent to the server for the subview');
+ assert.deepEqual(context, {
+ base_model_name: 'product',
+ tree_view_ref: 'some_other_tree_view',
+ }, 'The correct _view_ref should have been sent to the server for the subview');
}
}
return this._super.apply(this, arguments);
@@ -4568,6 +4570,35 @@ QUnit.module('Views', {
});
});
+ QUnit.test('non inline subview and create=0 in action context', async function (assert) {
+ // the create=0 should apply on the main view (form), but not on subviews
+ assert.expect(2);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="product_ids" mode="kanban"/></form>',
+ archs: {
+ "product,false,kanban": `<kanban>
+ <templates><t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t></templates>
+ </kanban>`,
+ },
+ res_id: 1,
+ viewOptions: {
+ context: {create: false},
+ mode: 'edit',
+ },
+ });
+
+ assert.containsNone(form, '.o_form_button_create');
+ assert.containsOnce(form, '.o-kanban-button-new');
+
+ form.destroy();
+ });
+
QUnit.test('readonly fields with modifiers may be saved', async function (assert) {
// the readonly property on the field description only applies on view,
// this is not a DB constraint. It should be seen as a default value,
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
index ca969fb9ac9..a179c265ca1 100644
--- a/addons/web/static/tests/views/graph_tests.js
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -176,6 +176,27 @@ QUnit.module('Views', {
graph.destroy();
});
+ QUnit.test('field id not in groupBy', async function (assert) {
+ assert.expect(1);
+
+ var graph = await createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Partners">' +
+ '<field name="id"/>' +
+ '</graph>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ assert.deepEqual(args.kwargs.groupby, [],
+ 'groupby should not contain id field');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ graph.destroy();
+ });
+
QUnit.test('switching mode', async function (assert) {
assert.expect(6);
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index 9dd78f3b724..8e64c191467 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -411,8 +411,10 @@ QUnit.module('Views', {
'The correct _view_ref should have been sent to the server, first time');
}
if (ev.data.modelName === 'badassery') {
- assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_other_tree_view'},
- 'The correct _view_ref should have been sent to the server for the subview');
+ assert.deepEqual(evaluatedContext, {
+ base_model_name: 'instrument',
+ tree_view_ref: 'some_other_tree_view',
+ }, 'The correct _view_ref should have been sent to the server for the subview');
}
},
},
diff --git a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js
index 5c7a41ae8f3..8ec600d14c5 100644
--- a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js
+++ b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js
@@ -776,7 +776,7 @@ registry.colorpicker = SnippetOption.extend({
if ($selected.length) {
if ($selected.data('color')) {
this.$target.addClass(this.colorPrefix + $selected.data('color'));
- } else {
+ } else if ($selected.hasClass('o_custom_color')) {
this.$target.css('background-color', $selected.css('background-color'));
}
}
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 1771b299c7a..c303c4f95a4 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -723,9 +723,6 @@ class Website(models.Model):
domain_part, url = rule.build(value, append_unknown=False)
if not query_string or query_string.lower() in url.lower():
page = {'loc': url}
- for key, val in value.items():
- if key.startswith('__'):
- page[key[2:]] = val
if url in ('/sitemap.xml',):
continue
if url in url_set:
@@ -749,9 +746,9 @@ class Website(models.Model):
for page in pages:
record = {'loc': page['url'], 'id': page['id'], 'name': page['name']}
if page.view_id and page.view_id.priority != 16:
- record['__priority'] = min(round(page.view_id.priority / 32.0, 1), 1)
+ record['priority'] = min(round(page.view_id.priority / 32.0, 1), 1)
if page['write_date']:
- record['__lastmod'] = page['write_date'].date()
+ record['lastmod'] = page['write_date'].date()
yield record
@api.multi
diff --git a/addons/website/static/src/js/content/menu.js b/addons/website/static/src/js/content/menu.js
index 77149eeb06c..2d33afb3d6f 100644
--- a/addons/website/static/src/js/content/menu.js
+++ b/addons/website/static/src/js/content/menu.js
@@ -19,6 +19,7 @@ publicWidget.registry.affixMenu = publicWidget.Widget.extend({
this.$headerClone.insertAfter(this.$target);
this.$headers = this.$target.add(this.$headerClone);
this.$dropdowns = this.$headers.find('.dropdown');
+ this.$dropdownMenus = this.$headers.find('.dropdown-menu');
this.$navbarCollapses = this.$headers.find('.navbar-collapse');
this._adaptDefaultOffset();
@@ -94,7 +95,7 @@ publicWidget.registry.affixMenu = publicWidget.Widget.extend({
this.$headerClone.toggleClass('affixed', wOffset > (hOffset + 300));
// Reset opened menus
- this.$dropdowns.removeClass('show');
+ this.$dropdowns.add(this.$dropdownMenus).removeClass('show');
this.$navbarCollapses.removeClass('show').attr('aria-expanded', false);
},
});
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 661aa693227..337402f32e9 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -908,8 +908,14 @@ table.table_desc tr td {
background-color: inherit;
}
}
- &.s_parallax_is_fixed > .s_parallax_bg {
- background-attachment: fixed;
+ @include media-breakpoint-up(xl) {
+ // Fixed backgrounds are disabled when using a mobile/tablet device,
+ // which is not a big deal but, on some of them (iOS...), defining the
+ // background as fixed breaks the background-size/position props.
+ // So we enable this only for >= XL devices.
+ &.s_parallax_is_fixed > .s_parallax_bg {
+ background-attachment: fixed;
+ }
}
}
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 212ba6e3b38..f6fa3338915 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -367,7 +367,7 @@
<h5>Connect with us</h5>
<ul class="list-unstyled">
<li><a href="/contactus">Contact us</a></li>
- <li><i class="fa fa-phone"/> <span t-field="res_company.phone"/></li>
+ <li><i class="fa fa-phone"/> <span class="o_force_ltr" t-field="res_company.phone"/></li>
<li><i class="fa fa-envelope"/> <span t-field="res_company.email"/></li>
</ul>
<p>
diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py
index 1d09540e8cf..51a2deb3158 100644
--- a/addons/website_blog/controllers/main.py
+++ b/addons/website_blog/controllers/main.py
@@ -230,7 +230,7 @@ class WebsiteBlog(http.Controller):
blog_url = QueryURL('', ['blog', 'tag'], blog=blog_post.blog_id, tag=tag, date_begin=date_begin, date_end=date_end)
if not blog_post.blog_id.id == blog.id:
- return request.redirect("/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post)))
+ return request.redirect("/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post)), code=301)
tags = request.env['blog.tag'].search([])
diff --git a/addons/website_event_sale/models/sale_order.py b/addons/website_event_sale/models/sale_order.py
index cd25e6e1943..c13e8a4c23e 100644
--- a/addons/website_event_sale/models/sale_order.py
+++ b/addons/website_event_sale/models/sale_order.py
@@ -41,7 +41,10 @@ class SaleOrder(models.Model):
values['product_id'] = ticket.product_id.id
values['event_id'] = ticket.event_id.id
values['event_ticket_id'] = ticket.id
- values['price_unit'] = ticket.price_reduce
+ if order.pricelist_id.discount_policy == 'without_discount':
+ values['price_unit'] = ticket.price
+ else:
+ values['price_unit'] = ticket.price_reduce
values['name'] = ticket.get_ticket_multiline_description_sale()
# avoid writing related values that end up locking the product record
diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py
index 4627d68b5a9..a9406e6b685 100644
--- a/addons/website_event_track/models/event_track.py
+++ b/addons/website_event_track/models/event_track.py
@@ -159,7 +159,7 @@ class Track(models.Model):
def message_get_suggested_recipients(self):
recipients = super(Track, self).message_get_suggested_recipients()
for track in self:
- if track.partner_email != track.partner_id.email:
+ if track.partner_email and track.partner_email != track.partner_id.email:
track._message_add_suggested_recipient(recipients, email=track.partner_email, reason=_('Speaker Email'))
return recipients
diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py
index c6a310569d8..7cd12411ec6 100644
--- a/addons/website_sale/models/website.py
+++ b/addons/website_sale/models/website.py
@@ -206,7 +206,7 @@ class Website(models.Model):
salesperson_id = affiliate_id if self.env['res.users'].sudo().browse(affiliate_id).exists() else request.website.salesperson_id.id
addr = partner.address_get(['delivery'])
if not request.website.is_public_user():
- last_sale_order = self.env['sale.order'].search([('partner_id', '=', partner.id)], limit=1, order="date_order desc, id desc")
+ last_sale_order = self.env['sale.order'].sudo().search([('partner_id', '=', partner.id)], limit=1, order="date_order desc, id desc")
if last_sale_order: # first = me
addr['delivery'] = last_sale_order.partner_shipping_id.id
default_user_id = partner.parent_id.user_id.id or partner.user_id.id
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 4c58a7c5603..1496a5abfa4 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1597,7 +1597,7 @@
<b>Communication: </b><span t-esc='order.reference'/>
</div>
</div>
- <div t-if="(payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.acquirer_id.provider == 'transfer') and (payment_tx_id.partner_id.country_id.code in ['BE', 'AT', 'DE', 'FI', 'NL']) and (payment_tx_id.currency_id.name == 'EUR')">
+ <div t-if="(payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.acquirer_id.provider == 'transfer') and (payment_tx_id.currency_id.name == 'EUR')">
<div class="card-body" t-if="payment_tx_id.acquirer_id.journal_id.bank_account_id.qr_code_valid">
<h3>Or scan me with your banking app.</h3>
<img class="border border-dark rounded" t-att-src="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(order.amount_total,payment_tx_id.reference)"/>
diff --git a/addons/website_sale_comparison/models/website_sale_comparison.py b/addons/website_sale_comparison/models/website_sale_comparison.py
index 9c2fbca9082..24875d32ac1 100644
--- a/addons/website_sale_comparison/models/website_sale_comparison.py
+++ b/addons/website_sale_comparison/models/website_sale_comparison.py
@@ -9,7 +9,7 @@ class ProductAttributeCategory(models.Model):
_description = "Product Attribute Category"
_order = 'sequence'
- name = fields.Char("Category Name", required=True)
+ name = fields.Char("Category Name", required=True, translate=True)
sequence = fields.Integer("Sequence", default=10)
diff --git a/addons/website_sale_stock/models/product_template.py b/addons/website_sale_stock/models/product_template.py
index b0706bfee30..7ad389ce46a 100644
--- a/addons/website_sale_stock/models/product_template.py
+++ b/addons/website_sale_stock/models/product_template.py
@@ -17,10 +17,10 @@ class ProductTemplate(models.Model):
custom_message = fields.Text(string='Custom Message', default='', translate=True)
@api.multi
- def _get_combination_info(self, combination=False, product_id=False, add_qty=1, pricelist=False, reference_product=False, only_template=False):
+ def _get_combination_info(self, combination=False, product_id=False, add_qty=1, pricelist=False, parent_combination=False, only_template=False):
combination_info = super(ProductTemplate, self)._get_combination_info(
combination=combination, product_id=product_id, add_qty=add_qty, pricelist=pricelist,
- parent_combination=reference_product, only_template=only_template)
+ parent_combination=parent_combination, only_template=only_template)
if not self.env.context.get('website_sale_stock_get_quantity'):
return combination_info
diff --git a/doc/cla/corporate/arxilead.md b/doc/cla/corporate/arxilead.md
new file mode 100644
index 00000000000..8640fa2ff95
--- /dev/null
+++ b/doc/cla/corporate/arxilead.md
@@ -0,0 +1,15 @@
+Portugal, 2019-08-13
+
+Arxilead Tecnologia e Gestão Lda. agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Rita Santos rita.santos@arxi.pt https://github.com/Arxilead
+
+List of contributors:
+
+* Marcelo Pereira marcelo.pereira@arxi.pt https://github.com/marcelopereiraarxi
+* Nuno Silva nuno.silva@arxi.pt https://github.com/nunosilvaarxi
+* Reinaldo Ramos reinaldo.ramos@arxi.pt https://github.com/reinaldoramosarxi
diff --git a/doc/cla/corporate/tvtmarine-automation.md b/doc/cla/corporate/tvtmarine-automation.md
index 59e3c250ab3..d5f077c852b 100644
--- a/doc/cla/corporate/tvtmarine-automation.md
+++ b/doc/cla/corporate/tvtmarine-automation.md
@@ -13,7 +13,10 @@ David Tran david.tran@tvtmarine.com https://github.com/tvtma/
List of contributors:
David Tran david.tran@tvtmarine.com https://github.com/davidtranhp
-Long Do long.do@tvtmarine.com https://github.com/hoanglong87
+Phong Dao phong.dao@tvtmarine.com https://github.com/phongdao
Hao Hoang hao.hoang@ma.tvtmarine.com https://github.com/hoanghao2001
Tommy Tran tu.tran@tvtmarine.com https://github.com/tutran81
-Emily Ha emily.ha@tvtmarine.com https://github.com/emilyha
+Trinh Anh Ngoc ngoc.trinh@tvtmarine.com https://github.com/trinhanhngoc
+
+(up to 2019-07-14)
+Long Do long.do@tvtmarine.com https://github.com/hoanglong87 \ No newline at end of file
diff --git a/doc/cla/individual/hari4274.md b/doc/cla/individual/hari4274.md
new file mode 100644
index 00000000000..24ff61a8cd6
--- /dev/null
+++ b/doc/cla/individual/hari4274.md
@@ -0,0 +1,11 @@
+India, 2019-08-24
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Hariprasath bala.hariprasath@gmail.com https://github.com/hari4274 \ No newline at end of file
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index 05e546dfec6..16f3608836d 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -1969,7 +1969,7 @@ Relational fields
.. autoclass:: web.relational_fields.FieldSelection
- Supported field types: *selection*, *many2one*
+ Supported field types: *selection*
.. attribute:: placeholder
@@ -2010,7 +2010,7 @@ Relational fields
- many2one (FieldMany2One)
Default widget for many2one fields.
- - Supported field types: *selection, many2one*
+ - Supported field types: *many2one*
Attributes:
diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst
index f0c0452c8d5..5213add7765 100644
--- a/doc/reference/orm.rst
+++ b/doc/reference/orm.rst
@@ -1118,6 +1118,12 @@ A domain is a list of criteria, each criterion being a triple (either a
Takes the semantics of the model into account (i.e following the
relationship field named by
:attr:`~odoo.models.Model._parent_name`).
+ ``parent_of``
+ is a parent (ascendant) of a ``value`` record.
+
+ Takes the semantics of the model into account (i.e following the
+ relationship field named by
+ :attr:`~odoo.models.Model._parent_name`).
``value``
variable type, must be comparable (through ``operator``) to the named
diff --git a/odoo/addons/base/data/ir_module_module.xml b/odoo/addons/base/data/ir_module_module.xml
index 4f8ce3f9b60..82f41049a34 100644
--- a/odoo/addons/base/data/ir_module_module.xml
+++ b/odoo/addons/base/data/ir_module_module.xml
@@ -136,7 +136,7 @@
<field name="author">Odoo S.A.</field>
<field name="to_buy" eval="True"/>
<field name="icon">/base/static/img/icons/project_forecast.png</field>
- <field name="website">https://www.odoo.com/page/project?utm_source=db&amp;utm_medium=module</field>
+ <field name="website">https://www.odoo.com/page/project-management?utm_source=db&amp;utm_medium=module</field>
</record>
<record model="ir.module.module" id="base.module_sale_subscription">
diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py
index 1b6c2124622..60a348c39bb 100644
--- a/odoo/addons/base/models/ir_model.py
+++ b/odoo/addons/base/models/ir_model.py
@@ -1280,7 +1280,7 @@ class IrModelAccess(models.Model):
msg_params['groups_list'] = groups
else:
msg_tail = _("No group currently allows this operation.")
- msg_tail += ' - ({} {}, {} {})'.format(_('Operation:'), mode, _('User:'), self._uid)
+ msg_tail += u' - ({} {}, {} {})'.format(_('Operation:'), mode, _('User:'), self._uid)
_logger.info('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, self._uid, model)
msg = '%s %s' % (msg_heads[mode], msg_tail)
raise AccessError(msg % msg_params)
diff --git a/odoo/addons/base/models/res_bank.py b/odoo/addons/base/models/res_bank.py
index d9a0433ecc2..729f1c884c1 100644
--- a/odoo/addons/base/models/res_bank.py
+++ b/odoo/addons/base/models/res_bank.py
@@ -138,11 +138,6 @@ class ResPartnerBank(models.Model):
@api.multi
def _validate_qr_code_arguments(self):
for bank in self:
- if bank.currency_id.name == False:
- currency = bank.company_id.currency_id
- else:
- currency = bank.currency_id
bank.qr_code_valid = (bank.bank_bic
and bank.company_id.name
- and bank.acc_number
- and (currency.name == 'EUR'))
+ and bank.acc_number)
diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py
index 2a8f9f7c8cb..3b33f26fb07 100644
--- a/odoo/addons/base/models/res_partner.py
+++ b/odoo/addons/base/models/res_partner.py
@@ -828,10 +828,14 @@ class Partner(models.Model):
e.g. ``"Raoul Grosbedon <r.g@grosbedon.fr>"``"""
assert email, 'an email is required for find_or_create to work'
emails = tools.email_split(email)
+ name_emails = tools.email_split_and_format(email)
if emails:
email = emails[0]
+ name_email = name_emails[0]
+ else:
+ name_email = email
partners = self.search([('email', '=ilike', email)], limit=1)
- return partners.id or self.name_create(email)[0]
+ return partners.id or self.name_create(name_email)[0]
def _get_gravatar_image(self, email):
email_hash = hashlib.md5(email.lower().encode('utf-8')).hexdigest()
diff --git a/odoo/addons/base/tests/test_base.py b/odoo/addons/base/tests/test_base.py
index 231f1daadd2..5051fec4dad 100644
--- a/odoo/addons/base/tests/test_base.py
+++ b/odoo/addons/base/tests/test_base.py
@@ -69,17 +69,25 @@ class TestBase(TransactionCase):
def test_10_res_partner_find_or_create(self):
res_partner = self.env['res.partner']
+
email = SAMPLES[0][0]
partner_id, dummy = res_partner.name_create(email)
found_id = res_partner.find_or_create(email)
self.assertEqual(partner_id, found_id, 'find_or_create failed')
+ self.assertEqual(SAMPLES[0][1], res_partner.browse([found_id]).name, 'Partner name is incorrect')
+
partner_id2, dummy2 = res_partner.name_create('sarah.john@connor.com')
found_id2 = res_partner.find_or_create('john@connor.com')
self.assertNotEqual(partner_id2, found_id2, 'john@connor.com match sarah.john@connor.com')
+ self.assertEqual('john@connor.com', res_partner.browse([found_id2]).name, 'Partner name is incorrect')
+
new_id = res_partner.find_or_create(SAMPLES[1][0])
self.assertTrue(new_id > partner_id, 'find_or_create failed - should have created new one')
+ self.assertEqual(SAMPLES[1][2], res_partner.browse([new_id]).name, 'Partner name is incorrect')
+
new_id2 = res_partner.find_or_create(SAMPLES[2][0])
self.assertTrue(new_id2 > new_id, 'find_or_create failed - should have created new one again')
+ self.assertEqual(SAMPLES[2][1], res_partner.browse([new_id2]).name, 'Partner name is incorrect')
def test_15_res_partner_name_search(self):
res_partner = self.env['res.partner']
diff --git a/odoo/addons/base/views/ir_qweb_widget_templates.xml b/odoo/addons/base/views/ir_qweb_widget_templates.xml
index 6da8ec0efd6..47bf04a62ff 100644
--- a/odoo/addons/base/views/ir_qweb_widget_templates.xml
+++ b/odoo/addons/base/views/ir_qweb_widget_templates.xml
@@ -32,9 +32,9 @@
<span itemprop="addressCountry" t-esc="country_id"/>
</div>
</div>
- <div t-if="phone and 'phone' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-phone fa-fw' role="img" aria-label="Phone" title="Phone"/> <span itemprop="telephone" t-esc="phone"/></div>
- <div t-if="mobile and 'mobile' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-mobile fa-fw' role="img" aria-label="Mobile" title="Mobile"/> <span itemprop="telephone" t-esc="mobile"/></div>
- <div t-if="fax and 'fax' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-fax fa-fw' role="img" aria-label="Fax" title="Fax"/> <span itemprop="faxNumber" t-esc="fax"/></div>
+ <div t-if="phone and 'phone' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-phone fa-fw' role="img" aria-label="Phone" title="Phone"/> <span class="o_force_ltr" itemprop="telephone" t-esc="phone"/></div>
+ <div t-if="mobile and 'mobile' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-mobile fa-fw' role="img" aria-label="Mobile" title="Mobile"/> <span class="o_force_ltr" itemprop="telephone" t-esc="mobile"/></div>
+ <div t-if="fax and 'fax' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-fax fa-fw' role="img" aria-label="Fax" title="Fax"/> <span class="o_force_ltr" itemprop="faxNumber" t-esc="fax"/></div>
<div t-if="website and 'website' in fields">
<i t-if="not options.get('no_marker')" class='fa fa-globe' role="img" aria-label="Website" title="Website"/>
<a t-att-href="website and '%s%s' % ('http://' if '://' not in website else '',website)"><span itemprop="website" t-esc="website"/></a>
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py
index b6a743d874c..80e410e7301 100644
--- a/odoo/tools/mail.py
+++ b/odoo/tools/mail.py
@@ -34,7 +34,7 @@ safe_attrs = clean.defs.safe_attrs | frozenset(
['style',
'data-o-mail-quote', # quote detection
'data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-type', 'data-oe-expression', 'data-oe-translation-id', 'data-oe-nodeid',
- 'data-publish', 'data-id', 'data-res_id', 'data-member_id', 'data-view-id'
+ 'data-publish', 'data-id', 'data-res_id', 'data-interval', 'data-member_id', 'data-scroll-background-ratio', 'data-view-id',
])