flectra/addons/account_voucher/models/account_voucher.py

490 lines
24 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import fields, models, api, _
from flectra.addons import decimal_precision as dp
from flectra.exceptions import UserError, ValidationError
class AccountVoucher(models.Model):
_name = 'account.voucher'
_description = 'Accounting Voucher'
_inherit = ['mail.thread']
_order = "date desc, id desc"
@api.model
def _default_journal(self):
voucher_type = self._context.get('voucher_type', 'sale')
company_id = self._context.get('company_id', self.env.user.company_id.id)
domain = [
('type', '=', voucher_type),
('company_id', '=', company_id),
]
return self.env['account.journal'].search(domain, limit=1)
voucher_type = fields.Selection([
('sale', 'Sale'),
('purchase', 'Purchase')
], string='Type', readonly=True, states={'draft': [('readonly', False)]}, oldname="type")
name = fields.Char('Payment Reference',
readonly=True, states={'draft': [('readonly', False)]}, default='')
date = fields.Date("Bill Date", readonly=True,
index=True, states={'draft': [('readonly', False)]},
copy=False, default=fields.Date.context_today)
account_date = fields.Date("Accounting Date",
readonly=True, index=True, states={'draft': [('readonly', False)]},
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'))]")
line_ids = fields.One2many('account.voucher.line', 'voucher_id', 'Voucher Lines',
readonly=True, copy=True,
states={'draft': [('readonly', False)]})
narration = fields.Text('Notes', readonly=True, states={'draft': [('readonly', False)]})
currency_id = fields.Many2one('res.currency', compute='_get_journal_currency',
string='Currency', readonly=True, required=True, default=lambda self: self._get_currency())
company_id = fields.Many2one('res.company', 'Company', store=True,
required=True, readonly=True, states={'draft': [('readonly', False)]},
related='journal_id.company_id', default=lambda self: self._get_company())
state = fields.Selection([
('draft', 'Draft'),
('cancel', 'Cancelled'),
('proforma', 'Pro-forma'),
('posted', 'Posted')
], 'Status', readonly=True, track_visibility='onchange', copy=False, default='draft',
help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Voucher.\n"
" * The 'Pro-forma' status is used when the voucher does not have a voucher number.\n"
" * The 'Posted' status is used when user create voucher,a voucher number is generated and voucher entries are created in account.\n"
" * The 'Cancelled' status is used when user cancel voucher.")
reference = fields.Char('Bill Reference', readonly=True, states={'draft': [('readonly', False)]},
help="The partner reference of this document.", copy=False)
amount = fields.Monetary(string='Total', store=True, readonly=True, compute='_compute_total')
tax_amount = fields.Monetary(readonly=True, store=True, compute='_compute_total')
tax_correction = fields.Monetary(readonly=True, states={'draft': [('readonly', False)]},
help='In case we have a rounding problem in the tax, use this field to correct it')
number = fields.Char(readonly=True, copy=False)
move_id = fields.Many2one('account.move', 'Journal Entry', copy=False)
partner_id = fields.Many2one('res.partner', 'Partner', change_default=1, readonly=True, states={'draft': [('readonly', False)]})
paid = fields.Boolean(compute='_check_paid', help="The Voucher has been totally paid.")
pay_now = fields.Selection([
('pay_now', 'Pay Directly'),
('pay_later', 'Pay Later'),
], 'Payment', index=True, readonly=True, states={'draft': [('readonly', False)]}, default='pay_later')
date_due = fields.Date('Due Date', readonly=True, index=True, states={'draft': [('readonly', False)]})
branch_id = fields.Many2one('res.branch', 'Branch', ondelete="restrict",
default=lambda self: self.env['res.users']._get_default_branch())
@api.constrains('company_id', 'branch_id')
def _check_company_branch(self):
for record in self:
if record.branch_id and record.company_id != record.branch_id.company_id:
raise ValidationError(
_('Configuration Error of Company:\n'
'The Company (%s) in the voucher and '
'the Company (%s) of Branch must '
'be the same company!') % (record.company_id.name,
record.branch_id.company_id.name)
)
@api.one
@api.depends('move_id.line_ids.reconciled', 'move_id.line_ids.account_id.internal_type')
def _check_paid(self):
self.paid = any([((line.account_id.internal_type, 'in', ('receivable', 'payable')) and line.reconciled) for line in self.move_id.line_ids])
@api.model
def _get_currency(self):
journal = self.env['account.journal'].browse(self.env.context.get('default_journal_id', False))
if journal.currency_id:
return journal.currency_id.id
return self.env.user.company_id.currency_id.id
@api.model
def _get_company(self):
return self._context.get('company_id', self.env.user.company_id.id)
@api.multi
@api.depends('name', 'number')
def name_get(self):
return [(r.id, (r.number or _('Voucher'))) for r in self]
@api.one
@api.depends('journal_id', 'company_id')
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):
for voucher in self:
total = 0
tax_amount = 0
for line in voucher.line_ids:
tax_info = line.tax_ids.compute_all(line.price_unit, voucher.currency_id, line.quantity, line.product_id, voucher.partner_id)
total += tax_info.get('total_included', 0.0)
tax_amount += sum([t.get('amount',0.0) for t in tax_info.get('taxes', False)])
voucher.amount = total + voucher.tax_correction
voucher.tax_amount = tax_amount
@api.onchange('date')
def onchange_date(self):
self.account_date = self.date
@api.onchange('partner_id', 'pay_now')
def onchange_partner_id(self):
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
else:
account_type = self.voucher_type == 'purchase' and 'payable' or 'receivable'
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):
self.action_move_line_create()
@api.multi
def action_cancel_draft(self):
self.write({'state': 'draft'})
@api.multi
def cancel_voucher(self):
for voucher in self:
voucher.move_id.button_cancel()
voucher.move_id.unlink()
self.write({'state': 'cancel', 'move_id': False})
@api.multi
def unlink(self):
for voucher in self:
if voucher.state not in ('draft', 'cancel'):
raise UserError(_('Cannot delete voucher(s) which are already opened or paid.'))
return super(AccountVoucher, self).unlink()
@api.multi
def first_move_line_get(self, move_id, company_currency, current_currency):
debit = credit = 0.0
if self.voucher_type == 'purchase':
credit = self._convert_amount(self.amount)
elif self.voucher_type == 'sale':
debit = self._convert_amount(self.amount)
if debit < 0.0: debit = 0.0
if credit < 0.0: credit = 0.0
sign = debit - credit < 0 and -1 or 1
#set the first line of the voucher
move_line = {
'name': self.name or '/',
'debit': debit,
'credit': credit,
'account_id': self.account_id.id,
'move_id': move_id,
'journal_id': self.journal_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),
'date': self.account_date,
'date_maturity': self.date_due,
'payment_id': self._context.get('payment_id'),
'branch_id': self.branch_id.id,
}
return move_line
@api.multi
def account_move_get(self):
if self.number:
name = self.number
elif self.journal_id.sequence_id:
if not self.journal_id.sequence_id.active:
raise UserError(_('Please activate the sequence of selected journal !'))
name = self.journal_id.sequence_id.with_context(ir_sequence_date=self.date).next_by_id()
else:
raise UserError(_('Please define a sequence on the journal.'))
move = {
'name': name,
'journal_id': self.journal_id.id,
'narration': self.narration,
'date': self.account_date,
'ref': self.reference,
'branch_id': self.branch_id.id
}
return move
@api.multi
def _convert_amount(self, amount):
'''
This function convert the amount given in company currency. It takes either the rate in the voucher (if the
payment_rate_currency_id is relevant) either the rate encoded in the system.
:param amount: float. The amount to convert
:param voucher: id of the voucher on which we want the conversion
:param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date
field in order to select the good rate to use.
:return: the amount in the currency of the voucher's company
:rtype: float
'''
for voucher in self:
return voucher.currency_id.compute(amount, voucher.company_id.currency_id)
@api.multi
def voucher_pay_now_payment_create(self):
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 {
'name': name,
'payment_type': payment_type,
'payment_method_id': payment_methods and payment_methods[0].id or False,
'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.payment_journal_id.id,
'company_id': self.company_id.id,
'communication': self.name,
'state': 'reconciled',
}
@api.multi
def voucher_move_line_create(self, line_total, move_id, company_currency, current_currency):
'''
Create one account move line, on the given account move, per voucher line where amount is not 0.0.
It returns Tuple with tot_line what is total of difference between debit and credit and
a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists).
:param voucher_id: Voucher id what we are working with
:param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines.
:param move_id: Account move wher those lines will be joined.
:param company_currency: id of currency of the company to which the voucher belong
:param current_currency: id of currency of the voucher
:return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method)
:rtype: tuple(float, list of int)
'''
for line in self.line_ids:
#create one move line per voucher line where amount is not 0.0
if not line.price_subtotal:
continue
line_subtotal = line.price_subtotal
if self.voucher_type == 'sale':
line_subtotal = -1 * line.price_subtotal
# convert the amount set on the voucher line into the currency of the voucher's company
# this calls res_curreny.compute() with the right context,
# so that it will take either the rate on the voucher if it is relevant or will use the default behaviour
amount = self._convert_amount(line.price_unit*line.quantity)
move_line = {
'journal_id': self.journal_id.id,
'name': line.name or '/',
'account_id': line.account_id.id,
'move_id': move_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,
'debit': abs(amount) if self.voucher_type == 'purchase' else 0.0,
'date': self.account_date,
'tax_ids': [(4,t.id) for t in line.tax_ids],
'amount_currency': line_subtotal if current_currency != company_currency else 0.0,
'currency_id': company_currency != current_currency and current_currency or False,
'payment_id': self._context.get('payment_id'),
}
self.env['account.move.line'].with_context(apply_taxes=True).create(move_line)
return line_total
@api.multi
def action_move_line_create(self):
'''
Confirm the vouchers given in ids and create the journal entries for each of them
'''
for voucher in self:
local_context = dict(self._context, force_company=voucher.journal_id.company_id.id)
if voucher.move_id:
continue
company_currency = voucher.journal_id.company_id.currency_id.id
current_currency = voucher.currency_id.id or company_currency
# we select the context to use accordingly if it's a multicurrency case or not
# But for the operations made by _convert_amount, we always need to give the date in the context
ctx = local_context.copy()
ctx['date'] = voucher.account_date
ctx['check_move_validity'] = False
# Create a payment to allow the reconciliation when pay_now = 'pay_now'.
if self.pay_now == 'pay_now' and self.amount > 0:
ctx['payment_id'] = self.env['account.payment'].create(self.voucher_pay_now_payment_create()).id
# Create the account move record.
move = self.env['account.move'].create(voucher.account_move_get())
# Get the name of the account_move just created
# Create the first line of the voucher
move_line = self.env['account.move.line'].with_context(ctx).create(voucher.with_context(ctx).first_move_line_get(move.id, company_currency, current_currency))
line_total = move_line.debit - move_line.credit
if voucher.voucher_type == 'sale':
line_total = line_total - voucher._convert_amount(voucher.tax_amount)
elif voucher.voucher_type == 'purchase':
line_total = line_total + voucher._convert_amount(voucher.tax_amount)
# Create one move line per voucher line where amount is not 0.0
line_total = voucher.with_context(ctx).voucher_move_line_create(line_total, move.id, company_currency, current_currency)
# Add tax correction to move line if any tax correction specified
if voucher.tax_correction != 0.0:
tax_move_line = self.env['account.move.line'].search([('move_id', '=', move.id), ('tax_line_id', '!=', False)], limit=1)
if len(tax_move_line):
tax_move_line.write({'debit': tax_move_line.debit + voucher.tax_correction if tax_move_line.debit > 0 else 0,
'credit': tax_move_line.credit + voucher.tax_correction if tax_move_line.credit > 0 else 0})
# We post the voucher.
voucher.write({
'move_id': move.id,
'state': 'posted',
'number': move.name
})
move.post()
return True
@api.multi
def _track_subtype(self, init_values):
if 'state' in init_values:
return 'account_voucher.mt_voucher_state_change'
return super(AccountVoucher, self)._track_subtype(init_values)
class AccountVoucherLine(models.Model):
_name = 'account.voucher.line'
_description = 'Voucher Lines'
name = fields.Text(string='Description', required=True)
sequence = fields.Integer(default=10,
help="Gives the sequence of this line when displaying the voucher.")
voucher_id = fields.Many2one('account.voucher', 'Voucher', required=1, ondelete='cascade')
product_id = fields.Many2one('product.product', string='Product',
ondelete='set null', index=True)
account_id = fields.Many2one('account.account', string='Account',
required=True, domain=[('deprecated', '=', False)],
help="The income or expense account related to the selected product.")
price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price'), oldname='amount')
price_subtotal = fields.Monetary(string='Amount',
store=True, readonly=True, compute='_compute_subtotal')
quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'),
required=True, default=1)
account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account')
company_id = fields.Many2one('res.company', related='voucher_id.company_id', string='Company', store=True, readonly=True)
tax_ids = fields.Many2many('account.tax', string='Tax', help="Only for tax excluded from price")
currency_id = fields.Many2one('res.currency', related='voucher_id.currency_id')
branch_id = fields.Many2one('res.branch',
related='voucher_id.branch_id',
string='Branch',
readonly=True, store=True)
@api.one
@api.depends('price_unit', 'tax_ids', 'quantity', 'product_id', 'voucher_id.currency_id')
def _compute_subtotal(self):
self.price_subtotal = self.quantity * self.price_unit
if self.tax_ids:
taxes = self.tax_ids.compute_all(self.price_unit, self.voucher_id.currency_id, self.quantity, product=self.product_id, partner=self.voucher_id.partner_id)
self.price_subtotal = taxes['total_excluded']
@api.onchange('product_id', 'voucher_id', 'price_unit', 'company_id')
def _onchange_line_details(self):
if not self.voucher_id or not self.product_id or not self.voucher_id.partner_id:
return
onchange_res = self.product_id_change(
self.product_id.id,
self.voucher_id.partner_id.id,
self.price_unit,
self.company_id.id,
self.voucher_id.currency_id.id,
self.voucher_id.voucher_type)
for fname, fvalue in onchange_res['value'].items():
setattr(self, fname, fvalue)
def _get_account(self, product, fpos, type):
accounts = product.product_tmpl_id.get_product_accounts(fpos)
if type == 'sale':
return accounts['income']
return accounts['expense']
@api.multi
def product_id_change(self, product_id, partner_id=False, price_unit=False, company_id=None, currency_id=None, type=None):
# TDE note: mix of old and new onchange badly written in 9, multi but does not use record set
context = self._context
company_id = company_id if company_id is not None else context.get('company_id', False)
company = self.env['res.company'].browse(company_id)
currency = self.env['res.currency'].browse(currency_id)
if not partner_id:
raise UserError(_("You must first select a partner!"))
part = self.env['res.partner'].browse(partner_id)
if part.lang:
self = self.with_context(lang=part.lang)
product = self.env['product.product'].browse(product_id)
fpos = part.property_account_position_id
account = self._get_account(product, fpos, type)
values = {
'name': product.partner_ref,
'account_id': account.id,
}
if type == 'purchase':
values['price_unit'] = price_unit or product.standard_price
taxes = product.supplier_taxes_id or account.tax_ids
if product.description_purchase:
values['name'] += '\n' + product.description_purchase
else:
values['price_unit'] = price_unit or product.lst_price
taxes = product.taxes_id or account.tax_ids
if product.description_sale:
values['name'] += '\n' + product.description_sale
values['tax_ids'] = taxes.ids
if company and currency:
if company.currency_id != currency:
if type == 'purchase':
values['price_unit'] = price_unit or product.standard_price
values['price_unit'] = values['price_unit'] * currency.rate
return {'value': values, 'domain': {}}