[MERGE] forward port branch 10.0 up to f69655829a

This commit is contained in:
Christophe Simonis
2017-08-09 15:53:37 +02:00
45 changed files with 381 additions and 104 deletions
@@ -300,7 +300,10 @@ class AccountBankStatement(models.Model):
#try to assign partner to bank_statement_line
stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id]
refs = list(set([st.name for st in st_lines_left if not stl.partner_id]))
if st_lines_left and stl_to_assign_partner and refs:
if st_lines_left and stl_to_assign_partner and refs\
and st_lines_left[0].journal_id.default_credit_account_id\
and st_lines_left[0].journal_id.default_debit_account_id:
sql_query = """SELECT aml.partner_id, aml.ref, stl.id
FROM account_move_line aml
JOIN account_account acc ON acc.id = aml.account_id
@@ -589,13 +592,14 @@ class AccountBankStatementLine(models.Model):
acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true"
select_clause = "SELECT aml.id "
from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id "
where_clause = """WHERE aml.company_id = %(company_id)s
AND (
(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s
AND aml.payment_id IS NOT NULL)
OR
("""+acc_type+""" AND aml.reconciled = false)
)"""
account_clause = ''
if self.journal_id.default_credit_account_id and self.journal_id.default_debit_account_id:
account_clause = "(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s AND aml.payment_id IS NOT NULL) OR"
where_clause = """WHERE aml.company_id = %(company_id)s
AND (
""" + account_clause + """
("""+acc_type+""" AND aml.reconciled = false)
)"""
where_clause = where_clause + ' AND aml.partner_id = %(partner_id)s' if self.partner_id else where_clause
where_clause = where_clause + ' AND aml.id NOT IN %(excluded_ids)s' if excluded_ids else where_clause
if split:
+2 -2
View File
@@ -762,7 +762,6 @@ class AccountInvoice(models.Model):
if tax.amount_type == "group":
for child_tax in tax.children_tax_ids:
done_taxes.append(child_tax.id)
done_taxes.append(tax.id)
res.append({
'invoice_tax_line_id': tax_line.id,
'tax_line_id': tax_line.tax_id.id,
@@ -774,8 +773,9 @@ class AccountInvoice(models.Model):
'account_id': tax_line.account_id.id,
'account_analytic_id': tax_line.account_analytic_id.id,
'invoice_id': self.id,
'tax_ids': [(6, 0, done_taxes)] if tax_line.tax_id.include_base_amount else []
'tax_ids': [(6, 0, list(done_taxes))] if tax_line.tax_id.include_base_amount else []
})
done_taxes.append(tax.id)
return res
def inv_line_characteristic_hashcode(self, invoice_line):
+98 -26
View File
@@ -512,6 +512,7 @@ class AccountMoveLine(models.Model):
{2}
WHERE
a.reconcile IS TRUE
AND l.full_reconcile_id is NULL
{3}
{4}
{5}
@@ -1476,6 +1477,50 @@ class AccountPartialReconcile(models.Model):
move.post()
return line_to_reconcile, partial_rec
# Do not forwardport in master as of 2017-07-20
def _fix_multiple_exchange_rates_diff(self, amls_to_fix, amount_diff, diff_in_currency, currency, move):
self.ensure_one()
move_lines = self.env['account.move.line'].with_context(check_move_validity=False)
partial_reconciles = self.with_context(skip_full_reconcile_check=True)
amount_diff = self.company_id.currency_id.round(amount_diff)
diff_in_currency = currency.round(diff_in_currency)
for aml in amls_to_fix:
account_payable_line = move_lines.create({
'name': _('Currency exchange rate difference'),
'debit': amount_diff < 0 and -aml.amount_residual or 0.0,
'credit': amount_diff > 0 and aml.amount_residual or 0.0,
'account_id': self.debit_move_id.account_id.id,
'move_id': move.id,
'currency_id': currency.id,
'amount_currency': -aml.amount_residual_currency,
'partner_id': self.debit_move_id.partner_id.id,
})
move_lines.create({
'name': _('Currency exchange rate difference'),
'debit': amount_diff > 0 and aml.amount_residual or 0.0,
'credit': amount_diff < 0 and -aml.amount_residual or 0.0,
'account_id': amount_diff > 0 and self.company_id.currency_exchange_journal_id.default_debit_account_id.id or self.company_id.currency_exchange_journal_id.default_credit_account_id.id,
'move_id': move.id,
'currency_id': currency.id,
'amount_currency': aml.amount_residual_currency,
'partner_id': self.debit_move_id.partner_id.id})
partial_rec = super(AccountPartialReconcile, partial_reconciles).create({
'debit_move_id': aml.credit and account_payable_line.id or aml.id,
'credit_move_id': aml.debit and account_payable_line.id or aml.id,
'amount': abs(aml.amount_residual),
'amount_currency': abs(aml.amount_residual_currency),
'currency_id': currency.id,
})
move_lines |= account_payable_line
partial_reconciles |= partial_rec
partial_reconciles._compute_partial_lines()
return move_lines, partial_reconciles
def create_tax_cash_basis_entry(self, percentage_before_rec):
self.ensure_one()
move_date = self.debit_move_id.date
@@ -1578,30 +1623,16 @@ class AccountPartialReconcile(models.Model):
}
return self.env['account.move'].create(move_vals)
@api.model
def create(self, vals):
aml = []
if vals.get('debit_move_id', False):
aml.append(vals['debit_move_id'])
if vals.get('credit_move_id', False):
aml.append(vals['credit_move_id'])
# Get value of matched percentage from both move before reconciliating
lines = self.env['account.move.line'].browse(aml)
if lines[0].account_id.internal_type in ('receivable', 'payable'):
percentage_before_rec = lines._get_matched_percentage()
# Reconcile
res = super(AccountPartialReconcile, self).create(vals)
# if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry
if lines[0].account_id.internal_type in ('receivable', 'payable'):
res.create_tax_cash_basis_entry(percentage_before_rec)
# Do not forwardport in master as of 2017-07-20
def _compute_partial_lines(self):
if self._context.get('skip_full_reconcile_check'):
#when running the manual reconciliation wizard, don't check the partials separately for full
#reconciliation or exchange rate because it is handled manually after the whole processing
return res
return self
#check if the reconcilation is full
#first, gather all journal items involved in the reconciliation just created
partial_rec_set = OrderedDict.fromkeys([x for x in res])
aml_set = self.env['account.move.line']
partial_rec_set = OrderedDict.fromkeys([x for x in self])
aml_set = aml_to_balance = self.env['account.move.line']
total_debit = 0
total_credit = 0
total_amount_currency = 0
@@ -1609,7 +1640,7 @@ class AccountPartialReconcile(models.Model):
#possible to compute the exchange difference entry and it has to be done manually.
currency = list(partial_rec_set)[0].currency_id
maxdate = None
aml_to_balance = None
for partial_rec in partial_rec_set:
if partial_rec.currency_id != currency:
#no exchange rate entry will be created
@@ -1617,7 +1648,7 @@ class AccountPartialReconcile(models.Model):
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in aml_set:
if aml.amount_residual or aml.amount_residual_currency:
aml_to_balance = aml
aml_to_balance |= aml
maxdate = max(aml.date, maxdate)
total_debit += aml.debit
total_credit += aml.credit
@@ -1635,16 +1666,21 @@ class AccountPartialReconcile(models.Model):
aml_ids = aml_set.ids
#then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
digits_rounding_precision = aml_set[0].company_id.currency_id.rounding
if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
exchange_move_id = False
exchange_partial_rec_id = False
if currency and aml_to_balance:
exchange_move = (self.env['account.move']
.create(self.env['account.full.reconcile']
._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id)))
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
rate_diff_aml, rate_diff_partial_rec = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate)
aml_ids.append(rate_diff_aml.id)
partial_rec_ids.append(rate_diff_partial_rec.id)
exchange_move_id = rate_diff_aml.move_id.id
exchange_partial_rec_id = rate_diff_partial_rec.id
rate_diff_amls, rate_diff_partial_recs = partial_rec._fix_multiple_exchange_rates_diff(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move)
aml_ids += rate_diff_amls.ids
partial_rec_ids += rate_diff_partial_recs.ids
exchange_move.post()
exchange_move_id = exchange_move.id
exchange_partial_rec_id = rate_diff_partial_recs[-1:].id
#mark the reference of the full reconciliation on the partial ones and on the entries
self.env['account.full.reconcile'].with_context(check_move_validity=False).create({
'partial_reconcile_ids': [(4, p_id) for p_id in partial_rec_ids],
@@ -1652,6 +1688,25 @@ class AccountPartialReconcile(models.Model):
'exchange_move_id': exchange_move_id,
'exchange_partial_rec_id': exchange_partial_rec_id,
})
# Do not forwardport in master as of 2017-07-20
@api.model
def create(self, vals):
aml = []
if vals.get('debit_move_id', False):
aml.append(vals['debit_move_id'])
if vals.get('credit_move_id', False):
aml.append(vals['credit_move_id'])
# Get value of matched percentage from both move before reconciliating
lines = self.env['account.move.line'].browse(aml)
if lines[0].account_id.internal_type in ('receivable', 'payable'):
percentage_before_rec = lines._get_matched_percentage()
# Reconcile
res = super(AccountPartialReconcile, self).create(vals)
# if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry
if lines[0].account_id.internal_type in ('receivable', 'payable'):
res.create_tax_cash_basis_entry(percentage_before_rec)
res._compute_partial_lines()
return res
@api.multi
@@ -1697,3 +1752,20 @@ class AccountFullReconcile(models.Model):
rec.exchange_move_id = False
to_reverse.reverse_moves()
return super(AccountFullReconcile, self).unlink()
# Do not forwardport in master as of 2017-07-20
@api.model
def _prepare_exchange_diff_move(self, move_date, company):
if not company.currency_exchange_journal_id:
raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
if not company.income_currency_exchange_account_id.id:
raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
if not company.expense_currency_exchange_account_id.id:
raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
res = {'journal_id': company.currency_exchange_journal_id.id}
# The move date should be the maximum date between payment and invoice
# (in case of payment in advance). However, we should make sure the
# move date is not recorded after the end of year closing.
if move_date > company.fiscalyear_lock_date:
res['date'] = move_date
return res
+145 -1
View File
@@ -473,4 +473,148 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(round(aml.debit, 2), line['debit'])
self.assertEquals(round(aml.credit, 2), line['credit'])
self.assertEquals(round(aml.amount_currency, 2), line['amount_currency'])
self.assertEquals(aml.currency_id.id, line['currency_id'])
self.assertEquals(aml.currency_id.id, line['currency_id'])
def test_partial_reconcile_currencies(self):
# client Account (payable, rsa)
# Debit Credit
# --------------------------------------------------------
# Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100
# Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66
# Pay c: 25/0.8 = 31.25 |
#
# Debit_currency = 100 | Credit currency = 100
# Debit = 147.91 | Credit = 166.66
# Balance Debit = 18.75
# Counterpart Credit goes in Exchange diff
dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.env.ref('base.main_company').id)], limit=1)
account_expenses = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1)
self.bank_journal_euro.write({'default_debit_account_id': self.account_rsa.id,
'default_credit_account_id': self.account_rsa.id})
dest_journal_id.write({'default_debit_account_id': self.account_rsa.id,
'default_credit_account_id': self.account_rsa.id})
# Setting up rates for USD (main_company is in EUR)
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01',
'rate': 0.5,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '08' + '-01',
'rate': 0.75,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '09' + '-01',
'rate': 0.80,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
# Preparing Invoices (from vendor)
invoice_a = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
'reference_type': 'none',
'currency_id': self.currency_usd_id,
'name': 'invoice to vendor',
'account_id': self.account_rsa.id,
'type': 'in_invoice',
'date_invoice': time.strftime('%Y') + '-' + '07' + '-01',
})
self.account_invoice_line_model.create({'product_id': self.product.id,
'quantity': 1,
'price_unit': 50,
'invoice_id': invoice_a.id,
'name': 'product that cost ' + str(50),
'account_id': account_expenses.id,
})
invoice_b = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
'reference_type': 'none',
'currency_id': self.currency_usd_id,
'name': 'invoice to vendor',
'account_id': self.account_rsa.id,
'type': 'in_invoice',
'date_invoice': time.strftime('%Y') + '-' + '08' + '-01',
})
self.account_invoice_line_model.create({'product_id': self.product.id,
'quantity': 1,
'price_unit': 50,
'invoice_id': invoice_b.id,
'name': 'product that cost ' + str(50),
'account_id': account_expenses.id,
})
invoice_a.action_invoice_open()
invoice_b.action_invoice_open()
# Preparing Payments
# One partial for invoice_a (fully assigned to it)
payment_a = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 25,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '07' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
# One that will complete the payment of a, the rest goes to b
payment_b = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 50,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '08' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
# The last one will complete the payment of b
payment_c = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 25,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '09' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
payment_a.post()
payment_b.post()
payment_c.post()
# Assigning payments to invoices
debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
invoice_a.assign_outstanding_credit(debit_line_a.id)
invoice_a.assign_outstanding_credit(debit_line_b.id)
invoice_b.assign_outstanding_credit(debit_line_b.id)
invoice_b.assign_outstanding_credit(debit_line_c.id)
# Asserting correctness (only in the payable account)
full_reconcile = False
for inv in (invoice_a + invoice_b):
self.assertTrue(inv.reconciled)
for aml in (inv.payment_move_line_ids + inv.move_id.line_ids).filtered(lambda l: l.account_id == self.account_rsa):
self.assertEqual(aml.amount_residual, 0.0)
self.assertEqual(aml.amount_residual_currency, 0.0)
self.assertTrue(aml.reconciled)
if not full_reconcile:
full_reconcile = aml.full_reconcile_id
else:
self.assertTrue(aml.full_reconcile_id == full_reconcile)
full_rec_move = full_reconcile.exchange_move_id
# Globally check whether the amount is correct
self.assertEqual(full_rec_move.amount, 18.75)
# Checking if the direction of the move is correct
full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
self.assertEqual(full_rec_payable.balance, 18.75)
+1 -1
View File
@@ -16,7 +16,7 @@
<form string="Configure Accounting" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
+1 -1
View File
@@ -7,7 +7,7 @@
<form string="General Settings" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
+3
View File
@@ -272,6 +272,9 @@ class EventEvent(models.Model):
for attendee in self.registration_ids.filtered(filter_func):
self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send)
@api.multi
def _is_event_registrable(self):
return True
class EventRegistration(models.Model):
_name = 'event.registration'
@@ -9,7 +9,7 @@
<form class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Events">
<field name="module_event_sale" widget="radio"/>
+6
View File
@@ -24,6 +24,12 @@ class Event(models.Model):
event_ticket_ids = fields.One2many('event.event.ticket', 'event_id', string='Event Ticket',
default=lambda self: self._default_tickets(), copy=True)
@api.multi
def _is_event_registrable(self):
self.ensure_one()
if not self.event_ticket_ids:
return True
return all(self.event_ticket_ids.with_context(active_test=False).mapped(lambda t: t.product_id.active))
class EventTicket(models.Model):
@@ -8,7 +8,7 @@
<form string="Configuration" class= "oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Settings">
<field name="group_attendance_use_pin" widget="radio"/>
@@ -27,4 +27,4 @@
<menuitem id="hr_attendance.menu_hr_attendance_settings" name="Configuration" parent="menu_hr_attendance_root"
sequence="99" action="action_hr_attendance_settings" groups="hr_attendance.group_hr_attendance_manager"/>
</odoo>
</odoo>
+1 -1
View File
@@ -384,7 +384,7 @@ class HrExpenseSheet(models.Model):
_order = "accounting_date desc, id desc"
name = fields.Char(string='Expense Report Summary', required=True)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
state = fields.Selection([('submit', 'Submitted'),
('approve', 'Approved'),
('post', 'Posted'),
+1 -1
View File
@@ -392,7 +392,7 @@
<field name="accounting_date"/>
<field name="employee_id"/>
<field name="currency_id" invisible="1"/>
<field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="total_amount" sum="Total Amount"/>
<field name="state"/>
<field name="message_unread" invisible="1"/>
</tree>
+1 -1
View File
@@ -9,7 +9,7 @@
<form class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Expenses" name="config_alias">
<label for="id" string="Expense Email Alias"/>
+1 -1
View File
@@ -15,7 +15,7 @@
<form string="Configure Payroll" class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Payroll">
<label for="id" string="Settings"/>
@@ -8,7 +8,7 @@
<form string="Configure HR Recruitment" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Job Posting</h2>
+1
View File
@@ -581,6 +581,7 @@ class MailTemplate(models.Model):
'name': attachment[0],
'datas_fname': attachment[0],
'datas': attachment[1],
'type': 'binary',
'res_model': 'mail.message',
'res_id': mail.mail_message_id.id,
}
+1
View File
@@ -1717,6 +1717,7 @@ class MailThread(models.AbstractModel):
data_attach = {
'name': name,
'datas': base64.b64encode(str(content)),
'type': 'binary',
'datas_fname': name,
'description': name,
'res_model': message_data['model'],
@@ -7,7 +7,7 @@
<form string="Configure Mass Mailing" class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Mass Mailing" name="mass_mailing_group">
<field name="group_mass_mailing_campaign" widget="radio"/>
@@ -8,7 +8,7 @@
<form string="Configure Manufacturing" class= "oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Logistics" name="logistics">
<field name="company_id" invisible="1"/>
+7 -1
View File
@@ -15,6 +15,12 @@ _logger = logging.getLogger(__name__)
# changelog https://stripe.com/docs/upgrades#api-changelog
STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal
INT_CURRENCIES = [
'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', 'RWF', 'KRW', 'VUV',
'VND', 'XOF'
];
class PaymentAcquirerStripe(models.Model):
_inherit = 'payment.acquirer'
@@ -84,7 +90,7 @@ class PaymentTransactionStripe(models.Model):
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
charge_params = {
'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support)
'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else self.amount*100),
'currency': self.currency_id.name,
'metadata[reference]': self.reference
}
+26 -6
View File
@@ -1,6 +1,14 @@
odoo.define('payment_stripe.stripe', function(require) {
"use strict";
var ajax = require('web.ajax');
// The following currencies are integer only, see
// https://stripe.com/docs/currencies#zero-decimal
var int_currencies = [
'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí',
'RWF', 'KRW', 'VUV', 'VND', 'XOF'
];
var handler = StripeCheckout.configure({
key: $("input[name='stripe_key']").val(),
image: $("input[name='stripe_image']").val(),
@@ -50,19 +58,31 @@ odoo.define('payment_stripe.stripe', function(require) {
e.preventDefault();
if ($('.o_website_payment').length !== 0) {
var currency = $("input[name='currency']").val();
var amount = parseFloat($("input[name='amount']").val() || '0.0');
if (!_.contains(int_currencies, currency)) {
amount = amount*100;
}
ajax.jsonRpc('/website_payment/transaction', 'call', {
reference: $("input[name='invoice_num']").val(),
amount: $("input[name='amount']").val(),
currency_id: $("input[name='currency_id']").val(),
amount: amount,
currency_id: currency,
acquirer_id: acquirer_id
})
handler.open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
currency: $("input[name='currency']").val(),
amount: $("input[name='amount']").val()*100
currency: currency,
amount: amount,
});
} else {
var currency = $("input[name='currency']").val();
var amount = parseFloat($("input[name='amount']").val() || '0.0');
if (!_.contains(int_currencies, currency)) {
amount = amount*100;
}
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', {
so_id: so_id,
so_token: so_token
@@ -71,8 +91,8 @@ odoo.define('payment_stripe.stripe', function(require) {
handler.open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
currency: $("input[name='currency']").val(),
amount: $("input[name='amount']").val()*100
currency: currency,
amount: amount,
});
});
}
+1 -1
View File
@@ -820,7 +820,7 @@ class PosOrderLine(models.Model):
def create(self, values):
if values.get('order_id') and not values.get('name'):
# set name based on the sequence specified on the config
config_id = self.env['pos.order'].browse(values['order_id']).session_id.config_id.id
config_id = self.order_id.browse(values['order_id']).session_id.config_id.id
# HACK: sequence created in the same transaction as the config
# cf TODO master is pos.config create
# remove me saas-15
@@ -356,12 +356,13 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.')
# I generate an invoice from the order
self.invoice = self.pos_order_pos1.action_pos_order_invoice()
res = self.pos_order_pos1.action_pos_order_invoice()
self.assertIn('res_id', res, "No invoice created")
# I test that the total of the attached invoice is correct
self.amount_total = self.pos_order_pos1.amount_total
invoice = self.env['account.invoice'].browse(res['res_id'])
self.assertEqual(
float_compare(self.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct")
float_compare(invoice.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct")
"""In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports"""
@@ -7,7 +7,7 @@
<form class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Importable Point of Sale Data">
<label for="div" string="Beverages"/>
+3 -1
View File
@@ -318,7 +318,9 @@ class ProductProduct(models.Model):
@api.model
def create(self, vals):
product = super(ProductProduct, self.with_context(create_product_product=True)).create(vals)
product._set_standard_price(vals.get('standard_price', 0.0))
# When a unique variant is created from tmpl then the standard price is set by _set_standard_price
if not (self.env.context.get('create_from_tmpl') and len(product.product_tmpl_id.product_variant_ids) == 1):
product._set_standard_price(vals.get('standard_price') or 0.0)
return product
@api.multi
+1 -2
View File
@@ -111,8 +111,7 @@ class Pricelist(models.Model):
uom_id = self._context['uom']
if uom_id:
# rebrowse with uom if given
product_ids = [item[0].id for item in products_qty_partner]
products = self.env['product.product'].with_context(uom=uom_id).browse(product_ids)
products = [item[0].with_context(uom=uom_id) for item in products_qty_partner]
products_qty_partner = [(products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner)]
else:
products = [item[0] for item in products_qty_partner]
+4 -2
View File
@@ -163,7 +163,9 @@ class ProductTemplate(models.Model):
# Support context pricelists specified as display_name or ID for compatibility
if isinstance(pricelist_id_or_name, basestring):
pricelist = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
pricelist_data = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
if pricelist_data:
pricelist = self.env['product.pricelist'].browse(pricelist_data[0][0])
elif isinstance(pricelist_id_or_name, (int, long)):
pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name)
@@ -279,7 +281,7 @@ class ProductTemplate(models.Model):
tools.image_resize_images(vals)
template = super(ProductTemplate, self).create(vals)
if "create_product_product" not in self._context:
template.create_variant_ids()
template.with_context(create_from_tmpl=True).create_variant_ids()
# This is needed to set given values to first variant after creation
related_vals = {}
+1 -1
View File
@@ -8,7 +8,7 @@
<form class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Project Management" name="project_management">
<field name="company_id" invisible="1"/>
+1 -1
View File
@@ -7,7 +7,7 @@
<form string="Configure Purchases" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="po_double_validation" invisible="1"/>
+2 -2
View File
@@ -8,7 +8,7 @@
<form string="Configure Sales" class="oe_form_configuration" name="sale_config_form">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2 id="integration_h2" invisible="1">Integrations</h2>
@@ -84,4 +84,4 @@
<field name="view_mode">form</field>
<field name="target">inline</field>
</record>
</odoo>
</odoo>
@@ -8,7 +8,7 @@
<form string="Configure Warehouse" class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<group string="Traceability" name="traceability">
<field name="group_stock_production_lot" widget="radio"/>
+6 -6
View File
@@ -662,7 +662,7 @@ class Database(http.Controller):
request.session.authenticate(name, post['login'], password)
return http.local_redirect('/web/')
except Exception, e:
error = "Database creation error: %s" % str(e) or repr(e)
error = "Database creation error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False)
@@ -673,7 +673,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name])
return http.local_redirect('/web/database/manager')
except Exception, e:
error = "Database duplication error: %s" % str(e) or repr(e)
error = "Database duplication error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/drop', type='http', auth="none", methods=['POST'], csrf=False)
@@ -683,7 +683,7 @@ class Database(http.Controller):
request._cr = None # dropping a database leads to an unusable cursor
return http.local_redirect('/web/database/manager')
except Exception, e:
error = "Database deletion error: %s" % str(e) or repr(e)
error = "Database deletion error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False)
@@ -701,7 +701,7 @@ class Database(http.Controller):
return response
except Exception, e:
_logger.exception('Database.backup')
error = "Database backup error: %s" % str(e) or repr(e)
error = "Database backup error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
@@ -711,7 +711,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)])
return http.local_redirect('/web/database/manager')
except Exception, e:
error = "Database restore error: %s" % str(e) or repr(e)
error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
@@ -720,7 +720,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', [master_pwd, master_pwd_new])
return http.local_redirect('/web/database/manager')
except Exception, e:
error = "Master password update error: %s" % str(e) or repr(e)
error = "Master password update error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/list', type='json', auth='none')
+1 -1
View File
@@ -941,7 +941,7 @@ var FormView = View.extend(common.FieldManagerMixin, {
var self = this;
return this.reload_mutex.exec(function() {
if (self.dataset.index === null || self.dataset.index === undefined) {
self.trigger("previous_view");
self.do_action('reload');
return $.Deferred().reject().promise();
}
if (self.dataset.index < 0) {
@@ -416,8 +416,9 @@ animation.registry.media_video = animation.Class.extend({
if (!this.$target.has('> iframe').length) {
var editor = '<div class="css_editable_mode_display">&nbsp;</div>';
var size = '<div class="media_iframe_video_size">&nbsp;</div>';
this.$target.html(editor+size+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
this.$target.html(editor+size);
}
this.$target.html(this.$target.html()+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
return this._super.apply(this, arguments);
},
});
+1 -1
View File
@@ -9,7 +9,7 @@
<form string="Configure Website" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<div>
+1 -3
View File
@@ -165,13 +165,11 @@ class WebsiteEventController(http.Controller):
@http.route(['/event/<model("event.event"):event>/register'], type='http', auth="public", website=True)
def event_register(self, event, **post):
if event.state == 'done':
return request.redirect("/event/%s" % slug(event))
values = {
'event': event,
'main_object': event,
'range': range,
'registrable': event._is_event_registrable()
}
return request.render("website_event.event_description_full", values)
@@ -309,7 +309,8 @@
<template id="event_description_full">
<t t-call="website_event.event_details">
<div class="col-md-8">
<t t-call="website_event.registration_template"/>
<div t-if="event.state == 'done' or not registrable" class="alert alert-info">Registration for this event is now closed</div>
<t t-if="event.state != 'done' and registrable" t-call="website_event.registration_template"/>
<div class="clearfix"/>
<hr/>
<div itemprop="description" t-field="event.description"></div>
+1 -10
View File
@@ -3,7 +3,6 @@
from odoo import http, _
from odoo.addons.website_event.controllers.main import WebsiteEventController
from odoo.addons.website.models.website import slug
from odoo.http import request
@@ -11,16 +10,8 @@ class WebsiteEventSaleController(WebsiteEventController):
@http.route(['/event/<model("event.event"):event>/register'], type='http', auth="public", website=True)
def event_register(self, event, **post):
if event.state == 'done':
return request.redirect("/event/%s" % slug(event))
event = event.with_context(pricelist=request.website.get_current_pricelist().id)
values = {
'event': event,
'main_object': event,
'range': range,
}
return request.render("website_event.event_description_full", values)
return super(WebsiteEventSaleController, self).event_register(event, **post)
def _process_tickets_details(self, data):
ticket_post = {}
+5 -5
View File
@@ -1206,11 +1206,11 @@
<div class="clearfix" />
<t t-if="mode == ('new', 'billing')">
<div t-attf-class="form-group #{error.get('company_name') and 'has-error' or ''} col-md-6">
<label class="control-label font-weight-normal" for="company_name">Company Name</label>
<label class="control-label font-weight-normal label-optional" for="company_name">Company Name</label>
<input type="text" name="company_name" class="form-control" t-att-value="'company_name' in checkout and checkout['company_name']" />
</div>
<div t-attf-class="form-group #{error.get('vat') and 'has-error' or ''} col-md-6 div_vat">
<label class="control-label font-weight-normal" for="vat">TIN / VAT </label>
<label class="control-label font-weight-normal label-optional" for="vat">TIN / VAT </label>
<input type="text" name="vat" class="form-control" t-att-value="'vat' in checkout and checkout['vat']" />
</div>
</t>
@@ -1220,14 +1220,14 @@
<input type="text" name="street" class="form-control" t-att-value="'street' in checkout and checkout['street']" />
</div>
<div t-attf-class="form-group #{error.get('street2') and 'has-error' or ''} col-md-12 div_street2">
<label class="control-label" for="street2">Street 2</label>
<label class="control-label label-optional" for="street2">Street 2</label>
<input type="text" name="street2" class="form-control" t-att-value="'street2' in checkout and checkout['street2']" />
</div>
<div class="clearfix" />
<t t-set='zip_city' t-value='country and [x for x in country.get_address_fields() if x in ["zip", "city"]] or ["city", "zip"]'/>
<t t-if="'zip' in zip_city and zip_city.index('zip') &lt; zip_city.index('city')">
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-sm-4 div_zip">
<label class="control-label" for="zip">Zip Code</label>
<label class="control-label label-optional" for="zip">Zip Code</label>
<input type="text" name="zip" class="form-control" t-att-value="'zip' in checkout and checkout['zip']" />
</div>
</t>
@@ -1237,7 +1237,7 @@
</div>
<t t-if="'zip' in zip_city and zip_city.index('zip') &gt; zip_city.index('city')">
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-sm-4 div_zip">
<label class="control-label" for="zip">Zip Code</label>
<label class="control-label label-optional" for="zip">Zip Code</label>
<input type="text" name="zip" class="form-control" t-att-value="'zip' in checkout and checkout['zip']" />
</div>
</t>
+14
View File
@@ -0,0 +1,14 @@
Finland, 2017-08-01
Vizucom Oy agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0.
We declare that we are authorized and able to make this agreement and sign this declaration.
Signed,
Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
List of contributors:
Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
Henri Alasentie henri.alasentie@vizucom.com https://github.com/halasentie
+1 -1
View File
@@ -208,7 +208,7 @@ class CurrencyRate(models.Model):
rate = fields.Float(digits=(12, 6), help='The rate of the currency to the currency of rate 1')
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True)
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.user._get_company())
default=lambda self: self.env.user.company_id)
@api.model
def name_search(self, name, args=None, operator='ilike', limit=80):
+1
View File
@@ -1440,6 +1440,7 @@ class Root(object):
httprequest = werkzeug.wrappers.Request(environ)
httprequest.app = self
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict
threading.current_thread().url = httprequest.url
explicit_session = self.setup_session(httprequest)
self.setup_db(httprequest)
+2
View File
@@ -163,6 +163,8 @@ def application_unproxied(environ, start_response):
del threading.current_thread().uid
if hasattr(threading.current_thread(), 'dbname'):
del threading.current_thread().dbname
if hasattr(threading.current_thread(), 'url'):
del threading.current_thread().url
with odoo.api.Environment.manage():
# Try all handlers until one returns some result (i.e. not None).
+7 -1
View File
@@ -393,11 +393,17 @@ class HttpCase(TransactionCase):
t0 = int(time.time())
for thread in threading.enumerate():
if thread.name.startswith('odoo.service.http.request.'):
thread.join_retry_count = 10
while thread.isAlive():
# Need a busyloop here as thread.join() masks signals
# and would prevent the forced shutdown.
thread.join(0.05)
time.sleep(0.05)
thread.join_retry_count -= 1
if thread.join_retry_count < 0:
_logger.warning("Stop waiting for thread %s handling request for url %s",
thread.name, thread.url)
break
time.sleep(0.5)
t1 = int(time.time())
if t0 != t1:
_logger.info('remaining requests')
+5 -3
View File
@@ -1038,15 +1038,17 @@ def dumpstacks(sig=None, frame=None):
# modified for python 2.5 compatibility
threads_info = {th.ident: {'name': th.name,
'uid': getattr(th, 'uid', 'n/a'),
'dbname': getattr(th, 'dbname', 'n/a')}
'dbname': getattr(th, 'dbname', 'n/a'),
'url': getattr(th, 'url', 'n/a')}
for th in threading.enumerate()}
for threadId, stack in sys._current_frames().items():
thread_info = threads_info.get(threadId, {})
code.append("\n# Thread: %s (id:%s) (db:%s) (uid:%s)" %
code.append("\n# Thread: %s (id:%s) (db:%s) (uid:%s) (url:%s)" %
(thread_info.get('name', 'n/a'),
threadId,
thread_info.get('dbname', 'n/a'),
thread_info.get('uid', 'n/a')))
thread_info.get('uid', 'n/a'),
thread_info.get('url', 'n/a')))
for line in extract_stack(stack):
code.append(line)