[ODOO] Upstream Changes of Odoo to Flectra till commit ref. d636c6c95c

This commit is contained in:
flectra-admin 2018-04-05 13:55:40 +05:30
parent e67b7e553c
commit 487ed76bc1
631 changed files with 10472 additions and 3607 deletions

View File

@ -391,7 +391,7 @@ class AccountJournal(models.Model):
belongs_to_company = fields.Boolean('Belong to the user\'s current company', compute="_belong_to_company", search="_search_company_journals",)
# Bank journals fields
bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False, domain="[('partner_id','=', company_id)]")
bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False)
bank_statements_source = fields.Selection([('undefined', 'Undefined Yet'),('manual', 'Record Manually')], string='Bank Feeds', default='undefined')
bank_acc_number = fields.Char(related='bank_account_id.acc_number')
bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id')
@ -433,7 +433,7 @@ class AccountJournal(models.Model):
for journal in self:
if journal.sequence_id and journal.sequence_number_next:
sequence = journal.sequence_id._get_current_sequence()
sequence.number_next = journal.sequence_number_next
sequence.sudo().number_next = journal.sequence_number_next
@api.multi
# do not depend on 'refund_sequence_id.date_range_ids', because
@ -512,9 +512,16 @@ class AccountJournal(models.Model):
@api.multi
def write(self, vals):
for journal in self:
company = journal.company_id
if ('company_id' in vals and journal.company_id.id != vals['company_id']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its company.'))
company = self.env['res.company'].browse(vals['company_id'])
if self.bank_account_id.company_id and self.bank_account_id.company_id != company:
self.bank_account_id.write({
'company_id': company.id,
'partner_id': company.partner_id.id,
})
if ('code' in vals and journal.code != vals['code']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its short name.'))
@ -528,8 +535,16 @@ class AccountJournal(models.Model):
self.default_debit_account_id.currency_id = vals['currency_id']
if not 'default_credit_account_id' in vals and self.default_credit_account_id:
self.default_credit_account_id.currency_id = vals['currency_id']
if 'bank_account_id' in vals and not vals.get('bank_account_id'):
raise UserError(_('You cannot empty the bank account once set.'))
if self.bank_account_id:
self.bank_account_id.currency_id = vals['currency_id']
if 'bank_account_id' in vals:
if not vals.get('bank_account_id'):
raise UserError(_('You cannot empty the bank account once set.'))
else:
bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id'])
if bank_account.partner_id != company.partner_id:
raise UserError(_("The partners of the journal's company and the related bank account mismatch."))
result = super(AccountJournal, self).write(vals)
# Create the bank_account_id if necessary

View File

@ -341,7 +341,7 @@ class AccountBankStatement(models.Model):
def link_bank_to_partner(self):
for statement in self:
for st_line in statement.line_ids:
if st_line.bank_account_id and st_line.partner_id and st_line.bank_account_id.partner_id != st_line.partner_id:
if st_line.bank_account_id and st_line.partner_id and not st_line.bank_account_id.partner_id:
st_line.bank_account_id.partner_id = st_line.partner_id
@ -938,7 +938,7 @@ class AccountBankStatementLine(models.Model):
'currency_id': currency.id,
'amount': abs(total),
'communication': self._get_communication(payment_methods[0] if payment_methods else False),
'name': self.statement_id.name,
'name': self.statement_id.name or _("Bank Statement %s") % self.date,
})
# Complete dicts to create both counterpart move lines and write-offs

View File

@ -221,7 +221,7 @@ class AccountInvoice(models.Model):
@api.depends('move_id.line_ids.amount_residual')
def _compute_payments(self):
payment_lines = set()
for line in self.move_id.line_ids:
for line in self.move_id.line_ids.filtered(lambda l: l.account_id.id == self.account_id.id):
payment_lines.update(line.mapped('matched_credit_ids.credit_move_id.id'))
payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id'))
self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines))
@ -347,7 +347,7 @@ class AccountInvoice(models.Model):
payment_move_line_ids = fields.Many2many('account.move.line', string='Payment Move Lines', compute='_compute_payments', store=True)
user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange',
readonly=True, states={'draft': [('readonly', False)]},
default=lambda self: self.env.user)
default=lambda self: self.env.user, copy=False)
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position',
readonly=True, states={'draft': [('readonly', False)]})
commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', compute_sudo=True,
@ -1195,25 +1195,7 @@ class AccountInvoice(models.Model):
@api.model
def line_get_convert(self, line, part):
return {
'date_maturity': line.get('date_maturity', False),
'partner_id': part,
'name': line['name'],
'debit': line['price'] > 0 and line['price'],
'credit': line['price'] < 0 and -line['price'],
'account_id': line['account_id'],
'analytic_line_ids': line.get('analytic_line_ids', []),
'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
'currency_id': line.get('currency_id', False),
'quantity': line.get('quantity', 1.00),
'product_id': line.get('product_id', False),
'product_uom_id': line.get('uom_id', False),
'analytic_account_id': line.get('account_analytic_id', False),
'invoice_id': line.get('invoice_id', False),
'tax_ids': line.get('tax_ids', False),
'tax_line_id': line.get('tax_line_id', False),
'analytic_tag_ids': line.get('analytic_tag_ids', False),
}
return self.env['product.product']._convert_prepared_anglosaxon_line(line, part)
@api.multi
def action_cancel(self):
@ -1343,6 +1325,7 @@ class AccountInvoice(models.Model):
values['state'] = 'draft'
values['number'] = False
values['origin'] = invoice.number
values['payment_term_id'] = False
values['refund_invoice_id'] = invoice.id
if date:
@ -1735,6 +1718,7 @@ class AccountPaymentTerm(models.Model):
def compute(self, value, date_ref=False):
date_ref = date_ref or fields.Date.today()
amount = value
sign = value < 0 and -1 or 1
result = []
if self.env.context.get('currency_id'):
currency = self.env['res.currency'].browse(self.env.context['currency_id'])
@ -1742,7 +1726,7 @@ class AccountPaymentTerm(models.Model):
currency = self.env.user.company_id.currency_id
for line in self.line_ids:
if line.value == 'fixed':
amt = currency.round(line.value_amount)
amt = sign * currency.round(line.value_amount)
elif line.value == 'percent':
amt = currency.round(value * (line.value_amount / 100.0))
elif line.value == 'balance':

View File

@ -224,7 +224,7 @@ class account_journal(models.Model):
data as its first element, and the arguments dictionary to use to run
it as its second.
"""
return ("""SELECT state, amount_total, currency_id AS currency
return ("""SELECT state, amount_total, currency_id AS currency, type
FROM account_invoice
WHERE journal_id = %(journal_id)s AND state = 'open';""", {'journal_id':self.id})
@ -234,7 +234,7 @@ class account_journal(models.Model):
gather the bills in draft state data, and the arguments
dictionary to use to run it as its second.
"""
return ("""SELECT state, amount_total, currency_id AS currency
return ("""SELECT state, amount_total, currency_id AS currency, type
FROM account_invoice
WHERE journal_id = %(journal_id)s AND state = 'draft';""", {'journal_id':self.id})
@ -247,7 +247,9 @@ class account_journal(models.Model):
for result in results_dict:
cur = self.env['res.currency'].browse(result.get('currency'))
rslt_count += 1
rslt_sum += cur.compute(result.get('amount_total'), target_currency)
type_factor = result.get('type') in ('in_refund', 'out_refund') and -1 or 1
rslt_sum += type_factor * cur.compute(result.get('amount_total'), target_currency)
return (rslt_count, rslt_sum)
@api.multi
@ -352,6 +354,8 @@ class account_journal(models.Model):
})
[action] = self.env.ref('account.%s' % action_name).read()
if not self.env.context.get('use_domain'):
ctx['search_default_journal_id'] = self.id
action['context'] = ctx
action['domain'] = self._context.get('use_domain', [])
account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False)

View File

