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 @@
-
Scan me with your banking app.
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 @@
Communication:
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 @@
+
' + string + '
').contents();
+ }
return _parseAndTransform(children, transformFunction)
.replace(new RegExp(openToken, "g"), "<");
}
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 @@
-
-
Or scan me with your banking app.
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 = $('
Connect with us
Or scan me with your banking app.
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
@@ -1115,6 +1115,12 @@ A domain is a list of criteria, each criterion being a triple (either a
``child_of``
is a child (descendant) 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`).
+ ``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`).
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 @@