[MERGE] forward port branch 9.0 up to 58cbcba

This commit is contained in:
Christophe Simonis
2016-08-19 16:59:29 +02:00
64 changed files with 521 additions and 318 deletions
+11 -2
View File
@@ -231,7 +231,7 @@ class AccountJournal(models.Model):
#groups_id = fields.Many2many('res.groups', 'account_journal_group_rel', 'journal_id', 'group_id', string='Groups') #groups_id = fields.Many2many('res.groups', 'account_journal_group_rel', 'journal_id', 'group_id', string='Groups')
currency_id = fields.Many2one('res.currency', help='The currency used to enter statement', string="Currency", oldname='currency') currency_id = fields.Many2one('res.currency', help='The currency used to enter statement', string="Currency", oldname='currency')
company_id = fields.Many2one('res.company', string='Company', required=True, index=1, default=lambda self: self.env.user.company_id, company_id = fields.Many2one('res.company', string='Company', required=True, index=True, default=lambda self: self.env.user.company_id,
help="Company related to this journal") help="Company related to this journal")
refund_sequence = fields.Boolean(string='Dedicated Refund Sequence', help="Check this box if you don't want to share the same sequence for invoices and refunds made from this journal", default=False) refund_sequence = fields.Boolean(string='Dedicated Refund Sequence', help="Check this box if you don't want to share the same sequence for invoices and refunds made from this journal", default=False)
@@ -632,7 +632,16 @@ class AccountTax(models.Model):
if self.amount_type == 'fixed': if self.amount_type == 'fixed':
# Use copysign to take into account the sign of the base amount which includes the sign # Use copysign to take into account the sign of the base amount which includes the sign
# of the quantity and the sign of the price_unit # of the quantity and the sign of the price_unit
return math.copysign(quantity, base_amount) * self.amount # Amount is the fixed price for the tax, it can be negative
# Base amount included the sign of the quantity and the sign of the unit price and when
# a product is returned, it can be done either by changing the sign of quantity or by changing the
# sign of the price unit.
# When the price unit is equal to 0, the sign of the quantity is absorbed in base_amount then
# a "else" case is needed.
if base_amount:
return math.copysign(quantity, base_amount) * self.amount
else:
return quantity * self.amount
if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include): if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include):
return base_amount * self.amount / 100 return base_amount * self.amount / 100
if self.amount_type == 'percent' and self.price_include: if self.amount_type == 'percent' and self.price_include:
+151 -125
View File
@@ -129,7 +129,7 @@ class AccountBankStatement(models.Model):
name = fields.Char(string='Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True) name = fields.Char(string='Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True)
reference = fields.Char(string='External Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True, help="Used to hold the reference of the external mean that created this statement (name of imported file, reference of online synchronization...)") reference = fields.Char(string='External Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True, help="Used to hold the reference of the external mean that created this statement (name of imported file, reference of online synchronization...)")
date = fields.Date(required=True, states={'confirm': [('readonly', True)]}, select=True, copy=False, default=fields.Date.context_today) date = fields.Date(required=True, states={'confirm': [('readonly', True)]}, index=True, copy=False, default=fields.Date.context_today)
date_done = fields.Datetime(string="Closed On") date_done = fields.Datetime(string="Closed On")
balance_start = fields.Monetary(string='Starting Balance', states={'confirm': [('readonly', True)]}, default=_default_opening_balance) balance_start = fields.Monetary(string='Starting Balance', states={'confirm': [('readonly', True)]}, default=_default_opening_balance)
balance_end_real = fields.Monetary('Ending Balance', states={'confirm': [('readonly', True)]}) balance_end_real = fields.Monetary('Ending Balance', states={'confirm': [('readonly', True)]})
@@ -289,58 +289,54 @@ class AccountBankStatement(models.Model):
""" """
statements = self statements = self
bsl_obj = self.env['account.bank.statement.line'] bsl_obj = self.env['account.bank.statement.line']
# NB : The field account_id can be used at the statement line creation/import to avoid the reconciliation process on it later on, # NB : The field account_id can be used at the statement line creation/import to avoid the reconciliation process on it later on,
# this is why we filter out statements lines where account_id is set # this is why we filter out statements lines where account_id is set
st_lines_filter = [('journal_entry_ids', '=', False), ('account_id', '=', False)]
sql_query = """SELECT stl.id
FROM account_bank_statement_line stl
WHERE account_id IS NULL AND not exists (select 1 from account_move m where m.statement_line_id = stl.id)
AND company_id = %s
"""
params = (self.env.user.company_id.id,)
if statements: if statements:
st_lines_filter += [('statement_id', 'in', statements.ids)] sql_query += ' AND stl.statement_id IN %s'
params += (tuple(statements.ids),)
sql_query += ' ORDER BY stl.id'
self.env.cr.execute(sql_query, params)
st_lines_left = self.env['account.bank.statement.line'].browse([line.get('id') for line in self.env.cr.dictfetchall()])
# Try to automatically reconcile statement lines #try to assign partner to bank_statement_line
automatic_reconciliation_entries = [] stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id]
st_lines_left = self.env['account.bank.statement.line'] refs = list(set([st.name for st in st_lines_left if not stl.partner_id]))
for st_line in bsl_obj.search(st_lines_filter): if st_lines_left and stl_to_assign_partner and refs:
res = st_line.auto_reconcile() sql_query = """SELECT aml.partner_id, aml.ref, stl.id
if not res: FROM account_move_line aml
st_lines_left = (st_lines_left | st_line) JOIN account_account acc ON acc.id = aml.account_id
else: JOIN account_bank_statement_line stl ON aml.ref = stl.name
automatic_reconciliation_entries.append(res.ids) WHERE (aml.company_id = %s
AND aml.partner_id IS NOT NULL)
# Try to set statement line's partner AND (
for st_line in st_lines_left: (aml.statement_id IS NULL AND aml.account_id IN %s)
if st_line.name and not st_line.partner_id: OR
additional_domain = [('ref', '=', st_line.name)] (acc.internal_type IN ('payable', 'receivable') AND aml.reconciled = false)
match_recs = st_line.get_move_lines_for_reconciliation(limit=1, additional_domain=additional_domain, overlook_partner=True) )
if match_recs and match_recs[0].partner_id: AND aml.ref IN %s
st_line.write({'partner_id': match_recs[0].partner_id.id}) """
params = (self.env.user.company_id.id, (st_lines_left[0].journal_id.default_credit_account_id.id, st_lines_left[0].journal_id.default_debit_account_id.id), tuple(refs))
# Collect various informations for the reconciliation widget if statements:
notifications = [] sql_query += 'AND stl.id IN %s'
num_auto_reconciled = len(automatic_reconciliation_entries) params += (tuple(stl_to_assign_partner),)
if num_auto_reconciled > 0: self.env.cr.execute(sql_query, params)
auto_reconciled_message = num_auto_reconciled > 1 \ results = self.env.cr.dictfetchall()
and _("%d transactions were automatically reconciled.") % num_auto_reconciled \ st_line = self.env['account.bank.statement.line']
or _("1 transaction was automatically reconciled.") for line in results:
notifications += [{ st_line.browse(line.get('id')).write({'partner_id': line.get('partner_id')})
'type': 'info',
'message': auto_reconciled_message,
'details': {
'name': _("Automatically reconciled items"),
'model': 'account.move',
'ids': automatic_reconciliation_entries
}
}]
lines = []
for el in statements:
lines.extend(el.line_ids.ids)
lines = list(set(lines))
return { return {
'st_lines_ids': st_lines_left.ids, 'st_lines_ids': st_lines_left.ids,
'notifications': notifications, 'notifications': [],
'statement_name': len(statements) == 1 and statements[0].name or False, 'statement_name': len(statements) == 1 and statements[0].name or False,
'num_already_reconciled_lines': statements and bsl_obj.search_count([('journal_entry_ids', '!=', False), ('id', 'in', lines)]) or 0, 'num_already_reconciled_lines': 0,
} }
@api.multi @api.multi
@@ -449,6 +445,40 @@ class AccountBankStatementLine(models.Model):
# Reconciliation interface methods # Reconciliation interface methods
#################################################### ####################################################
@api.multi
def reconciliation_widget_auto_reconcile(self, num_already_reconciled_lines):
automatic_reconciliation_entries = self.env['account.bank.statement.line']
unreconciled = self.env['account.bank.statement.line']
for stl in self:
res = stl.auto_reconcile()
if res:
automatic_reconciliation_entries += stl
else:
unreconciled += stl
# Collect various informations for the reconciliation widget
notifications = []
num_auto_reconciled = len(automatic_reconciliation_entries)
if num_auto_reconciled > 0:
auto_reconciled_message = num_auto_reconciled > 1 \
and _("%d transactions were automatically reconciled.") % num_auto_reconciled \
or _("1 transaction was automatically reconciled.")
notifications += [{
'type': 'info',
'message': auto_reconciled_message,
'details': {
'name': _("Automatically reconciled items"),
'model': 'account.move',
'ids': automatic_reconciliation_entries.ids
}
}]
return {
'st_lines_ids': unreconciled.ids,
'notifications': notifications,
'statement_name': False,
'num_already_reconciled_lines': num_auto_reconciled + num_already_reconciled_lines,
}
@api.multi @api.multi
def get_data_for_reconciliation_widget(self, excluded_ids=None): def get_data_for_reconciliation_widget(self, excluded_ids=None):
""" Returns the data required to display a reconciliation widget, for each statement line in self """ """ Returns the data required to display a reconciliation widget, for each statement line in self """
@@ -561,86 +591,66 @@ class AccountBankStatementLine(models.Model):
return self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity asc, id asc") return self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity asc, id asc")
def _get_domain_maker_move_line_amount(self): def _get_common_sql_query(self, overlook_partner = False, excluded_ids = None, split = False):
""" Returns a function that can create the appropriate domain to search on move.line amount based on statement.line currency/amount """ acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true"
company_currency = self.journal_id.company_id.currency_id select_clause = "SELECT aml.id "
st_line_currency = self.currency_id or self.journal_id.currency_id from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id "
currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False where_clause = """WHERE aml.company_id = %(company_id)s
field = currency and 'amount_residual_currency' or 'amount_residual' AND (
precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places (aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s)
OR
def ret(comparator, amount, p=precision, f=field, c=currency): ("""+acc_type+""" AND aml.reconciled = false)
if comparator == '<': )"""
if amount < 0: where_clause = where_clause + ' AND aml.partner_id = %(partner_id)s' if self.partner_id else where_clause
domain = [(f, '<', 0), (f, '>', amount)] where_clause = where_clause + ' AND aml.id NOT IN %(excluded_ids)s' if excluded_ids else where_clause
else: if split:
domain = [(f, '>', 0), (f, '<', amount)] return select_clause, from_clause, where_clause
elif comparator == '=': return select_clause + from_clause + where_clause
if f == 'amount_residual':
liquidity_field = amount > 0 and 'debit' or 'credit'
domain = [
'|', (f, '=', float_round(amount, precision_digits=p)),
'&', ('account_id.internal_type', '=', 'liquidity'),
(liquidity_field, '=', amount),
]
else:
domain = [
'|', (f, '=', float_round(amount, precision_digits=p)),
'&', ('account_id.internal_type', '=', 'liquidity'),
('amount_currency', '=', amount),
]
else:
raise UserError(_("Programmation error : domain_maker_move_line_amount requires comparator '=' or '<'"))
domain += [('currency_id', '=', c)]
return domain
return ret
def get_reconciliation_proposition(self, excluded_ids=None): def get_reconciliation_proposition(self, excluded_ids=None):
""" Returns move lines that constitute the best guess to reconcile a statement line """ Returns move lines that constitute the best guess to reconcile a statement line
Note: it only looks for move lines in the same currency as the statement line. Note: it only looks for move lines in the same currency as the statement line.
""" """
self.ensure_one()
if not excluded_ids:
excluded_ids = []
amount = self.amount_currency or self.amount
company_currency = self.journal_id.company_id.currency_id
st_line_currency = self.currency_id or self.journal_id.currency_id
currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False
precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places
params = {'company_id': self.env.user.company_id.id,
'account_payable_receivable': (self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id),
'amount': float_round(amount, precision_digits=precision),
'partner_id': self.partner_id.id,
'excluded_ids': tuple(excluded_ids),
'ref': self.name,
}
# Look for structured communication match # Look for structured communication match
if self.name: if self.name:
overlook_partner = not self.partner_id # If the transaction has no partner, look for match in payable and receivable account anyway add_to_select = ", CASE WHEN aml.ref = %(ref)s THEN 1 ELSE 2 END as temp_field_order "
domain = [('ref', '=', self.name)] add_to_from = " JOIN account_move m ON m.id = aml.move_id "
match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=2, additional_domain=domain, overlook_partner=overlook_partner) select_clause, from_clause, where_clause = self._get_common_sql_query(overlook_partner=True, excluded_ids=excluded_ids, split=True)
if match_recs and len(match_recs) == 1: sql_query = select_clause + add_to_select + from_clause + add_to_from + where_clause
return match_recs sql_query += " AND (aml.ref= %(ref)s or m.name = %(ref)s) \
elif len(match_recs) == 0: ORDER BY temp_field_order, date_maturity asc, aml.id asc"
move = self.env['account.move'].search([('name', '=', self.name)], limit=1) self.env.cr.execute(sql_query, params)
if move: results = self.env.cr.fetchone()
domain = [('move_id', '=', move.id)] if results:
match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=2, additional_domain=domain, overlook_partner=overlook_partner) return self.env['account.move.line'].browse(results[0])
if match_recs and len(match_recs) == 1:
return match_recs
# How to compare statement line amount and move lines amount
amount_domain_maker = self._get_domain_maker_move_line_amount()
amount = self.amount_currency or self.amount
# Look for a single move line with the same amount # Look for a single move line with the same amount
match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=1, additional_domain=amount_domain_maker('=', amount)) field = currency and 'amount_residual_currency' or 'amount_residual'
if match_recs: liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit'
return match_recs sql_query = self._get_common_sql_query(excluded_ids=excluded_ids) + \
" AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = %(amount)s)) \
ORDER BY date_maturity asc, aml.id asc LIMIT 1"
self.env.cr.execute(sql_query, params)
results = self.env.cr.fetchone()
if results:
return self.env['account.move.line'].browse(results[0])
if not self.partner_id: return self.env['account.move.line']
return self.env['account.move.line']
# Select move lines until their total amount is greater than the statement line amount
domain = [('reconciled', '=', False)]
domain += [('account_id.user_type_id.type', '=', amount > 0 and 'receivable' or 'payable')] # Make sure we can't mix receivable and payable
domain += amount_domain_maker('<', amount) # Will also enforce > 0
mv_lines = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=5, additional_domain=domain)
st_line_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id
ret = self.env['account.move.line']
total = 0
for line in mv_lines:
total += line.currency_id and line.amount_residual_currency or line.amount_residual
if float_compare(total, abs(amount), precision_digits=st_line_currency.rounding) != -1:
break
ret = (ret | line)
return ret
def _get_move_lines_for_auto_reconcile(self): def _get_move_lines_for_auto_reconcile(self):
""" Returns the move lines that the method auto_reconcile can use to try to reconcile the statement line """ """ Returns the move lines that the method auto_reconcile can use to try to reconcile the statement line """
@@ -654,28 +664,44 @@ class AccountBankStatementLine(models.Model):
self.ensure_one() self.ensure_one()
match_recs = self.env['account.move.line'] match_recs = self.env['account.move.line']
# How to compare statement line amount and move lines amount amount = self.amount_currency or self.amount
amount_domain_maker = self._get_domain_maker_move_line_amount() company_currency = self.journal_id.company_id.currency_id
equal_amount_domain = amount_domain_maker('=', self.amount_currency or self.amount) st_line_currency = self.currency_id or self.journal_id.currency_id
currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False
precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places
params = {'company_id': self.env.user.company_id.id,
'account_payable_receivable': (self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id),
'amount': float_round(amount, precision_digits=precision),
'partner_id': self.partner_id.id,
'ref': self.name,
}
field = currency and 'amount_residual_currency' or 'amount_residual'
liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit'
# Look for structured communication match # Look for structured communication match
if self.name: if self.name:
overlook_partner = not self.partner_id # If the transaction has no partner, look for match in payable and receivable account anyway sql_query = self._get_common_sql_query() + \
domain = equal_amount_domain + [('ref', '=', self.name)] " AND aml.ref = %(ref)s AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = %(amount)s)) \
match_recs = self.get_move_lines_for_reconciliation(limit=2, additional_domain=domain, overlook_partner=overlook_partner) ORDER BY date_maturity asc, aml.id asc"
if match_recs and len(match_recs) != 1: self.env.cr.execute(sql_query, params)
match_recs = self.env.cr.dictfetchall()
if len(match_recs) > 1:
return False return False
# Look for a single move line with the same partner, the same amount # Look for a single move line with the same partner, the same amount
if not match_recs: if not match_recs:
if self.partner_id: if self.partner_id:
match_recs = self.get_move_lines_for_reconciliation(limit=2, additional_domain=equal_amount_domain) sql_query = self._get_common_sql_query() + \
if match_recs and len(match_recs) != 1: " AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = %(amount)s)) \
ORDER BY date_maturity asc, aml.id asc"
self.env.cr.execute(sql_query, params)
match_recs = self.env.cr.dictfetchall()
if len(match_recs) > 1:
return False return False
if not match_recs: if not match_recs:
return False return False
match_recs = self.env['account.move.line'].browse([aml.get('id') for aml in match_recs])
# Now reconcile # Now reconcile
counterpart_aml_dicts = [] counterpart_aml_dicts = []
payment_aml_rec = self.env['account.move.line'] payment_aml_rec = self.env['account.move.line']
@@ -117,7 +117,7 @@ class account_journal(models.Model):
for i in range(0,6): for i in range(0,6):
if i == 0: if i == 0:
query += "("+select_sql_clause+" and date < '"+start_date.strftime(DF)+"')" query += "("+select_sql_clause+" and date < '"+start_date.strftime(DF)+"')"
elif i == 6: elif i == 5:
query += " UNION ALL ("+select_sql_clause+" and date >= '"+start_date.strftime(DF)+"')" query += " UNION ALL ("+select_sql_clause+" and date >= '"+start_date.strftime(DF)+"')"
else: else:
next_date = start_date + timedelta(days=7) next_date = start_date + timedelta(days=7)
@@ -303,6 +303,7 @@ class account_journal(models.Model):
invoice_type = _journal_invoice_type_map[(self.type, self._context.get('invoice_type'))] invoice_type = _journal_invoice_type_map[(self.type, self._context.get('invoice_type'))]
ctx = self._context.copy() ctx = self._context.copy()
ctx.pop('group_by', None)
ctx.update({ ctx.update({
'journal_type': self.type, 'journal_type': self.type,
'default_journal_id': self.id, 'default_journal_id': self.id,
@@ -336,6 +337,7 @@ class account_journal(models.Model):
'default_payment_type': payment_type, 'default_payment_type': payment_type,
'default_journal_id': self.id 'default_journal_id': self.id
}) })
ctx.pop('group_by', None)
action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_account_payments') action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_account_payments')
if action_rec: if action_rec:
action = action_rec.read([])[0] action = action_rec.read([])[0]
@@ -351,6 +353,7 @@ class account_journal(models.Model):
ctx = dict(self.env.context, default_journal_id=self.id) ctx = dict(self.env.context, default_journal_id=self.id)
if ctx.get('search_default_journal', False): if ctx.get('search_default_journal', False):
ctx.update(search_default_journal_id=self.id) ctx.update(search_default_journal_id=self.id)
ctx.pop('group_by', None)
ir_model_obj = self.pool['ir.model.data'] ir_model_obj = self.pool['ir.model.data']
model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account', action_name) model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account', action_name)
action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context) action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context)
+16 -8
View File
@@ -89,7 +89,7 @@ class AccountMove(models.Model):
company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True, company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True,
default=lambda self: self.env.user.company_id) default=lambda self: self.env.user.company_id)
matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method")
statement_line_id = fields.Many2one('account.bank.statement.line', string='Bank statement line reconciled with this entry', copy=False, readonly=True) statement_line_id = fields.Many2one('account.bank.statement.line', index=True, string='Bank statement line reconciled with this entry', copy=False, readonly=True)
# Dummy Account field to search on account.move by account_id # Dummy Account field to search on account.move by account_id
dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False) dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False)
@@ -230,6 +230,16 @@ class AccountMoveLine(models.Model):
_description = "Journal Item" _description = "Journal Item"
_order = "date desc, id desc" _order = "date desc, id desc"
def init(self, cr):
""" change index on partner_id to a multi-column index on (partner_id, ref), the new index will behave in the
same way when we search on partner_id, with the addition of being optimal when having a query that will
search on partner_id and ref at the same time (which is the case when we open the bank reconciliation widget)
"""
cr.execute('DROP INDEX IF EXISTS account_move_line_partner_id_index')
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('account_move_line_partner_id_ref_idx',))
if not cr.fetchone():
cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)')
@api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state') @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state')
def _amount_residual(self): def _amount_residual(self):
""" Computes the residual amount of a move line from a reconciliable account in the company currency and the line's currency. """ Computes the residual amount of a move line from a reconciliable account in the company currency and the line's currency.
@@ -363,7 +373,7 @@ class AccountMoveLine(models.Model):
move_id = fields.Many2one('account.move', string='Journal Entry', ondelete="cascade", move_id = fields.Many2one('account.move', string='Journal Entry', ondelete="cascade",
help="The move of this entry line.", index=True, required=True, auto_join=True) help="The move of this entry line.", index=True, required=True, auto_join=True)
narration = fields.Text(related='move_id.narration', string='Internal Note') narration = fields.Text(related='move_id.narration', string='Internal Note')
ref = fields.Char(related='move_id.ref', string='Partner Reference', store=True, copy=False) ref = fields.Char(related='move_id.ref', string='Partner Reference', store=True, copy=False, index=True)
payment_id = fields.Many2one('account.payment', string="Originator Payment", help="Payment that created this entry") payment_id = fields.Many2one('account.payment', string="Originator Payment", help="Payment that created this entry")
statement_id = fields.Many2one('account.bank.statement', string='Statement', statement_id = fields.Many2one('account.bank.statement', string='Statement',
help="The bank statement used for bank reconciliation", index=True, copy=False) help="The bank statement used for bank reconciliation", index=True, copy=False)
@@ -389,7 +399,7 @@ class AccountMoveLine(models.Model):
# TODO: put the invoice link and partner_id on the account_move # TODO: put the invoice link and partner_id on the account_move
invoice_id = fields.Many2one('account.invoice', oldname="invoice") invoice_id = fields.Many2one('account.invoice', oldname="invoice")
partner_id = fields.Many2one('res.partner', string='Partner', index=True, ondelete='restrict') partner_id = fields.Many2one('res.partner', string='Partner', ondelete='restrict')
user_type_id = fields.Many2one('account.account.type', related='account_id.user_type_id', index=True, store=True, oldname="user_type") user_type_id = fields.Many2one('account.account.type', related='account_id.user_type_id', index=True, store=True, oldname="user_type")
_sql_constraints = [ _sql_constraints = [
@@ -1134,8 +1144,6 @@ class AccountMoveLine(models.Model):
@api.multi @api.multi
def write(self, vals): def write(self, vals):
if vals.get('tax_line_id') or vals.get('tax_ids'):
raise UserError(_('You cannot change the tax, you should remove and recreate lines.'))
if ('account_id' in vals) and self.env['account.account'].browse(vals['account_id']).deprecated: if ('account_id' in vals) and self.env['account.account'].browse(vals['account_id']).deprecated:
raise UserError(_('You cannot use deprecated account.')) raise UserError(_('You cannot use deprecated account.'))
if any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')): if any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')):
@@ -1420,11 +1428,11 @@ class AccountPartialReconcile(models.Model):
exchange_partial_rec_id = rate_diff_partial_rec.id exchange_partial_rec_id = rate_diff_partial_rec.id
#mark the reference of the full reconciliation on the partial ones and on the entries #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({ self.env['account.full.reconcile'].with_context(check_move_validity=False).create({
'partial_reconcile_ids': [(6, 0, partial_rec_ids)], 'partial_reconcile_ids': [(4, p_id) for p_id in partial_rec_ids],
'reconciled_line_ids': [(6, 0, aml_ids)], 'reconciled_line_ids': [(4, a_id) for a_id in aml_ids],
'exchange_move_id': exchange_move_id, 'exchange_move_id': exchange_move_id,
'exchange_partial_rec_id': exchange_partial_rec_id, 'exchange_partial_rec_id': exchange_partial_rec_id,
}) })
return res return res
@api.multi @api.multi
+1 -1
View File
@@ -338,7 +338,7 @@ class ResPartner(models.Model):
else: else:
domain += [('partner_id', 'in', self.ids)] domain += [('partner_id', 'in', self.ids)]
#adding the overdue lines #adding the overdue lines
overdue_domain = ['|', '&', ('date_maturity', '!=', False), ('date_maturity', '<=', date), '&', ('date_maturity', '=', False), ('date', '<=', date)] overdue_domain = ['|', '&', ('date_maturity', '!=', False), ('date_maturity', '<', date), '&', ('date_maturity', '=', False), ('date', '<', date)]
if overdue_only: if overdue_only:
domain += overdue_domain domain += overdue_domain
return domain return domain
@@ -1127,6 +1127,7 @@ var bankStatementReconciliation = abstractReconciliation.extend({
this.reconciliation_menu_id = false; // Used to update the needaction badge this.reconciliation_menu_id = false; // Used to update the needaction badge
// The same move line cannot be selected for multiple reconciliations // The same move line cannot be selected for multiple reconciliations
this.excluded_move_lines_ids = {}; this.excluded_move_lines_ids = {};
this.widget_childrens = [];
}, },
serverPreprocessResultHandler: function(data) { serverPreprocessResultHandler: function(data) {
@@ -1178,44 +1179,64 @@ var bankStatementReconciliation = abstractReconciliation.extend({
single_statement: self.single_statement, single_statement: self.single_statement,
total_lines: self.num_already_reconciled_lines+self.lines.length total_lines: self.num_already_reconciled_lines+self.lines.length
})); }));
self.updateProgressbar(); self.$el.find('.js_automatic_reconciliation').click(function() {
var reconciliations_to_show = self.lines.slice(0, self.num_reconciliations_fetched_in_batch); // Let odoo try to reconcile entries for the user
self.last_displayed_reconciliation_index = reconciliations_to_show.length; self.model_bank_statement_line
self.$(".reconciliation_lines_container").css("opacity", 0); .call("reconciliation_widget_auto_reconcile", [self.lines || undefined, self.num_already_reconciled_lines])
.then(function(data){ self.serverPreprocessResultHandler(data); })
// If everything is reconciled, show end message .then(function(){ self.$('.js_automatic_reconciliation').hide();
if (self.lines.length === 0) { return self.display_reconciliation_propositions();
if (self.notifications)
self.displayNotifications(self.notifications, 0);
self.displayDoneMessage(true);
return;
}
// Display the reconciliations
return self.model_bank_statement_line
.call("get_data_for_reconciliation_widget", [reconciliations_to_show])
.then(function (data) {
var child_promises = [];
var datum = data.shift();
if (datum !== undefined)
child_promises.push(self.displayReconciliation(datum.st_line.id, 'match', false, true, datum.st_line, datum.reconciliation_proposition));
while ((datum = data.shift()) !== undefined)
child_promises.push(self.displayReconciliation(datum.st_line.id, 'inactive', false, true, datum.st_line, datum.reconciliation_proposition));
// When reconciliations are instanciated, make an entrance
$.when.apply($, child_promises).then(function(){
self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed, function() {
if (self.notifications) {
self.displayNotifications(self.notifications);
self.updateShowMoreButton();
}
}); });
});
}); });
return self.display_reconciliation_propositions();
}); });
}); });
}, },
display_reconciliation_propositions: function() {
var self = this;
self.updateProgressbar();
var reconciliations_to_show = self.lines.slice(0, self.num_reconciliations_fetched_in_batch);
self.last_displayed_reconciliation_index = reconciliations_to_show.length;
self.$(".reconciliation_lines_container").css("opacity", 0);
// Delete previous bankStatementReconciliationLine
$.each(self.widget_childrens, function(index, child) {
child.destroy();
});
// If everything is reconciled, show end message
if (self.lines.length === 0) {
self.$(".reconciliation_lines_container").hide();
if (self.notifications)
self.displayNotifications(self.notifications, 0);
self.displayDoneMessage(true);
return;
}
// Display the reconciliations
return self.model_bank_statement_line
.call("get_data_for_reconciliation_widget", [reconciliations_to_show])
.then(function (data) {
var child_promises = [];
var datum = data.shift();
if (datum !== undefined)
child_promises.push(self.displayReconciliation(datum.st_line.id, 'match', false, true, datum.st_line, datum.reconciliation_proposition));
while ((datum = data.shift()) !== undefined)
child_promises.push(self.displayReconciliation(datum.st_line.id, 'inactive', false, true, datum.st_line, datum.reconciliation_proposition));
// When reconciliations are instanciated, make an entrance
$.when.apply($, child_promises).then(function(){
self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed, function() {
if (self.notifications) {
self.displayNotifications(self.notifications);
self.updateShowMoreButton();
}
});
});
});
},
statementNameClickHandler: function() { statementNameClickHandler: function() {
if (! this.single_statement) return; if (! this.single_statement) return;
this.$(".statement_name span").hide(); this.$(".statement_name span").hide();
@@ -1320,6 +1341,7 @@ var bankStatementReconciliation = abstractReconciliation.extend({
reconciliation_proposition: initial_data_provided ? reconciliation_proposition : undefined, reconciliation_proposition: initial_data_provided ? reconciliation_proposition : undefined,
}; };
var widget = new self.children_widget(self, context); var widget = new self.children_widget(self, context);
this.widget_childrens.push(widget);
return widget.appendTo(self.$(".reconciliation_lines_container")); return widget.appendTo(self.$(".reconciliation_lines_container"));
}, },
@@ -12,6 +12,7 @@
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" t-att-aria-valuemax="total_lines" style="width: 0%;"> <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" t-att-aria-valuemax="total_lines" style="width: 0%;">
</div> </div>
</div> </div>
<button class="btn btn-default js_automatic_reconciliation pull-right" title="Let odoo try to reconcile entries for the user">Automatic reconciliation</button>
<div class="oe_clear o_clear"></div> <div class="oe_clear o_clear"></div>
<div class="notification_area"></div> <div class="notification_area"></div>
<div class="interface_options oe_form o_form"></div> <div class="interface_options oe_form o_form"></div>
+6 -4
View File
@@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta
from openerp import api, fields, models, _ from openerp import api, fields, models, _
from openerp.exceptions import UserError, ValidationError from openerp.exceptions import UserError, ValidationError
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
from openerp.tools import float_compare
class AccountAssetCategory(models.Model): class AccountAssetCategory(models.Model):
@@ -417,11 +418,12 @@ class AccountAssetDepreciationLine(models.Model):
categ_type = line.asset_id.category_id.type categ_type = line.asset_id.category_id.type
debit_account = line.asset_id.category_id.account_asset_id.id debit_account = line.asset_id.category_id.account_asset_id.id
credit_account = line.asset_id.category_id.account_depreciation_id.id credit_account = line.asset_id.category_id.account_depreciation_id.id
prec = self.env['decimal.precision'].precision_get('Account')
move_line_1 = { move_line_1 = {
'name': asset_name, 'name': asset_name,
'account_id': credit_account, 'account_id': credit_account,
'debit': 0.0, 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount,
'credit': amount, 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
'journal_id': journal_id, 'journal_id': journal_id,
'partner_id': partner_id, 'partner_id': partner_id,
'currency_id': company_currency != current_currency and current_currency.id or False, 'currency_id': company_currency != current_currency and current_currency.id or False,
@@ -432,8 +434,8 @@ class AccountAssetDepreciationLine(models.Model):
move_line_2 = { move_line_2 = {
'name': asset_name, 'name': asset_name,
'account_id': debit_account, 'account_id': debit_account,
'credit': 0.0, 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount,
'debit': amount, 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
'journal_id': journal_id, 'journal_id': journal_id,
'partner_id': partner_id, 'partner_id': partner_id,
'currency_id': company_currency != current_currency and current_currency.id or False, 'currency_id': company_currency != current_currency and current_currency.id or False,
@@ -54,7 +54,8 @@ class AssetModify(models.TransientModel):
if 'method_end' in fields and asset.method_time == 'end': if 'method_end' in fields and asset.method_time == 'end':
res.update({'method_end': asset.method_end}) res.update({'method_end': asset.method_end})
if self.env.context.get('active_id'): if self.env.context.get('active_id'):
res['asset_method_time'] = self._get_asset_method_time() asset = self.env['account.asset.asset'].browse(self.env.context.get('active_id'))
res['asset_method_time'] = asset.method_time
return res return res
@api.multi @api.multi
+2 -2
View File
@@ -102,8 +102,8 @@ class AccountVoucher(models.Model):
pay_now = fields.Selection([ pay_now = fields.Selection([
('pay_now', 'Pay Directly'), ('pay_now', 'Pay Directly'),
('pay_later', 'Pay Later'), ('pay_later', 'Pay Later'),
], 'Payment', select=True, readonly=True, states={'draft': [('readonly', False)]}, default='pay_later') ], 'Payment', index=True, readonly=True, states={'draft': [('readonly', False)]}, default='pay_later')
date_due = fields.Date('Due Date', readonly=True, select=True, states={'draft': [('readonly', False)]}) date_due = fields.Date('Due Date', readonly=True, index=True, states={'draft': [('readonly', False)]})
@api.onchange('partner_id', 'pay_now') @api.onchange('partner_id', 'pay_now')
def onchange_partner_id(self): def onchange_partner_id(self):
+1 -1
View File
@@ -411,7 +411,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
return $.when([{ return $.when([{
type: 'error', type: 'error',
record: false, record: false,
message: error.data.arguments[1], message: error.data.arguments && error.data.arguments[1] || error.message,
}]); }]);
}) ; }) ;
}, },
+1 -1
View File
@@ -291,7 +291,7 @@ class event_registration(models.Model):
string='Status', default='draft', readonly=True, copy=False, track_visibility='onchange') string='Status', default='draft', readonly=True, copy=False, track_visibility='onchange')
email = fields.Char(string='Email') email = fields.Char(string='Email')
phone = fields.Char(string='Phone') phone = fields.Char(string='Phone')
name = fields.Char(string='Attendee Name', select=True) name = fields.Char(string='Attendee Name', index=True)
@api.one @api.one
@api.constrains('event_id', 'state') @api.constrains('event_id', 'state')
@@ -68,7 +68,7 @@ class RegistrationEditorLine(models.TransientModel):
event_ticket_id = fields.Many2one('event.event.ticket', string='Event Ticket') event_ticket_id = fields.Many2one('event.event.ticket', string='Event Ticket')
email = fields.Char(string='Email') email = fields.Char(string='Email')
phone = fields.Char(string='Phone') phone = fields.Char(string='Phone')
name = fields.Char(string='Name', select=True) name = fields.Char(string='Name', index=True)
@api.one @api.one
def get_registration_data(self): def get_registration_data(self):
+2 -2
View File
@@ -261,7 +261,7 @@ class hr_employee(osv.osv):
user_field_lst.append(name) user_field_lst.append(name)
return user_field_lst return user_field_lst
_constraints = [(osv.osv._check_recursion, _('Error! You cannot create recursive hierarchy of Employee(s).'), ['parent_id']),] _constraints = [(osv.osv._check_recursion, 'Error! You cannot create recursive hierarchy of Employee(s).', ['parent_id']),]
class hr_department(osv.osv): class hr_department(osv.osv):
@@ -291,7 +291,7 @@ class hr_department(osv.osv):
} }
_constraints = [ _constraints = [
(osv.osv._check_recursion, _('Error! You cannot create recursive departments.'), ['parent_id']) (osv.osv._check_recursion, 'Error! You cannot create recursive departments.', ['parent_id'])
] ]
def name_get(self, cr, uid, ids, context=None): def name_get(self, cr, uid, ids, context=None):
+1 -1
View File
@@ -236,7 +236,7 @@ class HrEquipmentRequest(models.Model):
employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee_get) employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee_get)
department_id = fields.Many2one('hr.department', string='Department') department_id = fields.Many2one('hr.department', string='Department')
category_id = fields.Many2one('hr.equipment.category', string='Category') category_id = fields.Many2one('hr.equipment.category', string='Category')
equipment_id = fields.Many2one('hr.equipment', string='Asset', select=True) equipment_id = fields.Many2one('hr.equipment', string='Asset', index=True)
user_id = fields.Many2one('res.users', string='Assigned to', track_visibility='onchange') user_id = fields.Many2one('res.users', string='Assigned to', track_visibility='onchange')
stage_id = fields.Many2one('hr.equipment.stage', string='Stage', track_visibility='onchange', default=_default_stage) stage_id = fields.Many2one('hr.equipment.stage', string='Stage', track_visibility='onchange', default=_default_stage)
priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority') priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority')
@@ -1,7 +1,5 @@
#-*- coding:utf-8 -*- #-*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from datetime import date, datetime, timedelta
from openerp import api from openerp import api
from openerp.osv import fields, osv from openerp.osv import fields, osv
@@ -94,13 +92,12 @@ class hr_payslip(osv.osv):
move_pool = self.pool.get('account.move') move_pool = self.pool.get('account.move')
hr_payslip_line_pool = self.pool['hr.payslip.line'] hr_payslip_line_pool = self.pool['hr.payslip.line']
precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Payroll') precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Payroll')
timenow = time.strftime('%Y-%m-%d')
for slip in self.browse(cr, uid, ids, context=context): for slip in self.browse(cr, uid, ids, context=context):
line_ids = [] line_ids = []
debit_sum = 0.0 debit_sum = 0.0
credit_sum = 0.0 credit_sum = 0.0
date = timenow date = slip.date or slip.date_to
name = _('Payslip of %s') % (slip.employee_id.name) name = _('Payslip of %s') % (slip.employee_id.name)
move = { move = {
@@ -152,7 +149,6 @@ class hr_payslip(osv.osv):
raise UserError(_('The Expense Journal "%s" has not properly configured the Credit Account!') % (slip.journal_id.name)) raise UserError(_('The Expense Journal "%s" has not properly configured the Credit Account!') % (slip.journal_id.name))
adjust_credit = (0, 0, { adjust_credit = (0, 0, {
'name': _('Adjustment Entry'), 'name': _('Adjustment Entry'),
'date': timenow,
'partner_id': False, 'partner_id': False,
'account_id': acc_id, 'account_id': acc_id,
'journal_id': slip.journal_id.id, 'journal_id': slip.journal_id.id,
@@ -120,20 +120,20 @@ class Applicant(models.Model):
help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma") help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma")
probability = fields.Float("Probability") probability = fields.Float("Probability")
partner_id = fields.Many2one('res.partner', "Contact") partner_id = fields.Many2one('res.partner', "Contact")
create_date = fields.Datetime("Creation Date", readonly=True, select=True) create_date = fields.Datetime("Creation Date", readonly=True, index=True)
write_date = fields.Datetime("Update Date", readonly=True) write_date = fields.Datetime("Update Date", readonly=True)
stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', track_visibility='onchange', stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', track_visibility='onchange',
domain="['|', ('job_id', '=', False), ('job_id', '=', job_id)]", domain="['|', ('job_id', '=', False), ('job_id', '=', job_id)]",
copy=False, select=1, copy=False, index=True,
default=_default_stage_id) default=_default_stage_id)
last_stage_id = fields.Many2one('hr.recruitment.stage', "Last Stage", last_stage_id = fields.Many2one('hr.recruitment.stage', "Last Stage",
help="Stage of the applicant before being in the current stage. Used for lost cases analysis.") help="Stage of the applicant before being in the current stage. Used for lost cases analysis.")
categ_ids = fields.Many2many('hr.applicant.category', string="Tags") categ_ids = fields.Many2many('hr.applicant.category', string="Tags")
company_id = fields.Many2one('res.company', "Company", default=_default_company_id) company_id = fields.Many2one('res.company', "Company", default=_default_company_id)
user_id = fields.Many2one('res.users', "Responsible", track_visibility="onchange", default=lambda self: self.env.uid) user_id = fields.Many2one('res.users', "Responsible", track_visibility="onchange", default=lambda self: self.env.uid)
date_closed = fields.Datetime("Closed", readonly=True, select=True) date_closed = fields.Datetime("Closed", readonly=True, index=True)
date_open = fields.Datetime("Assigned", readonly=True, select=True) date_open = fields.Datetime("Assigned", readonly=True, index=True)
date_last_stage_update = fields.Datetime("Last Stage Update", select=True, default=fields.Datetime.now) date_last_stage_update = fields.Datetime("Last Stage Update", index=True, default=fields.Datetime.now)
date_action = fields.Date("Next Action Date") date_action = fields.Date("Next Action Date")
title_action = fields.Char("Next Action", size=64) title_action = fields.Char("Next Action", size=64)
priority = fields.Selection(AVAILABLE_PRIORITIES, "Appreciation", default='0') priority = fields.Selection(AVAILABLE_PRIORITIES, "Appreciation", default='0')
+2 -2
View File
@@ -147,7 +147,7 @@ class LunchOrderLine(models.Model):
('confirmed', 'Received'), ('confirmed', 'Received'),
('ordered', 'Ordered'), ('ordered', 'Ordered'),
('cancelled', 'Cancelled')], ('cancelled', 'Cancelled')],
'Status', readonly=True, select=True, default='new') 'Status', readonly=True, index=True, default='new')
cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move')
currency_id = fields.Many2one('res.currency', related='order_id.currency_id') currency_id = fields.Many2one('res.currency', related='order_id.currency_id')
@@ -235,7 +235,7 @@ class LunchAlert(models.Model):
alert_type = fields.Selection([('specific', 'Specific Day'), alert_type = fields.Selection([('specific', 'Specific Day'),
('week', 'Every Week'), ('week', 'Every Week'),
('days', 'Every Day')], ('days', 'Every Day')],
string='Recurrency', required=True, select=True, default='specific') string='Recurrency', required=True, index=True, default='specific')
specific_day = fields.Date('Day', default=fields.Date.context_today) specific_day = fields.Date('Day', default=fields.Date.context_today)
monday = fields.Boolean('Monday') monday = fields.Boolean('Monday')
tuesday = fields.Boolean('Tuesday') tuesday = fields.Boolean('Tuesday')
+1
View File
@@ -23,6 +23,7 @@ class MailChatController(openerp.addons.bus.controllers.main.BusController):
partner_id = request.env.user.partner_id.id partner_id = request.env.user.partner_id.id
if partner_id: if partner_id:
channels = list(channels) # do not alter original list
for mail_channel in request.env['mail.channel'].search([('channel_partner_ids', 'in', [partner_id])]): for mail_channel in request.env['mail.channel'].search([('channel_partner_ids', 'in', [partner_id])]):
channels.append((request.db, 'mail.channel', mail_channel.id)) channels.append((request.db, 'mail.channel', mail_channel.id))
# personal and needaction channel # personal and needaction channel
+1 -1
View File
@@ -45,7 +45,7 @@ class Channel(models.Model):
('channel', 'Channel')], ('channel', 'Channel')],
'Channel Type', default='channel') 'Channel Type', default='channel')
description = fields.Text('Description') description = fields.Text('Description')
uuid = fields.Char('UUID', size=50, select=True, default=lambda self: '%s' % uuid.uuid4()) uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4())
email_send = fields.Boolean('Send messages by email', default=False) email_send = fields.Boolean('Send messages by email', default=False)
# multi users channel # multi users channel
channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen') channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen')
+4 -4
View File
@@ -18,13 +18,13 @@ class Followers(models.Model):
_description = 'Document Followers' _description = 'Document Followers'
res_model = fields.Char( res_model = fields.Char(
'Related Document Model', required=True, select=1, help='Model of the followed resource') 'Related Document Model', required=True, index=True, help='Model of the followed resource')
res_id = fields.Integer( res_id = fields.Integer(
'Related Document ID', select=1, help='Id of the followed resource') 'Related Document ID', index=True, help='Id of the followed resource')
partner_id = fields.Many2one( partner_id = fields.Many2one(
'res.partner', string='Related Partner', ondelete='cascade', select=1) 'res.partner', string='Related Partner', ondelete='cascade', index=True)
channel_id = fields.Many2one( channel_id = fields.Many2one(
'mail.channel', string='Listener', ondelete='cascade', select=1) 'mail.channel', string='Listener', ondelete='cascade', index=True)
subtype_ids = fields.Many2many( subtype_ids = fields.Many2many(
'mail.message.subtype', string='Subtype', 'mail.message.subtype', string='Subtype',
help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall.") help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall.")
+6 -6
View File
@@ -57,12 +57,12 @@ class Message(models.Model):
help='Attachments are linked to a document through model / res_id and to the message ' help='Attachments are linked to a document through model / res_id and to the message '
'through this field.') 'through this field.')
parent_id = fields.Many2one( parent_id = fields.Many2one(
'mail.message', 'Parent Message', select=True, ondelete='set null', 'mail.message', 'Parent Message', index=True, ondelete='set null',
help="Initial thread message.") help="Initial thread message.")
child_ids = fields.One2many('mail.message', 'parent_id', 'Child Messages') child_ids = fields.One2many('mail.message', 'parent_id', 'Child Messages')
# related document # related document
model = fields.Char('Related Document Model', select=1) model = fields.Char('Related Document Model', index=True)
res_id = fields.Integer('Related Document ID', select=1) res_id = fields.Integer('Related Document ID', index=True)
record_name = fields.Char('Message Record Name', help="Name get of the related document.") record_name = fields.Char('Message Record Name', help="Name get of the related document.")
# characteristics # characteristics
message_type = fields.Selection([ message_type = fields.Selection([
@@ -73,13 +73,13 @@ class Message(models.Model):
help="Message type: email for email message, notification for system " help="Message type: email for email message, notification for system "
"message, comment for other messages such as user replies", "message, comment for other messages such as user replies",
oldname='type') oldname='type')
subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', select=1) subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', index=True)
# origin # origin
email_from = fields.Char( email_from = fields.Char(
'From', default=_get_default_from, 'From', default=_get_default_from,
help="Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter.") help="Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter.")
author_id = fields.Many2one( author_id = fields.Many2one(
'res.partner', 'Author', select=1, 'res.partner', 'Author', index=True,
ondelete='set null', default=_get_default_author, ondelete='set null', default=_get_default_author,
help="Author of the message. If not set, email_from may hold an email address that did not match any partner.") help="Author of the message. If not set, email_from may hold an email address that did not match any partner.")
author_avatar = fields.Binary("Author's avatar", related='author_id.image_small') author_avatar = fields.Binary("Author's avatar", related='author_id.image_small')
@@ -108,7 +108,7 @@ class Message(models.Model):
no_auto_thread = fields.Boolean( no_auto_thread = fields.Boolean(
'No threading for answers', 'No threading for answers',
help='Answers do not go in the original document discussion thread. This has an impact on the generated message-id.') help='Answers do not go in the original document discussion thread. This has an impact on the generated message-id.')
message_id = fields.Char('Message-Id', help='Message unique identifier', select=1, readonly=1, copy=False) message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False)
reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.') reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.')
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server') mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
+1 -1
View File
@@ -116,7 +116,7 @@ class MailTemplate(models.Model):
name = fields.Char('Name') name = fields.Char('Name')
model_id = fields.Many2one('ir.model', 'Applies to', help="The type of document this template can be used with") model_id = fields.Many2one('ir.model', 'Applies to', help="The type of document this template can be used with")
model = fields.Char('Related Document Model', related='model_id.model', select=True, store=True, readonly=True) model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True)
lang = fields.Char('Language', lang = fields.Char('Language',
help="Optional translation language (ISO code) to select when sending out an email. " help="Optional translation language (ISO code) to select when sending out an email. "
"If not set, the english version will be used. " "If not set, the english version will be used. "
+1 -1
View File
@@ -25,7 +25,7 @@ class MailTracking(models.Model):
new_value_text = fields.Text('New Value Text', readonly=1) new_value_text = fields.Text('New Value Text', readonly=1)
new_value_datetime = fields.Datetime('New Value Datetime', readonly=1) new_value_datetime = fields.Datetime('New Value Datetime', readonly=1)
mail_message_id = fields.Many2one('mail.message', 'Message ID', required=True, select=True, ondelete='cascade') mail_message_id = fields.Many2one('mail.message', 'Message ID', required=True, index=True, ondelete='cascade')
@api.model @api.model
def create_tracking_values(self, initial_value, new_value, col_name, col_info): def create_tracking_values(self, initial_value, new_value, col_name, col_info):
+2 -2
View File
@@ -25,8 +25,8 @@ class Invite(models.TransientModel):
result['message'] = _('<div><p>Hello,</p><p>%s invited you to follow a new document.</p></div>') % user_name result['message'] = _('<div><p>Hello,</p><p>%s invited you to follow a new document.</p></div>') % user_name
return result return result
res_model = fields.Char('Related Document Model', required=True, select=1, help='Model of the followed resource') res_model = fields.Char('Related Document Model', required=True, index=True, help='Model of the followed resource')
res_id = fields.Integer('Related Document ID', select=1, help='Id of the followed resource') res_id = fields.Integer('Related Document ID', index=True, help='Id of the followed resource')
partner_ids = fields.Many2many('res.partner', string='Recipients', help="List of partners that will be added as follower of the current document.") partner_ids = fields.Many2many('res.partner', string='Recipients', help="List of partners that will be added as follower of the current document.")
channel_ids = fields.Many2many('mail.channel', string='Channels', help='List of channels that will be added as listeners of the current document.', channel_ids = fields.Many2many('mail.channel', string='Channels', help='List of channels that will be added as listeners of the current document.',
domain=[('channel_type', '=', 'channel')]) domain=[('channel_type', '=', 'channel')])
+1 -1
View File
@@ -120,7 +120,7 @@ class MailComposer(models.TransientModel):
auto_delete = fields.Boolean('Delete Emails', help='Delete sent emails (mass mailing only)') auto_delete = fields.Boolean('Delete Emails', help='Delete sent emails (mass mailing only)')
auto_delete_message = fields.Boolean('Delete Message Copy', help='Do not keep a copy of the email in the document communication history (mass mailing only)') auto_delete_message = fields.Boolean('Delete Message Copy', help='Do not keep a copy of the email in the document communication history (mass mailing only)')
template_id = fields.Many2one( template_id = fields.Many2one(
'mail.template', 'Use template', select=True, 'mail.template', 'Use template', index=True,
domain="[('model', '=', model)]") domain="[('model', '=', model)]")
# mail_message updated fields # mail_message updated fields
message_type = fields.Selection(default="comment") message_type = fields.Selection(default="comment")
+1 -2
View File
@@ -31,8 +31,7 @@
</tr> </tr>
<tr t-foreach="get_children(o.bom_line_ids)" t-as="l"> <tr t-foreach="get_children(o.bom_line_ids)" t-as="l">
<td style="padding-left: 20px;"> <td style="padding-left: 20px;">
<span style="color: white;" t-esc="'... '*(l['level'])"/>[ <span style="color: white;" t-esc="'... '*(l['level'])"/>
<span t-esc="l['pcode']"/>]
<span t-esc="l['pname']"/> <span t-esc="l['pname']"/>
</td> </td>
<td> <td>
+2 -2
View File
@@ -11,8 +11,8 @@ class SaleOrderLine(models.Model):
res = super(SaleOrderLine, self)._action_procurement_create() res = super(SaleOrderLine, self)._action_procurement_create()
orders = list(set(x.order_id for x in self)) orders = list(set(x.order_id for x in self))
for order in orders: for order in orders:
reassign = order.picking_ids.filtered(lambda x: x.state=='confirmed' or ((x.state=='partially_available') and not x.printed)) reassign = order.picking_ids.filtered(lambda x: x.state=='confirmed' or ((x.state in ['partially_available', 'waiting']) and not x.printed))
if reassign: if reassign:
reassign.do_unreserve() reassign.do_unreserve()
reassign.action_assign() reassign.action_assign()
return res return res
+17 -1
View File
@@ -87,6 +87,22 @@ class AccountInvoice(models.Model):
if purchase_ids: if purchase_ids:
self.origin = ', '.join(purchase_ids.mapped('name')) self.origin = ', '.join(purchase_ids.mapped('name'))
@api.onchange('partner_id', 'company_id')
def _onchange_partner_id(self):
res = super(AccountInvoice, self)._onchange_partner_id()
if not self.env.context.get('default_journal_id') and self.partner_id and self.currency_id and\
self.type in ['in_invoice', 'in_refund'] and\
self.currency_id != self.partner_id.property_purchase_currency_id:
journal_domain = [
('type', '=', 'purchase'),
('company_id', '=', self.company_id.id),
('currency_id', '=', self.partner_id.property_purchase_currency_id.id),
]
default_journal_id = self.env['account.journal'].search(journal_domain, limit=1)
if default_journal_id:
self.journal_id = default_journal_id
return res
@api.model @api.model
def invoice_line_move_line_get(self): def invoice_line_move_line_get(self):
res = super(AccountInvoice, self).invoice_line_move_line_get() res = super(AccountInvoice, self).invoice_line_move_line_get()
@@ -163,6 +179,6 @@ class AccountInvoiceLine(models.Model):
""" Override AccountInvoice_line to add the link to the purchase order line it is related to""" """ Override AccountInvoice_line to add the link to the purchase order line it is related to"""
_inherit = 'account.invoice.line' _inherit = 'account.invoice.line'
purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', select=True, readonly=True) purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', index=True, readonly=True)
purchase_id = fields.Many2one('purchase.order', related='purchase_line_id.order_id', string='Purchase Order', store=False, readonly=True, purchase_id = fields.Many2one('purchase.order', related='purchase_line_id.order_id', string='Purchase Order', store=False, readonly=True,
help='Associated Purchase Order. Filled in automatically when a PO is chosen on the vendor bill.') help='Associated Purchase Order. Filled in automatically when a PO is chosen on the vendor bill.')
+11 -9
View File
@@ -93,7 +93,7 @@ class PurchaseOrder(models.Model):
'cancel': [('readonly', True)], 'cancel': [('readonly', True)],
} }
name = fields.Char('Order Reference', required=True, select=True, copy=False, default='New') name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New')
origin = fields.Char('Source Document', copy=False,\ origin = fields.Char('Source Document', copy=False,\
help="Reference of the document that generated this purchase order " help="Reference of the document that generated this purchase order "
"request (e.g. a sale order or an internal procurement request)") "request (e.g. a sale order or an internal procurement request)")
@@ -102,9 +102,9 @@ class PurchaseOrder(models.Model):
"It's used to do the matching when you receive the " "It's used to do the matching when you receive the "
"products as this reference is usually written on the " "products as this reference is usually written on the "
"delivery order sent by your vendor.") "delivery order sent by your vendor.")
date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, select=True, copy=False, default=fields.Datetime.now,\ date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\
help="Depicts the date where the Quotation should be validated and converted into a purchase order.") help="Depicts the date where the Quotation should be validated and converted into a purchase order.")
date_approve = fields.Date('Approval Date', readonly=1, select=True, copy=False) date_approve = fields.Date('Approval Date', readonly=1, index=True, copy=False)
partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always') partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always')
dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES,\ dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES,\
help="Put an address if you want to deliver directly from the vendor to the customer. "\ help="Put an address if you want to deliver directly from the vendor to the customer. "\
@@ -118,7 +118,7 @@ class PurchaseOrder(models.Model):
('purchase', 'Purchase Order'), ('purchase', 'Purchase Order'),
('done', 'Locked'), ('done', 'Locked'),
('cancel', 'Cancelled') ('cancel', 'Cancelled')
], string='Status', readonly=True, select=True, copy=False, default='draft', track_visibility='onchange') ], string='Status', readonly=True, index=True, copy=False, default='draft', track_visibility='onchange')
order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states=READONLY_STATES, copy=True) order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states=READONLY_STATES, copy=True)
notes = fields.Text('Terms and Conditions') notes = fields.Text('Terms and Conditions')
@@ -133,7 +133,7 @@ class PurchaseOrder(models.Model):
picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0) picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0)
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False) picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False)
date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', inverse='_inverse_date_planned', required=True, select=True, oldname='minimum_planned_date') date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', inverse='_inverse_date_planned', required=True, index=True, oldname='minimum_planned_date')
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always') amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always')
amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all') amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all')
@@ -145,13 +145,13 @@ class PurchaseOrder(models.Model):
product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
create_uid = fields.Many2one('res.users', 'Responsible') create_uid = fields.Many2one('res.users', 'Responsible')
company_id = fields.Many2one('res.company', 'Company', required=True, select=1, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id) company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id)
picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\ picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\
help="This will determine picking type of incoming shipment") help="This will determine picking type of incoming shipment")
default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\ default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\
help="Technical field used to display the Drop Ship Address", readonly=True) help="Technical field used to display the Drop Ship Address", readonly=True)
group_id = fields.Many2one('procurement.group', string="Procurement Group") group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False)
@api.model @api.model
def name_search(self, name, args=None, operator='ilike', limit=100): def name_search(self, name, args=None, operator='ilike', limit=100):
@@ -281,6 +281,8 @@ class PurchaseOrder(models.Model):
@api.multi @api.multi
def button_confirm(self): def button_confirm(self):
for order in self: for order in self:
if order.state not in ['draft', 'sent']:
continue
order._add_supplier_to_product() order._add_supplier_to_product()
# Deal with double validation process # Deal with double validation process
if order.company_id.po_double_validation == 'one_step'\ if order.company_id.po_double_validation == 'one_step'\
@@ -516,7 +518,7 @@ class PurchaseOrderLine(models.Model):
name = fields.Text(string='Description', required=True) name = fields.Text(string='Description', required=True)
product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True) product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True)
date_planned = fields.Datetime(string='Scheduled Date', required=True, select=True) date_planned = fields.Datetime(string='Scheduled Date', required=True, index=True)
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
product_uom = fields.Many2one('product.uom', string='Product Unit of Measure', required=True) product_uom = fields.Many2one('product.uom', string='Product Unit of Measure', required=True)
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True) product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True)
@@ -527,7 +529,7 @@ class PurchaseOrderLine(models.Model):
price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True) price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True)
price_tax = fields.Monetary(compute='_compute_amount', string='Tax', store=True) price_tax = fields.Monetary(compute='_compute_amount', string='Tax', store=True)
order_id = fields.Many2one('purchase.order', string='Order Reference', select=True, required=True, ondelete='cascade') order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade')
account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account', domain=[('account_type', '=', 'normal')]) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account', domain=[('account_type', '=', 'normal')])
company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True) company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True)
state = fields.Selection(related='order_id.state', stored=True) state = fields.Selection(related='order_id.state', stored=True)
+1 -1
View File
@@ -250,7 +250,7 @@
<group> <group>
<group> <group>
<field name="date_planned"/> <field name="date_planned"/>
<field name="picking_type_id" domain="[('code','=','incoming')]" options="{'no_create': True}" context="{'special_shortened_wh_name': True}" groups="stock.group_stock_multi_locations"/> <field name="picking_type_id" domain="[('code','=','incoming')]" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="dest_address_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('default_location_dest_id_usage', '!=', 'customer')], 'required': [('default_location_dest_id_usage', '=', 'customer')]}"/> <field name="dest_address_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('default_location_dest_id_usage', '!=', 'customer')], 'required': [('default_location_dest_id_usage', '=', 'customer')]}"/>
<field name="default_location_dest_id_usage" invisible="1"/> <field name="default_location_dest_id_usage" invisible="1"/>
<field name="incoterm_id"/> <field name="incoterm_id"/>
@@ -257,11 +257,6 @@ class purchase_requisition(osv.osv):
if not confirm: if not confirm:
raise UserError(_('You have no line selected for buying.')) raise UserError(_('You have no line selected for buying.'))
#check for complete RFQ
for quotation in tender.purchase_ids:
if (self.check_valid_quotation(cr, uid, quotation, context=context)):
#Set PO state to confirm
po.button_confirm(cr, uid, [quotation.id], context=context)
#get other confirmed lines per supplier #get other confirmed lines per supplier
for po_line in tender.po_line_ids: for po_line in tender.po_line_ids:
+15 -10
View File
@@ -1555,12 +1555,18 @@ class stock_picking(models.Model):
def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None): def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None):
""" Unreserve quants then try to reassign quants.""" """ Unreserve quants then try to reassign quants."""
if context is None:
context = {}
stock_move_obj = self.pool.get('stock.move') stock_move_obj = self.pool.get('stock.move')
if not move_ids: if not move_ids:
self.do_unreserve(cr, uid, [picking.id], context=context) self.do_unreserve(cr, uid, [picking.id], context=context)
self.action_assign(cr, uid, [picking.id], context=context) self.action_assign(cr, uid, [picking.id], context=context)
else: else:
stock_move_obj.do_unreserve(cr, uid, move_ids, context=context) if 'no_state_change' in context:
move = stock_move_obj.browse(cr, uid, move_ids, context=context)
stock_move_obj.do_unreserve(cr, uid, [m.id for m in move if m.reserved_quant_ids], context=context)
else:
stock_move_obj.do_unreserve(cr, uid, move_ids, context=context)
stock_move_obj.action_assign(cr, uid, move_ids, no_prepare=True, context=context) stock_move_obj.action_assign(cr, uid, move_ids, no_prepare=True, context=context)
def do_new_transfer(self, cr, uid, ids, context=None): def do_new_transfer(self, cr, uid, ids, context=None):
@@ -1667,6 +1673,7 @@ class stock_picking(models.Model):
if moves_reassign and (picking.location_id.usage not in ("supplier", "production", "inventory")): if moves_reassign and (picking.location_id.usage not in ("supplier", "production", "inventory")):
ctx = dict(context) ctx = dict(context)
ctx['reserve_only_ops'] = True #unnecessary to assign other quants than those involved with pack operations as they will be unreserved anyways. ctx['reserve_only_ops'] = True #unnecessary to assign other quants than those involved with pack operations as they will be unreserved anyways.
ctx['no_state_change'] = True
self.rereserve_quants(cr, uid, picking, move_ids=picking.move_lines.ids, context=ctx) self.rereserve_quants(cr, uid, picking, move_ids=picking.move_lines.ids, context=ctx)
self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context) self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
@@ -2047,10 +2054,11 @@ class stock_move(osv.osv):
if move.state in ('done', 'cancel'): if move.state in ('done', 'cancel'):
raise UserError(_('Cannot unreserve a done move')) raise UserError(_('Cannot unreserve a done move'))
quant_obj.quants_unreserve(cr, uid, move, context=context) quant_obj.quants_unreserve(cr, uid, move, context=context)
if self.find_move_ancestors(cr, uid, move, context=context): if not context.get('no_state_change'):
self.write(cr, uid, [move.id], {'state': 'waiting'}, context=context) if self.find_move_ancestors(cr, uid, move, context=context):
else: self.write(cr, uid, [move.id], {'state': 'waiting'}, context=context)
self.write(cr, uid, [move.id], {'state': 'confirmed'}, context=context) else:
self.write(cr, uid, [move.id], {'state': 'confirmed'}, context=context)
def _prepare_procurement_from_move(self, cr, uid, move, context=None): def _prepare_procurement_from_move(self, cr, uid, move, context=None):
origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or move.origin or move.picking_id.name or "/") origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or move.origin or move.picking_id.name or "/")
@@ -2625,6 +2633,8 @@ class stock_move(osv.osv):
operations = set() operations = set()
move_qty = {} move_qty = {}
for move in self.browse(cr, uid, ids, context=context): for move in self.browse(cr, uid, ids, context=context):
if move.picking_id:
pickings.add(move.picking_id.id)
move_qty[move.id] = move.product_qty move_qty[move.id] = move.product_qty
for link in move.linked_move_operation_ids: for link in move.linked_move_operation_ids:
operations.add(link.operation_id) operations.add(link.operation_id)
@@ -4890,11 +4900,6 @@ class stock_picking_type(osv.osv):
name = record.name name = record.name
if record.warehouse_id: if record.warehouse_id:
name = record.warehouse_id.name + ': ' +name name = record.warehouse_id.name + ': ' +name
if context.get('special_shortened_wh_name'):
if record.warehouse_id:
name = record.warehouse_id.name
else:
name = _('Customer') + ' (' + record.name + ')'
res.append((record.id, name)) res.append((record.id, name))
return res return res
+10
View File
@@ -233,6 +233,16 @@ class sale_order_line(osv.osv):
return result return result
@api.onchange('product_id') @api.onchange('product_id')
def product_id_change(self):
warning = self.onchange_product_id_warning()
product_info = self.product_id
if product_info.sale_line_warn != 'no-message':
if product_info.sale_line_warn == 'block':
return warning
result = super(sale_order_line, self).product_id_change()
result['warning'] = warning and warning.get('warning')
return result
def onchange_product_id_warning(self): def onchange_product_id_warning(self):
if not self.product_id: if not self.product_id:
return return
+1 -6
View File
@@ -25,11 +25,6 @@ import werkzeug.utils
import werkzeug.wrappers import werkzeug.wrappers
from openerp.api import Environment from openerp.api import Environment
try:
import xlwt
except ImportError:
xlwt = None
import openerp import openerp
import openerp.modules.registry import openerp.modules.registry
from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound
@@ -37,7 +32,7 @@ from openerp.modules import get_resource_path
from openerp.tools import topological_sort from openerp.tools import topological_sort
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.tools import ustr from openerp.tools import ustr
from openerp.tools.misc import str2bool from openerp.tools.misc import str2bool, xlwt
from openerp import http from openerp import http
from openerp.http import request, serialize_exception as _serialize_exception from openerp.http import request, serialize_exception as _serialize_exception
from openerp.exceptions import AccessError from openerp.exceptions import AccessError
+2 -5
View File
@@ -1,13 +1,10 @@
from openerp import http from openerp import http
import json import json
from openerp.http import request, serialize_exception as _serialize_exception from openerp.http import request, serialize_exception as _serialize_exception
from openerp.tools.misc import xlwt
from cStringIO import StringIO from cStringIO import StringIO
from collections import deque from collections import deque
try:
import xlwt
except ImportError:
xlwt = None
class TableExporter(http.Controller): class TableExporter(http.Controller):
@@ -21,7 +18,7 @@ class TableExporter(http.Controller):
jdata = json.loads(data) jdata = json.loads(data)
nbr_measures = jdata['nbr_measures'] nbr_measures = jdata['nbr_measures']
workbook = xlwt.Workbook() workbook = xlwt.Workbook()
worksheet = workbook.add_sheet(jdata['title'][:30]) worksheet = workbook.add_sheet(jdata['title'])
header_bold = xlwt.easyxf("font: bold on; pattern: pattern solid, fore_colour gray25;") header_bold = xlwt.easyxf("font: bold on; pattern: pattern solid, fore_colour gray25;")
header_plain = xlwt.easyxf("pattern: pattern solid, fore_colour gray25;") header_plain = xlwt.easyxf("pattern: pattern solid, fore_colour gray25;")
bold = xlwt.easyxf("font: bold on;") bold = xlwt.easyxf("font: bold on;")
+1 -1
View File
@@ -688,7 +688,7 @@ var DataSetSearch = DataSet.extend({
.limit(options.limit || false); .limit(options.limit || false);
q = q.order_by.apply(q, this._sort); q = q.order_by.apply(q, this._sort);
return q.all().done(function (records) { return this.orderer.add(q.all()).done(function (records) {
// FIXME: not sure about that one, *could* have discarded count // FIXME: not sure about that one, *could* have discarded count
q.count().done(function (count) { self._length = count; }); q.count().done(function (count) { self._length = count; });
self.ids = _(records).pluck('id'); self.ids = _(records).pluck('id');
+11 -3
View File
@@ -478,6 +478,7 @@ var FieldCharDomain = common.AbstractField.extend(common.ReinitializeFieldMixin,
this.$('.o_debug_input').val(this.get('value')); this.$('.o_debug_input').val(this.get('value'));
} }
} else { } else {
this.$('.o_form_input').val('');
this.$('.o_count').text(_t('No selected record')); this.$('.o_count').text(_t('No selected record'));
var $arrow = this.$('button span').detach(); var $arrow = this.$('button span').detach();
this.$('button').text(_('Select records ')).append($("<span/>").addClass('fa fa-arrow-right')); this.$('button').text(_('Select records ')).append($("<span/>").addClass('fa fa-arrow-right'));
@@ -1073,7 +1074,7 @@ var FieldRadio = common.AbstractField.extend(common.ReinitializeFieldMixin, {
render_value: function () { render_value: function () {
var self = this; var self = this;
this.$el.toggleClass("oe_readonly", this.get('effective_readonly')); this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true); this.$("input").prop("checked", false).filter(function () {return this.value == self.get_value();}).prop("checked", true);
this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : ""); this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
} }
}); });
@@ -1528,13 +1529,17 @@ var FieldStatus = common.AbstractField.extend({
return fields; return fields;
}); });
}, },
on_click_stage: function (ev) { on_click_stage: _.debounce(function (ev) {
var self = this; var self = this;
var $li = $(ev.currentTarget); var $li = $(ev.currentTarget);
var ul = $li.closest('.oe_form_field_status');
if (this.view.is_disabled) { if (this.view.is_disabled) {
return; return;
} }
var val; var val;
if (ul.attr('disabled')) {
return;
}
if (this.field.type == "many2one") { if (this.field.type == "many2one") {
val = parseInt($li.data("id"), 10); val = parseInt($li.data("id"), 10);
} }
@@ -1551,13 +1556,16 @@ var FieldStatus = common.AbstractField.extend({
this.view.recursive_save().done(function() { this.view.recursive_save().done(function() {
var change = {}; var change = {};
change[self.name] = val; change[self.name] = val;
ul.attr('disabled', true);
self.view.dataset.write(self.view.datarecord.id, change).done(function() { self.view.dataset.write(self.view.datarecord.id, change).done(function() {
self.view.reload(); self.view.reload();
}).always(function() {
ul.removeAttr('disabled');
}); });
}); });
} }
} }
}, }, 300),
}); });
var FieldMonetary = FieldFloat.extend({ var FieldMonetary = FieldFloat.extend({
+3 -1
View File
@@ -32,7 +32,9 @@
var modal = $(this).parentsUntil('body', '.modal'); var modal = $(this).parentsUntil('body', '.modal');
if (modal.hasClass('o_database_backup')) { if (modal.hasClass('o_database_backup')) {
$(modal).modal('hide'); $(modal).modal('hide');
$('.list-group').before("<div class='alert alert-info'>The backup may take some time before being ready</div>"); if (!$('.alert-backup-long').length) {
$('.list-group').before("<div class='alert alert-info alert-backup-long'>The backup may take some time before being ready</div>");
}
} }
}); });
}); });
+2 -1
View File
@@ -1,6 +1,7 @@
import openerp.tests import openerp.tests
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True)
class TestUi(openerp.tests.HttpCase): class TestUi(openerp.tests.HttpCase):
post_install = True post_install = True
@@ -561,6 +561,10 @@
<xpath expr="//div[@id='wrap']" position="inside"> <xpath expr="//div[@id='wrap']" position="inside">
<div class="oe_structure"> <div class="oe_structure">
<section class="container"> <section class="container">
<t t-if="not version">
<meta http-equiv="refresh" content="0;URL='/website/info'" />
</t>
<t t-if="version">
<h1><t t-esc="res_company.name"/> <h1><t t-esc="res_company.name"/>
<small>Odoo Version <t t-raw="version.get('server_version')"/></small> <small>Odoo Version <t t-raw="version.get('server_version')"/></small>
</h1> </h1>
@@ -608,6 +612,7 @@
</t> </t>
</dl> </dl>
</div> </div>
</t>
</section> </section>
</div> </div>
</xpath> </xpath>
+2 -2
View File
@@ -228,8 +228,8 @@ class WebsiteBlog(http.Controller):
all_post_ids = blog_post_obj.search(cr, uid, [('blog_id', '=', blog.id)], context=context) all_post_ids = blog_post_obj.search(cr, uid, [('blog_id', '=', blog.id)], context=context)
# should always return at least the current post # should always return at least the current post
current_blog_post_index = all_post_ids.index(blog_post.id) current_blog_post_index = all_post_ids.index(blog_post.id)
next_post_id = all_post_ids[0 if current_blog_post_index == len(all_post_ids) - 1 \ nb_posts = len(all_post_ids)
else current_blog_post_index + 1] next_post_id = all_post_ids[(current_blog_post_index + 1) % nb_posts] if nb_posts > 1 else None
next_post = next_post_id and blog_post_obj.browse(cr, uid, next_post_id, context=context) or False next_post = next_post_id and blog_post_obj.browse(cr, uid, next_post_id, context=context) or False
values = { values = {
+1 -1
View File
@@ -216,7 +216,7 @@ class WebsiteForm(http.Controller):
# If some attachments didn't match a field on the model, # If some attachments didn't match a field on the model,
# we create a mail.message to link them to the record # we create a mail.message to link them to the record
if orphan_attachment_ids: if orphan_attachment_ids:
if model.name != 'mail.mail': if model.model != 'mail.mail':
values = { values = {
'body': _('<p>Attached files : </p>'), 'body': _('<p>Attached files : </p>'),
'model': model.model, 'model': model.model,
+6 -6
View File
@@ -209,14 +209,14 @@ class Post(models.Model):
) )
# history # history
create_date = fields.Datetime('Asked on', select=True, readonly=True) create_date = fields.Datetime('Asked on', index=True, readonly=True)
create_uid = fields.Many2one('res.users', string='Created by', select=True, readonly=True) create_uid = fields.Many2one('res.users', string='Created by', index=True, readonly=True)
write_date = fields.Datetime('Update on', select=True, readonly=True) write_date = fields.Datetime('Update on', index=True, readonly=True)
bump_date = fields.Datetime('Bumped on', readonly=True, bump_date = fields.Datetime('Bumped on', readonly=True,
help="Technical field allowing to bump a question. Writing on this field will trigger " help="Technical field allowing to bump a question. Writing on this field will trigger "
"a write on write_date and therefore bump the post. Directly writing on write_date " "a write on write_date and therefore bump the post. Directly writing on write_date "
"is currently not supported and this field is a workaround.") "is currently not supported and this field is a workaround.")
write_uid = fields.Many2one('res.users', string='Updated by', select=True, readonly=True) write_uid = fields.Many2one('res.users', string='Updated by', index=True, readonly=True)
relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True) relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True)
# vote # vote
@@ -244,7 +244,7 @@ class Post(models.Model):
# closing # closing
closed_reason_id = fields.Many2one('forum.post.reason', string='Reason') closed_reason_id = fields.Many2one('forum.post.reason', string='Reason')
closed_uid = fields.Many2one('res.users', string='Closed by', select=1) closed_uid = fields.Many2one('res.users', string='Closed by', index=True)
closed_date = fields.Datetime('Closed on', readonly=True) closed_date = fields.Datetime('Closed on', readonly=True)
# karma calculation and access # karma calculation and access
@@ -812,7 +812,7 @@ class Vote(models.Model):
post_id = fields.Many2one('forum.post', string='Post', ondelete='cascade', required=True) post_id = fields.Many2one('forum.post', string='Post', ondelete='cascade', required=True)
user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self._uid) user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self._uid)
vote = fields.Selection([('1', '1'), ('-1', '-1'), ('0', '0')], string='Vote', required=True, default='1') vote = fields.Selection([('1', '1'), ('-1', '-1'), ('0', '0')], string='Vote', required=True, default='1')
create_date = fields.Datetime('Create Date', select=True, readonly=True) create_date = fields.Datetime('Create Date', index=True, readonly=True)
forum_id = fields.Many2one('forum.forum', string='Forum', related="post_id.forum_id", store=True) forum_id = fields.Many2one('forum.forum', string='Forum', related="post_id.forum_id", store=True)
recipient_id = fields.Many2one('res.users', string='To', related="post_id.create_uid", store=True) recipient_id = fields.Many2one('res.users', string='To', related="post_id.create_uid", store=True)
+1 -1
View File
@@ -18,7 +18,7 @@ class Users(models.Model):
['country_id', 'city', 'website', 'website_description', 'website_published'])) ['country_id', 'city', 'website', 'website_description', 'website_published']))
return init_res return init_res
create_date = fields.Datetime('Create Date', readonly=True, copy=False, select=True) create_date = fields.Datetime('Create Date', readonly=True, copy=False, index=True)
karma = fields.Integer('Karma', default=0) karma = fields.Integer('Karma', default=0)
badge_ids = fields.One2many('gamification.badge.user', 'user_id', string='Badges', copy=False) badge_ids = fields.One2many('gamification.badge.user', 'user_id', string='Badges', copy=False)
gold_badge = fields.Integer('Gold badges count', compute="_get_user_badge_level") gold_badge = fields.Integer('Gold badges count', compute="_get_user_badge_level")
@@ -1,17 +1,11 @@
.o_website_quote .bs-sidebar { .o_website_quote .bs-sidebar {
position: fixed; position: relative;
width: 100%;
z-index : 1; z-index : 1;
background-color: #f7f5fa; background-color: #f7f5fa;
border-radius: 5px; border-radius: 5px;
} }
/* mobile preview: the left menu overlaps the quote */
@media (max-width: 992px) {
.o_website_quote .bs-sidebar {
position: relative;
}
}
.o_website_quote .bs-sidenav { .o_website_quote .bs-sidenav {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
@@ -56,14 +50,6 @@
.o_website_quote .bs-sidebar .nav > .active > ul { .o_website_quote .bs-sidebar .nav > .active > ul {
display: block; display: block;
} }
.o_website_quote .bs-sidebar {
width: 213px;
}
}
@media (min-width: 1200px) {
.o_website_quote .bs-sidebar {
width: 263px;
}
} }
@media print { @media print {
body { body {
@@ -2,6 +2,7 @@ odoo.define('website_quote.website_quote', function (require) {
'use strict'; 'use strict';
var ajax = require('web.ajax'); var ajax = require('web.ajax');
var config = require('web.config');
var Widget = require('web.Widget'); var Widget = require('web.Widget');
var website = require('website.website'); var website = require('website.website');
@@ -175,6 +176,22 @@ if(!$('.o_website_quote').length) {
nav_menu.setElement($('[data-id="quote_sidebar"]')); nav_menu.setElement($('[data-id="quote_sidebar"]'));
nav_menu.start($('body[data-target=".navspy"]')); nav_menu.start($('body[data-target=".navspy"]'));
var $bs_sidebar = $(".o_website_quote .bs-sidebar");
$(window).on('resize', _.throttle(adapt_sidebar_position, 200, {leading: false}));
adapt_sidebar_position();
function adapt_sidebar_position() {
$bs_sidebar.css({
position: "",
width: "",
});
if (config.device.size_class >= config.device.SIZES.MD) {
$bs_sidebar.css({
position: "fixed",
width: $bs_sidebar.outerWidth(),
});
}
}
}); });
odoo.define('website_quote.payment_method', function (require) { odoo.define('website_quote.payment_method', function (require) {
+2 -2
View File
@@ -295,8 +295,8 @@ class website_slides(http.Controller):
payload = request.httprequest.content_length payload = request.httprequest.content_length
# payload is total request content size so it's not exact size of file. # payload is total request content size so it's not exact size of file.
# already add client validation this is for double check if client alter. # already add client validation this is for double check if client alter.
if (payload / 1024 / 1024 > 15): if (payload / 1024 / 1024 > 25):
return {'error': _('File is too big. File size cannot exceed 15MB')} return {'error': _('File is too big. File size cannot exceed 25MB')}
values = dict((fname, post[fname]) for fname in [ values = dict((fname, post[fname]) for fname in [
'name', 'url', 'tag_ids', 'slide_type', 'channel_id', 'name', 'url', 'tag_ids', 'slide_type', 'channel_id',
+1 -1
View File
@@ -192,7 +192,7 @@ class EmbeddedSlide(models.Model):
_description = 'Embedded Slides View Counter' _description = 'Embedded Slides View Counter'
_rec_name = 'slide_id' _rec_name = 'slide_id'
slide_id = fields.Many2one('slide.slide', string="Presentation", required=True, select=1) slide_id = fields.Many2one('slide.slide', string="Presentation", required=True, index=True)
url = fields.Char('Third Party Website URL', required=True) url = fields.Char('Third Party Website URL', required=True)
count_views = fields.Integer('# Views', default=1) count_views = fields.Integer('# Views', default=1)
@@ -86,8 +86,8 @@ var SlideDialog = Widget.extend({
this.reset_file(); this.reset_file();
return; return;
} }
if (file.size / 1024 / 1024 > 15) { if (file.size / 1024 / 1024 > 25) {
this.display_alert(_t("File is too big. File size cannot exceed 15MB")); this.display_alert(_t("File is too big. File size cannot exceed 25MB"));
this.reset_file(); this.reset_file();
return; return;
} }
+2 -2
View File
@@ -230,7 +230,7 @@
<main class="container-fluid {{ ' '.join(classes) }}"> <main class="container-fluid {{ ' '.join(classes) }}">
{% if pagename != master_doc %} {% if pagename != master_doc %}
<div class="row"> <div class="row">
{% if 'has-toc' not in meta %} {% if 'has-toc' not in meta and not (pagename in toc) %}
<aside> <aside>
<div class="navbar-aside text-center"> <div class="navbar-aside text-center">
{{ toc }} {{ toc }}
@@ -242,7 +242,7 @@
</div> </div>
</aside> </aside>
{% endif %} {% endif %}
<article class="doc-body {% if 'has-toc' in meta %}doc-toc{% endif %}"> <article class="doc-body {% if 'has-toc' in meta %}doc-toc{% endif %}{% if pagename in toc%}index-category{% endif %}">
{% endif %} {% endif %}
{% block body %} {% endblock %} {% block body %} {% endblock %}
{% if pagename != master_doc %}</article> {% if pagename != master_doc %}</article>
+7
View File
@@ -12353,6 +12353,13 @@ article.doc-body section.doc-content:first-of-type > p:first-child {
font-size: 22.5px; font-size: 22.5px;
} }
} }
article.doc-body.index-category {
min-height: 300px;
min-height: 30vh;
}
article.doc-body.index-category li.toctree-l1 {
padding: 5px 0;
}
.content-switcher { .content-switcher {
margin-top: 1.5em; margin-top: 1.5em;
} }
+9
View File
@@ -742,6 +742,15 @@ article.doc-body {
.lead; .lead;
} }
} }
&.index-category {
min-height: 300px;
min-height: 30vh;
li.toctree-l1 {
padding: 5px 0;
}
}
} }
.content-switcher { .content-switcher {
+15
View File
@@ -0,0 +1,15 @@
Belgium, 2016-08-09
BeOpen agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Kim Huysmans kim.huysmans@rhea.be https://github.com/huysmki
List of contributors:
Kim Huysmans kim.huysmans@rhea.be https://github.com/huysmki
+17
View File
@@ -0,0 +1,17 @@
France, 2016-05-18
iRaiser agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Bruno PLANCHER bplancher@iraiser.eu https://github.com/bplancher
List of contributors:
Bruno PLANCHER bplancher@iraiser.eu https://github.com/bplancher
Damien NICOLAS dnicolas@iraiser.eu https://github.com/gordonzola
+7
View File
@@ -231,6 +231,12 @@ just about any SSL termination proxy, but requires the following setup:
* your SSL termination proxy should also automatically redirect non-secure * your SSL termination proxy should also automatically redirect non-secure
connections to the secure port connections to the secure port
.. warning::
In case you are using the Point of Sale module in combinaison with a `POSBox`_,
you must disable the HTTPS configuration for the route ``/pos/web`` to avoid
mix-content errors.
Configuration sample Configuration sample
-------------------- --------------------
@@ -422,3 +428,4 @@ which will generate a 32 characters pseudorandom printable string.
.. _use an SSH tunnel: .. _use an SSH tunnel:
http://www.postgresql.org/docs/9.3/static/ssh-tunnels.html http://www.postgresql.org/docs/9.3/static/ssh-tunnels.html
.. _WSGI: http://wsgi.readthedocs.org/ .. _WSGI: http://wsgi.readthedocs.org/
.. _POSBox: https://www.odoo.com/page/point-of-sale-hardware#part_2
+8 -7
View File
@@ -625,9 +625,10 @@ class ir_model_constraint(Model):
ids.reverse() ids.reverse()
for data in self.browse(cr, uid, ids, context): for data in self.browse(cr, uid, ids, context):
model = data.model.model model = data.model.model
model_obj = self.pool.get(model) if model in self.pool:
if not model_obj: table = self.pool[model]._table
continue else:
table = model.replace('.', '_')
name = openerp.tools.ustr(data.name) name = openerp.tools.ustr(data.name)
typ = data.type typ = data.type
@@ -641,17 +642,17 @@ class ir_model_constraint(Model):
if typ == 'f': if typ == 'f':
# test if FK exists on this table (it could be on a related m2m table, in which case we ignore it) # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it)
cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid) cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table)) WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, table))
if cr.fetchone(): if cr.fetchone():
cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),) cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (table, name),)
_logger.info('Dropped FK CONSTRAINT %s@%s', name, model) _logger.info('Dropped FK CONSTRAINT %s@%s', name, model)
if typ == 'u': if typ == 'u':
# test if constraint exists # test if constraint exists
cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid) cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid)
WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table)) WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, table))
if cr.fetchone(): if cr.fetchone():
cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),) cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (table, name),)
_logger.info('Dropped CONSTRAINT %s@%s', name, model) _logger.info('Dropped CONSTRAINT %s@%s', name, model)
self.unlink(cr, uid, ids, context) self.unlink(cr, uid, ids, context)
+4 -2
View File
@@ -227,10 +227,12 @@ class CurrencyRate(models.Model):
_description = "Currency Rate" _description = "Currency Rate"
_order = "name desc" _order = "name desc"
name = fields.Datetime(string='Date', required=True, index=True, default=lambda self:fields.Date.today() + ' 00:00:00') name = fields.Datetime(string='Date', required=True, index=True,
default=lambda self: fields.Date.today() + ' 00:00:00')
rate = fields.Float(digits=(12, 6), help='The rate of the currency to the currency of rate 1') 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) currency_id = fields.Many2one('res.currency', string='Currency', readonly=True)
company_id = fields.Many2one('res.company', string='Company') company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.user._get_company())
@api.model @api.model
def name_search(self, name, args=None, operator='ilike', limit=80): def name_search(self, name, args=None, operator='ilike', limit=80):
@@ -23,6 +23,28 @@
</field> </field>
</record> </record>
<record id="view_currency_rate_form" model="ir.ui.view">
<field name="name">res.currency.rate.form</field>
<field name="model">res.currency.rate</field>
<field name="arch" type="xml">
<form string="Currency Rate">
<sheet>
<group>
<group>
<field name="name"/>
<field name="rate"/>
</group>
<group>
<field name="currency_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<act_window <act_window
id="act_view_currency_rates" id="act_view_currency_rates"
name="Currency Rates" name="Currency Rates"
+8 -9
View File
@@ -128,11 +128,14 @@ def main(args):
# bit overkill, but better safe than sorry I guess # bit overkill, but better safe than sorry I guess
csv.field_size_limit(500 * 1024 * 1024) csv.field_size_limit(500 * 1024 * 1024)
if config["db_name"]: preload = []
try: if config['db_name']:
openerp.service.db._create_empty_database(config["db_name"]) preload = config['db_name'].split(',')
except openerp.service.db.DatabaseExists: for db_name in preload:
pass try:
openerp.service.db._create_empty_database(db_name)
except openerp.service.db.DatabaseExists:
pass
if config["test_file"]: if config["test_file"]:
config["test_enable"] = True config["test_enable"] = True
@@ -150,10 +153,6 @@ def main(args):
if config['workers']: if config['workers']:
openerp.multi_process = True openerp.multi_process = True
preload = []
if config['db_name']:
preload = config['db_name'].split(',')
stop = config["stop_after_init"] stop = config["stop_after_init"]
setup_pid_file() setup_pid_file()
+2 -2
View File
@@ -210,7 +210,7 @@ class WebRequest(object):
to a database. to a database.
""" """
if not self.db: if not self.db:
return RuntimeError('request not bound to a database') raise RuntimeError('request not bound to a database')
return openerp.api.Environment(self.cr, self.uid, self.context) return openerp.api.Environment(self.cr, self.uid, self.context)
@lazy_property @lazy_property
@@ -246,7 +246,7 @@ class WebRequest(object):
# can not be a lazy_property because manual rollback in _call_function # can not be a lazy_property because manual rollback in _call_function
# if already set (?) # if already set (?)
if not self.db: if not self.db:
return RuntimeError('request not bound to a database') raise RuntimeError('request not bound to a database')
if not self._cr: if not self._cr:
self._cr = self.registry.cursor() self._cr = self.registry.cursor()
return self._cr return self._cr
+1 -1
View File
@@ -1354,7 +1354,7 @@ class BaseModel(object):
except ValidationError, e: except ValidationError, e:
raise raise
except Exception, e: except Exception, e:
raise ValidationError("Error while validating constraint\n\n%s" % tools.ustr(e)) raise ValidationError("%s\n\n%s" % (_("Error while validating constraint"), tools.ustr(e)))
@api.model @api.model
def default_get(self, fields_list): def default_get(self, fields_list):
+1 -5
View File
@@ -859,11 +859,7 @@ def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kin
fp.close() fp.close()
def convert_sql_import(cr, fp): def convert_sql_import(cr, fp):
queries = fp.read().split(';') cr.execute(fp.read())
for query in queries:
new_query = ' '.join(query.split())
if new_query:
cr.execute(new_query)
def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init', def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
noupdate=False): noupdate=False):
+22 -1
View File
@@ -14,6 +14,7 @@ import subprocess
import logging import logging
import os import os
import passlib.utils import passlib.utils
import re
import socket import socket
import sys import sys
import threading import threading
@@ -333,6 +334,26 @@ def topological_sort(elems):
return result return result
try:
import xlwt
# add some sanitizations to respect the excel sheet name restrictions
# as the sheet name is often translatable, can not control the input
class PatchedWorkbook(xlwt.Workbook):
def add_sheet(self, name):
# invalid Excel character: []:*?/\
name = re.sub(r'[\[\]:*?/\\]', '', name)
# maximum size is 31 characters
name = name[:31]
return super(PatchedWorkbook, self).add_sheet(name)
xlwt.Workbook = PatchedWorkbook
except ImportError:
xlwt = None
class UpdateableStr(local): class UpdateableStr(local):
""" Class that stores an updateable string (used in wizards) """ Class that stores an updateable string (used in wizards)
""" """
@@ -928,7 +949,7 @@ class CountingStream(object):
def stripped_sys_argv(*strip_args): def stripped_sys_argv(*strip_args):
"""Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses""" """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
strip_args = sorted(set(strip_args) | set(['-s', '--save', '-u', '--update', '-i', '--init'])) strip_args = sorted(set(strip_args) | set(['-s', '--save', '-u', '--update', '-i', '--init', '--i18n-overwrite']))
assert all(config.parser.has_option(s) for s in strip_args) assert all(config.parser.has_option(s) for s in strip_args)
takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args) takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)