@ -107,7 +107,7 @@ class AccountMove(models.Model):
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")
# 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, readonly=True)
tax_cash_basis_rec_id = fields.Many2one(
'account.partial.reconcile',
string='Tax Cash Basis Entry of',
@ -291,7 +291,7 @@ class AccountMoveLine(models.Model):
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', 'move_id.state')
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.
This amount will be 0 for fully reconciled lines or lines from a non-reconciliable account, the original line amount
@ -525,7 +525,7 @@ class AccountMoveLine(models.Model):
raise ValidationError(_("You cannot create journal items with a secondary currency without filling both 'currency' and 'amount currency' field."))
@api.multi
@api.constrains('amount_currency')
@api.constrains('amount_currency', 'debit', 'credit')
def _check_currency_amount(self):
for line in self:
if line.amount_currency:
@ -936,7 +936,9 @@ class AccountMoveLine(models.Model):
def _get_pair_to_reconcile(self):
#field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set)
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
company_currency_id = self[0].account_id.company_id.currency_id
account_curreny_id = self[0].account_id.currency_id
field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual'
#reconciliation on bank accounts are special cases as we don't want to set them as reconciliable
#but we still want to reconcile entries that are reversed together in order to clear those lines
#in the bank reconciliation report.
@ -952,7 +954,7 @@ class AccountMoveLine(models.Model):
elif self._context.get('skip_full_reconcile_check') == 'amount_currency_only':
field = 'amount_residual_currency'
#target the pair of move in self that are the oldest
sorted_moves = sorted(self, key=lambda a: a.date)
sorted_moves = sorted(self, key=lambda a: a.date_maturity or a.date)
debit = credit = False
for aml in sorted_moves:
if credit and debit:
@ -974,8 +976,9 @@ class AccountMoveLine(models.Model):
#there is no more pair to reconcile so return what move_line are left
if not sm_credit_move or not sm_debit_move:
return self
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
company_currency_id = self[0].account_id.company_id.currency_id
account_curreny_id = self[0].account_id.currency_id
field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual'
if not sm_debit_move.debit and not sm_debit_move.credit:
#both debit and credit field are 0, consider the amount_residual_currency field because it's an exchange difference entry
field = 'amount_residual_currency'
@ -1198,6 +1201,11 @@ class AccountMoveLine(models.Model):
account_move_line.payment_id.write({'invoice_ids': [(3, invoice.id, None)]})
rec_move_ids += account_move_line.matched_debit_ids
rec_move_ids += account_move_line.matched_credit_ids
if self.env.context.get('invoice_id'):
current_invoice = self.env['account.invoice'].browse(self.env.context['invoice_id'])
rec_move_ids = rec_move_ids.filtered(
lambda r: (r.debit_move_id + r.credit_move_id) & current_invoice.move_id.line_ids
)
return rec_move_ids.unlink()
####################################################
@ -1361,7 +1369,7 @@ class AccountMoveLine(models.Model):
record.payment_id.state = 'reconciled'
result = super(AccountMoveLine, self).write(vals)
if self._context.get('check_move_validity', True):
if self._context.get('check_move_validity', True) and any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')):
move_ids = set()
for line in self:
if line.move_id.id not in move_ids:
@ -1381,7 +1389,7 @@ class AccountMoveLine(models.Model):
raise UserError(_('You cannot do this modification on a reconciled entry. You can just change some non legal fields or you must unreconcile first.\n%s.') % err_msg)
if line.move_id.id not in move_ids:
move_ids.add(line.move_id.id)
self.env['account.move'].browse(list(move_ids))._check_lock_date()
self.env['account.move'].browse(list(move_ids))._check_lock_date()
return True
####################################################

View File

@ -102,7 +102,7 @@ class account_abstract_payment(models.AbstractModel):
class account_register_payments(models.TransientModel):
_name = "account.register.payments"
_inherit = ['account.abstract.payment']
_inherit = 'account.abstract.payment'
_description = "Register payments on multiple invoices"
invoice_ids = fields.Many2many('account.invoice', string='Invoices', copy=False)
@ -332,7 +332,7 @@ class account_payment(models.Model):
def _compute_journal_domain_and_types(self):
journal_type = ['bank', 'cash']
domain = []
if self.currency_id.is_zero(self.amount):
if self.currency_id.is_zero(self.amount) and self.has_invoices:
# In case of payment with 0 amount, allow to select a journal of type 'general' like
# 'Miscellaneous Operations' and set this journal by default.
journal_type = ['general']
@ -340,7 +340,7 @@ class account_payment(models.Model):
else:
if self.payment_type == 'inbound':
domain.append(('at_least_one_inbound', '=', True))
else:
elif self.payment_type == 'outbound':
domain.append(('at_least_one_outbound', '=', True))
return {'domain': domain, 'journal_types': set(journal_type)}
@ -349,9 +349,16 @@ class account_payment(models.Model):
jrnl_filters = self._compute_journal_domain_and_types()
journal_types = jrnl_filters['journal_types']
domain_on_types = [('type', 'in', list(journal_types))]
if self.journal_id.type not in journal_types:
self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1)
return {'domain': {'journal_id': jrnl_filters['domain'] + domain_on_types}}
journal_domain = jrnl_filters['domain'] + domain_on_types
default_journal_id = self.env.context.get('default_journal_id')
if not default_journal_id:
if self.journal_id.type not in journal_types:
self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1)
else:
journal_domain = journal_domain.append(('id', '=', default_journal_id))
return {'domain': {'journal_id': journal_domain}}
@api.one
@api.depends('invoice_ids', 'payment_type', 'partner_type', 'partner_id')
@ -382,6 +389,8 @@ class account_payment(models.Model):
self.partner_type = 'customer'
elif self.payment_type == 'outbound':
self.partner_type = 'supplier'
else:
self.partner_type = False
# Set payment method domain
res = self._onchange_journal()
if not res.get('domain', {}):
@ -522,7 +531,7 @@ class account_payment(models.Model):
if any(len(record.invoice_ids) != 1 for record in self):
# For multiple invoices, there is account.register.payments wizard
raise UserError(_("This method should only be called to process a single invoice's payment."))
self.post();
return self.post()
def _create_payment_entry(self, amount):
""" Create a journal entry corresponding to a payment, if the payment references invoice(s) they are reconciled.
@ -571,9 +580,9 @@ class account_payment(models.Model):
writeoff_line['amount_currency'] = amount_currency_wo
writeoff_line['currency_id'] = currency_id
writeoff_line = aml_obj.create(writeoff_line)
if counterpart_aml['debit'] or writeoff_line['credit']:
if counterpart_aml['debit'] or (writeoff_line['credit'] and not counterpart_aml['credit']):
counterpart_aml['debit'] += credit_wo - debit_wo
if counterpart_aml['credit'] or writeoff_line['debit']:
if counterpart_aml['credit'] or (writeoff_line['debit'] and not counterpart_aml['debit']):
counterpart_aml['credit'] += debit_wo - credit_wo
counterpart_aml['amount_currency'] -= amount_currency_wo

View File

@ -745,6 +745,9 @@ class WizardMultiChartsAccounts(models.TransientModel):
res.setdefault('domain', {})
res['domain']['sale_tax_id'] = repr(sale_tax_domain)
res['domain']['purchase_tax_id'] = repr(purchase_tax_domain)
else:
self.sale_tax_id = False
self.purchase_tax_id = False
if self.chart_template_id.transfer_account_id:
self.transfer_account_id = self.chart_template_id.transfer_account_id.id
if self.chart_template_id.code_digits:
@ -775,7 +778,7 @@ class WizardMultiChartsAccounts(models.TransientModel):
if company_id:
company = self.env['res.company'].browse(company_id)
currency_id = company.on_change_country(company.country_id.id)['value']['currency_id']
res.update({'currency_id': currency_id.id})
res.update({'currency_id': currency_id})
chart_templates = account_chart_template.search([('visible', '=', True)])
if chart_templates:

View File

@ -136,6 +136,12 @@ Best Regards,'''))
company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code, digits)
if values.get('accounts_code_digits'):
company.reflect_code_digits_change(digits)
#forbid the change of currency_id if there are already some accounting entries existing
if 'currency_id' in values and values['currency_id'] != company.currency_id.id:
if self.env['account.move.line'].search([('company_id', '=', company.id)]):
raise UserError(_('You cannot change the currency of the company since some journal items already exist'))
return super(ResCompany, self).write(values)
@api.model
@ -271,7 +277,7 @@ Best Regards,'''))
default_journal = self.env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', self.id)], limit=1)
if not default_journal:
raise UserError(_("No miscellaneous journal could be found. Please create one before proceeding."))
raise UserError(_("Please install a chart of accounts or create a miscellaneous journal before proceeding."))
self.account_opening_move_id = self.env['account.move'].create({
'name': _('Opening Journal Entry'),

View File

@ -71,3 +71,28 @@ class ProductTemplate(models.Model):
if not fiscal_pos:
fiscal_pos = self.env['account.fiscal.position']
return fiscal_pos.map_accounts(accounts)
class ProductProduct(models.Model):
_inherit = "product.product"
@api.model
def _convert_prepared_anglosaxon_line(self, line, partner):
return {
'date_maturity': line.get('date_maturity', False),
'partner_id': partner,
'name': line['name'],
'debit': line['price'] > 0 and line['price'],
'credit': line['price'] < 0 and -line['price'],
'account_id': line['account_id'],
'analytic_line_ids': line.get('analytic_line_ids', []),
'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)),
'currency_id': line.get('currency_id', False),
'quantity': line.get('quantity', 1.00),
'product_id': line.get('product_id', False),
'product_uom_id': line.get('uom_id', False),
'analytic_account_id': line.get('account_analytic_id', False),
'invoice_id': line.get('invoice_id', False),
'tax_ids': line.get('tax_ids', False),
'tax_line_id': line.get('tax_line_id', False),
'analytic_tag_ids': line.get('analytic_tag_ids', False),
}

View File

@ -27,7 +27,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
res = []
total = []
cr = self.env.cr
user_company = self.env.user.company_id.id
company_ids = self.env.context.get('company_ids', (self.env.user.company_id.id,))
move_state = ['draft', 'posted']
branch = ''
if branch_id:
@ -44,7 +44,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
if reconciled_after_date:
reconciliation_clause = '(l.reconciled IS FALSE OR l.id IN %s)'
arg_list += (tuple(reconciled_after_date),)
arg_list += (date_from, user_company)
arg_list += (date_from, tuple(company_ids))
query = '''
SELECT DISTINCT l.partner_id, UPPER(res_partner.name)
FROM account_move_line AS l left join res_partner on l.partner_id = res_partner.id, account_account, account_move am
@ -54,7 +54,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
AND (account_account.internal_type IN %s)
AND ''' + reconciliation_clause + branch +'''
AND (l.date <= %s)
AND l.company_id = %s
AND l.company_id IN %s
ORDER BY UPPER(res_partner.name)'''
cr.execute(query, arg_list)
@ -79,8 +79,8 @@ class ReportAgedPartnerBalance(models.AbstractModel):
AND (COALESCE(l.date_maturity,l.date) > %s)\
AND ((l.partner_id IN %s) OR (l.partner_id IS NULL))
AND (l.date <= %s) ''' + branch + '''
AND l.company_id = %s'''
cr.execute(query, (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from, user_company))
AND l.company_id IN %s'''
cr.execute(query, (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from, tuple(company_ids)))
aml_ids = cr.fetchall()
aml_ids = aml_ids and [x[0] for x in aml_ids] or []
for line in self.env['account.move.line'].browse(aml_ids):
@ -120,7 +120,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
else:
dates_query += ' <= %s)'
args_list += (periods[str(i)]['stop'],)
args_list += (date_from, user_company)
args_list += (date_from, tuple(company_ids))
query = '''SELECT l.id
FROM account_move_line AS l, account_account, account_move am
@ -130,7 +130,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
AND ((l.partner_id IN %s) OR (l.partner_id IS NULL))
AND ''' + dates_query + branch +'''
AND (l.date <= %s)
AND l.company_id = %s'''
AND l.company_id IN %s'''
cr.execute(query, args_list)
partners_amount = {}
aml_ids = cr.fetchall()
@ -193,7 +193,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
values['name'] = _('Unknown Partner')
values['trust'] = False
if at_least_one_amount:
if at_least_one_amount or self._context.get('include_nullified_amount'):
res.append(values)
return res, total, lines

View File

@ -14,7 +14,7 @@ class ReportPartnerLedger(models.AbstractModel):
full_account = []
currency = self.env['res.currency']
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
query = """
SELECT "account_move_line".id, "account_move_line".date, j.code, acc.code as a_code, acc.name as a_name, "account_move_line".ref, m.name as move_name, "account_move_line".name, "account_move_line".debit, "account_move_line".credit, "account_move_line".amount_currency,"account_move_line".currency_id, c.symbol AS currency_code
@ -51,7 +51,7 @@ class ReportPartnerLedger(models.AbstractModel):
return
result = 0.0
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
query = """SELECT sum(""" + field + """)
@ -95,7 +95,7 @@ class ReportPartnerLedger(models.AbstractModel):
AND NOT a.deprecated""", (tuple(data['computed']['ACCOUNT_TYPE']),))
data['computed']['account_ids'] = [a for (a,) in self.env.cr.fetchall()]
params = [tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
query = """
SELECT DISTINCT "account_move_line".partner_id
FROM """ + query_get_data[0] + """, account_account AS account, account_move AS am

View File

@ -524,17 +524,37 @@ var StatementModel = BasicModel.extend({
*/
togglePartialReconcile: function (handle) {
var line = this.getLine(handle);
var props = _.filter(line.reconciliation_proposition, {'invalid': false});
var prop = props[0];
if (props.length !== 1 || Math.abs(line.st_line.amount) >= Math.abs(prop.amount)) {
// Retrieve the toggle proposition
var selected;
_.each(line.reconciliation_proposition, function (prop) {
if (!prop.invalid) {
if (((line.balance.amount < 0 || !line.partial_reconcile) && prop.amount > 0 && line.st_line.amount > 0 && line.st_line.amount < prop.amount) ||
((line.balance.amount > 0 || !line.partial_reconcile) && prop.amount < 0 && line.st_line.amount < 0 && line.st_line.amount > prop.amount)) {
selected = prop;
return false;
}
}
});
// If no toggled proposition found, reject it
if (selected == null)
return $.Deferred().reject();
}
prop.partial_reconcile = !prop.partial_reconcile;
if (!prop.partial_reconcile) {
// Inverse partial_reconcile value
selected.partial_reconcile = !selected.partial_reconcile;
if (!selected.partial_reconcile) {
return this._computeLine(line);
}
// Compute the write_off
var format_options = { currency_id: line.st_line.currency_id };
selected.write_off_amount = selected.amount + line.balance.amount;
selected.write_off_amount_str = field_utils.format.monetary(Math.abs(selected.write_off_amount), {}, format_options);
selected.write_off_amount_str = selected.write_off_amount_str.replace('&nbsp;', ' ');
return this._computeLine(line).then(function () {
if (prop.partial_reconcile) {
if (selected.partial_reconcile) {
line.balance.amount = 0;
line.balance.type = 1;
line.mode = 'inactive';
@ -594,7 +614,7 @@ var StatementModel = BasicModel.extend({
handles = [handle];
} else {
_.each(this.lines, function (line, handle) {
if (!line.reconciled && !line.balance.amount && line.reconciliation_proposition.length) {
if (!line.reconciled && line.balance && !line.balance.amount && line.reconciliation_proposition.length) {
handles.push(handle);
}
});
@ -1051,7 +1071,7 @@ var StatementModel = BasicModel.extend({
// Do not forward port in master. @CSN will change this
var amount = prop.computed_with_tax && -prop.base_amount || -prop.amount;
if (prop.partial_reconcile === true) {
amount = -line.st_line.amount;
amount = -prop.write_off_amount;
}
var result = {
name : prop.label,

View File

@ -354,7 +354,21 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
// reconciliation_proposition
var $props = this.$('.accounting_view tbody').empty();
var props = _.filter(state.reconciliation_proposition, {'display': true});
// loop state propositions
var props = [];
var nb_debit_props = 0;
var nb_credit_props = 0;
_.each(state.reconciliation_proposition, function (prop) {
if (prop.display) {
props.push(prop);
if (prop.amount < 0)
nb_debit_props += 1;
else if (prop.amount > 0)
nb_credit_props += 1;
}
});
_.each(props, function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
@ -362,18 +376,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
.appendTo($line.find('.cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
}
if ((state.balance.amount_currency !== 0 || line.partial_reconcile) && props.length === 1 &&
line.already_paid === false &&
(
(state.st_line.amount > 0 && state.st_line.amount < props[0].amount) ||
(state.st_line.amount < 0 && state.st_line.amount > props[0].amount))
) {
if (line.already_paid === false &&
((state.balance.amount_currency < 0 || line.partial_reconcile) && nb_credit_props == 1
&& line.amount > 0 && state.st_line.amount > 0 && state.st_line.amount < line.amount) ||
((state.balance.amount_currency > 0 || line.partial_reconcile) && nb_debit_props == 1
&& line.amount < 0 && state.st_line.amount < 0 && state.st_line.amount > line.amount)) {
var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left');
var text;
if (line.partial_reconcile) {
text = _t("Undo the partial reconciliation.");
$cell.text(state.st_line.amount_str);
$cell.text(line.write_off_amount_str);
} else {
text = _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open.");
}
@ -415,14 +427,15 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
this._renderCreate(state);
}
var data = this.model.get(this.handleCreateRecord).data;
this.model.notifyChanges(this.handleCreateRecord, state.createForm);
var record = this.model.get(this.handleCreateRecord);
_.each(this.fields, function (field, fieldName) {
if (self._avoidFieldUpdate[fieldName]) return;
if (fieldName === "partner_id") return;
if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
field.reset(record);
}
this.model.notifyChanges(this.handleCreateRecord, state.createForm).then(function () {
var record = self.model.get(self.handleCreateRecord);
_.each(self.fields, function (field, fieldName) {
if (self._avoidFieldUpdate[fieldName]) return;
if (fieldName === "partner_id") return;
if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
field.reset(record);
}
});
});
}
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
@ -626,7 +639,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
return;
}
if(event.keyCode === 13) {
if (_.findWhere(this.model.lines, {mode: 'create'}).balance.amount) {
var created_lines = _.findWhere(this.model.lines, {mode: 'create'});
if (created_lines && created_lines.balance.amount) {
this._onCreateProposition();
}
return;

View File

@ -34,17 +34,22 @@ var db = {
fields: {
id: {string: "ID", type: 'integer'},
code: {string: "code", type: 'integer'},
display_name: {string: "Displayed name", type: 'char'},
name: {string: "Displayed name", type: 'char'},
},
records: [
{id: 282, code: 100000, display_name: "100000 Fixed Asset Account"},
{id: 283, code: 101000, display_name: "101000 Current Assets"},
{id: 284, code: 101110, display_name: "101110 Stock Valuation Account"},
{id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)"},
{id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)"},
{id: 287, code: 101200, display_name: "101200 Account Receivable"},
{id: 288, code: 101300, display_name: "101300 Tax Paid"},
{id: 308, code: 101401, display_name: "101401 Bank"},
{id: 282, code: 100000, name: "100000 Fixed Asset Account"},
{id: 283, code: 101000, name: "101000 Current Assets"},
{id: 284, code: 101110, name: "101110 Stock Valuation Account"},
{id: 285, code: 101120, name: "101120 Stock Interim Account (Received)"},
{id: 286, code: 101130, name: "101130 Stock Interim Account (Delivered)"},
{id: 287, code: 101200, name: "101200 Account Receivable"},
{id: 288, code: 101300, name: "101300 Tax Paid"},
{id: 308, code: 101401, name: "101401 Bank"},
{id: 500, code: 500, name: "500 Account"},
{id: 501, code: 501, name: "501 Account"},
{id: 502, code: 502, name: "502 Account"},
{id: 503, code: 503, name: "503 Account"},
{id: 504, code: 504, name: "504 Account"},
],
mark_as_reconciled: function () {
return $.when();
@ -767,13 +772,7 @@ QUnit.module('account', {
partner_id: false,
counterpart_aml_dicts:[],
payment_aml_ids: [392],
new_aml_dicts: [
{
"credit": 343.42,
"debit": 0,
"name": "Bank fees : Open balance"
}
],
new_aml_dicts: [],
}]
], "should call process_reconciliations with partial reconcile values");
}
@ -802,14 +801,14 @@ QUnit.module('account', {
assert.notOk( widget.$('.cell_left .line_info_button').length, "should not display the partial reconciliation alert");
widget.$('.accounting_view thead td:first').trigger('click');
widget.$('.match .cell_account_code:first').trigger('click');
assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 0, "should not display the partial reconciliation alert");
assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 1, "should display the partial reconciliation alert");
assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should not display the reconcile button");
assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert");
widget.$('.accounting_view .cell_left .line_info_button').trigger('click');
assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 0, "should not display a partial reconciliation alert");
assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should not display the partial reconciliation information");
assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 1, "should display a partial reconciliation alert");
assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_true'), "should display the partial reconciliation information");
assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button");
assert.strictEqual( widget.$el.data('mode'), "match", "should be inactive mode");
assert.strictEqual( widget.$el.data('mode'), "inactive", "should be inactive mode");
widget.$('button.btn-default:not(hidden)').trigger('click');
clientAction.destroy();
@ -1027,13 +1026,77 @@ QUnit.module('account', {
clientAction.destroy();
});
QUnit.test('Reconciliation create line (many2one test)', function (assert) {
assert.expect(5);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
var def = $.Deferred();
testUtils.addMockEnvironment(clientAction, {
data: this.params.data,
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
archs: {
"account.account,false,list": '<tree string="Account"><field name="code"/><field name="name"/></tree>',
"account.account,false,search": '<search string="Account"><field name="code"/></search>',
},
mockRPC: function (route, args) {
if (args.method === 'name_get') {
return def.then(this._super.bind(this, route, args));
}
return this._super(route, args);
},
});
clientAction.prependTo($('#qunit-fixture'));
var widget = clientAction.widgets[0];
// open the first line in write-off mode
widget.$('.accounting_view tfoot td:first').trigger('click');
// select an account with the many2one (drop down)
widget.$('.create .create_account_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(101200)').trigger('mouseenter').trigger('click');
assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Display the selected account");
assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "101200", "Display the code of the selected account");
// use the many2one select dialog to change the account
widget.$('.create .create_account_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(Search)').trigger('mouseenter').trigger('click');
// select the account who does not appear in the drop drown
$('.modal tr.o_data_row:contains(502)').click();
assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Selected account does not change");
// wait the name_get to render the changes
def.resolve();
assert.strictEqual(widget.$('.create .create_account_id input').val(), "502 Account", "Display the selected account");
assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "502", "Display the code of the selected account");
clientAction.destroy();
});
QUnit.test('Reconciliation create line with taxes', function (assert) {
assert.expect(13);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
'data': this.params.data,
data: this.params.data,
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
});
clientAction.appendTo($('#qunit-fixture'));
@ -1045,26 +1108,26 @@ QUnit.module('account', {
widget.$('.create .create_label input').val('test1').trigger('input');
widget.$('.create .create_amount input').val('1100').trigger('input');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "1100.00", "should display the value 1100.00 in left column");
assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$\u00a01100.00", "should display the value 1100.00 in left column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines");
widget.$('.create .create_tax_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1000.00100.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01000.00$\u00a0100.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
widget.$('.create .create_tax_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1100.00220.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "145.00", "should display 'Create Write-off' with 145.00 in right column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
clientAction.destroy();
@ -1076,7 +1139,7 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
'data': this.params.data,
data: this.params.data,
});
clientAction.appendTo($('#qunit-fixture'));

View File

@ -17,7 +17,9 @@ class TestPayment(AccountingTestCase):
self.currency_chf_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_eur_id = self.env.ref("base.EUR").id
self.env.ref('base.main_company').write({'currency_id': self.currency_eur_id})
company = self.env.ref('base.main_company')
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_eur_id, company.id])
self.product = self.env.ref("product.product_product_4")
self.payment_method_manual_in = self.env.ref("account.account_payment_method_manual_in")
self.payment_method_manual_out = self.env.ref("account.account_payment_method_manual_out")
@ -318,3 +320,53 @@ class TestPayment(AccountingTestCase):
self.assertEqual(payment_id.payment_type, 'outbound')
self.assertEqual(payment_id.partner_id, self.partner_china_exp)
self.assertEqual(payment_id.partner_type, 'supplier')
def test_payment_and_writeoff_in_other_currency(self):
# Use case:
# Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD.
# Mark invoice as fully paid with a write_off
# Check that all the aml are correctly created.
invoice = self.create_invoice(amount=25, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
# register payment on invoice
payment = self.payment_model.create({'payment_type': 'inbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'customer',
'partner_id': self.partner_agrolait.id,
'amount': 25,
'currency_id': self.currency_usd_id,
'payment_date': time.strftime('%Y') + '-07-15',
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.account_payable.id,
'journal_id': self.bank_journal_euro.id,
'invoice_ids': [(4, invoice.id, None)]
})
payment.post()
self.check_journal_items(payment.move_line_ids, [
{'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id},
{'account_id': self.account_payable.id, 'debit': 8.65, 'credit': 0.0, 'amount_currency': 13.22, 'currency_id': self.currency_usd_id},
{'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 25.0, 'amount_currency': -38.22, 'currency_id': self.currency_usd_id},
])
# Use case:
# Company is in EUR, create a vendor bill for 25 EUR and register payment of 25 USD.
# Mark invoice as fully paid with a write_off
# Check that all the aml are correctly created.
invoice = self.create_invoice(amount=25, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
# register payment on invoice
payment = self.payment_model.create({'payment_type': 'inbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'supplier',
'partner_id': self.partner_agrolait.id,
'amount': 25,
'currency_id': self.currency_usd_id,
'payment_date': time.strftime('%Y') + '-07-15',
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.account_payable.id,
'journal_id': self.bank_journal_euro.id,
'invoice_ids': [(4, invoice.id, None)]
})
payment.post()
self.check_journal_items(payment.move_line_ids, [
{'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id},
{'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id},
{'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id},
])

View File

@ -25,7 +25,8 @@ class TestReconciliation(AccountingTestCase):
self.currency_swiss_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_euro_id = self.env.ref("base.EUR").id
self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id})
company = self.env.ref('base.main_company')
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id])
self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1)
self.product = self.env.ref("product.product_product_4")
@ -620,3 +621,39 @@ class TestReconciliation(AccountingTestCase):
# Checking if the direction of the move is correct
full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
self.assertEqual(full_rec_payable.balance, 18.75)
def test_unreconcile(self):
# Use case:
# 2 invoices paid with a single payment. Unreconcile the payment with one invoice, the
# other invoice should remain reconciled.
inv1 = self.create_invoice(invoice_amount=10, currency_id=self.currency_usd_id)
inv2 = self.create_invoice(invoice_amount=20, currency_id=self.currency_usd_id)
payment = self.env['account.payment'].create({
'payment_type': 'inbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'customer',
'partner_id': self.partner_agrolait_id,
'amount': 100,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_usd.id,
})
payment.post()
credit_aml = payment.move_line_ids.filtered('credit')
# Check residual before assignation
self.assertAlmostEquals(inv1.residual, 10)
self.assertAlmostEquals(inv2.residual, 20)
# Assign credit and residual
inv1.assign_outstanding_credit(credit_aml.id)
inv2.assign_outstanding_credit(credit_aml.id)
self.assertAlmostEquals(inv1.residual, 0)
self.assertAlmostEquals(inv2.residual, 0)
# Unreconcile one invoice at a time and check residual
credit_aml.with_context(invoice_id=inv1.id).remove_move_reconcile()
self.assertAlmostEquals(inv1.residual, 10)
self.assertAlmostEquals(inv2.residual, 0)
credit_aml.with_context(invoice_id=inv2.id).remove_move_reconcile()
self.assertAlmostEquals(inv1.residual, 10)
self.assertAlmostEquals(inv2.residual, 20)

View File

@ -251,7 +251,7 @@
<group>
<field name="origin" attrs="{'invisible': [('origin', '=', False)]}"/>
<field name="date_invoice" string="Bill Date"/>
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/>
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/>
<field name="move_name" invisible="1"/>
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
<field name="company_currency_id" invisible="1"/>
@ -297,7 +297,7 @@
<field name="amount"/>
<field name="amount_rounding" invisible="1"/>
<field name="amount_total" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" invisible="1" force_save="1"/>
</tree>
</field>
</div>
@ -396,7 +396,7 @@
</group>
<group>
<field name="date_invoice"/>
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/>
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/>
<field name="move_name" invisible="1"/>
<field name="user_id" groups="base.group_user"/>
<label for="currency_id" groups="base.group_multi_currency"/>
@ -504,7 +504,7 @@
<field name="amount" invisible="1"/>
<field name="amount_rounding" invisible="1"/>
<field name="amount_total"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" invisible="1" force_save="1"/>
</tree>
</field>
</page>
@ -535,10 +535,8 @@
<field name="number" string="Invoice" filter_domain="['|','|','|', ('number','ilike',self), ('origin','ilike',self), ('reference', 'ilike', self), ('partner_id', 'child_of', self)]"/>
<field name="journal_id" />
<filter name="draft" string="Draft" domain="[('state','=','draft')]"/>
<filter name="invoices" string="Invoices" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_invoice','in_invoice'))]"/>
<filter name="refunds" string="Credit Notes" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_refund','in_refund'))]"/>
<separator/>
<filter name="unpaid" string="Not Paid" domain="[('state','=','open')]"/>
<filter name="unpaid" string="Open" domain="[('state', '=', 'open')]"/>
<filter name="paid" string="Paid" domain="[('state', '=', 'paid')]"/>
<filter name="late" string="Overdue" domain="['&amp;', ('date_due', '&lt;', time.strftime('%%Y-%%m-%%d')), ('state', '=', 'open')]" help="Overdue invoices, maturity date passed"/>
<separator/>
<field name="partner_id" operator="child_of"/>
@ -570,33 +568,25 @@
</field>
</record>
<!-- TODO: remove in master -->
<record id="view_account_invoice_filter_inherit_invoices" model="ir.ui.view">
<field name="name">account.invoice.select.invoices</field>
<field name="model">account.invoice</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="view_account_invoice_filter"/>
<field name="arch" type="xml">
<data>
<xpath expr="filter[@name='invoices']" position="attributes">
<attribute name="string">Not Draft</attribute>
</xpath>
<filter name="refunds" position="replace"/>
</data>
<data/>
</field>
</record>
<!-- TODO: remove in master -->
<record id="view_account_invoice_filter_inherit_credit_notes" model="ir.ui.view">
<field name="name">account.invoice.select.credit.notes</field>
<field name="model">account.invoice</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="view_account_invoice_filter"/>
<field name="arch" type="xml">
<data>
<xpath expr="filter[@name='refunds']" position="attributes">
<attribute name="string">Not Draft</attribute>
</xpath>
<filter name="invoices" position="replace"/>
</data>
<data/>
</field>
</record>
@ -667,7 +657,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','out_invoice')]</field>
<field name="context">{'type':'out_invoice', 'journal_type': 'sale'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_invoices"/>
<field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a customer invoice.
@ -708,7 +698,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','out_refund')]</field>
<field name="context">{'default_type': 'out_refund', 'type': 'out_refund', 'journal_type': 'sale'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_credit_notes"/>
<field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a credit note.
@ -746,7 +736,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_invoice')]</field>
<field name="context">{'default_type': 'in_invoice', 'type': 'in_invoice', 'journal_type': 'purchase'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_invoices"/>
<field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to record a new vendor bill.
@ -782,7 +772,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_refund')]</field>
<field name="context">{'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'}</field>
<field name="search_view_id" ref="view_account_invoice_filter_inherit_credit_notes"/>
<field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to record a new vendor credit note.

View File

@ -117,16 +117,12 @@
<span>View</span>
</div>
<div>
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_invoice','in_invoice'))]}">
<span t-if="journal_type == 'sale'">Invoices</span>
<span t-if="journal_type == 'purchase'">Bills</span>
</a>
<a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_invoice')]}">Invoices</a>
<a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_invoice')]}">Bills</a>
</div>
<div>
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_refund','in_refund'))], 'invoice_type': 'refund'}">
<span t-if="journal_type == 'sale'">Credit Notes</span>
<span t-if="journal_type == 'purchase'">Credit Notes</span>
</a>
<a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
<a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
</div>
<div>
<a type="object" name="action_open_reconcile">Payments Matching</a>

View File

@ -179,7 +179,7 @@
</td>
</tr>
<tr t-foreach="range(max(5-len(invoice.invoice_line_ids),0))" t-as="l">
<td>&amp;nbsp;</td>
<td t-translation="off">&amp;nbsp;</td>
<td class="hidden"></td>
<td></td>
<td></td>
@ -253,8 +253,8 @@
</template>
<template id="portal_invoice_error" name="Invoice error/warning display">
<div class="row">
<div t-att-class="'col-md-6 col-md-offset-3 text-center alert alert-dismissable %s' % ('alert-danger' if error else 'alert-warning')">
<div class="row mr16">
<div t-attf-class="'col-md-12 mr16 ml16 alert alert-dismissable' #{'alert-danger' if error else 'alert-warning'}">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
<t t-if="error == 'generic'" name="generic">
There was an error processing this page.
@ -264,8 +264,8 @@
</template>
<template id="portal_invoice_success" name="Invoice success display">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center alert alert-dismissable alert-success">
<div class="row mr16">
<div class="col-md-12 mr16 ml16 alert alert-dismissable alert-success">
<a href="#" class="close" data-dismiss="alert" aria-label="close" title="close">×</a>
</div>
</div>

View File

@ -56,6 +56,7 @@
id="action_report_financial"
model="account.financial.report"
string="Financial report"
menu="False"
report_type="qweb-pdf"
name="account.report_financial"
file="account.report_financial"

View File

@ -16,7 +16,7 @@
<field name="code" placeholder="code"/>
<field name="name"/>
<field name="user_type_id" widget="selection"/>
<field name="tax_ids" widget="many2many_tags"/>
<field name="tax_ids" widget="many2many_tags" domain="[('company_id','=',company_id)]"/>
<field name="tag_ids" widget="many2many_tags" domain="[('applicability', '!=', 'taxes')]" context="{'default_applicability': 'accounts'}" options="{'no_create_edit': True}"/>
<field name="group_id"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>

View File

@ -92,7 +92,7 @@
</td>
</tr>
<tr t-foreach="range(max(5-len(o.invoice_line_ids),0))" t-as="l">
<td>&amp;nbsp;</td>
<td t-translation="off">&amp;nbsp;</td>
<td class="hidden"></td>
<td></td>
<td></td>
@ -116,18 +116,18 @@
<t t-foreach="o._get_tax_amount_by_group()" t-as="amount_by_group">
<tr style="border-bottom:1px solid #dddddd;">
<t t-if="len(o.tax_line_ids) == 1 and o.amount_untaxed == amount_by_group[2]">
<td><span t-esc="amount_by_group[0]"/></td>
<td class="text-right">
<span t-esc="amount_by_group[3]"/>
</td>
</t>
<t t-else="">
<td>
<span t-esc="amount_by_group[0]"/>
<span>&amp;nbsp;<span>on</span>
<t t-esc="amount_by_group[4]"/>
</span>
</td>
<td class="text-right">
<span t-esc="amount_by_group[3]"/>
</td>
</t>
<t t-else="">
<td><span t-esc="amount_by_group[0]"/></td>
<td class="text-right">
<span t-esc="amount_by_group[3]" />
</td>

View File

@ -59,7 +59,7 @@ class AccountInvoiceLine(models.Model):
@api.onchange('product_id')
def _onchange_product_id(self):
res = super(AccountInvoiceLine, self)._onchange_product_id()
rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.partner_id.id, self.env.uid,
rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid,
fields.Date.today(), company_id=self.company_id.id)
self.account_analytic_id = rec.analytic_id.id
return res
@ -67,7 +67,7 @@ class AccountInvoiceLine(models.Model):
def _set_additional_fields(self, invoice):
if not self.account_analytic_id:
rec = self.env['account.analytic.default'].account_get(
self.product_id.id, self.invoice_id.partner_id.id, self.env.uid,
self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid,
fields.Date.today(), company_id=self.company_id.id)
if rec:
self.account_analytic_id = rec.analytic_id.id

View File

@ -195,7 +195,10 @@ class AccountBankStatementImport(models.TransientModel):
bank_account_id = partner_bank.id
partner_id = partner_bank.partner_id.id
else:
bank_account_id = self.env['res.partner.bank'].create({'acc_number': line_vals['account_number']}).id
bank_account_id = self.env['res.partner.bank'].create({
'acc_number': line_vals['account_number'],
'partner_id': False,
}).id
line_vals['partner_id'] = partner_id
line_vals['bank_account_id'] = bank_account_id

View File

@ -32,7 +32,7 @@ class AccountRegisterPayments(models.TransientModel):
res = super(AccountRegisterPayments, self)._prepare_payment_vals(invoices)
if self.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'):
res.update({
'check_amount_in_words': self.check_amount_in_words,
'check_amount_in_words': self.currency_id.amount_to_text(res['amount']) if self.multi else self.check_amount_in_words,
'check_manual_sequencing': self.check_manual_sequencing,
})
return res

View File

@ -1,2 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
# Part of , Flectra. See LICENSE file for full copyright and licensing details.
from flectra.api import Environment, SUPERUSER_ID
def uninstall_hook(cr, registry):
env = Environment(cr, SUPERUSER_ID, {})
res_ids = env['ir.model.data'].search([
('model', '=', 'ir.ui.menu'),
('module', '=', 'account')
]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': False})
def post_init_hook(cr, registry):
env = Environment(cr, SUPERUSER_ID, {})
res_ids = env['ir.model.data'].search([
('model', '=', 'ir.ui.menu'),
('module', '=', 'account'),
]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': True})

View File

@ -29,4 +29,6 @@ You could use this simplified accounting in case you work with an (external) acc
'qweb': [
],
'application': True,
'uninstall_hook': 'uninstall_hook',
'post_init_hook': 'post_init_hook',
}

View File

@ -94,9 +94,9 @@ class PaymentPortal(http.Controller):
# proceed to the payment
res = tx.confirm_invoice_token()
if res is not True:
params['error'] = res
return request.redirect(_build_url_w_params(error_url, params))
params['success'] = 'pay_invoice'
if tx.state != 'authorized' or not tx.acquirer_id.capture_manually:
if res is not True:
params['error'] = res
return request.redirect(_build_url_w_params(error_url, params))
params['success'] = 'pay_invoice'
return request.redirect(_build_url_w_params(success_url, params))

View File

@ -175,3 +175,10 @@ class PaymentTransaction(models.Model):
})
return tx
def _post_process_after_done(self, **kwargs):
# set invoice id in payment transaction when payment being done from sale order
res = super(PaymentTransaction, self)._post_process_after_done()
if kwargs.get('invoice_id'):
self.account_invoice_id = kwargs['invoice_id']
return res

View File

@ -12,6 +12,36 @@
</xpath>
</template>
<!-- duplicata of the template payment.payment_confirmation_status.
The duplication avoid to break an existing installation in stable version-->
<template id="payment_confirmation_status">
<div t-if="payment_tx_id and payment_tx_id.state == 'done'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.done_msg' t-raw="payment_tx_id.acquirer_id.done_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'pending'" class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.pending_msg' t-raw="payment_tx_id.acquirer_id.pending_msg"/>
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'cancel'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'error'" class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<span t-if='payment_tx_id.acquirer_id.error_msg' t-raw="payment_tx_id.acquirer_id.error_msg"/>
</div>
<div t-if="payment_tx_id and payment_tx_id.state == 'authorized'" class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
Your payment has been authorized.
<span t-if='payment_tx_id.acquirer_id.post_msg' t-raw="payment_tx_id.acquirer_id.post_msg"/>
</div>
</template>
<template id="portal_invoice_page_inherit_payment" name="Payment on My Invoices" inherit_id="account.portal_invoice_page">
<xpath expr="//div[@id='o_portal_account_actions']/a" position="before">
<t t-set="tx_ids" t-value="invoice.payment_tx_ids.filtered(lambda tx: tx.state in ('pending', 'authorized', 'done'))"/>
@ -28,6 +58,13 @@
<i class="fa fa-check-circle"/> Paid
</a>
</xpath>
<xpath expr="//t[@t-call='account.portal_invoice_success']" position="after">
<div t-if="invoice.payment_tx_ids and invoice.amount_total and not success and not error" class="o_account_payment_tx_status" t-att-data-invoice-id="invoice.id">
<t t-call="account_payment.payment_confirmation_status">
<t t-set="payment_tx_id" t-value="invoice.payment_tx_id"/>
</t>
</div>
</xpath>
<xpath expr="//div[hasclass('panel-body')]" position="after">
<div class="panel-body" t-if="not tx_ids and invoice.state == 'open' and invoice.amount_total" id="portal_pay">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="col-md-offset-3 col-md-6">
@ -35,8 +72,8 @@
<t t-call="payment.payment_tokens_list">
<t t-set="mode" t-value="'payment'"/>
<t t-set="partner_id" t-value="invoice.partner_id.id"/>
<t t-set="success_url" t-value="'/my/invoices/%s?%s' % (invoice.id, keep_query())"/>
<t t-set="error_url" t-value="'/my/invoices/%s?%s' % (invoice.id, keep_query())"/>
<t t-set="success_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="error_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="access_token" t-value="access_token or ''"/>
<t t-set="callback_method" t-value="''"/>
<t t-set="form_action" t-value="'/invoice/pay/' + str(invoice.id) + '/s2s_token_tx/'"/>

View File

@ -37,6 +37,9 @@ class AccountVoucher(models.Model):
help="Effective date for accounting entries", copy=False, default=fields.Date.context_today)
journal_id = fields.Many2one('account.journal', 'Journal',
required=True, readonly=True, states={'draft': [('readonly', False)]}, default=_default_journal)
payment_journal_id = fields.Many2one('account.journal', string='Payment Method', readonly=True, store=False,
states={'draft': [('readonly', False)]}, domain="[('type', 'in', ['cash', 'bank'])]",
compute='_compute_payment_journal_id', inverse='_inverse_payment_journal_id')
account_id = fields.Many2one('account.account', 'Account',
required=True, readonly=True, states={'draft': [('readonly', False)]},
domain="[('deprecated', '=', False), ('internal_type','=', (pay_now == 'pay_now' and 'liquidity' or voucher_type == 'purchase' and 'payable' or 'receivable'))]")
@ -115,6 +118,29 @@ class AccountVoucher(models.Model):
def _get_journal_currency(self):
self.currency_id = self.journal_id.currency_id.id or self.company_id.currency_id.id
@api.depends('company_id', 'pay_now', 'account_id')
def _compute_payment_journal_id(self):
for voucher in self:
if voucher.pay_now != 'pay_now':
continue
domain = [
('type', 'in', ('bank', 'cash')),
('company_id', '=', voucher.company_id.id),
]
if voucher.account_id and voucher.account_id.internal_type == 'liquidity':
field = 'default_debit_account_id' if voucher.voucher_type == 'sale' else 'default_credit_account_id'
domain.append((field, '=', voucher.account_id.id))
voucher.payment_journal_id = self.env['account.journal'].search(domain, limit=1)
def _inverse_payment_journal_id(self):
for voucher in self:
if voucher.pay_now != 'pay_now':
continue
if voucher.voucher_type == 'sale':
voucher.account_id = voucher.payment_journal_id.default_debit_account_id
else:
voucher.account_id = voucher.payment_journal_id.default_credit_account_id
@api.multi
@api.depends('tax_correction', 'line_ids.price_subtotal')
def _compute_total(self):
@ -139,11 +165,8 @@ class AccountVoucher(models.Model):
@api.onchange('partner_id', 'pay_now')
def onchange_partner_id(self):
if self.pay_now == 'pay_now':
liq_journal = self.env['account.journal'].search([('type', 'in', ('bank', 'cash'))], limit=1)
self.account_id = liq_journal.default_debit_account_id \
if self.voucher_type == 'sale' else liq_journal.default_credit_account_id
else:
pay_journal_domain = [('type', 'in', ['cash', 'bank'])]
if self.pay_now != 'pay_now':
if self.partner_id:
self.account_id = self.partner_id.property_account_receivable_id \
if self.voucher_type == 'sale' else self.partner_id.property_account_payable_id
@ -152,6 +175,12 @@ class AccountVoucher(models.Model):
domain = [('deprecated', '=', False), ('internal_type', '=', account_type)]
self.account_id = self.env['account.account'].search(domain, limit=1)
else:
if self.voucher_type == 'purchase':
pay_journal_domain.append(('outbound_payment_method_ids', '!=', False))
else:
pay_journal_domain.append(('inbound_payment_method_ids', '!=', False))
return {'domain': {'payment_journal_id': pay_journal_domain}}
@api.multi
def proforma_voucher(self):
@ -193,7 +222,7 @@ class AccountVoucher(models.Model):
'account_id': self.account_id.id,
'move_id': move_id,
'journal_id': self.journal_id.id,
'partner_id': self.partner_id.id,
'partner_id': self.partner_id.commercial_partner_id.id,
'currency_id': company_currency != current_currency and current_currency or False,
'amount_currency': (sign * abs(self.amount) # amount < 0 for refunds
if company_currency != current_currency else 0.0),
@ -241,19 +270,29 @@ class AccountVoucher(models.Model):
@api.multi
def voucher_pay_now_payment_create(self):
payment_methods = self.journal_id.outbound_payment_method_ids
if self.voucher_type == 'sale':
payment_methods = self.journal_id.inbound_payment_method_ids
payment_type = 'inbound'
partner_type = 'customer'
sequence_code = 'account.payment.customer.invoice'
else:
payment_methods = self.journal_id.outbound_payment_method_ids
payment_type = 'outbound'
partner_type = 'supplier'
sequence_code = 'account.payment.supplier.invoice'
name = self.env['ir.sequence'].with_context(ir_sequence_date=self.date).next_by_code(sequence_code)
return {
'payment_type': 'outbound',
'name': name,
'payment_type': payment_type,
'payment_method_id': payment_methods and payment_methods[0].id or False,
'partner_type': 'supplier',
'partner_id': self.partner_id.id,
'partner_type': partner_type,
'partner_id': self.partner_id.commercial_partner_id.id,
'amount': self.amount,
'currency_id': self.currency_id.id,
'payment_date': self.date,
'journal_id': self.journal_id.id,
'journal_id': self.payment_journal_id.id,
'company_id': self.company_id.id,
'communication': self.name,
'name': self.name,
'state': 'reconciled',
}
@ -285,7 +324,7 @@ class AccountVoucher(models.Model):
'name': line.name or '/',
'account_id': line.account_id.id,
'move_id': move_id,
'partner_id': self.partner_id.id,
'partner_id': self.partner_id.commercial_partner_id.id,
'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False,
'quantity': 1,
'credit': abs(amount) if self.voucher_type == 'sale' else 0.0,

View File

@ -22,8 +22,8 @@ class TestAccountVoucherBranch(common.TransactionCase):
self.model_users = self.env['res.users']
self.branch_2 = self.env.ref('base_branch_company.data_branch_2')
self.income_type = self.env['account.account.type'].search([('name', '=', 'Income')])
account_obj = self.env.ref['account.account']
self.account_receivable = account_obj.create(
# account_obj = self.env.ref['account.account']
self.account_receivable = self.model_account.create(
{'code': 'X1012', 'name': 'Account Receivable - Test',
'user_type_id': self.env.ref('account.data_account_type_receivable').id,
'reconcile': True})
@ -78,13 +78,12 @@ class TestAccountVoucherBranch(common.TransactionCase):
return user_obj.id
def receipt_create(self, journal_id, branch_id):
print ("@@@@@@@@@@@@@@@@",self.receivable_account )
vals = {
'name': 'Test Voucher',
'partner_id': self.partner.id,
'journal_id': journal_id,
'voucher_type': 'sale',
'account_id': self.receivable_account.id,
'account_id': self.account_receivable.id,
'branch_id': branch_id.id,
'company_id': self.main_company.id,
'date': date.today(),
@ -95,7 +94,7 @@ class TestAccountVoucherBranch(common.TransactionCase):
'product_id': self.apple_product.id,
'price_unit': 500,
'quantity': 10,
'account_id': self.receivable_account.id,
'account_id': self.account_receivable.id,
'voucher_id': voucher_obj.id,
}
self.model_voucher_line.create(line_vals)

View File

@ -230,14 +230,19 @@
<group>
<field name="voucher_type" invisible="True"/>
<field name="currency_id" invisible="True"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<field name="branch_id" groups="base_branch_company.group_multi_branch"/>
<field name="partner_id" domain="[('customer','=',True)]" string="Customer" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/>
<field name="pay_now" required="1"/>
<field name="branch_id" groups="base_branch_company.group_multi_branch"/>
<field name="account_id" groups="account.group_account_user"/>
<field name="payment_journal_id"
attrs="{'invisible': [('pay_now', '!=', 'pay_now')], 'required': [('pay_now', '=', 'pay_now')]}"/>
<field name="account_id" attrs="{'invisible': [('pay_now', '=', 'pay_now')]}" groups="account.group_account_user"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
<group>
<field name="journal_id" domain="[('type','=','sale')]" widget="selection" groups="account.group_account_user"/>
<field name="journal_id"
domain="[('type','=','sale')]"
widget="selection"
groups="account.group_account_user"/>
<field name="date"/>
<field name="date_due" attrs="{'invisible':[('pay_now','=','pay_now')]}"/>
<field name="account_date"/>
@ -339,27 +344,29 @@
<h1>
<field name="number" readonly="1"/>
</h1>
<field name="voucher_type" invisible="True"/>
<group>
<group>
<field name="voucher_type" invisible="True"/>
<field name="currency_id" invisible="True"/>
<field name="partner_id" domain="[('supplier','=',True)]" string="Vendor" context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}" />
<field name="pay_now" required="1"/>
<field name="account_id" groups="account.group_account_user"/>
<field name="name" colspan="2" attrs="{'invisible': [('pay_now', '=', 'pay_later')]}"/>
<field name="reference"/>
<field name="branch_id" groups="base_branch_company.group_multi_branch"/>
<field name="payment_journal_id"
attrs="{'invisible': [('pay_now', '!=', 'pay_now')], 'required': [('pay_now', '=', 'pay_now')]}"/>
<field name="account_id" groups="account.group_account_user" attrs="{'invisible': [('pay_now', '=', 'pay_now')]}" />
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<field name="branch_id" groups="base_branch_company.group_multi_branch"/>
<field name="reference"/>
</group>
<group>
<field name="account_date"/>
<field name="journal_id"
domain="[('type','=','purchase')]"
widget="selection"
groups="account.group_account_user"/>
<field name="date" string="Bill Date"/>
<field name="date_due" attrs="{'invisible': [('pay_now', '=', 'pay_now')]}"/>
<field name="account_date"/>
<field name="name" colspan="2" attrs="{'invisible': [('pay_now', '=', 'pay_later')]}"/>
<field name="paid" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="journal_id"
domain="[('type','=','purchase')]"
widget="selection"
groups="account.group_account_user"/>
</group>
</group>
<notebook>

View File

@ -96,3 +96,6 @@ class ResUsers(models.Model):
internally
"""
return default_crypt_context
def _get_session_token_fields(self):
return super(ResUsers, self)._get_session_token_fields() | {'password_crypt'}

View File

@ -117,3 +117,6 @@ class ResUsers(models.Model):
res = self.sudo().search([('id', '=', self.env.uid), ('oauth_access_token', '=', password)])
if not res:
raise
def _get_session_token_fields(self):
return super(ResUsers, self)._get_session_token_fields() | {'oauth_access_token'}

View File

@ -45,6 +45,9 @@
</div>
<div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777">
${user.signature | safe}
<p style="font-size: 11px; margin-top: 10px;">
<strong>Sent by ${user.company_id.name} using <a href="www.flectrahq.com" style="text-decoration:none; color: #875A7B;">Flectra</a></strong>
</p>
</div>]]></field>
<field name="user_signature" eval="False"/>
</record>
@ -81,6 +84,9 @@
</div>
<div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777">
${user.signature | safe}
<p style="font-size: 11px; margin-top: 10px;">
<strong>Sent by ${user.company_id.name} using <a href="www.flectrahq.com" style="text-decoration:none; color: #875A7B;">Flectra</a></strong>
</p>
</div>]]></field>
<field name="user_signature" eval="False"/>
</record>
@ -122,6 +128,9 @@
</div>
<div style="padding:0px;width:600px;margin:auto; margin-top: 10px; background: #fff repeat top /100%;color:#777777">
${user.signature | safe}
<p style="font-size: 11px; margin-top: 10px;">
<strong>Sent by ${user.company_id.name} using <a href="www.flectrahq.com" style="text-decoration:none; color: #875A7B;">Flectra</a></strong>
</p>
</div></field>
<field name="user_signature" eval="False"/>
</record>

