diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index d57fb164ee4..3981ed91640 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -667,8 +667,9 @@ class AccountBankStatementLine(models.Model): #create the res.partner.bank if needed if self.account_number and self.partner_id and not self.bank_account_id: - bank_account = self.env['res.partner.bank'].search( - [('acc_number', '=', self.account_number), ('partner_id', '=', self.partner_id.id)]) + # Search bank account without partner to handle the case the res.partner.bank already exists but is set + # on a different partner. + bank_account = self.env['res.partner.bank'].search([('acc_number', '=', self.account_number)]) if not bank_account: bank_account = self.env['res.partner.bank'].create({ 'acc_number': self.account_number, 'partner_id': self.partner_id.id diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index ef7c145e072..50760c25ff9 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -992,6 +992,7 @@ class AccountInvoice(models.Model): @api.multi def get_taxes_values(self): tax_grouped = {} + round_curr = self.currency_id.round for line in self.invoice_line_ids: if not line.account_id: continue @@ -1003,9 +1004,10 @@ class AccountInvoice(models.Model): if key not in tax_grouped: tax_grouped[key] = val + tax_grouped[key]['base'] = round_curr(val['base']) else: tax_grouped[key]['amount'] += val['amount'] - tax_grouped[key]['base'] += val['base'] + tax_grouped[key]['base'] += round_curr(val['base']) return tax_grouped @api.multi @@ -1734,17 +1736,17 @@ class AccountInvoiceLine(models.Model): self.price_unit = 0.0 domain['uom_id'] = [] else: + self_lang = self if part.lang: - product = self.product_id.with_context(lang=part.lang) - else: - product = self.product_id - + self_lang = self.with_context(lang=part.lang) + + product = self_lang.product_id account = self.get_invoice_line_account(type, product, fpos, company) if account: self.account_id = account.id self._set_taxes() - product_name = self._get_invoice_line_name_from_product() + product_name = self_lang._get_invoice_line_name_from_product() if product_name != None: self.name = product_name diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 7b038433905..b19485674a4 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -775,7 +775,7 @@ class AccountMoveLine(models.Model): (multiple_currency and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0): exchange_move_id = False # Eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates - if to_balance and any([residual for aml, residual in to_balance.values()]): + if to_balance and any([not float_is_zero(residual, precision_rounding=digits_rounding_precision) for aml, residual in to_balance.values()]): exchange_move = self.env['account.move'].create( self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=amls[0].company_id)) part_reconcile = self.env['account.partial.reconcile'] @@ -804,7 +804,7 @@ class AccountMoveLine(models.Model): (debit_moves + credit_moves).read([field]) to_create = [] cash_basis = debit_moves and debit_moves[0].account_id.internal_type in ('receivable', 'payable') or False - cash_basis_percentage_before_rec = [] + cash_basis_percentage_before_rec = {} while (debit_moves and credit_moves): debit_move = debit_moves[0] credit_move = credit_moves[0] @@ -839,7 +839,7 @@ class AccountMoveLine(models.Model): if cash_basis: tmp_set = debit_move | credit_move - cash_basis_percentage_before_rec.append(tmp_set._get_matched_percentage()) + cash_basis_percentage_before_rec.update(tmp_set._get_matched_percentage()) to_create.append({ 'debit_move_id': debit_move.id, @@ -850,14 +850,12 @@ class AccountMoveLine(models.Model): }) part_rec = self.env['account.partial.reconcile'] - index = 0 with self.env.norecompute(): for partial_rec_dict in to_create: new_rec = self.env['account.partial.reconcile'].create(partial_rec_dict) part_rec += new_rec if cash_basis: - new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec[index]) - index += 1 + new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec) self.recompute() return debit_moves+credit_moves @@ -1468,10 +1466,14 @@ class AccountPartialReconcile(models.Model): #move_date is the max of the 2 reconciled items if move_date < move.date: move_date = move.date + percentage_before = percentage_before_rec[move.id] + percentage_after = move.line_ids[0]._get_matched_percentage()[move.id] + # update the percentage before as the move can be part of + # multiple partial reconciliations + percentage_before_rec[move.id] = percentage_after + for line in move.line_ids: if not line.tax_exigible: - percentage_before = percentage_before_rec[move.id] - percentage_after = line._get_matched_percentage()[move.id] #amount is the current cash_basis amount minus the one before the reconciliation amount = line.balance * percentage_after - line.balance * percentage_before rounded_amt = self._get_amount_tax_cash_basis(amount, line) @@ -1543,6 +1545,7 @@ class AccountPartialReconcile(models.Model): 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, 'partner_id': line.partner_id.id, }) + if newly_created_move: if move_date > (self.company_id.period_lock_date or date.min) and newly_created_move.date != move_date: # The move date should be the maximum date between payment and invoice (in case diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index e69aa30e431..c83d2b31ed3 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -736,12 +736,15 @@ class account_payment(models.Model): """ Return dict to create the payment move """ journal = journal or self.journal_id - return { + move_vals = { 'date': self.payment_date, 'ref': self.communication or '', 'company_id': self.company_id.id, 'journal_id': journal.id, } + if self.move_name: + move_vals['name'] = self.move_name + return move_vals def _get_shared_move_line_vals(self, debit, credit, amount_currency, move_id, invoice_id=False): """ Returns values common to both move lines (except for debit, credit and amount_currency which are reversed) diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index a192b2210af..5e756f28247 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -903,11 +903,11 @@ var StatementModel = BasicModel.extend({ }); var company_currency = session.get_currency(line.st_line.currency_id); var company_precision = company_currency && company_currency.digits[1] || 2; - total = utils.round_precision(total*1000, company_precision)/1000 || 0; + total = utils.round_decimals(total*1000, company_precision)/1000 || 0; if(isOtherCurrencyId){ var other_currency = session.get_currency(isOtherCurrencyId); var other_precision = other_currency && other_currency.digits[1] || 2; - amount_currency = utils.round_precision(amount_currency, other_precision) + amount_currency = utils.round_decimals(amount_currency, other_precision) } line.balance = { amount: total, diff --git a/addons/account/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py index a6b3459c9b9..83822ef8ffb 100644 --- a/addons/account/tests/test_account_customer_invoice.py +++ b/addons/account/tests/test_account_customer_invoice.py @@ -103,3 +103,66 @@ class TestAccountCustomerInvoice(AccountTestUsers): # I clicked on Add Credit Note button. self.account_invoice_refund_0.invoice_refund() + + def test_customer_invoice_tax(self): + + self.env.user.company_id.tax_calculation_rounding_method = 'round_globally' + + payment_term = self.env.ref('account.account_payment_term_advance') + journalrec = self.env['account.journal'].search([('type', '=', 'sale')])[0] + partner3 = self.env.ref('base.res_partner_3') + account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id + + tax = self.env['account.tax'].create({ + 'name': 'Tax 15.0', + 'amount': 15.0, + 'amount_type': 'percent', + 'type_tax_use': 'sale', + }) + + invoice_line_data = [ + (0, 0, + { + 'product_id': self.env.ref('product.product_product_1').id, + 'quantity': 40.0, + 'account_id': account_id, + 'name': 'product test 1', + 'discount' : 10.00, + 'price_unit': 2.27, + 'invoice_line_tax_ids': [(6, 0, [tax.id])], + } + ), + (0, 0, + { + 'product_id': self.env.ref('product.product_product_2').id, + 'quantity': 21.0, + 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id, + 'name': 'product test 2', + 'discount' : 10.00, + 'price_unit': 2.77, + 'invoice_line_tax_ids': [(6, 0, [tax.id])], + } + ), + (0, 0, + { + 'product_id': self.env.ref('product.product_product_3').id, + 'quantity': 21.0, + 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id, + 'name': 'product test 3', + 'discount' : 10.00, + 'price_unit': 2.77, + 'invoice_line_tax_ids': [(6, 0, [tax.id])], + } + ) + ] + + invoice = self.env['account.invoice'].create(dict( + name="Test Customer Invoice", + reference_type="none", + payment_term_id=payment_term.id, + journal_id=journalrec.id, + partner_id=partner3.id, + invoice_line_ids=invoice_line_data + )) + + self.assertEquals(invoice.amount_untaxed, sum([x.base for x in invoice.tax_line_ids])) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index be446c818a5..970989a970f 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -49,6 +49,58 @@ class TestReconciliation(AccountingTestCase): 'payment_type': 'inbound', }) + self.expense_account = self.env['account.account'].create({ + 'name': 'EXP', + 'code': 'EXP', + 'user_type_id': self.env.ref('account.data_account_type_expenses').id, + 'company_id': company.id, + }) + # cash basis intermediary account + self.tax_waiting_account = self.env['account.account'].create({ + 'name': 'TAX_WAIT', + 'code': 'TWAIT', + 'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id, + 'reconcile': True, + 'company_id': company.id, + }) + # cash basis final account + self.tax_final_account = self.env['account.account'].create({ + 'name': 'TAX_TO_DEDUCT', + 'code': 'TDEDUCT', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + 'company_id': company.id, + }) + self.tax_base_amount_account = self.env['account.account'].create({ + 'name': 'TAX_BASE', + 'code': 'TBASE', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + 'company_id': company.id, + }) + + # Journals + self.purchase_journal = self.env['account.journal'].create({ + 'name': 'purchase', + 'code': 'PURCH', + 'type': 'purchase', + }) + self.cash_basis_journal = self.env['account.journal'].create({ + 'name': 'CABA', + 'code': 'CABA', + 'type': 'general', + }) + + # Tax Cash Basis + self.tax_cash_basis = self.env['account.tax'].create({ + 'name': 'cash basis 20%', + 'type_tax_use': 'purchase', + 'company_id': company.id, + 'amount': 20, + 'account_id': self.tax_waiting_account.id, + 'tax_exigibility': 'on_payment', + 'cash_basis_account_id': self.tax_final_account.id, + 'cash_basis_base_account_id': self.tax_base_amount_account.id, + }) + def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None): #we create an invoice in given currency invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, @@ -1107,3 +1159,288 @@ class TestReconciliation(AccountingTestCase): receivable_lines = invoice.move_id.line_ids.filtered(lambda line: line.account_id == self.account_rcv).sorted('date_maturity')[0] self.assertTrue(receivable_lines.matched_credit_ids) + + def test_reconciliation_cash_basis01(self): + # Simulates an expense made up by 2 lines + # one is subject to a cash basis tax + # the other is not subject to tax + + company = self.env.ref('base.main_company') + company.tax_cash_basis_journal_id = self.cash_basis_journal + + AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False) + + # Purchase + purchase_move = self.env['account.move'].create({ + 'name': 'purchase', + 'journal_id': self.purchase_journal.id, + }) + + purchase_payable_line0 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 100, + 'move_id': purchase_move.id, + }) + purchase_payable_line1 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 50, + 'move_id': purchase_move.id, + }) + AccountMoveLine.create({ + 'name': 'expensNoTax', + 'account_id': self.expense_account.id, + 'debit': 50, + 'move_id': purchase_move.id, + }) + AccountMoveLine.create({ + 'name': 'expenseTaxed', + 'account_id': self.expense_account.id, + 'debit': 83.33, + 'move_id': purchase_move.id, + 'tax_ids': [(4, self.tax_cash_basis.id, False)], + }) + tax_line = AccountMoveLine.create({ + 'name': 'TaxLine', + 'account_id': self.tax_waiting_account.id, + 'debit': 16.67, + 'move_id': purchase_move.id, + 'tax_line_id': self.tax_cash_basis.id, + }) + purchase_move.post() + + # Payment Move + payment_move = self.env['account.move'].create({ + 'name': 'payment', + 'journal_id': self.bank_journal_euro.id, + }) + payment_payable_line = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'debit': 150, + 'move_id': payment_move.id, + }) + AccountMoveLine.create({ + 'account_id': self.account_euro.id, + 'credit': 150, + 'move_id': payment_move.id, + }) + payment_move.post() + + to_reconcile = (purchase_move + payment_move).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable') + to_reconcile.reconcile() + + cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)]) + + self.assertEqual(len(cash_basis_moves), 2) + self.assertTrue(cash_basis_moves.exists()) + + # check reconciliation in Payable account + self.assertTrue(purchase_payable_line0.full_reconcile_id.exists()) + self.assertEqual(purchase_payable_line0.full_reconcile_id.reconciled_line_ids, + purchase_payable_line0 + purchase_payable_line1 + payment_payable_line) + + cash_basis_aml_ids = cash_basis_moves.mapped('line_ids') + # check reconciliation in the tax waiting account + self.assertTrue(tax_line.full_reconcile_id.exists()) + self.assertEqual(tax_line.full_reconcile_id.reconciled_line_ids, + cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + tax_line) + + self.assertEqual(len(cash_basis_aml_ids), 8) + + # check amounts + cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.amount == 33.34) + cash_basis_move2 = cash_basis_moves.filtered(lambda m: m.amount == 66.66) + + self.assertTrue(cash_basis_move1.exists()) + self.assertTrue(cash_basis_move2.exists()) + + # For first move + move_lines = cash_basis_move1.line_ids + base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account) + self.assertEqual(len(base_amount_tax_lines), 2) + self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 27.78) + self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 27.78) + + self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit, + 5.56) + self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit, + 5.56) + + # For second move + move_lines = cash_basis_move2.line_ids + base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account) + self.assertEqual(len(base_amount_tax_lines), 2) + self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 55.55) + self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 55.55) + + self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit, + 11.11) + self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit, + 11.11) + + def test_reconciliation_cash_basis02(self): + # Simulates an invoice made up by 2 lines + # both subjected to cash basis taxes + # with 2 payment terms + # And partial payment not martching any payment term + + company = self.env.ref('base.main_company') + company.tax_cash_basis_journal_id = self.cash_basis_journal + tax_cash_basis10percent = self.tax_cash_basis.copy({'amount': 10}) + tax_waiting_account10 = self.tax_waiting_account.copy({ + 'name': 'TAX WAIT 10', + 'code': 'TWAIT1', + }) + + + AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False) + + # Purchase + purchase_move = self.env['account.move'].create({ + 'name': 'invoice', + 'journal_id': self.purchase_journal.id, + }) + + purchase_payable_line0 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 105, + 'move_id': purchase_move.id, + }) + purchase_payable_line1 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'credit': 50, + 'move_id': purchase_move.id, + }) + AccountMoveLine.create({ + 'name': 'expenseTaxed 10%', + 'account_id': self.expense_account.id, + 'debit': 50, + 'move_id': purchase_move.id, + 'tax_ids': [(4, tax_cash_basis10percent.id, False)], + }) + tax_line0 = AccountMoveLine.create({ + 'name': 'TaxLine0', + 'account_id': tax_waiting_account10.id, + 'debit': 5, + 'move_id': purchase_move.id, + 'tax_line_id': tax_cash_basis10percent.id, + }) + AccountMoveLine.create({ + 'name': 'expenseTaxed 20%', + 'account_id': self.expense_account.id, + 'debit': 83.33, + 'move_id': purchase_move.id, + 'tax_ids': [(4, self.tax_cash_basis.id, False)], + }) + tax_line1 = AccountMoveLine.create({ + 'name': 'TaxLine1', + 'account_id': self.tax_waiting_account.id, + 'debit': 16.67, + 'move_id': purchase_move.id, + 'tax_line_id': self.tax_cash_basis.id, + }) + purchase_move.post() + + # Payment Move + payment_move0 = self.env['account.move'].create({ + 'name': 'payment', + 'journal_id': self.bank_journal_euro.id, + }) + payment_payable_line0 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'debit': 40, + 'move_id': payment_move0.id, + }) + AccountMoveLine.create({ + 'account_id': self.account_euro.id, + 'credit': 40, + 'move_id': payment_move0.id, + }) + payment_move0.post() + + # Payment Move + payment_move1 = self.env['account.move'].create({ + 'name': 'payment', + 'journal_id': self.bank_journal_euro.id, + }) + payment_payable_line1 = AccountMoveLine.create({ + 'account_id': self.account_rsa.id, + 'debit': 115, + 'move_id': payment_move1.id, + }) + AccountMoveLine.create({ + 'account_id': self.account_euro.id, + 'credit': 115, + 'move_id': payment_move1.id, + }) + payment_move1.post() + + (purchase_move + payment_move0).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable').reconcile() + (purchase_move + payment_move1).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable').reconcile() + + cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)]) + + self.assertEqual(len(cash_basis_moves), 3) + self.assertTrue(cash_basis_moves.exists()) + + # check reconciliation in Payable account + self.assertTrue(purchase_payable_line0.full_reconcile_id.exists()) + self.assertEqual(purchase_payable_line0.full_reconcile_id.reconciled_line_ids, + purchase_payable_line0 + purchase_payable_line1 + payment_payable_line0 + payment_payable_line1) + + cash_basis_aml_ids = cash_basis_moves.mapped('line_ids') + + # check reconciliation in the tax waiting account + self.assertTrue(tax_line0.full_reconcile_id.exists()) + self.assertEqual(tax_line0.full_reconcile_id.reconciled_line_ids, + cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + tax_line0) + + self.assertTrue(tax_line1.full_reconcile_id.exists()) + self.assertEqual(tax_line1.full_reconcile_id.reconciled_line_ids, + cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + tax_line1) + + self.assertEqual(len(cash_basis_aml_ids), 24) + + # check amounts + expected_move_amounts = [ + {'base_20': 56.45, 'tax_20': 11.29, 'base_10': 33.87, 'tax_10': 3.39}, + {'base_20': 21.50, 'tax_20': 4.30, 'base_10': 12.90, 'tax_10': 1.29}, + {'base_20': 5.38, 'tax_20': 1.08, 'base_10': 3.23, 'tax_10': 0.32}, + ] + + index = 0 + for cb_move in cash_basis_moves.sorted('amount', reverse=True): + expected = expected_move_amounts[index] + move_lines = cb_move.line_ids + base_amount_tax_lines20per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '20%' in l.name) + base_amount_tax_lines10per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '10%' in l.name) + self.assertEqual(len(base_amount_tax_lines20per), 2) + + self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('credit')), expected['base_20']) + self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('debit')), expected['base_20']) + + self.assertEqual(len(base_amount_tax_lines10per), 2) + self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('credit')), expected['base_10']) + self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('debit')), expected['base_10']) + + self.assertAlmostEqual( + (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per) + .filtered(lambda l: l.account_id == self.tax_waiting_account).credit, + expected['tax_20'] + ) + self.assertAlmostEqual( + (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per) + .filtered(lambda l: 'TaxLine1' in l.name).debit, + expected['tax_20'] + ) + + self.assertAlmostEqual( + (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per) + .filtered(lambda l: l.account_id == tax_waiting_account10).credit, + expected['tax_10'] + ) + self.assertAlmostEqual( + (move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per) + .filtered(lambda l: 'TaxLine0' in l.name).debit, + expected['tax_10'] + ) + index += 1 diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 45696253ffa..15219345394 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -291,7 +291,7 @@ - + diff --git a/addons/barcodes/static/src/js/barcode_field.js b/addons/barcodes/static/src/js/barcode_field.js index ca5baa90b49..d8a7a1c853e 100644 --- a/addons/barcodes/static/src/js/barcode_field.js +++ b/addons/barcodes/static/src/js/barcode_field.js @@ -4,7 +4,7 @@ odoo.define('barcodes.field', function(require) { var AbstractField = require('web.AbstractField'); var basicFields = require('web.basic_fields'); var fieldRegistry = require('web.field_registry'); -var is_special_key = require('barcodes.BarcodeEvents').BarcodeEvents.is_special_key; +var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents; // Field in which the user can both type normally and scan barcodes @@ -42,7 +42,7 @@ var FieldFloatScannable = basicFields.FieldFloat.extend({ _onKeypress: function (e) { /* only simulate a keypress if it has been previously prevented */ if (e.dispatched_by_barcode_reader !== true) { - if (!is_special_key(e)) { + if (!BarcodeEvents.is_special_key(e)) { e.preventDefault(); } return; diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index eb969156b09..accc102c2b7 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -16,8 +16,13 @@ class StockQuantPackage(models.Model): @api.depends('quant_ids') def _compute_weight(self): weight = 0.0 - for quant in self.quant_ids: - weight += quant.quantity * quant.product_id.weight + if self.env.context.get('picking_id'): + current_picking_move_line_ids = self.env['stock.move.line'].search([('result_package_id', '=', self.id), ('picking_id', '=', self.env.context['picking_id'])]) + for ml in current_picking_move_line_ids: + weight += ml.product_uom_id._compute_quantity(ml.qty_done,ml.product_id.uom_id) * ml.product_id.weight + else: + for quant in self.quant_ids: + weight += quant.quantity * quant.product_id.weight self.weight = weight def _get_default_weight_uom(self): diff --git a/addons/hr_holidays/i18n/hr_holidays.pot b/addons/hr_holidays/i18n/hr_holidays.pot index 15161b395a5..68e174d4053 100644 --- a/addons/hr_holidays/i18n/hr_holidays.pot +++ b/addons/hr_holidays/i18n/hr_holidays.pot @@ -658,6 +658,11 @@ msgstr "" msgid "Analyze from" msgstr "" +#. module: hr_holidays +#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave_type__double_validation +msgid "Apply Double Validation" +msgstr "" + #. module: hr_holidays #: model_terms:ir.ui.view,arch_db:hr_holidays.view_evaluation_report_graph msgid "Appraisal Analysis" diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 31aab06c21a..24062454dca 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -427,8 +427,7 @@ class HolidaysRequest(models.Model): employee = self.env['hr.employee'].browse(employee_id) return employee.get_work_days_data(date_from, date_to)['days'] - time_delta = date_to - date_from - return math.ceil(time_delta.days + float(time_delta.seconds) / 86400) + return self.env.user.company_id.resource_calendar_id.get_work_hours_count() / HOURS_PER_DAY #################################################### # ORM Overrides methods @@ -612,7 +611,7 @@ class HolidaysRequest(models.Model): 'date_from': self.date_from, 'date_to': self.date_to, 'notes': self.notes, - 'number_of_days': self.number_of_days, + 'number_of_days': employee.get_work_days_data(self.date_from, self.date_to)['days'], 'parent_id': self.id, 'employee_id': employee.id } diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index af3baf9830b..c542ea14909 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -67,6 +67,14 @@ class HolidaysType(models.Model): ('hr', 'Payroll Officer'), ('manager', 'Team Leader'), ('both', 'Team Leader and Payroll Officer')], default='hr', string='Validation By') + # TODO: remove me in master, the behavior is exactly the same if you choose 'hr' or 'manager' + # in the validation_type field. This field is used only to hide this possibility to the user + # to avoid misunderstandings. This field and its corresponding's functions must be removed once + # the functional part is implemented. + double_validation = fields.Boolean(string='Apply Double Validation', + compute='_compute_validation_type', inverse='_inverse_validation_type', + help="When selected, the Allocation/Leave Requests for this type require a second validation to be approved.") + allocation_type = fields.Selection([ ('fixed', 'Fixed by HR'), ('fixed_allocation', 'Fixed by HR + allocation request'), @@ -88,6 +96,25 @@ class HolidaysType(models.Model): leave_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Leave Notification Subtype') allocation_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Allocation Notification Subtype') + # TODO: remove me in master + @api.depends('validation_type') + def _compute_validation_type(self): + for holiday_type in self: + if holiday_type.validation_type == 'both': + holiday_type.double_validation = True + else: + holiday_type.double_validation = False + + # TODO: remove me in master + def _inverse_validation_type(self): + for holiday_type in self: + if holiday_type.double_validation == True: + holiday_type.validation_type = 'both' + else: + #IF to preserve the information (hr or manager) + if holiday_type.validation_type == 'both': + holiday_type.validation_type = 'hr' + @api.multi @api.constrains('validity_start', 'validity_stop') def _check_validity_dates(self): diff --git a/addons/hr_holidays/views/hr_leave_type_views.xml b/addons/hr_holidays/views/hr_leave_type_views.xml index 64562ab1f05..c7d48c1d319 100644 --- a/addons/hr_holidays/views/hr_leave_type_views.xml +++ b/addons/hr_holidays/views/hr_leave_type_views.xml @@ -49,7 +49,8 @@ - + + @@ -110,7 +111,8 @@ - + + diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py index e5e65390304..a224be4ce9a 100644 --- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py +++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py @@ -144,13 +144,13 @@ class HrContract(models.Model): ) @api.depends('yearly_cost_before_charges', 'social_security_contributions', 'wage', - 'social_security_contributions', 'double_holidays', 'warrants_cost', 'meal_voucher_paid_by_employer') + 'social_security_contributions', 'warrants_cost', 'meal_voucher_paid_by_employer') def _compute_final_yearly_costs(self): for contract in self: contract.final_yearly_costs = ( contract.yearly_cost_before_charges + contract.social_security_contributions + - contract.double_holidays + + contract.wage * 0.92 + contract.warrants_cost + (220.0 * contract.meal_voucher_paid_by_employer) ) @@ -197,11 +197,11 @@ class HrContract(models.Model): for contract in self: contract.monthly_yearly_costs = contract.final_yearly_costs / 12.0 - @api.depends('wage') + @api.depends('wage_with_holidays') def _compute_holidays_advantages(self): for contract in self: - contract.double_holidays = contract.wage * 0.92 - contract.thirteen_month = contract.wage + contract.double_holidays = contract.wage_with_holidays * 0.92 + contract.thirteen_month = contract.wage_with_holidays @api.onchange('transport_mode_car', 'transport_mode_public', 'transport_mode_others') def _onchange_transport_mode(self): diff --git a/addons/l10n_nl/data/account_fiscal_position_tax_template.xml b/addons/l10n_nl/data/account_fiscal_position_tax_template.xml index c026f355bbb..e59c852ef24 100644 --- a/addons/l10n_nl/data/account_fiscal_position_tax_template.xml +++ b/addons/l10n_nl/data/account_fiscal_position_tax_template.xml @@ -112,7 +112,7 @@ - + diff --git a/addons/mail/static/src/js/models/mail_failure.js b/addons/mail/static/src/js/models/mail_failure.js index 390b195f8d9..438430c752f 100644 --- a/addons/mail/static/src/js/models/mail_failure.js +++ b/addons/mail/static/src/js/models/mail_failure.js @@ -83,7 +83,7 @@ var MailFailure = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { documentModel: this._documentModel, id: 'mail_failure', imageSRC: this._moduleIcon, - title: this._recordName, + title: this._modelName, }; return preview; }, diff --git a/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js b/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js index 9a5b5abb4cc..f995b4e09b3 100644 --- a/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js +++ b/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js @@ -94,17 +94,17 @@ var ThreadTypingMixin = { * Get the text to display when some partners are typing something on the * thread: * - * - single typing partner: + * - single typing partner: * - * A is typing... + * A is typing... * - * - two typing partners: + * - two typing partners: * - * A and B are typing... + * A and B are typing... * - * - three or more typing partners: + * - three or more typing partners: * - * A, B and more are typing... + * A, B and more are typing... * * The choice of the members name for display is not random: it displays * the user that have been typing for the longest time. Also, this function diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index 140266285c7..b59d7b91434 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -490,7 +490,7 @@ MailManager.include({ var self = this; var partnerID = data.partner_id; this.call('bus_service', 'sendNotification', data.title, data.message, function ( ){ - self.call('mail_service', 'openDmWindow', partnerID); + self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID); }); }, /** diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index 64cf6bb88c9..c8e05957afb 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -531,9 +531,6 @@ MailManager.include({ * @private */ _repositionThreadWindows: function () { - if (this._areAllThreadWindowsHidden()) { - return; - } this._computeAvailableSlotsForThreadWindows(); this._repositionVisibleThreadWindows(); @@ -575,9 +572,6 @@ MailManager.include({ * the focus is on the thread window. */ _updateThreadWindowsFromMessage: function (message, options) { - if (this._areAllThreadWindowsHidden()) { - return; - } _.each(this._threadWindows, function (threadWindow) { if (_.contains(message.getThreadIDs(), threadWindow.getID())) { threadWindow.update(options); 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 cd5ffa7db94..57907a55943 100644 --- a/addons/mail/static/src/js/thread_windows/thread_window.js +++ b/addons/mail/static/src/js/thread_windows/thread_window.js @@ -132,9 +132,9 @@ var ThreadWindow = AbstractThreadWindow.extend({ * * 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 + * - 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. * diff --git a/addons/mrp/static/src/js/mrp_bom_report.js b/addons/mrp/static/src/js/mrp_bom_report.js index 76cd0b53fd8..8a96c5d6d67 100644 --- a/addons/mrp/static/src/js/mrp_bom_report.js +++ b/addons/mrp/static/src/js/mrp_bom_report.js @@ -25,7 +25,8 @@ var MrpBomReport = stock_report_generic.extend({ return this._rpc({ model: 'report.mrp.report_bom_structure', method: 'get_html', - args: args + args: args, + context: this.given_context, }) .then(function (result) { self.data = result; diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js index 50872153d8c..74d32c74acc 100644 --- a/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js +++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js @@ -112,7 +112,7 @@ var PartnerAutocompleteMixin = { var self = this; var removeUselessFields = function (company) { - var fields = 'label,description,domain,logo,legal_name'.split(','); + var fields = 'label,description,domain,logo,legal_name,ignored'.split(','); fields.forEach(function (field) { delete company[field]; }); diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 9fa51724678..afed11da38e 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -871,24 +871,25 @@ class PaymentTransaction(models.Model): def create(self, values): # call custom create method if defined (i.e. ogone_create for ogone) acquirer = self.env['payment.acquirer'].browse(values['acquirer_id']) - partner = self.env['res.partner'].browse(values['partner_id']) + if values.get('partner_id'): + partner = self.env['res.partner'].browse(values['partner_id']) - values.update({ - 'partner_name': partner.name, - 'partner_lang': partner.lang or 'en_US', - 'partner_email': partner.email, - 'partner_zip': partner.zip, - 'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''), - 'partner_city': partner.city, - 'partner_country_id': partner.country_id.id or self._get_default_partner_country_id(), - 'partner_phone': partner.phone, - }) + values.update({ + 'partner_name': partner.name, + 'partner_lang': partner.lang or 'en_US', + 'partner_email': partner.email, + 'partner_zip': partner.zip, + 'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''), + 'partner_city': partner.city, + 'partner_country_id': partner.country_id.id or self._get_default_partner_country_id(), + 'partner_phone': partner.phone, + }) # compute fees custom_method_name = '%s_compute_fees' % acquirer.provider if hasattr(acquirer, custom_method_name): fees = getattr(acquirer, custom_method_name)( - values.get('amount', 0.0), values.get('currency_id'), partner.country_id.id) + values.get('amount', 0.0), values.get('currency_id'), values['partner_country_id']) values['fees'] = fees # custom create diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index e4bd3382caa..bca2164dac1 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -149,7 +149,7 @@ class AuthorizeAPI(): root = self._base_tree('createCustomerProfileRequest') profile = etree.SubElement(root, "profile") etree.SubElement(profile, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8]) - etree.SubElement(profile, "email").text = partner.email + etree.SubElement(profile, "email").text = partner.email or '' payment_profile = etree.SubElement(profile, "paymentProfiles") etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual' billTo = etree.SubElement(payment_profile, "billTo") diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py index 314d4bdd26a..a45275b07d8 100644 --- a/addons/payment_authorize/tests/test_authorize.py +++ b/addons/payment_authorize/tests/test_authorize.py @@ -188,7 +188,7 @@ class AuthorizeForm(AuthorizeCommon): self.assertEqual(tx.state, 'cancel', 'Authorize: erroneous validation did not put tx into error state') -@odoo.tests.tagged('post_install', '-at_install', '-standard', 'external') +@odoo.tests.tagged('post_install', '-at_install', '-standard') class AuthorizeForm(AuthorizeCommon): def test_30_authorize_s2s(self): # be sure not to do stupid thing diff --git a/addons/payment_buckaroo/tests/test_buckaroo.py b/addons/payment_buckaroo/tests/test_buckaroo.py index 925385f402f..e23b1f0c002 100644 --- a/addons/payment_buckaroo/tests/test_buckaroo.py +++ b/addons/payment_buckaroo/tests/test_buckaroo.py @@ -32,12 +32,12 @@ class BuckarooForm(BuckarooCommon): # ---------------------------------------- form_values = { - 'add_returndata': None, + 'add_returndata': '', 'Brq_websitekey': self.buckaroo.brq_websitekey, 'Brq_amount': '2240.0', 'Brq_currency': 'EUR', 'Brq_invoicenumber': 'SO004', - 'Brq_signature': '1b8c10074c622d965272a91a9e88b5b3777d2474', # update me + 'Brq_signature': 'aa6fe072afdd9d1b463d55f43e6df4b272d3529a', # update me 'brq_test': 'True', 'Brq_return': urls.url_join(base_url, BuckarooController._return_url), 'Brq_returncancel': urls.url_join(base_url, BuckarooController._cancel_url), @@ -50,13 +50,14 @@ class BuckarooForm(BuckarooCommon): res = self.buckaroo.render( 'SO004', 2240.0, self.currency_euro.id, partner_id=None, - partner_values=self.buyer_values) - + values=self.buyer_values) # check form result tree = objectify.fromstring(res) - self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') + data_set = tree.xpath("//input[@name='data_set']") + self.assertEqual(len(data_set), 1, 'paypal: Found %d "data_set" input instead of 1' % len(data_set)) + self.assertEqual(data_set[0].get('data-action-url'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') for form_input in tree.input: - if form_input.get('name') in ['submit']: + if form_input.get('name') in ['submit', 'data_set']: continue self.assertEqual( form_input.get('value'), @@ -78,17 +79,18 @@ class BuckarooForm(BuckarooCommon): }) # render the button - res = self.buckaroo_id.render( + res = self.buckaroo.render( 'should_be_erased', 2240.0, self.currency_euro, - tx_id=tx.id, partner_id=None, - partner_values=self.buyer_values) + values=self.buyer_values) # check form result tree = objectify.fromstring(res) - self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') + data_set = tree.xpath("//input[@name='data_set']") + self.assertEqual(len(data_set), 1, 'paypal: Found %d "data_set" input instead of 1' % len(data_set)) + self.assertEqual(data_set[0].get('data-action-url'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') for form_input in tree.input: - if form_input.get('name') in ['submit']: + if form_input.get('name') in ['submit', 'data_set']: continue self.assertEqual( form_input.get('value'), @@ -116,7 +118,7 @@ class BuckarooForm(BuckarooCommon): 'BRQ_SERVICE_PAYPAL_PAYERLASTNAME': u'Tester', 'BRQ_SERVICE_PAYPAL_PAYERMIDDLENAME': u'de', 'BRQ_SERVICE_PAYPAL_PAYERSTATUS': u'verified', - 'Brq_signature': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91', + 'Brq_signature': u'9e75e51cbcb6fbc798b40d1787cc5b7c36557d56', 'BRQ_STATUSCODE': u'190', 'BRQ_STATUSCODE_DETAIL': u'S001', 'BRQ_STATUSMESSAGE': u'Transaction successfully processed', @@ -134,7 +136,7 @@ class BuckarooForm(BuckarooCommon): 'amount': 2240.0, 'acquirer_id': self.buckaroo.id, 'currency_id': self.currency_euro.id, - 'reference': 'SO004-1', + 'reference': 'SO004', 'partner_name': 'Norbert Buyer', 'partner_country_id': self.country_france.id}) @@ -153,14 +155,14 @@ class BuckarooForm(BuckarooCommon): 'partner_name': 'Norbert Buyer', 'partner_country_id': self.country_france.id}) + buckaroo_post_data['BRQ_INVOICENUMBER'] = 'SO004-2' # now buckaroo post is ok: try to modify the SHASIGN - buckaroo_post_data['BRQ_SIGNATURE'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a' + buckaroo_post_data['Brq_signature'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a' with self.assertRaises(ValidationError): tx.form_feedback(buckaroo_post_data, 'buckaroo') - # simulate an error - buckaroo_post_data['BRQ_STATUSCODE'] = 2 - buckaroo_post_data['BRQ_SIGNATURE'] = '4164b52adb1e6a2221d3d8a39d8c3e18a9ecb90b' + buckaroo_post_data['BRQ_STATUSCODE'] = '2' + buckaroo_post_data['Brq_signature'] = '9138e2bf09a708a4eb485ce7777e5406898b689d' tx.form_feedback(buckaroo_post_data, 'buckaroo') # check state diff --git a/addons/payment_buckaroo/views/payment_buckaroo_templates.xml b/addons/payment_buckaroo/views/payment_buckaroo_templates.xml index 95f1aa0ce9e..7b0749923c9 100644 --- a/addons/payment_buckaroo/views/payment_buckaroo_templates.xml +++ b/addons/payment_buckaroo/views/payment_buckaroo_templates.xml @@ -2,24 +2,26 @@ diff --git a/addons/payment_paypal/tests/test_paypal.py b/addons/payment_paypal/tests/test_paypal.py index b43aed25fcf..e70129f9333 100644 --- a/addons/payment_paypal/tests/test_paypal.py +++ b/addons/payment_paypal/tests/test_paypal.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from odoo import fields from odoo.addons.payment.models.payment_acquirer import ValidationError from odoo.addons.payment.tests.common import PaymentAcquirerCommon from odoo.addons.payment_paypal.controllers.main import PaypalController @@ -182,7 +183,6 @@ class PaypalForm(PaypalCommon): self.assertEqual(tx.state, 'pending', 'paypal: wrong state after receiving a valid pending notification') self.assertEqual(tx.state_message, 'multi_currency', 'paypal: wrong state message after receiving a valid pending notification') self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification') - self.assertFalse(tx.date, 'paypal: validation date should not be updated whenr receiving pending notification') # update tx tx.write({ @@ -196,4 +196,4 @@ class PaypalForm(PaypalCommon): # check self.assertEqual(tx.state, 'done', 'paypal: wrong state after receiving a valid pending notification') self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification') - self.assertEqual(tx.date, '2013-11-18 11:21:19', 'paypal: wrong validation date') + self.assertEqual(fields.Datetime.to_string(tx.date), '2013-11-18 11:21:19', 'paypal: wrong validation date') diff --git a/addons/payment_stripe/tests/test_stripe.py b/addons/payment_stripe/tests/test_stripe.py index 5308f54dbe0..1ef883de93f 100644 --- a/addons/payment_stripe/tests/test_stripe.py +++ b/addons/payment_stripe/tests/test_stripe.py @@ -59,9 +59,6 @@ class StripeTest(StripeCommon): # render the button res = self.stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values).decode('utf-8') - popup_script_src = 'script src="https://checkout.stripe.com/checkout.js"' - # check form result - self.assertIn(popup_script_src, res, "Stripe: popup script not found in template render") # Generated and received self.assertIn(self.buyer_values.get('partner_email'), res, 'Stripe: email input not found in rendered template') @@ -87,7 +84,7 @@ class StripeTest(StripeCommon): u'id': u'ch_172xfnGMfVJxozLwEjSfpfxD', u'invoice': None, u'livemode': False, - u'metadata': {u'reference': u'SO100'}, + u'metadata': {u'reference': u'SO100-1'}, u'object': u'charge', u'paid': True, u'receipt_email': None, @@ -137,6 +134,7 @@ class StripeTest(StripeCommon): tx.form_feedback(stripe_post_data, 'stripe') self.assertEqual(tx.state, 'done', 'Stripe: validation did not put tx into done state') self.assertEqual(tx.acquirer_reference, stripe_post_data.get('id'), 'Stripe: validation did not update tx id') + stripe_post_data['metadata']['reference'] = u'SO100-2' # reset tx tx = self.env['payment.transaction'].create({ 'amount': 4700, diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 2d17216eadc..a4919e7c17f 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -839,21 +839,18 @@ class PosOrder(models.Model): lots_necessary = True if picking_type: lots_necessary = picking_type and picking_type.use_existing_lots - qty = 0 qty_done = 0 pack_lots = [] pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', move.product_id.id)]) - pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots] - if pack_lot_names and lots_necessary: - for lot_name in list(set(pack_lot_names)): - stock_production_lot = StockProductionLot.search([('name', '=', lot_name), ('product_id', '=', move.product_id.id)]) + if pos_pack_lots and lots_necessary: + for pos_pack_lot in pos_pack_lots: + stock_production_lot = StockProductionLot.search([('name', '=', pos_pack_lot.lot_name), ('product_id', '=', move.product_id.id)]) if stock_production_lot: + # a serialnumber always has a quantity of 1 product, a lot number takes the full quantity of the order line + qty = 1.0 if stock_production_lot.product_id.tracking == 'lot': - # if a lot nr is set through the frontend it will refer to the full quantity - qty = move.product_uom_qty - else: # serial numbers - qty = 1.0 + qty = pos_pack_lot.pos_order_line_id.qty qty_done += qty pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty}) else: diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 4bf06abdc6b..b82bcb38eb5 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1943,16 +1943,11 @@ var PaymentScreenWidget = ScreenWidget.extend({ $('body').keypress(this.keyboard_handler); // that one comes from the pos, but we prefer to cover all the basis $('body').keydown(this.keyboard_keydown_handler); - // legacy vanilla JS listeners - window.document.body.addEventListener('keypress',this.keyboard_handler); - window.document.body.addEventListener('keydown',this.keyboard_keydown_handler); this._super(); }, hide: function(){ $('body').off('keypress', this.keyboard_handler); $('body').off('keydown', this.keyboard_keydown_handler); - window.document.body.removeEventListener('keypress',this.keyboard_handler); - window.document.body.removeEventListener('keydown',this.keyboard_keydown_handler); this._super(); }, // sets up listeners to watch for order changes diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 69568ff9bea..94858d7a98d 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -156,10 +156,7 @@ class ProductTemplate(models.Model): @api.multi def _compute_currency_id(self): - try: - main_company = self.sudo().env.ref('base.main_company') - except ValueError: - main_company = self.env['res.company'].sudo().search([], limit=1, order="id") + main_company = self.env['res.company']._get_main_company() for template in self: template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id @@ -929,6 +926,36 @@ class ProductTemplate(models.Model): return result combination = combination[:-1] + @api.multi + def _get_current_company(self, **kwargs): + """Get the most appropriate company for this product. + + If the company is set on the product, directly return it. Otherwise, + fallback to a contextual company. + + :param kwargs: kwargs forwarded to the fallback method. + + :return: the most appropriate company for this product + :rtype: recordset of one `res.company` + """ + self.ensure_one() + return self.company_id or self._get_current_company_fallback(**kwargs) + + @api.multi + def _get_current_company_fallback(self, **kwargs): + """Fallback to get the most appropriate company for this product. + + This should only be called from `_get_current_company` but is defined + separately to allow override. + + The final fallback will be the current user's company. + + :return: the fallback company for this product + :rtype: recordset of one `res.company` + """ + self.ensure_one() + return self.env.user.company_id + @api.model def get_empty_list_help(self, help): self = self.with_context( diff --git a/addons/product/report/product_product_templates.xml b/addons/product/report/product_product_templates.xml index bf8d0acfc10..2a612f556bc 100644 --- a/addons/product/report/product_product_templates.xml +++ b/addons/product/report/product_product_templates.xml @@ -12,7 +12,7 @@ Price: - + diff --git a/addons/sale/i18n/sale.pot b/addons/sale/i18n/sale.pot index a808ecba1e5..e9f665a9b3b 100644 --- a/addons/sale/i18n/sale.pot +++ b/addons/sale/i18n/sale.pot @@ -1580,6 +1580,11 @@ msgstr "" msgid "Invoicing address:" msgstr "" +#. module: sale +#: model_terms:ir.ui.view,arch_db:sale.sale_order_portal_content +msgid "Invoicing and Shipping Address" +msgstr "" + #. module: sale #: model_terms:ir.ui.view,arch_db:sale.report_saleorder_document msgid "Invoicing and shipping address:" diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index 4da13305464..3691067d106 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -261,7 +261,7 @@ class ProductTemplate(models.Model): if pricelist and pricelist.currency_id != product_template.currency_id: list_price = product_template.currency_id._convert( - list_price, pricelist.currency_id, product_template.company_id, + list_price, pricelist.currency_id, product_template._get_current_company(pricelist=pricelist), fields.Date.today() ) @@ -296,3 +296,11 @@ class ProductTemplate(models.Model): return False combination = self._get_first_possible_combination(parent_combination) return True if combination else self._is_combination_possible(combination, parent_combination) + + @api.multi + def _get_current_company_fallback(self, **kwargs): + """Override: if a pricelist is given, fallback to the company of the + pricelist if it is set, otherwise use the one from parent method.""" + res = super(ProductTemplate, self)._get_current_company_fallback(**kwargs) + pricelist = kwargs.get('pricelist') + return pricelist and pricelist.company_id or res diff --git a/addons/sale/static/src/js/product_configurator_mixin.js b/addons/sale/static/src/js/product_configurator_mixin.js index 458dd5a580c..1a74764aacc 100644 --- a/addons/sale/static/src/js/product_configurator_mixin.js +++ b/addons/sale/static/src/js/product_configurator_mixin.js @@ -281,6 +281,7 @@ var ProductConfiguratorMixin = { /** * Will return a deferred: + * * - If the product already exists, immediately resolves it with the product_id * - If the product does not exist yet ("dynamic" variant creation), this method will * create the product first and then resolve the deferred with the created product's id diff --git a/addons/sale/static/src/js/product_configurator_renderer.js b/addons/sale/static/src/js/product_configurator_renderer.js index 9a613501a2f..899656c8065 100644 --- a/addons/sale/static/src/js/product_configurator_renderer.js +++ b/addons/sale/static/src/js/product_configurator_renderer.js @@ -28,6 +28,7 @@ var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMix * Renders the product configurator within the form * * Will also: + * * - add events handling for variant changes * - trigger variant change to compute the price and other * variant specific changes diff --git a/addons/sale/tests/test_sale_product_attribute_value_config.py b/addons/sale/tests/test_sale_product_attribute_value_config.py index dd36a916778..95b1814f50e 100644 --- a/addons/sale/tests/test_sale_product_attribute_value_config.py +++ b/addons/sale/tests/test_sale_product_attribute_value_config.py @@ -185,6 +185,11 @@ class TestSaleProductAttributeValueConfig(TestSaleProductAttributeValueSetup): do_test(self) def test_02_get_combination_info(self): + # If using multi-company, company_id will be False, and this code should + # still work. + # The case with a company_id will be implicitly tested on website_sale. + self.computer.company_id = False + computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ram_8 = self._get_product_template_attribute_value(self.ram_8) computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1) diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml index 7f0338d1b6d..4962253e810 100644 --- a/addons/sale/views/sale_portal_templates.xml +++ b/addons/sale/views/sale_portal_templates.xml @@ -381,13 +381,16 @@
- Invoicing Address + Invoicing and Shipping Address + Invoicing Address
-
- Shipping Address -
-
+ +
+ Shipping Address +
+
+
diff --git a/addons/sale_timesheet/static/src/js/timesheet_plan.js b/addons/sale_timesheet/static/src/js/timesheet_plan.js index 7a6de7a3cfe..6bd1896b3da 100644 --- a/addons/sale_timesheet/static/src/js/timesheet_plan.js +++ b/addons/sale_timesheet/static/src/js/timesheet_plan.js @@ -73,7 +73,13 @@ var ProjectPlan = AbstractAction.extend({ * @param {string|html} dom */ _refreshPlan: function (dom) { - this.$('.o_content').html(dom); + // TODO: Not forward port this fix on master + var $dom = $(dom); + $dom.find('div.o_timesheet_plan_sale_timesheet_dashboard > table.table, ' + + 'div.o_timesheet_plan_sale_timesheet_people_time > table.table, ' + + 'div.o_project_plan_project_timesheet_forecast > table.table') + .wrap('
'); + this.$('.o_content').html($dom); }, /** diff --git a/addons/sale_timesheet/static/tests/timesheet_plan_tests.js b/addons/sale_timesheet/static/tests/timesheet_plan_tests.js index c54b892d20e..17e9377afb7 100644 --- a/addons/sale_timesheet/static/tests/timesheet_plan_tests.js +++ b/addons/sale_timesheet/static/tests/timesheet_plan_tests.js @@ -48,7 +48,7 @@ QUnit.module('Timesheet Plan', { mockRPC: function (route) { assert.step(route); if (route === '/timesheet/plan') { - return $.when({html_content: 'Banach-Tarski'}); + return $.when({html_content: '

Banach-Tarski

'}); } return this._super.apply(this, arguments); }, @@ -81,7 +81,7 @@ QUnit.module('Timesheet Plan', { action: {name: 'Fibonacci'}, mockRPC: function (route) { if (route === '/timesheet/plan') { - return $.when({html_content: 'Banach-Tarski'}); + return $.when({html_content: '

Banach-Tarski

'}); } return this._super.apply(this, arguments); }, diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 0c45da2985a..5d7ea8a67c0 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -533,6 +533,10 @@ class Picking(models.Model): def action_assign_owner(self): self.move_line_ids.write({'owner_id': self.owner_id.id}) + def action_assign_partner(self): + for picking in self: + picking.move_lines.write({'partner_id': picking.partner_id.id}) + @api.multi def do_print_picking(self): self.write({'printed': True}) diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index cac7000381a..3a75e9744a0 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -248,6 +248,9 @@ +