import time
from lxml import etree
import openerp.addons.decimal_precision as dp
+import openerp.exceptions
from openerp import netsvc
from openerp import pooler
company_id = context.get('company_id', user.company_id.id)
type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale_refund', 'in_refund': 'purchase_refund'}
journal_obj = self.pool.get('account.journal')
- res = journal_obj.search(cr, uid, [('type', '=', type2journal.get(type_inv, 'sale')),
- ('company_id', '=', company_id)],
- limit=1)
+ domain = [('company_id', '=', company_id)]
+ if isinstance(type_inv, list):
+ domain.append(('type', 'in', [type2journal.get(type) for type in type_inv if type2journal.get(type)]))
+ else:
+ domain.append(('type', '=', type2journal.get(type_inv, 'sale')))
+ res = journal_obj.search(cr, uid, domain, limit=1)
return res and res[0] or False
def _get_currency(self, cr, uid, context=None):
tt = type2journal.get(type_inv, 'sale')
result = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=',tt)], context=context)
if not result:
- raise osv.except_osv(_('No Analytic Journal !'),_("You must define an analytic journal of type '%s'!") % (tt,))
+ raise osv.except_osv(_('No Analytic Journal!'),_("You must define an analytic journal of type '%s'!") % (tt,))
return result[0]
def _get_type(self, cr, uid, context=None):
return [('none', _('Free Reference'))]
def _amount_residual(self, cr, uid, ids, name, args, context=None):
+ """Function of the field residua. It computes the residual amount (balance) for each invoice"""
+ if context is None:
+ context = {}
+ ctx = context.copy()
result = {}
+ currency_obj = self.pool.get('res.currency')
for invoice in self.browse(cr, uid, ids, context=context):
+ nb_inv_in_partial_rec = max_invoice_id = 0
result[invoice.id] = 0.0
if invoice.move_id:
- for m in invoice.move_id.line_id:
- if m.account_id.type in ('receivable','payable'):
- result[invoice.id] += m.amount_residual_currency
+ for aml in invoice.move_id.line_id:
+ if aml.account_id.type in ('receivable','payable'):
+ if aml.currency_id and aml.currency_id.id == invoice.currency_id.id:
+ result[invoice.id] += aml.amount_residual_currency
+ else:
+ ctx['date'] = aml.date
+ result[invoice.id] += currency_obj.compute(cr, uid, aml.company_id.currency_id.id, invoice.currency_id.id, aml.amount_residual, context=ctx)
+
+ if aml.reconcile_partial_id.line_partial_ids:
+ #we check if the invoice is partially reconciled and if there are other invoices
+ #involved in this partial reconciliation (and we sum these invoices)
+ for line in aml.reconcile_partial_id.line_partial_ids:
+ if line.invoice and invoice.type == line.invoice.type:
+ nb_inv_in_partial_rec += 1
+ #store the max invoice id as for this invoice we will make a balance instead of a simple division
+ max_invoice_id = max(max_invoice_id, line.invoice.id)
+ if nb_inv_in_partial_rec:
+ #if there are several invoices in a partial reconciliation, we split the residual by the number
+ #of invoice to have a sum of residual amounts that matches the partner balance
+ new_value = currency_obj.round(cr, uid, invoice.currency_id, result[invoice.id] / nb_inv_in_partial_rec)
+ if invoice.id == max_invoice_id:
+ #if it's the last the invoice of the bunch of invoices partially reconciled together, we make a
+ #balance to avoid rounding errors
+ result[invoice.id] = result[invoice.id] - ((nb_inv_in_partial_rec - 1) * new_value)
+ else:
+ result[invoice.id] = new_value
+
+ #prevent the residual amount on the invoice to be less than 0
+ result[invoice.id] = max(result[invoice.id], 0.0)
return result
# Give Journal Items related to the payment reconciled to this invoice
help="If you use payment terms, the due date will be computed automatically at the generation "\
"of accounting entries. The payment term may compute several due dates, for example 50% now and 50% in one month, but if you want to force a due date, make sure that the payment term is not set on the invoice. If you keep the payment term and the due date empty, it means direct payment."),
'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}, track_visibility='always'),
- 'payment_term': fields.many2one('account.payment.term', 'Payment Term',readonly=True, states={'draft':[('readonly',False)]},
+ 'payment_term': fields.many2one('account.payment.term', 'Payment Terms',readonly=True, states={'draft':[('readonly',False)]},
help="If you use payment terms, the due date will be computed automatically at the generation "\
"of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "\
"The payment term may compute several due dates, for example 50% now, 50% in one month."),
'tax_line': fields.one2many('account.invoice.tax', 'invoice_id', 'Tax Lines', readonly=True, states={'draft':[('readonly',False)]}),
'move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, select=1, ondelete='restrict', help="Link to the automatically generated Journal Items."),
- 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed', track_visibility='always',
+ 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Subtotal', track_visibility='always',
store={
'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
'account.invoice.tax': (_get_invoice_tax, None, 20),
('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
]
- def _find_partner(self, inv):
- '''
- Find the partner for which the accounting entries will be created
- '''
- #if the chosen partner is not a company and has a parent company, use the parent for the journal entries
- #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
- part = inv.partner_id
- if part.parent_id and not part.is_company:
- part = part.parent_id
- return part
+
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
context['view_id'] = view_id
return context
- def create(self, cr, uid, vals, context=None):
- if context is None:
- context = {}
- try:
- return super(account_invoice, self).create(cr, uid, vals, context)
- except Exception, e:
- if '"journal_id" viol' in e.args[0]:
- raise orm.except_orm(_('Configuration Error!'),
- _('There is no Sale/Purchase Journal(s) defined.'))
- else:
- raise orm.except_orm(_('Unknown Error!'), str(e))
-
def invoice_print(self, cr, uid, ids, context=None):
'''
This function prints the invoice and mark it as sent, so that we can see more easily the next step of the workflow
try:
compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
except ValueError:
- compose_form_id = False
+ compose_form_id = False
ctx = dict(context)
ctx.update({
'default_model': 'account.invoice',
context = {}
invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context)
unlink_ids = []
+
for t in invoices:
- if t['state'] in ('draft', 'cancel') and t['internal_number']== False:
- unlink_ids.append(t['id'])
+ if t['state'] not in ('draft', 'cancel'):
+ raise openerp.exceptions.Warning(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.'))
+ elif t['internal_number']:
+ raise openerp.exceptions.Warning(_('You cannot delete an invoice after it has been validated (and received a number). You can set it back to "Draft" state and modify its content, then re-confirm it.'))
else:
- raise osv.except_osv(_('Invalid Action!'), _('You can not delete an invoice which is not cancelled. You should refund it instead.'))
+ unlink_ids.append(t['id'])
+
osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
return True
return result
def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
- res = {}
+ res = {}
+ if isinstance(ids, (int, long)):
+ ids = [ids]
if not date_invoice:
date_invoice = time.strftime('%Y-%m-%d')
if not payment_term_id:
- return {'value':{'date_due': date_invoice}} #To make sure the invoice has a due date when no payment term
+ inv = self.browse(cr, uid, ids[0])
+ #To make sure the invoice due date should contain due date which is entered by user when there is no payment term defined
+ return {'value':{'date_due': inv.date_due and inv.date_due or date_invoice}}
pterm_list = self.pool.get('account.payment.term').compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
if pterm_list:
pterm_list = [line[0] for line in pterm_list]
return {'value': {}}
def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
+ #TODO: add the missing context parameter when forward-porting in trunk so we can remove
+ # this hack!
+ context = self.pool['res.users'].context_get(cr, uid)
+
val = {}
dom = {}
obj_journal = self.pool.get('account.journal')
obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id'])
if obj_l.company_id.id != company_id:
raise osv.except_osv(_('Configuration Error!'),
- _('Invoice line account\'s company and invoice\'s compnay does not match.'))
+ _('Invoice line account\'s company and invoice\'s company does not match.'))
else:
continue
if company_id and type:
- if type in ('out_invoice'):
- journal_type = 'sale'
- elif type in ('out_refund'):
- journal_type = 'sale_refund'
- elif type in ('in_refund'):
- journal_type = 'purchase_refund'
- else:
- journal_type = 'purchase'
+ journal_mapping = {
+ 'out_invoice': 'sale',
+ 'out_refund': 'sale_refund',
+ 'in_refund': 'purchase_refund',
+ 'in_invoice': 'purchase',
+ }
+ journal_type = journal_mapping[type]
journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)])
if journal_ids:
val['journal_id'] = journal_ids[0]
if r[1] == 'journal_id' and r[2] in journal_ids:
val['journal_id'] = r[2]
if not val.get('journal_id', False):
- raise osv.except_osv(_('Configuration Error!'), (_('Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.') % (journal_type)))
+ journal_type_map = dict(obj_journal._columns['type'].selection)
+ journal_type_label = self.pool['ir.translation']._get_source(cr, uid, None, ('code','selection'),
+ context.get('lang'),
+ journal_type_map.get(journal_type))
+ raise osv.except_osv(_('Configuration Error!'),
+ _('Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.') % ('"%s"' % journal_type_label))
dom = {'journal_id': [('id', 'in', journal_ids)]}
else:
journal_ids = obj_journal.search(cr, uid, [])
inv = self.browse(cr, uid, id)
cur_obj = self.pool.get('res.currency')
- company_currency = inv.company_id.currency_id.id
+ company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
if inv.type in ('out_invoice', 'in_refund'):
sign = 1
else:
else:
ref = self._convert_ref(cr, uid, inv.number)
if not inv.journal_id.analytic_journal_id:
- raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
+ raise osv.except_osv(_('No Analytic Journal!'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
il['analytic_lines'] = [(0,0, {
'name': il['name'],
'date': inv['date_invoice'],
return move_lines
def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj):
+ company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id
if not inv.tax_line:
for tax in compute_taxes.values():
ait_obj.create(cr, uid, tax)
if not key in compute_taxes:
raise osv.except_osv(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
base = compute_taxes[key]['base']
- if abs(base - tax.base) > inv.company_id.currency_id.rounding:
+ if abs(base - tax.base) > company_currency.rounding:
raise osv.except_osv(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.'))
for key in compute_taxes:
if not key in tax_key:
if not inv.journal_id.sequence_id:
raise osv.except_osv(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
if not inv.invoice_line:
- raise osv.except_osv(_('No Invoice Lines !'), _('Please create some invoice lines.'))
+ raise osv.except_osv(_('No Invoice Lines!'), _('Please create some invoice lines.'))
if inv.move_id:
continue
ctx.update({'lang': inv.partner_id.lang})
if not inv.date_invoice:
self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx)
- company_currency = inv.company_id.currency_id.id
+ company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
# create the analytical lines
# one move line per invoice line
iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx)
group_check_total = self.pool.get('res.groups').browse(cr, uid, group_check_total_id, context=context)
if group_check_total and uid in [x.id for x in group_check_total.users]:
if (inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0)):
- raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe encoded total does not match the computed total.'))
+ raise osv.except_osv(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.'))
if inv.payment_term:
total_fixed = total_percent = 0
total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml, context=ctx)
acc_id = inv.account_id.id
- name = inv['name'] or '/'
+ name = inv['name'] or inv['supplier_invoice_number'] or '/'
totlines = False
if inv.payment_term:
totlines = payment_term_obj.compute(cr,
date = inv.date_invoice or time.strftime('%Y-%m-%d')
- part = self._find_partner(inv)
+ part = self.pool.get("res.partner")._find_accounting_partner(inv.partner_id)
line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part.id, date, context=ctx)),iml)
'line_id': line,
'journal_id': journal_id,
'date': date,
- 'narration':inv.comment
+ 'narration': inv.comment,
+ 'company_id': inv.company_id.id,
}
period_id = inv.period_id and inv.period_id.id or False
ctx.update(company_id=inv.company_id.id,
if not ids:
return []
types = {
- 'out_invoice': 'Invoice ',
- 'in_invoice': 'Sup. Invoice ',
- 'out_refund': 'Refund ',
- 'in_refund': 'Supplier Refund ',
+ 'out_invoice': _('Invoice'),
+ 'in_invoice': _('Supplier Invoice'),
+ 'out_refund': _('Refund'),
+ 'in_refund': _('Supplier Refund'),
}
- return [(r['id'], (r['number']) or types[r['type']] + (r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')]
+ return [(r['id'], '%s %s' % (r['number'] or types[r['type']], r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')]
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
if not args:
return self.name_get(cr, user, ids, context)
def _refund_cleanup_lines(self, cr, uid, lines, context=None):
+ """Convert records to dict of values suitable for one2many line creation
+
+ :param list(browse_record) lines: records to convert
+ :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
+ """
clean_lines = []
for line in lines:
clean_line = {}
ref = invoice.reference
else:
ref = self._convert_ref(cr, uid, invoice.number)
- partner = invoice.partner_id
- if partner.parent_id and not partner.is_company:
- partner = partner.parent_id
+ partner = self.pool['res.partner']._find_accounting_partner(invoice.partner_id)
# Pay attention to the sign for both debit/credit AND amount_currency
l1 = {
'debit': direction * pay_amount>0 and direction * pay_amount,
_name = "account.invoice.line"
_description = "Invoice Line"
+ _order = "invoice_id,sequence,id"
_columns = {
'name': fields.text('Description', required=True),
'origin': fields.char('Source Document', size=256, help="Reference of the document that produced this invoice."),
'product_id': fields.many2one('product.product', 'Product', ondelete='set null', select=True),
'account_id': fields.many2one('account.account', 'Account', required=True, domain=[('type','<>','view'), ('type', '<>', 'closed')], help="The income or expense account related to the selected product."),
'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
- 'price_subtotal': fields.function(_amount_line, string='Subtotal', type="float",
+ 'price_subtotal': fields.function(_amount_line, string='Amount', type="float",
digits_compute= dp.get_precision('Account'), store=True),
'quantity': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount')),
# XXX this gets the default account for the user's company,
# it should get the default account for the invoice's company
# however, the invoice's company does not reach this point
- prop = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
+ if context is None:
+ context = {}
+ if context.get('type') in ('out_invoice','out_refund'):
+ prop = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
+ else:
+ prop = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category', context=context)
return prop and prop.id or False
_defaults = {
'discount': 0.0,
'price_unit': _price_unit_default,
'account_id': _default_account_id,
+ 'sequence': 10,
}
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
context = dict(context)
context.update({'company_id': company_id, 'force_company': company_id})
if not partner_id:
- raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
+ raise osv.except_osv(_('No Partner Defined!'),_("You must first select a partner!") )
if not product:
if type in ('in_invoice', 'in_refund'):
return {'value': {}, 'domain':{'product_uom':[]}}
new_price = res_final['value']['price_unit'] * currency.rate
res_final['value']['price_unit'] = new_price
- if result['uos_id'] != res.uom_id.id:
- selected_uom = self.pool.get('product.uom_id').browse(cr, uid, result['uos_id'], context=context)
- if res.uom_id.category_id.id == selected_uom.category_id.id:
- new_price = res_final['value']['price_unit'] * uom_id.factor_inv
- res_final['value']['price_unit'] = new_price
+ if result['uos_id'] and result['uos_id'] != res.uom_id.id:
+ selected_uom = self.pool.get('product.uom').browse(cr, uid, result['uos_id'], context=context)
+ new_price = self.pool.get('product.uom')._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uos_id'])
+ res_final['value']['price_unit'] = new_price
return res_final
def uos_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
if context is None:
context = {}
inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
- company_currency = inv.company_id.currency_id.id
-
+ company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
for line in inv.invoice_line:
mres = self.move_line_get_item(cr, uid, line, context)
if not mres:
cur_obj = self.pool.get('res.currency')
inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
cur = inv.currency_id
- company_currency = inv.company_id.currency_id.id
-
+ company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
for line in inv.invoice_line:
for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, (line.price_unit* (1-(line.discount or 0.0)/100.0)), line.quantity, line.product_id, inv.partner_id)['taxes']:
val={}
'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
}
+ def _find_accounting_partner(self, partner):
+ '''
+ Find the partner for which the accounting entries will be created
+ '''
+ # FIXME: after 7.0, to replace by function field partner.commercial_partner_id
+
+ #if the chosen partner is not a company and has a parent company, use the parent for the journal entries
+ #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
+ while not partner.is_company and partner.parent_id:
+ partner = partner.parent_id
+ return partner
+
def copy(self, cr, uid, id, default=None, context=None):
default = default or {}
default.update({'invoice_ids' : []})
return super(res_partner, self).copy(cr, uid, id, default, context)
-class mail_compose_message(osv.osv):
+class mail_compose_message(osv.Model):
_inherit = 'mail.compose.message'
def send_mail(self, cr, uid, ids, context=None):
context = context or {}
if context.get('default_model') == 'account.invoice' and context.get('default_res_id') and context.get('mark_invoice_as_sent'):
+ context = dict(context, mail_post_autofollow=True)
self.pool.get('account.invoice').write(cr, uid, [context['default_res_id']], {'sent': True}, context=context)
return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)