View File

@ -4,3 +4,4 @@ from . import res_config_settings
from . import ir_http
from . import res_partner
from . import res_users
from . import ir_model_fields

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and \
# licensing details
from flectra import models
# This is a nasty hack, targeted for V11 only
class IrModelFields(models.Model):
_inherit = 'ir.model.fields'
def unlink(self):
# Prevent the deletion of some `shared` fields... -_-
self = self.filtered(
lambda rec: not (
rec.model == 'res.config.settings' and
rec.name == 'auth_signup_uninvited'
)
)
return super(IrModelFields, self).unlink()

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import barcodes
from . import barcode_events_mixin
from . import ir_http

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and \
# licensing details.
from flectra import models
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
def session_info(self):
res = super(IrHttp, self).session_info()
res['max_time_between_keys_in_ms'] = int(
self.env['ir.config_parameter'].sudo().get_param(
'barcode.max_time_between_keys_in_ms', default='55'))
return res

View File

@ -3,6 +3,7 @@ flectra.define('barcodes.BarcodeEvents', function(require) {
var core = require('web.core');
var mixins = require('web.mixins');
var session = require('web.session');
// For IE >= 9, use this, new CustomEvent(), instead of new Event()
@ -26,7 +27,13 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
suffix: /[\n\r\t]+/,
// Keys from a barcode scanner are usually processed as quick as possible,
// but some scanners can use an intercharacter delay (we support <= 50 ms)
max_time_between_keys_in_ms: 55,
max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55,
// To be able to receive the barcode value, an input must be focused.
// On mobile devices, this causes the virtual keyboard to open.
// Unfortunately it is not possible to avoid this behavior...
// To avoid keyboard flickering at each detection of a barcode value,
// we want to keep it open for a while (800 ms).
inputTimeOut: 800,
init: function() {
mixins.PropertiesMixin.init.call(this);
@ -37,6 +44,30 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
// Bind event handler once the DOM is loaded
// TODO: find a way to be active only when there are listeners on the bus
$(_.bind(this.start, this, false));
// Mobile device detection
var isMobile = navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/webOS/i) ||
navigator.userAgent.match(/iPhone/i) ||
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPod/i) ||
navigator.userAgent.match(/BlackBerry/i) ||
navigator.userAgent.match(/Windows Phone/i);
this.isChromeMobile = isMobile && window.chrome;
// Creates an input who will receive the barcode scanner value.
if (this.isChromeMobile) {
this.$barcodeInput = $('<input/>', {
name: 'barcode',
type: 'text',
css: {
'position': 'absolute',
'opacity': 0,
},
});
}
this.__removeBarcodeField = _.debounce(this._removeBarcodeField, this.inputTimeOut);
},
handle_buffered_keys: function() {
@ -108,7 +139,7 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
e.key === "ArrowUp" || e.key === "ArrowDown" ||
e.key === "Escape" || e.key === "Tab" ||
e.key === "Backspace" || e.key === "Delete" ||
/F\d\d?/.test(e.key)) {
e.key === "Unidentified" || /F\d\d?/.test(e.key)) {
return true;
} else {
return false;
@ -166,8 +197,84 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
}
},
/**
* Try to detect the barcode value by listening all keydown events:
* Checks if a dom element who may contains text value has the focus.
* If not, it's probably because these events are triggered by a barcode scanner.
* To be able to handle this value, a focused input will be created.
*
* This function also has the responsibility to detect the end of the barcode value.
* (1) In most of cases, an optional key (tab or enter) is sent to mark the end of the value.
* So, we direclty handle the value.
* (2) If no end key is configured, we have to calculate the delay between each keydowns.
* 'max_time_between_keys_in_ms' depends of the device and may be configured.
* Exceeded this timeout, we consider that the barcode value is entirely sent.
*
* @private
* @param {jQuery.Event} e keydown event
*/
_listenBarcodeScanner: function (e) {
if (!$('input:text:focus, textarea:focus, [contenteditable]:focus').length) {
$('body').append(this.$barcodeInput);
this.$barcodeInput.focus();
}
if (this.$barcodeInput.is(":focus")) {
// Handle buffered keys immediately if the keypress marks the end
// of a barcode or after x milliseconds without a new keypress.
clearTimeout(this.timeout);
// On chrome mobile, e.which only works for some special characters like ENTER or TAB.
if (String.fromCharCode(e.which).match(this.suffix)) {
this._handleBarcodeValue(e);
} else {
this.timeout = setTimeout(this._handleBarcodeValue.bind(this, e),
this.max_time_between_keys_in_ms);
}
// if the barcode input doesn't receive keydown for a while, remove it.
this.__removeBarcodeField();
}
},
/**
* Retrieves the barcode value from the temporary input element.
* This checks this value and trigger it on the bus.
*
* @private
* @param {jQuery.Event} keydown event
*/
_handleBarcodeValue: function (e) {
var barcodeValue = this.$barcodeInput.val();
if (barcodeValue.match(this.regexp)) {
core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]);
this.$barcodeInput.val('');
}
},
/**
* Remove the temporary input created to store the barcode value.
* If nothing happens, this input will be removed, so the focus will be lost
* and the virtual keyboard on mobile devices will be closed.
*
* @private
*/
_removeBarcodeField: function () {
if (this.$barcodeInput) {
// Reset the value and remove from the DOM.
this.$barcodeInput.val('').remove();
}
},
start: function(prevent_key_repeat){
$('body').bind("keypress", this.__handler);
// Chrome Mobile isn't triggering keypress event.
// This is marked as Legacy in the DOM-Level-3 Standard.
// See: https://www.w3.org/TR/uievents/#legacy-keyboardevent-event-types
// This fix is only applied for Google Chrome Mobile but it should work for
// all other cases.
// In master, we could remove the behavior with keypress and only use keydown.
if (this.isChromeMobile) {
$('body').on("keydown", this._listenBarcodeScanner.bind(this));
} else {
$('body').bind("keypress", this.__handler);
}
if (prevent_key_repeat === true) {
$('body').bind("keydown", this.__keydown_handler);
$('body').bind('keyup', this.__keyup_handler);

View File

@ -155,7 +155,7 @@ FormController.include({
id: candidate.id,
data: candidateChanges,
};
return this.model.notifyChanges(this.handle, changes);
return this.model.notifyChanges(this.handle, changes, {notifyChange: activeBarcode.notifyChange});
},
/**
* @private
@ -211,6 +211,8 @@ FormController.include({
* @param {FlectraEvent} event
* @param {string} event.data.name: the current field name
* @param {string} [event.data.fieldName] optional for x2many sub field
* @param {boolean} [event.data.notifyChange] optional for x2many sub field
* do not trigger on change server side if a candidate has been found
* @param {string} [event.data.quantity] optional field to increase quantity
* @param {Object} [event.data.commands] optional added methods
* can use comand with specific barcode (with ReservedBarcodePrefixes)
@ -225,7 +227,9 @@ FormController.include({
handle: this.handle,
target: event.target,
widget: event.target.attrs && event.target.attrs.widget,
setQuantityWithKeypress: !! event.data.setQuantityWithKeypress,
fieldName: event.data.fieldName,
notifyChange: (event.data.notifyChange !== undefined) ? event.data.notifyChange : true,
quantity: event.data.quantity,
commands: event.data.commands || {},
candidate: this.activeBarcode[name] && this.activeBarcode[name].handle === this.handle ?
@ -319,7 +323,7 @@ FormController.include({
// only catch the event if we're not focused in
// another field and it's a number
if (!$(event.target).is('body') || !/[0-9]/.test(character)) {
if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) {
return;
}

View File

@ -4,6 +4,8 @@
from lxml import etree
from flectra import api, models, fields
from flectra.tools.translate import _
class Partner(models.Model):
_inherit = 'res.partner'
@ -28,10 +30,10 @@ class Partner(models.Model):
replacement_xml = """
<div>
<field name="country_enforce_cities" invisible="1"/>
<field name='city' attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/>
<field name='city_id' attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]"/>
<field name='city' placeholder="%s" attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/>
<field name='city_id' placeholder="%s" attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]"/>
</div>
"""
""" % (_('City'), _('City'))
city_id_node = etree.fromstring(replacement_xml)
city_node.getparent().replace(city_node, city_id_node)

View File

@ -62,9 +62,9 @@ class BaseGengoTranslations(models.TransientModel):
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'res.company',
'res_id': self.env.user.company_id.id,
'target': 'current',
'res_model': 'res.config.settings',
'target': 'inline',
'context': {'module' : 'general_settings'},
}
@api.model

View File

@ -89,6 +89,7 @@ _map_iban_template = {
'bg': 'BGkk BBBB SSSS DDCC CCCC CC', # Bulgaria
'bh': 'BHkk BBBB CCCC CCCC CCCC CC', # Bahrain
'br': 'BRkk BBBB BBBB SSSS SCCC CCCC CCCT N', # Brazil
'by': 'BYkk BBBB AAAA CCCC CCCC CCCC CCCC', # Belarus
'ch': 'CHkk BBBB BCCC CCCC CCCC C', # Switzerland
'cr': 'CRkk BBBC CCCC CCCC CCCC C', # Costa Rica
'cy': 'CYkk BBBS SSSS CCCC CCCC CCCC CCCC', # Cyprus

View File

@ -302,7 +302,8 @@ class Import(models.TransientModel):
try:
thousand_separator = decimal_separator = False
for val in preview_values:
if val == '':
val = val.strip()
if not val:
continue
# value might have the currency symbol left or right from the value
val = self._remove_currency_symbol(val)
@ -587,6 +588,7 @@ class Import(models.TransientModel):
thousand_separator = options.get('float_thousand_separator', ' ')
decimal_separator = options.get('float_decimal_separator', '.')
for line in data:
line[index] = line[index].strip()
if not line[index]:
continue
line[index] = line[index].replace(thousand_separator, '').replace(decimal_separator, '.')
@ -620,9 +622,11 @@ class Import(models.TransientModel):
# versions, for both data and pattern
user_format = pycompat.to_native(options.get('%s_format' % field['type']))
for num, line in enumerate(data):
if line[index]:
line[index] = line[index].strip()
if line[index]:
try:
line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index].strip()), user_format), server_format)
line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index]), user_format), server_format)
except ValueError as e:
raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e))
except Exception as e:

View File

@ -137,6 +137,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
model: 'base_import.import',
method: 'create',
args: [{res_model: this.res_model}],
kwargs: {context: session.user_context},
});
},
renderButtons: function() {
@ -280,6 +281,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
model: 'base_import.import',
method: 'parse_preview',
args: [this.id, this.import_options()],
kwargs: {context: session.user_context},
}).done(function (result) {
var signal = result.error ? 'preview_failed' : 'preview_succeeded';
self[signal](result);

View File

@ -145,12 +145,13 @@ def _is_studio_custom(path):
Returns True if any of the records contains a context with the key
studio in it, False if none of the records do
"""
path = os.path.join(path, 'data')
filenames = next(iter(os.walk(path)))[2]
filenames = [f for f in filenames if f.lower().endswith('.xml')]
filepaths = []
for level in os.walk(path):
filepaths += [os.path.join(level[0], fn) for fn in level[2]]
filepaths = [fp for fp in filepaths if fp.lower().endswith('.xml')]
for filename in filenames:
root = lxml.etree.parse(os.path.join(path, filename)).getroot()
for fp in filepaths:
root = lxml.etree.parse(fp).getroot()
for record in root:
# there might not be a context if it's a non-studio module

View File

@ -104,6 +104,8 @@ class ResConfigSettings(models.TransientModel):
@api.multi
def edit_external_header(self):
if not self.external_report_layout:
return False
return self._prepare_report_view_action('web.external_layout_' + self.external_report_layout)
@api.multi

View File

@ -30,16 +30,23 @@ class ResPartner(models.Model):
city = lines.pop()
return (cp, city)
else:
result = re.match('((?:L-|AT-)?[0-9\-]+) (.+)', lines[-1])
result = re.match('((?:L-|AT-)?[0-9\-]+[A-Z]{,2}) (.+)', lines[-1])
if result:
lines.pop()
return (result.group(1), result.group(2))
return False
def _set_address_field(partner, field, value):
partner[field] = value
non_set_address_fields.remove(field)
if stdnum_vat is None:
return {}
for partner in self:
# If a field is non set in this algorithm
# wipe it anyway
non_set_address_fields = set(['street', 'street2', 'city', 'zip', 'state_id', 'country_id'])
if not partner.vat:
return {}
if len(partner.vat) > 5 and partner.vat[:2].lower() in stdnum_vat.country_codes:
@ -68,14 +75,20 @@ class ResPartner(models.Model):
lines = [x.strip() for x in lines[0].split(',') if x]
if len(lines) == 1:
lines = [x.strip() for x in lines[0].split(' ') if x]
partner.street = lines.pop(0)
_set_address_field(partner, 'street', lines.pop(0))
if len(lines) > 0:
res = _check_city(lines, result['countryCode'])
if res:
partner.zip = res[0]
partner.city = res[1]
_set_address_field(partner, 'zip', res[0])
_set_address_field(partner, 'city', res[1])
if len(lines) > 0:
partner.street2 = lines.pop(0)
_set_address_field(partner, 'street2', lines.pop(0))
country = self.env['res.country'].search([('code', '=', result['countryCode'])], limit=1)
partner.country_id = country and country.id or False
_set_address_field(partner, 'country_id', country and country.id or False)
for field in non_set_address_fields:
if partner[field]:
partner[field] = False

View File

@ -402,5 +402,57 @@ QUnit.test('non-existing action in a dashboard', function (assert) {
form.destroy();
});
QUnit.test('clicking on a kanban\'s button should trigger the action', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'board',
data: this.data,
arch: '<form string="My Dashboard">' +
'<board style="2-1">' +
'<column>' +
'<action name="149" string="Partner" view_mode="kanban" id="action_0_1"></action>' +
'</column>' +
'</board>' +
'</form>',
archs: {
'partner,false,kanban':
'<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
'<div>' +
'<field name="foo"/>' +
'</div>' +
'<div><button name="sitting_on_a_park_bench" type="object">Eying little girls with bad intent</button>' +
'</div>' +
'</t></templates></kanban>',
},
intercepts: {
execute_action: function (event) {
var data = event.data;
assert.strictEqual(data.env.model, 'partner', "should have correct model");
assert.strictEqual(data.action_data.name, 'sitting_on_a_park_bench',
"should call correct method");
}
},
mockRPC: function (route) {
if (route === '/board/static/src/img/layout_1-1-1.png') {
return $.when();
}
if (route === '/web/action/load') {
return $.when({res_model: 'partner', view_mode: 'kanban', views: [[false, 'kanban']]});
}
if (route === '/web/dataset/search_read') {
return $.when({records: [{foo: 'aqualung'}]});
}
return this._super.apply(this, arguments);
}
});
form.$('.o_kanban_test').find('button:first').click();
form.destroy();
});
});

View File

@ -76,7 +76,8 @@ bus.Bus = Widget.extend({
this.last_partners_presence_check = now;
}
var data = {channels: self.channels, last: self.last, options: options};
session.rpc('/longpolling/poll', data, {shadow : true}).then(function(result) {
// The backend has a maximum cycle time of 50 seconds so give +10 seconds
session.rpc('/longpolling/poll', data, {shadow : true, timeout: 60000}).then(function(result) {
self.on_notification(result);
if(!self.stop){
self.poll();

View File

@ -434,7 +434,7 @@ class AlarmManager(models.AbstractModel):
result = False
if alarm.type == 'email':
result = meeting.attendee_ids._send_mail_to_attendees('calendar.calendar_template_meeting_reminder', force_send=True)
result = meeting.attendee_ids.filtered(lambda r: r.state != 'declined')._send_mail_to_attendees('calendar.calendar_template_meeting_reminder', force_send=True)
return result
def do_notif_reminder(self, alert):
@ -823,7 +823,7 @@ class Meeting(models.Model):
attendee_ids = fields.One2many('calendar.attendee', 'event_id', 'Participant', ondelete='cascade')
partner_ids = fields.Many2many('res.partner', 'calendar_event_res_partner_rel', string='Attendees', states={'done': [('readonly', True)]}, default=_default_partners)
alarm_ids = fields.Many2many('calendar.alarm', 'calendar_alarm_calendar_event_rel', string='Reminders', ondelete="restrict", copy=False)
is_highlighted = fields.Boolean(compute='_compute_is_highlighted', string='# Meetings Highlight')
is_highlighted = fields.Boolean(compute='_compute_is_highlighted', string='Is the Event Highlighted')
@api.multi
def _compute_attendee(self):
@ -1478,7 +1478,7 @@ class Meeting(models.Model):
partners_to_notify = meeting.partner_ids.ids
event_attendees_changes = attendees_create and real_ids and attendees_create[real_ids[0]]
if event_attendees_changes:
partners_to_notify.append(event_attendees_changes['removed_partners'].ids)
partners_to_notify.extend(event_attendees_changes['removed_partners'].ids)
self.env['calendar.alarm_manager'].notify_next_alarm(partners_to_notify)
if (values.get('start_date') or values.get('start_datetime') or

View File

@ -75,7 +75,7 @@
<field name="partner_ids"/>
<field name="location"/>
<field name="state" invisible="True"/>
<field name="duration"/>
<field name="duration" widget="float_time"/>
<field name="message_needaction" invisible="1"/>
</tree>
</field>
@ -122,10 +122,10 @@
<field name="stop" attrs="{'invisible': True}"/>
<field name="id" attrs="{'invisible': True}"/>
<field name="start_date" string="Starting at" attrs="{'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="stop_date" string="Ending at" attrs="{'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="start_datetime" string="Starting at" attrs="{'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="stop_datetime" invisible="1"/>
<label for="duration" attrs="{'invisible': [('allday','=',True)]}"/>
<div attrs="{'invisible': [('allday','=',True)]}">

View File

@ -359,7 +359,7 @@
<p>
This email address has been preconfigured as the default
for your sales department.<br/>
<a t-att-href="prepare_backend_url('sale.action_sale_config_settings')">(you can change it here)</a>
<a t-att-href="prepare_backend_url('sales_team.sales_team_config_action')">(you can change it here)</a>
</p>
<span class="arrow-down"></span>
</div>

View File

@ -968,6 +968,8 @@ class Lead(models.Model):
'nb_opportunities': 0,
}
today = fields.Date.from_string(fields.Date.context_today(self))
opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)])
for opp in opportunities:
@ -975,28 +977,28 @@ class Lead(models.Model):
if opp.activity_date_deadline:
if opp.date_deadline:
date_deadline = fields.Date.from_string(opp.date_deadline)
if date_deadline == date.today():
if date_deadline == today:
result['closing']['today'] += 1
if date.today() <= date_deadline <= date.today() + timedelta(days=7):
if today <= date_deadline <= today + timedelta(days=7):
result['closing']['next_7_days'] += 1
if date_deadline < date.today() and not opp.date_closed:
if date_deadline < today and not opp.date_closed:
result['closing']['overdue'] += 1
# Next activities
for activity in opp.activity_ids:
date_deadline = fields.Date.from_string(activity.date_deadline)
if date_deadline == date.today():
if date_deadline == today:
result['activity']['today'] += 1
if date.today() <= date_deadline <= date.today() + timedelta(days=7):
if today <= date_deadline <= today + timedelta(days=7):
result['activity']['next_7_days'] += 1
if date_deadline < date.today():
if date_deadline < today:
result['activity']['overdue'] += 1
# Won in Opportunities
if opp.date_closed and opp.stage_id.probability == 100:
date_closed = fields.Date.from_string(opp.date_closed)
if date.today().replace(day=1) <= date_closed <= date.today():
if today.replace(day=1) <= date_closed <= today:
if opp.planned_revenue:
result['won']['this_month'] += opp.planned_revenue
elif date.today() + relativedelta(months=-1, day=1) <= date_closed < date.today().replace(day=1):
elif today + relativedelta(months=-1, day=1) <= date_closed < today.replace(day=1):
if opp.planned_revenue:
result['won']['last_month'] += opp.planned_revenue
@ -1021,9 +1023,9 @@ class Lead(models.Model):
for activity in activites_done:
if activity['date']:
date_act = fields.Date.from_string(activity['date'])
if date.today().replace(day=1) <= date_act <= date.today():
if today.replace(day=1) <= date_act <= today:
result['done']['this_month'] += 1
elif date.today() + relativedelta(months=-1, day=1) <= date_act < date.today().replace(day=1):
elif today + relativedelta(months=-1, day=1) <= date_act < today.replace(day=1):
result['done']['last_month'] += 1
# Meetings
@ -1038,9 +1040,9 @@ class Lead(models.Model):
for meeting in meetings:
if meeting['start']:
start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
if start == date.today():
if start == today:
result['meeting']['today'] += 1
if date.today() <= start <= date.today() + timedelta(days=7):
if today <= start <= today + timedelta(days=7):
result['meeting']['next_7_days'] += 1
result['done']['target'] = self.env.user.target_sales_done

View File

@ -32,7 +32,7 @@ class ResConfigSettings(models.TransientModel):
@api.onchange('generate_lead_from_alias')
def _onchange_generate_lead_from_alias(self):
self.crm_alias_prefix = 'info' if self.generate_lead_from_alias else False
self.crm_alias_prefix = (self.crm_alias_prefix or 'info') if self.generate_lead_from_alias else False
@api.model
def get_values(self):

View File

@ -55,20 +55,20 @@
<field name="model_id">crm.opportunity.report</field>
<field name="domain">[('probability', '=', 100)]</field>
<field name="user_id" eval="False"/>
<field name="context">{'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count']}</field>
<field name="context">{'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count__']}</field>
</record>
<record id="filter_opportunity_opportunities_won_per_team" model="ir.filters">
<field name="name">Opportunities Won Per Team</field>
<field name="model_id">crm.opportunity.report</field>
<field name="domain">[('probability', '=', 100)]</field>
<field name="user_id" eval="False"/>
<field name="context">{'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected revenue']}</field>
<field name="context">{'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected_revenue']}</field>
</record>
<record id="filter_opportunity_salesperson" model="ir.filters">
<field name="name">Leads By Salespersons</field>
<field name="model_id">crm.opportunity.report</field>
<field name="user_id" eval="False"/>
<field name="context">{'col_group_by': ['create_date:month'], 'group_by': ['user_id'], 'measures': ['__count']}</field>
<field name="context">{'col_group_by': ['create_date:month'], 'group_by': ['user_id'], 'measures': ['__count__']}</field>
</record>
<record id="filter_opportunity_country" model="ir.filters">
<field name="name">Won By Country</field>
@ -81,7 +81,7 @@
<field name="name">Expected Revenue by Team</field>
<field name="model_id">crm.opportunity.report</field>
<field name="user_id" eval="False"/>
<field name="context">{'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count']}</field>
<field name="context">{'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count__']}</field>
</record>
<record id="ir_filters_crm_opportunity_report_next_action" model="ir.filters">
<field name="name">Team Activities</field>

View File

@ -252,8 +252,6 @@
<group>
<field name="name" string="Opportunity Title" placeholder="e.g. Customer Deal"/>
<field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/>
<field name="email_from" invisible="1"/>
<field name="phone" invisible="1"/>
<label for="planned_revenue"/>
<div class="o_row">
<field name="planned_revenue"/>
@ -267,6 +265,20 @@
<button string="Create &amp; Edit" name="edit_dialog" type="object" class="btn-primary"/>
<button string="Discard" class="btn-default" special="cancel"/>
</footer>
<field name="partner_name" invisible="1"/>
<field name="contact_name" invisible="1"/>
<field name="title" invisible="1"/>
<field name="street" invisible="1"/>
<field name="street2" invisible="1"/>
<field name="city" invisible="1"/>
<field name="state_id" invisible="1"/>
<field name="country_id" invisible="1"/>
<field name="email_from" invisible="1"/>
<field name="phone" invisible="1"/>
<field name="mobile" invisible="1"/>
<field name="zip" invisible="1"/>
<field name="function" invisible="1"/>
<field name="website" invisible="1"/>
</sheet>
</form>
</field>

View File

@ -379,7 +379,7 @@ class MergePartnerAutomatic(models.TransientModel):
:param partner_ids : list of partner ids to sort
"""
return self.env['res.partner'].browse(partner_ids).sorted(
key=lambda p: (p.active, p.create_date),
key=lambda p: (p.active, (p.create_date or '')),
reverse=True,
)

View File

@ -8,12 +8,12 @@ class Lead(models.Model):
_name = 'crm.lead'
_inherit = ['crm.lead', 'phone.validation.mixin']
@api.onchange('phone', 'country_id')
@api.onchange('phone', 'country_id', 'company_id')
def _onchange_phone_validation(self):
if self.phone:
self.phone = self.phone_format(self.phone)
@api.onchange('mobile', 'country_id')
@api.onchange('mobile', 'country_id', 'company_id')
def _onchange_mobile_validation(self):
if self.mobile:
self.mobile = self.phone_format(self.mobile)

View File

@ -8,12 +8,12 @@ class Contact(models.Model):
_name = 'res.partner'
_inherit = ['res.partner', 'phone.validation.mixin']
@api.onchange('phone', 'country_id')
@api.onchange('phone', 'country_id', 'company_id')
def _onchange_phone_validation(self):
if self.phone:
self.phone = self.phone_format(self.phone)
@api.onchange('mobile', 'country_id')
@api.onchange('mobile', 'country_id', 'company_id')
def _onchange_mobile_validation(self):
if self.mobile:
self.mobile = self.phone_format(self.mobile)

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import res_config_settings
from . import delivery_carrier
from . import delivery_grid
from . import product_template

View File

@ -199,8 +199,11 @@ class DeliveryCarrier(models.Model):
carrier.product_id.list_price = carrier.fixed_price
def fixed_rate_shipment(self, order):
price = self.fixed_price
if self.company_id.currency_id.id != order.currency_id.id:
price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price)
return {'success': True,
'price': self.fixed_price,
'price': price,
'error_message': False,
'warning_message': False}

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from flectra import models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
def set_values(self):
super(ResConfigSettings, self).set_values()
rule = self.env.ref('delivery.delivery_carrier_comp_rule', False)
if rule:
rule.write({'active': not bool(self.company_share_product)})

View File

@ -107,16 +107,12 @@ class StockPicking(models.Model):
@api.multi
def action_done(self):
# TDE FIXME: should work in batch
self.ensure_one()
res = super(StockPicking, self).action_done()
if self.carrier_id and self.carrier_id.integration_level == 'rate_and_ship':
self.send_to_shipper()
if self.carrier_id:
self._add_delivery_cost_to_so()
for pick in self:
if pick.carrier_id:
if pick.carrier_id.integration_level == 'rate_and_ship':
pick.send_to_shipper()
pick._add_delivery_cost_to_so()
return res
@api.multi

View File

@ -6,6 +6,7 @@
<field name="model_id" ref="model_delivery_carrier"/>
<field name="global" eval="True"/>
<field name="domain_force"> ['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
<field name="active" eval="False"/>
</record>
</data>
</flectra>

View File

@ -84,18 +84,6 @@
</tr>
<tr>
<td style="padding:15px 20px 0px 20px;">
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
<td style="padding:25px 0px; text-align:center;">
<p>
Don't forget to <strong>add it to your calendar</strong>:
</p>
<a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
<a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
<a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
</td>
</tr>
</table>
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
<td style="padding:25px 0px;vertical-align:top;">
@ -165,6 +153,16 @@
</tr>
</table>
% endif
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
<td style="padding:25px 0px;">
<strong>Add this event to your calendar</strong>
<a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
<a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
<a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
@ -180,6 +178,7 @@
<tbody>
<tr>
<td style="padding-top:10px;font-size: 12px;">
<div>Sent by ${object.company_id.name}</div>
% if 'website_url' in object.event_id and object.event_id.website_url:
<div>
Discover <a href="/event" style="text-decoration:none;color:#717188;">all our events</a>.
@ -347,6 +346,7 @@
<tbody>
<tr>
<td style="padding-top:10px;font-size: 12px;">
<p style="margin:0px 0px 9px 0px;padding-top:10px;">Sent by ${object.company_id.name}</p>
% if 'website_url' in object.event_id and object.event_id.website_url:
<div>
Discover <a href="/event" style="text-decoration:none;color:#717188;">all our events</a>.

View File

@ -317,7 +317,7 @@ class EventEvent(models.Model):
self.state = 'confirm'
@api.one
def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: True):
def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'):
for attendee in self.registration_ids.filtered(filter_func):
self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send)

View File

@ -8,4 +8,5 @@ class ResConfigSettings(models.TransientModel):
module_event_sale = fields.Boolean("Tickets")
module_website_event_track = fields.Boolean("Tracks and Agenda")
module_website_event_questions = fields.Boolean("Registration Survey")
module_event_barcode = fields.Boolean("Barcode")
module_website_event_sale = fields.Boolean("Online Ticketing")

View File

@ -96,6 +96,7 @@ class EventMailScheduler(models.Model):
@api.one
def execute(self):
now = fields.Datetime.now()
if self.interval_type == 'after_sub':
# update registration lines
lines = [
@ -105,9 +106,10 @@ class EventMailScheduler(models.Model):
if lines:
self.write({'mail_registration_ids': lines})
# execute scheduler on registrations
self.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT)).execute()
self.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= now).execute()
else:
if not self.mail_sent:
# Do not send emails if the mailing was scheduled before the event but the event is over
if not self.mail_sent and (self.interval_type != 'before_event' or self.event_id.date_end > now):
self.event_id.mail_attendees(self.template_id.id)
self.write({'mail_sent': True})
return True

View File

@ -48,6 +48,20 @@
</div>
</div>
</div>
<h2>Attendance</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_event_barcode" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane">
<label for="module_event_barcode"/>
<div class="text-muted">
Scan badges to confirm attendances
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>

View File

@ -11,7 +11,7 @@
<field name="event_registration_ids">
<tree string="Registration" editable="top" create="false" delete="false">
<field name="event_id" readonly='1' force_save="1"/>
<field name="registration_id" readonly='1'/>
<field name="registration_id" readonly='1' force_save="1"/>
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1'/>
<field name="name"/>
<field name="email"/>

View File

@ -247,9 +247,11 @@ class FleetVehicle(models.Model):
@return: the costs log view
"""
self.ensure_one()
copy_context = dict(self.env.context)
copy_context.pop('group_by', None)
res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
res.update(
context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True),
context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True),
domain=[('vehicle_id', '=', self.id)]
)
return res

View File

@ -1,8 +1,8 @@
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Flectra Fleet</h2>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Odoo Fleet</h2>
<h3 class="oe_slogan">Manage your vehicles, contracts, costs, insurances and assignments</h3>
<div class="col-md-12">
<div class="oe_span12">
<div class="oe_demo oe_picture oe_screenshot">
<img src="fleet_vehicles.png" height="300">
</div>
@ -10,13 +10,13 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Fleet management made easy</h2>
<div class="col-md-6">
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Fleet management made easy</h2>
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="vehicles_odometer.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32 text-justify'>
You won't need any specialized tracking system for company vehicles - with Odoo's smart app, can keep a close eye on your fleet in a few simple clicks. Manage everything through our user-friendly administrative system - fuel log entries, costs and many other features necessary for the management of your company's vehicles.
</p>
@ -24,27 +24,27 @@
</div>
</section>
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Manage leasing and all other contracts</h2>
<div class="col-md-6">
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Manage leasing and all other contracts</h2>
<div class="oe_span6">
<p class="oe_mt32 text-justify">
Supervise all contracts for your vehicles and receive a warning email when contracts reach their expiration date. Several visual tools are put in place to ensure that you'll remember to renew (or end) your contract. Organize the services around the vehicles and communicate with the qualified service providers; manage invoices and notes. Set up a vehicle policy within your company, as well as an insurance policy in order to manage your fleet in the most efficient way.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="vehicle_contract_form.png">
</div>
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Monitor all costs at once</h2>
<div class="col-md-6">
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Monitor all costs at once</h2>
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="fleet_pie_chart.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<p class="oe_mt32 text-justify">
Identify and trace the money spent by your company for each of the fleet vehicles. Recurring costs of your contracts such as leasing and services contracts are automatically added to your accounting at the beginning of each period of the frequency specified in the contracts, while all other costs like fuel and repairs are automatically added to your report.
</p>
@ -52,19 +52,19 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Analysis and reporting</h2>
<div class="col-md-6">
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Analysis and reporting</h2>
<div class="oe_span6">
<p class="oe_mt32 text-justify">
Show all costs associated with a given vehicle or with a type of service. Compare different types of costs (which vehicles cost the most; which services have been performed on which vehicles etc.) by using the reporting tool. Get really helpful insights about the effective return of each vehicle in order to improve your fleet investments.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="fleet_graph.png">
</div>
</div>
</section>
<section class="container oe_separator">
<section class="oe_container oe_separator">
</section>

View File

@ -18,7 +18,7 @@ The users can be evaluated using goals and numerical objectives to reach.
For non-numerical achievements, **badges** can be granted to users. From a simple "thank you" to an exceptional achievement, a badge is an easy way to exprimate gratitude to a user for their good work.
Both goals and badges are flexibles and can be adapted to a large range of modules and actions. When installed, this module creates easy goals to help new users to discover Odoo and configure their user profile.
Both goals and badges are flexibles and can be adapted to a large range of modules and actions. When installed, this module creates easy goals to help new users to discover Flectra and configure their user profile.
""",
'data': [

View File

@ -243,12 +243,11 @@
</table>
</td>
</tr>
<!--
<tr>
<td align="center">
Powered by <a target="_blank" href="https://www.flectra.com">Flectra</a>.
</td>
</tr> -->
</tr>
</table>
</td></tr>
</table>

View File

@ -1,34 +1,34 @@
<section class="container">
<div class="row oe_spaced">
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Google Calendar</h2>
<h3 class="oe_slogan">Get your meetings, your leaves... Get your calendar anywhere and never forget an event.</h3>
<div class="col-md-12">
<div class="oe_span12">
<img src="the_calendar.png" class="oe_picture oe_screenshot">
</div>
</div>
</section>
<section class="container oe_dark">
<div class="row">
<section class="oe_container oe_dark">
<div class="oe_row">
<h2 class="oe_slogan">Keep an eye on your events</h2>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32'>
See easily the purpose of the meeting, the start time and also the attendee(s)... All that without click on anything...
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="an_event.png">
</div>
</div>
</section>
<section class="container">
<div class="row">
<section class="oe_container">
<div class="oe_row">
<h2 class="oe_slogan">Create so easily an event</h2>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="create_quick.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32'>
In just one click you can create an event...<br/>
You can drag and drop your event if you want moved it to another timing.<br/>
@ -38,29 +38,29 @@
</div>
</section>
<section class="container oe_dark">
<div class="row">
<section class="oe_container oe_dark">
<div class="oe_row">
<h2 class="oe_slogan">Create recurrent event</h2>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32'>
You can also create recurrent events with only one event.<br/>
You need to create an event each monday of the week ? With only one it's possible, you could specify the recurrence and if one of this event is moved, or deleted, it's not a problem, you can untie your event from the others recurrences.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="recurrent.png">
</div>
</div>
</section>
<section class="container ">
<div class="row">
<section class="oe_container ">
<div class="oe_row">
<h2 class="oe_slogan">See all events you wants </h2>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="coworker.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32'>
See in your calendar, the event from others peoples where your are attendee, but also their events by simply adding your favorites coworkers.<br/>
Every coworker will have their own color in your calendar, and every attendee will have their avatar in the event...<br/>
@ -70,28 +70,28 @@
</div>
</section>
<section class="container oe_dark">
<div class="row">
<section class="oe_container oe_dark">
<div class="oe_row">
<h2 class="oe_slogan">Get an email</h2>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32'>
You will receive an email at creation of an event where you are attendee, but also when this event is updated for some fields as date start, ...
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="email.png">
</div>
</div>
</section>
<section class="container ">
<div class="row">
<section class="oe_container ">
<div class="oe_row">
<h2 class="oe_slogan">Be notified </h2>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="notification.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32'>
You can ask to have a alarm of type 'notification' in your Odoo.<br/>
You will have a notification in you Odoo which ever the page you are.
@ -100,15 +100,15 @@
</div>
</section>
<section class="container oe_dark">
<div class="row">
<section class="oe_container oe_dark">
<div class="oe_row">
<h2 class="oe_slogan">Google Calendar</h2>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32'>
With the plugin Google_calendar, you can synchronize your Odoo calendar with Google Calendar.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="calendar_in_action.png">
</div>

View File

@ -9,6 +9,7 @@ import werkzeug.urls
from flectra import api, fields, models
from flectra.exceptions import RedirectWarning, UserError
from flectra.tools import pycompat
from flectra.tools.safe_eval import safe_eval
from flectra.tools.translate import _
@ -153,6 +154,9 @@ class GoogleDrive(models.Model):
a length of 1 element only (batch processing is not supported in the code, though nothing really prevent it)
:return: the config id and config name
'''
# TO DO in master: fix my signature and my model
if isinstance(res_model, pycompat.string_types):
res_model = self.env['ir.model'].search([('model', '=', res_model)]).id
if not res_id:
raise UserError(_("Creating google drive may only be done by one at a time."))
# check if a model is configured with a template

View File

@ -1,78 +1,109 @@
flectra.define('google_drive.google_drive', function (require) {
flectra.define('google_drive.sidebar', function (require) {
"use strict";
var data = require('web.data');
/**
* The purpose of this file is to include the Sidebar widget to add Google
* Drive related items.
*/
var Sidebar = require('web.Sidebar');
Sidebar.include({
init: function () {
var self = this;
var ids;
this._super.apply(this, arguments);
var view = self.getParent();
var result;
if (view.fields_view && view.fields_view.type === "form") {
ids = [];
view.on("load_record", self, function (r) {
ids = [r.id];
self.add_gdoc_items(view, r.id);
});
// TO DO: clean me in master
/**
* @override
*/
start: function () {
var def;
if (this.options.viewType === "form") {
def = this._addGoogleDocItems(this.env.model, this.env.activeIds[0]);
}
return $.when(def).then(this._super.bind(this));
},
add_gdoc_items: function (view, res_id) {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {string} model
* @param {integer} resID
* @returns {Deferred}
*/
_addGoogleDocItems: function (model, resID) {
var self = this;
if (!resID) {
return $.when();
}
var gdoc_item = _.indexOf(_.pluck(self.items.other, 'classname'), 'oe_share_gdoc');
if (gdoc_item !== -1) {
self.items.other.splice(gdoc_item, 1);
}
if (res_id) {
view.sidebar_eval_context().done(function (context) {
var ds = new data.DataSet(this, 'google.drive.config', context);
ds.call('get_google_drive_config', [view.dataset.model, res_id, context]).done(function (r) {
if (!_.isEmpty(r)) {
_.each(r, function (res) {
var already_there = false;
for (var i = 0;i < self.items.other.length;i++){
if (self.items.other[i].classname === "oe_share_gdoc" && self.items.other[i].label.indexOf(res.name) > -1){
already_there = true;
break;
}
}
if (!already_there){
self.add_items('other', [{
label: res.name,
config_id: res.id,
res_id: res_id,
res_model: view.dataset.model,
callback: self.on_google_doc,
classname: 'oe_share_gdoc'
},
]);
}
});
return this._rpc({
args: [this.env.model, resID],
context: this.env.context,
method: 'get_google_drive_config',
model: 'google.drive.config',
}).then(function (r) {
if (!_.isEmpty(r)) {
_.each(r, function (res) {
var already_there = false;
for (var i = 0; i < self.items.other.length; i++) {
var item = self.items.other[i];
if (item.classname === 'oe_share_gdoc' && item.label.indexOf(res.name) > -1) {
already_there = true;
break;
}
}
if (!already_there) {
self._addItems('other', [{
callback: self._onGoogleDocItemClicked.bind(self, res.id, resID),
classname: 'oe_share_gdoc',
config_id: res.id,
label: res.name,
res_id: resID,
res_model: model,
}]);
}
});
});
}
}
});
},
on_google_doc: function (doc_item) {
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {integer} configID
* @param {integer} resID
*/
_onGoogleDocItemClicked: function (configID, resID) {
var self = this;
var domain = [['id', '=', doc_item.config_id]];
var domain = [['id', '=', configID]];
var fields = ['google_drive_resource_id', 'google_drive_client_id'];
this._rpc({
args: [domain, fields],
method: 'search_read',
model: 'google.drive.config',
}).then(function (configs) {
self._rpc({
args: [configID, resID, configs[0].google_drive_resource_id],
context: self.env.context,
method: 'get_google_drive_url',
model: 'google.drive.config',
method: 'search_read',
args: [domain, fields],
})
.then(function (configs) {
var ds = new data.DataSet(self, 'google.drive.config');
ds.call('get_google_drive_url', [doc_item.config_id, doc_item.res_id,configs[0].google_drive_resource_id, self.dataset.context]).done(function (url) {
if (url){
window.open(url, '_blank');
}
});
}).then(function (url) {
if (url){
window.open(url, '_blank');
}
});
});
},
});
});
return Sidebar;
});

View File

@ -0,0 +1,112 @@
flectra.define('google_drive.gdrive_integration', function (require) {
"use strict";
//rebuild
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var GoogleDriveSideBar = require('google_drive.sidebar');
var createView = testUtils.createView;
/*
* @override
* Avoid breaking other tests because of the new route
* that the module introduces
*/
var _addGoogleDocItemsOriginal = GoogleDriveSideBar.prototype._addGoogleDocItems;
var _addGoogleDocItemsMocked = function (model, resID) {
return $.when();
};
GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsMocked;
QUnit.module('gdrive_integration', {
beforeEach: function () {
// For our test to work, the _addGoogleDocItems function needs to be the original
GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsOriginal;
this.data = {
partner: {
fields: {
display_name: {string: "Displayed name", type: "char", searchable: true},
},
records: [{
id: 1,
display_name: "Locomotive Breath",
}],
},
'google.drive.config': {
fields: {
model_id: {string: 'Model', type: 'int'},
name: {string: 'Name', type: 'char'},
google_drive_resource_id: {string: 'Resource ID', type: 'char'},
},
records: [{
id: 27,
name: 'Cyberdyne Systems',
model_id: 1,
google_drive_resource_id: 'T1000',
}],
},
'ir.attachment': {
fields: {
name: {string: 'Name', type:'char'}
},
records: [],
}
};
},
afterEach: function() {
GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsMocked;
}
}, function () {
QUnit.module('Google Drive Sidebar');
QUnit.test('rendering of the google drive attachments in Sidebar', function (assert) {
assert.expect(3);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="display_name"/>' +
'</form>',
res_id: 1,
viewOptions: {sidebar: true},
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_config') {
assert.deepEqual(args.args, ['partner', 1],
'The route to get google drive config should have been called');
return $.when([{id: 27, name: 'Cyberdyne Systems'}]);
}
if (route === '/web/dataset/call_kw/google.drive.config/search_read'){
return $.when([{google_drive_resource_id: "T1000",
google_drive_client_id: "cyberdyne.org",
id: 1}]);
}
if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_url') {
assert.deepEqual(args.args, [27, 1, 'T1000'],
'The route to get the Google url should have been called');
// We don't return anything useful, otherwise it will open a new tab
return $.when();
}
return this._super.apply(this, arguments);
}
});
var google_action = form.sidebar.$('.oe_share_gdoc');
assert.strictEqual(google_action.length, 1,
'The button to the google action should be present');
// Trigger opening of the dynamic link
google_action.find('a:first').click();
form.destroy();
});
});
});

View File

@ -8,4 +8,9 @@
</xpath>
</template>
<template id="qunit_suite" name="google_drive tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/google_drive/static/tests/gdrive_test.js"></script>
</xpath>
</template>
</flectra>

View File

@ -111,13 +111,14 @@ class Employee(models.Model):
active = fields.Boolean('Active', related='resource_id.active', default=True, store=True)
# private partner
address_home_id = fields.Many2one(
'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.')
'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.',
groups="hr.group_hr_user")
is_address_home_a_company = fields.Boolean(
'The employee adress has a company linked',
compute='_compute_is_address_home_a_company',
)
country_id = fields.Many2one(
'res.country', 'Nationality (Country)')
'res.country', 'Nationality (Country)', groups="hr.group_hr_user")
gender = fields.Selection([
('male', 'Male'),
('female', 'Female'),
@ -125,7 +126,8 @@ class Employee(models.Model):
], groups="hr.group_hr_user", default="male")
marital = fields.Selection([
('single', 'Single'),
('married', 'Married (or similar)'),
('married', 'Married'),
('cohabitant', 'Legal Cohabitant'),
('widower', 'Widower'),
('divorced', 'Divorced')
], string='Marital Status', groups="hr.group_hr_user", default='single')
@ -139,9 +141,9 @@ class Employee(models.Model):
domain="[('partner_id', '=', address_home_id)]",
groups="hr.group_hr_user",
help='Employee bank salary account')
permit_no = fields.Char('Work Permit No')
visa_no = fields.Char('Visa No')
visa_expire = fields.Date('Visa Expire Date')
permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user")
visa_no = fields.Char('Visa No', groups="hr.group_hr_user")
visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user")
# image: all image fields are base64 encoded and PIL-supported
image = fields.Binary(
@ -269,7 +271,11 @@ class Employee(models.Model):
"""Checks that choosen address (res.partner) is not linked to a company.
"""
for employee in self:
employee.is_address_home_a_company = employee.address_home_id.parent_id.id is not False
try:
employee.is_address_home_a_company = employee.address_home_id.parent_id.id is not False
except AccessError:
employee.is_address_home_a_company = False
class Department(models.Model):
_name = "hr.department"

View File

@ -1,8 +1,8 @@
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Modern open source HR solution</h2>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Modern open source HR solution</h2>
<h3 class="oe_slogan">Manage the most important asset in your company: <b>People</b></h3>
<div class="col-md-12">
<div class="oe_span12">
<div class="oe_demo oe_picture oe_screenshot">
<img src="hr_employees.png" height="300">
</div>
@ -10,14 +10,14 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Successfully manage your employees</h2>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Successfully manage your employees</h2>
<h3 class="oe_slogan">Centralize all your HR information</h3>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="hr_departments.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<p class="oe_mt32 text-justify">
Oversee all important information by department at a glance. Restrict some information to HR managers, and make others public for all employees to easily find their colleagues. Get alerts for any new leave request, allocation request, application or upcoming appraisal.
</p>
@ -25,14 +25,14 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Boost Engagement With Social Tools</h2>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Boost Engagement With Social Tools</h2>
<h3 class="oe_slogan oe_mb32">Improve communication between employees and motivate them through rewards</h3>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="enterprise_social_network.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<h4>
<b>Enterprise Social Network</b>
</h4>
@ -41,8 +41,8 @@
</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="oe_row">
<div class="oe_span6">
<h4>
<b>Gamification</b>
</h4>
@ -50,11 +50,11 @@
Inspire achievement with challenges, goals and rewards. Design your own targets, define clear objectives and provide real time feedback and tangible results. Showcase the top performers to the entire team and publicly recognize a job well done.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="hr_badges.png">
</div>
</div>
</section>
<section class="container oe_separator">
<section class="oe_container oe_separator">
</section>

View File

@ -87,8 +87,9 @@
<field name="address_home_id"
context="{'show_address': 1}"
options='{"always_reload": True, "highlight_first_line": True}'/>
<field name="is_address_home_a_company" invisible="1" />
<div class="text-warning" attrs="{'invisible': [('is_address_home_a_company','=', False)]}">
<field name="is_address_home_a_company" invisible="1"/>
<div class="text-warning" groups="hr.group_hr_user"
attrs="{'invisible': [('is_address_home_a_company','=', False)]}">
Use here the home address of the employee.
This private address is used in the expense report reimbursement document.
It should be different from the work address.
@ -289,12 +290,14 @@
</field>
</record>
<!-- TODO remove me in master -->
<record id="hr_employee_action_subordinate_hierachy" model="ir.actions.act_window">
<field name="name">Subordinate Hierarchy</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">hr.employee</field>
<field name="domain">[('id','in',active_ids)]</field>
<field name="view_type">tree</field>
<field name="domain">['|', ('id','in',active_ids), ('parent_id', 'in', active_ids)]</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_partner_tree2"/>
<field name="binding_model_id" ref="hr.model_hr_employee"/>
</record>

View File

@ -2,7 +2,7 @@
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Online Expenses Management</h2>
<h3 class="oe_slogan">Easily Manage Employees' Expenses</h3>
<div class="col-md-12">
<div class="oe_span12">
<div class="oe_demo oe_picture oe_screenshot">
<img src="expense_sc_00.png" height="350">
</div>
@ -10,9 +10,9 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Save time on expense records</h2>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Save time on expense records</h2>
<h3 class="oe_slogan">Everything at one place</h3>
<p class="oe_mt32">
Manage your employees' daily expenses. Whether it's travel expenses or any other costs, access all your employees&apos; fee notes and complete, validate or refuse them. No need to download any specialized software or program to manage expenses - everything can be done directly in the app!
@ -23,39 +23,39 @@
</div>
</section>
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Stop losing receipts</h2>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Stop losing receipts</h2>
<h3 class="oe_slogan">Upload all receipts within expense records</h3>
<div class="col-md-6">
<div class="oe_span6">
<p class="oe_mt16 text-justify">
Allow employees to add copies of the receipts and proofs directly to the expense records to avoid losing them. Save time and increase your efficiency by keeping a clean and complete record of all expenses.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="expense_sc_02.png">
</div>
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Manage expenses per team</h2>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Manage expenses per team</h2>
<h3 class="oe_slogan">Have a clear overview of a team&apos;s spendings</h3>
<p class="oe_mt16 oe_mb32 text-center">
As a manager, follow expenses recorded by a full team to keep an eye on the costs and make sure to keep to the target and budget.
</p>
<div class="row_img oe_centered">
<div class="oe_row_img oe_centered">
<img class="oe_picture oe_screenshot" src="expense_sc_03.png">
</div>
</div>
</section>
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Share the workload between departments</h2>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Share the workload between departments</h2>
<h3 class="oe_slogan oe_mb32">Get everyone involved to save time</h3>
<div class="col-md-4">
<div class="oe_span4">
<img class="center-block" src="employees.png">
<div class="text-center">
<h4 class="oe_mt16">
@ -64,7 +64,7 @@
</div>
<p class="oe_mt8">Draft and confirm expenses, and load receipts to the expense records.</p>
</div>
<div class="col-md-4">
<div class="oe_span4">
<img class="center-block" src="managers.png">
<div class="text-center">
<h4 class="oe_mt16">
@ -73,7 +73,7 @@
</div>
<p class="oe_mt8">Comment, validate or refuse expenses, and add or edit information.</p>
</div>
<div class="col-md-4">
<div class="oe_span4">
<img class="center-block" src="accountants.png">
<div class="text-center">
<h4 class="oe_mt16">
@ -85,5 +85,5 @@
</div>
</section>
<section class="container oe_separator">
<section class="oe_container oe_separator">
</section>

View File

@ -27,13 +27,13 @@
<field eval="25" name="priority"/>
<field name="arch" type="xml">
<form string="Expenses" class="o_expense_form">
<header>
<header invisible="context.get('expense_adding_line')">
<button name="submit_expenses" states="draft" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit"/>
<field name="state" widget="statusbar" statusbar_visible="draft,reported,done"/>
<button name="view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
</header>
<sheet>
<div class="oe_button_box">
<div class="oe_button_box" invisible="context.get('expense_adding_line')">
<button name="action_get_attachment_view"
class="oe_stat_button"
icon="fa-book"
@ -61,7 +61,7 @@
<field name="reference"/>
<field name="date"/>
<field name="account_id" domain="[('internal_type', '=', 'other')]" groups="account.group_account_user"/>
<field name="employee_id"/>
<field name="employee_id" groups="hr_expense.group_hr_expense_user"/>
<field name="sheet_id" invisible="1"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
@ -162,7 +162,7 @@
<filter domain="[('state', '=', 'draft')]" string="To Submit" name="to_report" help="New Expense"/>
<filter domain="[('state', '=', 'reported')]" string="Reported" name="submitted" help="Confirmed Expenses"/>
<filter domain="[('state', '=', 'refused')]" string="Refused" name="refused" help="Refused Expenses"/>
<filter domain="[('state', '!=', 'cancel')]" string="Not Refused" name="uncancelled" help="Actual expense sheets, not the refused ones"/>
<filter domain="[('state', '!=', 'refused')]" string="Not Refused" name="uncancelled" help="Actual expense sheets, not the refused ones"/>
<separator />
<filter string="My Team Expenses" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="hr_expense.group_hr_expense_manager" help="Expenses of Your Team Member"/>
<filter string="My Expenses" domain="[('employee_id.user_id', '=', uid)]"/>
@ -421,7 +421,7 @@
</group>
<notebook>
<page string="Expense Lines">
<field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'not_delete': True, 'reload_on_button': True, 'no_create': True}">
<field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'not_delete': True, 'reload_on_button': True, 'no_create': True}" context="{'expense_adding_line': True}">
<tree decoration-danger="is_refused">
<field name="date"/>
<field name="name"/>

View File

@ -1,8 +1,8 @@
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Leaves management for all</h2>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Leaves management for all</h2>
<h3 class="oe_slogan">Manage employee vacations &amp; absence</h3>
<div class="col-md-12">
<div class="oe_span12">
<div class="oe_demo oe_picture oe_screenshot">
<img src="leave_request.png">
</div>
@ -10,11 +10,11 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Manage employee leaves</h2>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Manage employee leaves</h2>
<h3 class="oe_slogan">Keep track of all your employees vacations</h3>
<div class="col-md-6 text-justify">
<div class="oe_span6 text-justify">
<p class="oe_mt32">
Keep track of the vacation days taken by each employee. Employees enter their requests and managers approve and validate them, all in just a few clicks. The agenda of each employee is updated accordingly.
</p>
@ -22,20 +22,20 @@
Managers get a view of their whole team leaves in one complete view, to keep the team well-organized and to easily forecast the distribution of tasks during the absence of their members.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="dashboard.png">
</div>
</div>
</section>
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Approve or refuse leave requests</h2>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Approve or refuse leave requests</h2>
<h3 class="oe_slogan">Handle all requests from your employees</h3>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="leaves_sc_02.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<p class="oe_mt32 text-justify">
Allow employees to record their requests for vacation themselves, and get notified by email for every new request. Decide to either approve them or to refuse them, and add a note to your refusal to give an explanation to your employees.
</p>
@ -43,20 +43,20 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Get reports to plan ahead</h2>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Get reports to plan ahead</h2>
<h3 class="oe_slogan">Simple reporting tool</h3>
<div class="col-md-6">
<div class="oe_span6">
<p class="oe_mt32 text-justify">
Create reports in a single click for every leave request, with a detail per request type, employee, department and even for the whole company. Get statistics on the leaves and plan for the upcoming to make sure you keep productivity to its highest level.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="analysis.png">
</div>
</div>
</section>
<section class="container oe_separator">
<section class="oe_container oe_separator">
</section>

View File

@ -13,6 +13,7 @@ var FieldOrgChart = AbstractField.extend({
events: {
"click .o_employee_redirect": "_onEmployeeRedirect",
"click .o_employee_sub_redirect": "_onEmployeeSubRedirect",
},
/**
* @constructor

View File

@ -102,6 +102,7 @@ class HrPayslip(models.Model):
def refund_sheet(self):
for payslip in self:
copied_payslip = payslip.copy({'credit_note': True, 'name': _('Refund: ') + payslip.name})
copied_payslip.compute_sheet()
copied_payslip.action_payslip_done()
formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False)
treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False)
@ -189,7 +190,8 @@ class HrPayslip(models.Model):
leave_time = (interval[1] - interval[0]).seconds / 3600
current_leave_struct['number_of_hours'] += leave_time
work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id)
current_leave_struct['number_of_days'] += leave_time / work_hours
if work_hours:
current_leave_struct['number_of_days'] += leave_time / work_hours
# compute worked days
work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id)

View File

@ -2,7 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, fields, models, _
from flectra.exceptions import UserError
from flectra.exceptions import UserError, ValidationError
from flectra.tools.safe_eval import safe_eval
from flectra.addons import decimal_precision as dp
@ -76,7 +76,7 @@ class HrSalaryRuleCategory(models.Model):
_name = 'hr.salary.rule.category'
_description = 'Salary Rule Category'
name = fields.Char(required=True)
name = fields.Char(required=True, translate=True)
code = fields.Char(required=True)
parent_id = fields.Many2one('hr.salary.rule.category', string='Parent',
help="Linking a salary category to its parent is used only for the reporting purpose.")
@ -89,7 +89,7 @@ class HrSalaryRuleCategory(models.Model):
class HrSalaryRule(models.Model):
_name = 'hr.salary.rule'
name = fields.Char(required=True)
name = fields.Char(required=True, translate=True)
code = fields.Char(required=True,
help="The code of salary rules can be used as reference in computation of other rules. "
"In that case, it is case sensitive.")

View File

@ -7,6 +7,7 @@ from flectra import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
module_account_accountant = fields.Boolean(string='Account Accountant')
module_l10n_fr_hr_payroll = fields.Boolean(string='French Payroll')
module_l10n_be_hr_payroll = fields.Boolean(string='Belgium Payroll')
module_l10n_in_hr_payroll = fields.Boolean(string='Indian Payroll')

View File

@ -33,6 +33,20 @@
</div>
</div>
</div>
<h2>Accounting</h2>
<div class="row mt16 o_settings_container" id="hr_payroll_accountant">
<div class="col-md-6 col-xs-12 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_account_accountant" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane">
<label for="module_account_accountant" string="Payroll Entries"/>
<div class="text-muted">
Post payroll slips in accounting
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>

View File

@ -35,6 +35,7 @@ class HrPayslipEmployees(models.TransientModel):
'date_from': from_date,
'date_to': to_date,
'credit_note': run_data.get('credit_note'),
'company_id': employee.company_id.id,
}
payslips += self.env['hr.payslip'].create(res)
payslips.compute_sheet()

View File

@ -1,8 +1,8 @@
Manage Recruitment and Job applications
---------------------------------------
Publish, promote and organize your job offers with the Odoo
<a href="https://www.odoo.com/page/recruitment">Open Source Recruitment Application</a>.
Publish, promote and organize your job offers with the Flectra
<a href="https://flectrahq.com/page/recruitment">Open Source Recruitment Application</a>.
Organize your job board, promote your job announces and keep track of
application submissions easily. Follow every applicant and build up a database

View File

@ -82,6 +82,7 @@
<tbody>
<tr>
<td style="padding-top:10px;font-size: 12px;">
<div>Sent by ${object.company_id.name}</div>
% if 'website_url' in object.job_id and object.job_id.website_url:
<div>
Discover <a href="/jobs" style="text-decoration:none;color:#717188;">our others jobs</a>.
@ -228,6 +229,7 @@
<tbody>
<tr>
<td style="padding-top:10px;font-size: 12px;">
<div>Sent by ${object.company_id.name}</div>
% if 'website_url' in object.job_id and object.job_id.website_url:
<div>
Discover <a href="/jobs" style="text-decoration:none;color:#717188;">all our jobs</a>.
@ -366,6 +368,7 @@
<tbody>
<tr>
<td style="padding-top:10px;font-size: 12px;">
<div>Sent by ${object.company_id.name}</div>
% if 'website_url' in object.job_id and object.job_id.website_url:
<div>
Discover <a href="/jobs" style="text-decoration:none;color:#717188;">all our jobs</a>.

View File

@ -1,8 +1,8 @@
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Hiring Processes Made Easy</h2>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Hiring Processes Made Easy</h2>
<h3 class="oe_slogan">From the sourcing to the contract, handle your recruitment process easily</h3>
<div class="col-md-12">
<div class="oe_span12">
<div class="oe_demo oe_picture oe_screenshot">
<img src="job_positions.png" height="350">
</div>
@ -10,14 +10,14 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Manage your hiring process</h2>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Manage your hiring process</h2>
<h3 class="oe_slogan">Organize your vacancies job applications</h3>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture" src="hr_recruitment_sc_01.png">
</div>
<div class="col-md-6">
<div class="oe_span6">
<p class="oe_mt32 text-justify">
Set up your job board, promote your job listings and easily keep track of submitted applications. Follow every applicant and build a database of skills and profiles with indexed documents. No need to outsource your recruitment - handle everything internally in a simple and professional way.
</p>
@ -25,11 +25,11 @@
</div>
</section>
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Track job offers straight from the app</h2>
<h3 class="oe_slogan oe_mb32">See which channel drives most applications and gather all of them in Flectra.</h3>
<div class="col-md-6 text-center">
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Track job offers straight from the app</h2>
<h3 class="oe_slogan oe_mb32">See which channel drives most applications and gather all of them in Odoo.</h3>
<div class="oe_span6 text-center">
<span class="fa fa-file-text fa-2x"/>
<span class="fa fa-long-arrow-right fa-2x"/>
<span class="fa fa-envelope fa-2x"/>
@ -39,7 +39,7 @@
A new email address is automatically assigned to every job offer in order to route applications directly to the right one.
</p>
</div>
<div class="col-md-6 text-center">
<div class="oe_span6 text-center">
<span class="fa fa-file-text fa-2x"/>
<span class="fa fa-long-arrow-right fa-2x"/>
<span class="fa fa-database fa-2x"/>
@ -55,14 +55,14 @@
</div>
</section>
<section class="container oe_dark">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Customize your recruitment process</h2>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Customize your recruitment process</h2>
<h3 class="oe_slogan">Define your own stages and interviewers</h3>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="job_applications.png">
</div>
<div class="col-md-6 oe_mt32 text-justify">
<div class="oe_span6 oe_mt32 text-justify">
<h4>
<b>Create your own hiring strategies.</b>
</h4>
@ -77,16 +77,16 @@
</section>
<section class="container">
<div class="row oe_spaced">
<h2 class="oe_slogan" style="">Simplify application management</h2>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" style="color:#875A7B;">Simplify application management</h2>
<h3 class="oe_slogan">Index resumes, track applicants, search profiles</h3>
<div class="col-md-6">
<div class="oe_span6">
<p class='oe_mt32 text-justify'>
Follow applicants in your recruitment process with the smart kanban view. Save time by using templates to automate some communication. Documents like CVs and motivation letters are indexed automatically, allowing you to easily search for specific skills and build up a database of profiles.
</p>
</div>
<div class="col-md-6">
<div class="oe_span6">
<img class="oe_picture oe_screenshot" src="hr_recruitment_sc_04.png">
</div>
</div>

View File

@ -14,20 +14,31 @@ class AccountAnalyticLine(models.Model):
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
return result
task_id = fields.Many2one('project.task', 'Task')
task_id = fields.Many2one('project.task', 'Task', index=True)
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
employee_id = fields.Many2one('hr.employee', "Employee")
department_id = fields.Many2one('hr.department', "Department", related='employee_id.department_id', store=True, readonly=True)
department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True)
@api.onchange('project_id')
def onchange_project_id(self):
# reset task when changing project
self.task_id = False
# force domain on task when project is set
if self.project_id:
return {'domain': {
'task_id': [('project_id', '=', self.project_id.id)]
}}
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.user_id = self.employee_id.user_id
@api.depends('employee_id')
def _compute_department_id(self):
for line in self:
line.department_id = line.employee_id.department_id
@api.model
def create(self, vals):
vals = self._timesheet_preprocess(vals)

View File

@ -58,4 +58,19 @@ class Task(models.Model):
if context.get('default_parent_id', False):
vals['parent_id'] = context.pop('default_parent_id', None)
task = super(Task, self.with_context(context)).create(vals)
return task
return task
@api.multi
def write(self, values):
result = super(Task, self).write(values)
# reassign project_id on related timesheet lines
if 'project_id' in values:
project_id = values.get('project_id')
# a timesheet must have an analytic account (and a project)
if self and not project_id:
raise UserError(_('This task must have a project since they are linked to timesheets.'))
self.sudo().mapped('timesheet_ids').write({
'project_id': project_id,
'account_id': self.env['project.project'].browse(project_id).sudo().analytic_account_id.id
})
return result

View File

@ -7,5 +7,6 @@ from flectra import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
module_project_timesheet_synchro = fields.Boolean("Awesome Timesheet")
module_sale_timesheet = fields.Boolean("Time Billing")
module_project_timesheet_holidays = fields.Boolean("Leaves")

Some files were not shown because too many files have changed in this diff Show More