[ADD] : Added Upstream Patch for Account.

This commit is contained in:
Kunjal 2018-07-06 14:05:19 +05:30
parent cb0640a320
commit 5e965cc037
31 changed files with 347 additions and 85 deletions

View File

@ -11,6 +11,7 @@ from flectra.tools.misc import consteq
class MailController(MailController): class MailController(MailController):
@classmethod
def _redirect_to_record(cls, model, res_id, access_token=None): def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the invoice, but provided # If the current user doesn't have access to the invoice, but provided
# a valid access token, redirect him to the front-end view. # a valid access token, redirect him to the front-end view.

View File

@ -7,7 +7,7 @@
<!--Email template --> <!--Email template -->
<record id="email_template_edi_invoice" model="mail.template"> <record id="email_template_edi_invoice" model="mail.template">
<field name="name">Invoicing: Invoice email</field> <field name="name">Invoicing: Invoice email</field>
<field name="email_from">${(object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '')|safe}</field> <field name="email_from">${(object.user_id.email and '&quot;%s&quot; &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '')|safe}</field>
<field name="subject">${object.company_id.name} Invoice (Ref ${object.number or 'n/a'})</field> <field name="subject">${object.company_id.name} Invoice (Ref ${object.number or 'n/a'})</field>
<field name="partner_to">${object.partner_id.id}</field> <field name="partner_to">${object.partner_id.id}</field>
<field name="model_id" ref="account.model_account_invoice"/> <field name="model_id" ref="account.model_account_invoice"/>
@ -35,7 +35,7 @@ invoice
% if object.origin: % if object.origin:
(with reference: ${object.origin}) (with reference: ${object.origin})
% endif % endif
amounting in <strong>${object.amount_total} ${object.currency_id.name}</strong> amounting in <strong>${format_amount(object.amount_total, object.currency_id)}</strong>
from ${object.company_id.name}. from ${object.company_id.name}.
</p> </p>
@ -71,7 +71,7 @@ from ${object.company_id.name}.
<field name="body_html" type="html"> <field name="body_html" type="html">
<div> <div>
% set record = ctx.get('record') % set record = ctx.get('record')
% set company = record and record.company_id or user.company_id % set company = record and record.company_id or ctx.get('company')
<table border="0" width="100%" cellpadding="0" bgcolor="#ededed" style="padding: 20px; background-color: #ededed; border-collapse:separate;" summary="o_mail_notification"> <table border="0" width="100%" cellpadding="0" bgcolor="#ededed" style="padding: 20px; background-color: #ededed; border-collapse:separate;" summary="o_mail_notification">
<tbody> <tbody>
<!-- HEADER --> <!-- HEADER -->

View File

@ -14,7 +14,7 @@
<field name="lang">${object.partner_id.lang}</field> <field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="xml"> <field name="body_html" type="xml">
<p>Dear ${object.partner_id.name},</p> <p>Dear ${object.partner_id.name},</p>
<p>Thank you for your payment.<br />Here is your payment receipt <strong>${(object.name or '').replace('/','-')}</strong> amounting to <strong>${object.amount} ${object.currency_id.name}</strong> from ${object.company_id.name}.</p> <p>Thank you for your payment.<br />Here is your payment receipt <strong>${(object.name or '').replace('/','-')}</strong> amounting to <strong>${format_amount(object.amount, object.currency_id)}</strong> from ${object.company_id.name}.</p>
<p>If you have any questions, please do not hesitate to contact us.</p> <p>If you have any questions, please do not hesitate to contact us.</p>
<p>Best regards, <p>Best regards,
% if user and user.signature: % if user and user.signature:

View File

@ -792,7 +792,7 @@ class AccountTax(models.Model):
tag_ids = fields.Many2many('account.account.tag', 'account_tax_account_tag', string='Tags', help="Optional tags you may want to assign for custom reporting") tag_ids = fields.Many2many('account.account.tag', 'account_tax_account_tag', string='Tags', help="Optional tags you may want to assign for custom reporting")
tax_group_id = fields.Many2one('account.tax.group', string="Tax Group", default=_default_tax_group, required=True) tax_group_id = fields.Many2one('account.tax.group', string="Tax Group", default=_default_tax_group, required=True)
# Technical field to make the 'tax_exigibility' field invisible if the same named field is set to false in 'res.company' model # Technical field to make the 'tax_exigibility' field invisible if the same named field is set to false in 'res.company' model
hide_tax_exigibility = fields.Boolean(string='Hide Use Cash Basis Option', related='company_id.tax_exigibility') hide_tax_exigibility = fields.Boolean(string='Hide Use Cash Basis Option', related='company_id.tax_exigibility', readonly=True)
tax_exigibility = fields.Selection( tax_exigibility = fields.Selection(
[('on_invoice', 'Based on Invoice'), [('on_invoice', 'Based on Invoice'),
('on_payment', 'Based on Payment'), ('on_payment', 'Based on Payment'),

View File

@ -71,7 +71,7 @@ class AccountInvoice(models.Model):
@api.onchange('amount_total') @api.onchange('amount_total')
def _onchange_amount_total(self): def _onchange_amount_total(self):
for inv in self: for inv in self:
if inv.amount_total < 0: if float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1:
raise Warning(_('You cannot validate an invoice with a negative total amount. You should create a credit note instead.')) raise Warning(_('You cannot validate an invoice with a negative total amount. You should create a credit note instead.'))
@api.model @api.model
@ -406,7 +406,7 @@ class AccountInvoice(models.Model):
""" computes the prefix of the number that will be assigned to the first invoice/bill/refund of a journal, in order to """ computes the prefix of the number that will be assigned to the first invoice/bill/refund of a journal, in order to
let the user manually change it. let the user manually change it.
""" """
if not self.env.user._is_admin(): if not self.env.user._is_system():
for invoice in self: for invoice in self:
invoice.sequence_number_next_prefix = False invoice.sequence_number_next_prefix = False
invoice.sequence_number_next = '' invoice.sequence_number_next = ''
@ -562,7 +562,10 @@ class AccountInvoice(models.Model):
""" """
self.ensure_one() self.ensure_one()
self.sent = True self.sent = True
return self.env.ref('account.account_invoices').report_action(self) if self.user_has_groups('account.group_account_invoice'):
return self.env.ref('account.account_invoices').report_action(self)
else:
return self.env.ref('account.account_invoices_without_payment').report_action(self)
@api.multi @api.multi
def action_invoice_sent(self): def action_invoice_sent(self):
@ -602,7 +605,8 @@ class AccountInvoice(models.Model):
for invoice in self: for invoice in self:
# Delete non-manual tax lines # Delete non-manual tax lines
self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,)) self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,))
self.invalidate_cache() if self._cr.rowcount:
self.invalidate_cache()
# Generate one tax line per tax, however many invoice lines it's applied to # Generate one tax line per tax, however many invoice lines it's applied to
tax_grouped = invoice.get_taxes_values() tax_grouped = invoice.get_taxes_values()
@ -783,7 +787,7 @@ class AccountInvoice(models.Model):
to_open_invoices = self.filtered(lambda inv: inv.state != 'open') to_open_invoices = self.filtered(lambda inv: inv.state != 'open')
if to_open_invoices.filtered(lambda inv: inv.state != 'draft'): if to_open_invoices.filtered(lambda inv: inv.state != 'draft'):
raise UserError(_("Invoice must be in draft state in order to validate it.")) raise UserError(_("Invoice must be in draft state in order to validate it."))
if to_open_invoices.filtered(lambda inv: inv.amount_total < 0): if to_open_invoices.filtered(lambda inv: float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1):
raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead.")) raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead."))
to_open_invoices.action_date_assign() to_open_invoices.action_date_assign()
to_open_invoices.action_move_create() to_open_invoices.action_move_create()
@ -1523,6 +1527,13 @@ class AccountInvoiceLine(models.Model):
return accounts['income'] return accounts['income']
return accounts['expense'] return accounts['expense']
def _set_currency(self):
company = self.invoice_id.company_id
currency = self.invoice_id.currency_id
if company and currency:
if company.currency_id != currency:
self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate
def _set_taxes(self): def _set_taxes(self):
""" Used in on_change to set taxes and price.""" """ Used in on_change to set taxes and price."""
if self.invoice_id.type in ('out_invoice', 'out_refund'): if self.invoice_id.type in ('out_invoice', 'out_refund'):
@ -1541,8 +1552,10 @@ class AccountInvoiceLine(models.Model):
prec = self.env['decimal.precision'].precision_get('Product Price') prec = self.env['decimal.precision'].precision_get('Product Price')
if not self.price_unit or float_compare(self.price_unit, self.product_id.standard_price, precision_digits=prec) == 0: if not self.price_unit or float_compare(self.price_unit, self.product_id.standard_price, precision_digits=prec) == 0:
self.price_unit = fix_price(self.product_id.standard_price, taxes, fp_taxes) self.price_unit = fix_price(self.product_id.standard_price, taxes, fp_taxes)
self._set_currency()
else: else:
self.price_unit = fix_price(self.product_id.lst_price, taxes, fp_taxes) self.price_unit = fix_price(self.product_id.lst_price, taxes, fp_taxes)
self._set_currency()
@api.onchange('product_id') @api.onchange('product_id')
def _onchange_product_id(self): def _onchange_product_id(self):
@ -1591,8 +1604,6 @@ class AccountInvoiceLine(models.Model):
domain['uom_id'] = [('category_id', '=', product.uom_id.category_id.id)] domain['uom_id'] = [('category_id', '=', product.uom_id.category_id.id)]
if company and currency: if company and currency:
if company.currency_id != currency:
self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate
if self.uom_id and self.uom_id.id != product.uom_id.id: if self.uom_id and self.uom_id.id != product.uom_id.id:
self.price_unit = product.uom_id._compute_price(self.price_unit, self.uom_id) self.price_unit = product.uom_id._compute_price(self.price_unit, self.uom_id)

View File

@ -56,7 +56,8 @@ class AccountMove(models.Model):
total_amount += amount total_amount += amount
for partial_line in (line.matched_debit_ids + line.matched_credit_ids): for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
total_reconciled += partial_line.amount total_reconciled += partial_line.amount
if float_is_zero(total_amount, precision_rounding=move.currency_id.rounding): precision_currency = move.currency_id or move.company_id.currency_id
if float_is_zero(total_amount, precision_rounding=precision_currency.rounding):
move.matched_percentage = 1.0 move.matched_percentage = 1.0
else: else:
move.matched_percentage = total_reconciled / total_amount move.matched_percentage = total_reconciled / total_amount
@ -103,8 +104,7 @@ class AccountMove(models.Model):
partner_id = fields.Many2one('res.partner', compute='_compute_partner_id', string="Partner", store=True, readonly=True) partner_id = fields.Many2one('res.partner', compute='_compute_partner_id', string="Partner", store=True, readonly=True)
amount = fields.Monetary(compute='_amount_compute', store=True) amount = fields.Monetary(compute='_amount_compute', store=True)
narration = fields.Text(string='Internal Note') narration = fields.Text(string='Internal Note')
company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True, company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True)
default=lambda self: self.env.user.company_id)
matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method")
# Dummy Account field to search on account.move by account_id # Dummy Account field to search on account.move by account_id
dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True) dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True)
@ -1044,8 +1044,6 @@ class AccountMoveLine(models.Model):
raise UserError(_('Entries are not of the same account!')) raise UserError(_('Entries are not of the same account!'))
if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'): if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'):
raise UserError(_('The account %s (%s) is not marked as reconciliable !') % (all_accounts[0].name, all_accounts[0].code)) raise UserError(_('The account %s (%s) is not marked as reconciliable !') % (all_accounts[0].name, all_accounts[0].code))
if len(partners) > 1:
raise UserError(_('The partner has to be the same on all lines for receivable and payable accounts!'))
#reconcile everything that can be #reconcile everything that can be
remaining_moves = self.auto_reconcile_lines() remaining_moves = self.auto_reconcile_lines()
@ -1163,19 +1161,22 @@ class AccountMoveLine(models.Model):
#create an empty move that will hold all the exchange rate adjustments #create an empty move that will hold all the exchange rate adjustments
exchange_move = False exchange_move = False
if aml_to_balance_currency: if aml_to_balance_currency and any([residual for dummy, residual in aml_to_balance_currency.values()]):
exchange_move = self.env['account.move'].create( exchange_move = self.env['account.move'].create(
self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=self[0].company_id)) self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=self[0].company_id))
for currency, values in aml_to_balance_currency.items(): for currency, values in aml_to_balance_currency.items():
aml_to_balance = values[0] aml_to_balance = values[0]
total_amount_currency = values[1] total_amount_currency = values[1]
#eventually create journal entries to book the difference due to foreign currency's exchange rate that fluctuates if total_amount_currency:
aml_recs, partial_recs = self.env['account.partial.reconcile'].create_exchange_rate_entry(aml_to_balance, 0.0, total_amount_currency, currency, exchange_move) #eventually create journal entries to book the difference due to foreign currency's exchange rate that fluctuates
aml_recs, partial_recs = self.env['account.partial.reconcile'].create_exchange_rate_entry(aml_to_balance, 0.0, total_amount_currency, currency, exchange_move)
#add the ecxhange rate line and the exchange rate partial reconciliation in the et of the full reconcile #add the ecxhange rate line and the exchange rate partial reconciliation in the et of the full reconcile
self |= aml_recs self |= aml_recs
partial_rec_set |= partial_recs partial_rec_set |= partial_recs
else:
aml_to_balance.reconcile()
if exchange_move: if exchange_move:
exchange_move.post() exchange_move.post()
@ -1203,6 +1204,7 @@ class AccountMoveLine(models.Model):
rec_move_ids += account_move_line.matched_credit_ids rec_move_ids += account_move_line.matched_credit_ids
if self.env.context.get('invoice_id'): if self.env.context.get('invoice_id'):
current_invoice = self.env['account.invoice'].browse(self.env.context['invoice_id']) current_invoice = self.env['account.invoice'].browse(self.env.context['invoice_id'])
aml_to_keep = current_invoice.move_id.line_ids | current_invoice.move_id.line_ids.mapped('full_reconcile_id.exchange_move_id.line_ids')
rec_move_ids = rec_move_ids.filtered( rec_move_ids = rec_move_ids.filtered(
lambda r: (r.debit_move_id + r.credit_move_id) & current_invoice.move_id.line_ids lambda r: (r.debit_move_id + r.credit_move_id) & current_invoice.move_id.line_ids
) )

View File

@ -374,6 +374,12 @@ class account_payment(models.Model):
self.destination_account_id = self.partner_id.property_account_receivable_id.id self.destination_account_id = self.partner_id.property_account_receivable_id.id
else: else:
self.destination_account_id = self.partner_id.property_account_payable_id.id self.destination_account_id = self.partner_id.property_account_payable_id.id
elif self.partner_type == 'customer':
default_account = self.env['ir.property'].get('property_account_receivable_id', 'res.partner')
self.destination_account_id = default_account.id
elif self.partner_type == 'supplier':
default_account = self.env['ir.property'].get('property_account_payable_id', 'res.partner')
self.destination_account_id = default_account.id
@api.onchange('partner_type') @api.onchange('partner_type')
def _onchange_partner_type(self): def _onchange_partner_type(self):
@ -429,12 +435,17 @@ class account_payment(models.Model):
@api.multi @api.multi
def button_invoices(self): def button_invoices(self):
if self.partner_type == 'supplier':
views = [(self.env.ref('account.invoice_supplier_tree').id, 'tree'), (self.env.ref('account.invoice_supplier_form').id, 'form')]
else:
views = [(self.env.ref('account.invoice_tree').id, 'tree'), (self.env.ref('account.invoice_form').id, 'form')]
return { return {
'name': _('Paid Invoices'), 'name': _('Paid Invoices'),
'view_type': 'form', 'view_type': 'form',
'view_mode': 'tree,form', 'view_mode': 'tree,form',
'res_model': 'account.invoice', 'res_model': 'account.invoice',
'view_id': False, 'view_id': False,
'views': views,
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'domain': [('id', 'in', [x.id for x in self.invoice_ids])], 'domain': [('id', 'in', [x.id for x in self.invoice_ids])],
} }
@ -517,6 +528,7 @@ class account_payment(models.Model):
(transfer_credit_aml + transfer_debit_aml).reconcile() (transfer_credit_aml + transfer_debit_aml).reconcile()
rec.write({'state': 'posted', 'move_name': move.name}) rec.write({'state': 'posted', 'move_name': move.name})
return True
@api.multi @api.multi
def action_draft(self): def action_draft(self):

View File

@ -859,7 +859,7 @@ class WizardMultiChartsAccounts(models.TransientModel):
@api.multi @api.multi
def existing_accounting(self, company_id): def existing_accounting(self, company_id):
model_to_check = ['account.move.line', 'account.invoice', 'account.move', 'account.payment', 'account.bank.statement'] model_to_check = ['account.move.line', 'account.invoice', 'account.payment', 'account.bank.statement']
for model in model_to_check: for model in model_to_check:
if len(self.env[model].search([('company_id', '=', company_id.id)])) > 0: if len(self.env[model].search([('company_id', '=', company_id.id)])) > 0:
return True return True
@ -894,7 +894,7 @@ class WizardMultiChartsAccounts(models.TransientModel):
accounting_props.unlink() accounting_props.unlink()
# delete account, journal, tax, fiscal position and reconciliation model # delete account, journal, tax, fiscal position and reconciliation model
models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.tax', 'account.journal'] models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.tax', 'account.move', 'account.journal']
for model in models_to_delete: for model in models_to_delete:
res = self.env[model].search([('company_id', '=', self.company_id.id)]) res = self.env[model].search([('company_id', '=', self.company_id.id)])
if len(res): if len(res):

View File

@ -2,10 +2,12 @@
from datetime import timedelta, datetime from datetime import timedelta, datetime
import calendar import calendar
import time
from dateutil.relativedelta import relativedelta
from flectra import fields, models, api, _ from flectra import fields, models, api, _
from flectra.exceptions import ValidationError
from flectra.exceptions import UserError from flectra.exceptions import UserError
from flectra.tools.misc import DEFAULT_SERVER_DATE_FORMAT
from flectra.tools.float_utils import float_round, float_is_zero from flectra.tools.float_utils import float_round, float_is_zero
@ -62,6 +64,61 @@ Best Regards,'''))
account_setup_coa_done = fields.Boolean(string='Chart of Account Checked', help="Technical field holding the status of the chart of account setup step.") account_setup_coa_done = fields.Boolean(string='Chart of Account Checked', help="Technical field holding the status of the chart of account setup step.")
account_setup_bar_closed = fields.Boolean(string='Setup Bar Closed', help="Technical field set to True when setup bar has been closed by the user.") account_setup_bar_closed = fields.Boolean(string='Setup Bar Closed', help="Technical field set to True when setup bar has been closed by the user.")
@api.multi
def _check_lock_dates(self, vals):
'''Check the lock dates for the current companies. This can't be done in a api.constrains because we need
to perform some comparison between new/old values. This method forces the lock dates to be irreversible.
* You cannot define stricter conditions on advisors than on users. Then, the lock date on advisor must be set
after the lock date for users.
* You cannot lock a period that is not finished yet. Then, the lock date for advisors must be set after the
last day of the previous month.
* The new lock date for advisors must be set after the previous lock date.
:param vals: The values passed to the write method.
'''
period_lock_date = vals.get('period_lock_date') and\
time.strptime(vals['period_lock_date'], DEFAULT_SERVER_DATE_FORMAT)
fiscalyear_lock_date = vals.get('fiscalyear_lock_date') and\
time.strptime(vals['fiscalyear_lock_date'], DEFAULT_SERVER_DATE_FORMAT)
previous_month = datetime.strptime(fields.Date.today(), DEFAULT_SERVER_DATE_FORMAT) + relativedelta(months=-1)
days_previous_month = calendar.monthrange(previous_month.year, previous_month.month)
previous_month = previous_month.replace(day=days_previous_month[1]).timetuple()
for company in self:
old_fiscalyear_lock_date = company.fiscalyear_lock_date and\
time.strptime(company.fiscalyear_lock_date, DEFAULT_SERVER_DATE_FORMAT)
# The user attempts to remove the lock date for advisors
if old_fiscalyear_lock_date and not fiscalyear_lock_date and 'fiscalyear_lock_date' in vals:
raise ValidationError(_('The lock date for advisors is irreversible and can\'t be removed.'))
# The user attempts to set a lock date for advisors prior to the previous one
if old_fiscalyear_lock_date and fiscalyear_lock_date and fiscalyear_lock_date < old_fiscalyear_lock_date:
raise ValidationError(_('The new lock date for advisors must be set after the previous lock date.'))
# In case of no new fiscal year in vals, fallback to the oldest
if not fiscalyear_lock_date:
if old_fiscalyear_lock_date:
fiscalyear_lock_date = old_fiscalyear_lock_date
else:
continue
# The user attempts to set a lock date for advisors prior to the last day of previous month
if fiscalyear_lock_date > previous_month:
raise ValidationError(_('You cannot lock a period that is not finished yet. Please make sure that the lock date for advisors is not set after the last day of the previous month.'))
# In case of no new period lock date in vals, fallback to the one defined in the company
if not period_lock_date:
if company.period_lock_date:
period_lock_date = time.strptime(company.period_lock_date, DEFAULT_SERVER_DATE_FORMAT)
else:
continue
# The user attempts to set a lock date for advisors prior to the lock date for users
if period_lock_date < fiscalyear_lock_date:
raise ValidationError(_('You cannot define stricter conditions on advisors than on users. Please make sure that the lock date on advisor is set before the lock date for users.'))
@api.model @api.model
def _verify_fiscalyear_last_day(self, company_id, last_day, last_month): def _verify_fiscalyear_last_day(self, company_id, last_day, last_month):
company = self.browse(company_id) company = self.browse(company_id)

View File

@ -444,3 +444,12 @@ class ResPartner(models.Model):
action['domain'] = literal_eval(action['domain']) action['domain'] = literal_eval(action['domain'])
action['domain'].append(('partner_id', 'child_of', self.id)) action['domain'].append(('partner_id', 'child_of', self.id))
return action return action
@api.onchange('company_id')
def _onchange_company_id(self):
company = self.env['res.company']
if self.company_id:
company = self.company_id
else:
company = self.env.user.company_id
return {'domain': {'property_account_position_id': [('company_id', 'in', [company.id, False])]}}

View File

@ -35,22 +35,6 @@ class ProductTemplate(models.Model):
domain=[('deprecated', '=', False)], domain=[('deprecated', '=', False)],
help="The expense is accounted for when a vendor bill is validated, except in anglo-saxon accounting with perpetual inventory valuation in which case the expense (Cost of Goods Sold account) is recognized at the customer invoice validation. If the field is empty, it uses the one defined in the product category.") help="The expense is accounted for when a vendor bill is validated, except in anglo-saxon accounting with perpetual inventory valuation in which case the expense (Cost of Goods Sold account) is recognized at the customer invoice validation. If the field is empty, it uses the one defined in the product category.")
@api.multi
def write(self, vals):
#TODO: really? i don't see the reason we'd need that constraint..
check = self.ids and 'uom_po_id' in vals
if check:
self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)])
uoms = dict(self._cr.fetchall())
res = super(ProductTemplate, self).write(vals)
if check:
self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)])
if dict(self._cr.fetchall()) != uoms:
products = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids)])
if self.env['account.move.line'].search_count([('product_id', 'in', products.ids)]):
raise UserError(_('You can not change the unit of measure of a product that has been already used in an account journal item. If you need to change the unit of measure, you may deactivate this product.'))
return res
@api.multi @api.multi
def _get_product_accounts(self): def _get_product_accounts(self):
return { return {

View File

@ -193,7 +193,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
values['name'] = _('Unknown Partner') values['name'] = _('Unknown Partner')
values['trust'] = False values['trust'] = False
if at_least_one_amount or self._context.get('include_nullified_amount'): if at_least_one_amount or (self._context.get('include_nullified_amount') and lines[partner['partner_id']]):
res.append(values) res.append(values)
return res, total, lines return res, total, lines

View File

@ -38,7 +38,7 @@ class ReportGeneralLedger(models.AbstractModel):
init_wheres.append(init_where_clause.strip()) init_wheres.append(init_where_clause.strip())
init_filters = " AND ".join(init_wheres) init_filters = " AND ".join(init_wheres)
filters = init_filters.replace('account_move_line__move_id', 'm').replace('account_move_line', 'l') filters = init_filters.replace('account_move_line__move_id', 'm').replace('account_move_line', 'l')
sql = ("""SELECT 0 AS lid, l.account_id AS account_id, '' AS ldate, '' AS lcode, NULL AS amount_currency, '' AS lref, 'Initial Balance' AS lname, COALESCE(SUM(l.debit),0.0) AS debit, COALESCE(SUM(l.credit),0.0) AS credit, COALESCE(SUM(l.debit),0) - COALESCE(SUM(l.credit), 0) as balance, '' AS lpartner_id,\ sql = ("""SELECT 0 AS lid, l.account_id AS account_id, '' AS ldate, '' AS lcode, 0.0 AS amount_currency, '' AS lref, 'Initial Balance' AS lname, COALESCE(SUM(l.debit),0.0) AS debit, COALESCE(SUM(l.credit),0.0) AS credit, COALESCE(SUM(l.debit),0) - COALESCE(SUM(l.credit), 0) as balance, '' AS lpartner_id,\
'' AS move_name, '' AS mmove_id, '' AS currency_code,\ '' AS move_name, '' AS mmove_id, '' AS currency_code,\
NULL AS currency_id,\ NULL AS currency_id,\
'' AS invoice_id, '' AS invoice_type, '' AS invoice_number,\ '' AS invoice_id, '' AS invoice_type, '' AS invoice_number,\

View File

@ -25,7 +25,7 @@ class ReportOverdue(models.AbstractModel):
"FROM account_move_line l " "FROM account_move_line l "
"JOIN account_account_type at ON (l.user_type_id = at.id) " "JOIN account_account_type at ON (l.user_type_id = at.id) "
"JOIN account_move m ON (l.move_id = m.id) " "JOIN account_move m ON (l.move_id = m.id) "
"WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND NOT l.reconciled GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),)))) "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND l.full_reconcile_id IS NULL GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),))))
for row in self.env.cr.dictfetchall(): for row in self.env.cr.dictfetchall():
res[row.pop('partner_id')].append(row) res[row.pop('partner_id')].append(row)
return res return res

View File

@ -784,6 +784,7 @@ var StatementModel = BasicModel.extend({
model: 'account.tax', model: 'account.tax',
method: 'json_friendly_compute_all', method: 'json_friendly_compute_all',
args: args, args: args,
context: $.extend(self.context || {}, {'round': true}),
}) })
.then(function (result) { .then(function (result) {
_.each(result.taxes, function(tax){ _.each(result.taxes, function(tax){
@ -847,7 +848,7 @@ var StatementModel = BasicModel.extend({
}) : false, }) : false,
account_code: self.accounts[line.st_line.open_balance_account_id], account_code: self.accounts[line.st_line.open_balance_account_id],
}; };
line.balance.type = line.balance.amount_currency ? (line.balance.amount_currency > 0 && line.st_line.partner_id ? 0 : -1) : 1; line.balance.type = line.balance.amount_currency ? (line.st_line.partner_id ? 0 : -1) : 1;
}); });
}, },
/** /**
@ -963,6 +964,7 @@ var StatementModel = BasicModel.extend({
var formatOptions = { var formatOptions = {
currency_id: line.st_line.currency_id, currency_id: line.st_line.currency_id,
}; };
var amount = values.amount !== undefined ? values.amount : line.balance.amount;
var prop = { var prop = {
'id': _.uniqueId('createLine'), 'id': _.uniqueId('createLine'),
'label': values.label || line.st_line.name, 'label': values.label || line.st_line.name,
@ -974,8 +976,7 @@ var StatementModel = BasicModel.extend({
'debit': 0, 'debit': 0,
'credit': 0, 'credit': 0,
'base_amount': values.amount_type !== "percentage" ? 'base_amount': values.amount_type !== "percentage" ?
(values.amount || line.balance.amount) : (amount) : line.balance.amount * values.amount / 100,
line.balance.amount * values.amount / 100,
'percent': values.amount_type === "percentage" ? values.amount : null, 'percent': values.amount_type === "percentage" ? values.amount : null,
'link': values.link, 'link': values.link,
'display': true, 'display': true,

View File

@ -298,7 +298,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}; };
self.fields.partner_id.appendTo(self.$('.accounting_view caption')); self.fields.partner_id.appendTo(self.$('.accounting_view caption'));
}); });
this.$('thead .line_info_button').attr("data-content", qweb.render('reconciliation.line.statement_line.details', {'state': this._initialState})); $('<span class="line_info_button fa fa-info-circle"/>')
.appendTo(this.$('thead .cell_info_popover'))
.attr("data-content", qweb.render('reconciliation.line.statement_line.details', {'state': this._initialState}));
this.$el.popover({ this.$el.popover({
'selector': '.line_info_button', 'selector': '.line_info_button',
'placement': 'left', 'placement': 'left',
@ -626,7 +628,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
/** /**
* @private * @private
*/ */
_onFilterChange: function () { _onFilterChange: function (event) {
this.trigger_up('change_filter', {'data': _.str.strip($(event.target).val())}); this.trigger_up('change_filter', {'data': _.str.strip($(event.target).val())});
}, },
/** /**

View File

@ -159,6 +159,9 @@
} }
/* info popover */ /* info popover */
.popover {
max-width: none;
}
table.details { table.details {
vertical-align: top; vertical-align: top;
@ -209,6 +212,7 @@
} }
&[data-mode="match"] > .match { &[data-mode="match"] > .match {
max-height: none; max-height: none;
overflow: visible;
.o-transition(max-height, 400ms); .o-transition(max-height, 400ms);
} }
&[data-mode="create"] > .create { &[data-mode="create"] > .create {

View File

@ -128,7 +128,7 @@
<td class="cell_action"><span class="toggle_create fa fa-play"></span></td> <td class="cell_action"><span class="toggle_create fa fa-play"></span></td>
<td class="cell_account_code"><t t-esc="state.balance.account_code"/></td> <td class="cell_account_code"><t t-esc="state.balance.account_code"/></td>
<td class="cell_due_date"></td> <td class="cell_due_date"></td>
<td class="cell_label"><t t-if="state.balance.amount_currency &lt; 0">Create Write-off</t><t t-elif="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart</t></td> <td class="cell_label"><t t-if="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart or Create Write-off</t></td>
<td class="cell_left"><t t-if="state.balance.amount_currency &lt; 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td> <td class="cell_left"><t t-if="state.balance.amount_currency &lt; 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
<td class="cell_right"><t t-if="state.balance.amount_currency &gt; 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td> <td class="cell_right"><t t-if="state.balance.amount_currency &gt; 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
<td class="cell_info_popover"></td> <td class="cell_info_popover"></td>
@ -251,7 +251,7 @@
<tr><td>Description</td><td><t t-esc="state.st_line.name"/></td></tr> <tr><td>Description</td><td><t t-esc="state.st_line.name"/></td></tr>
<tr><td>Amount</td><td><t t-raw="state.st_line.amount_str"/><t t-if="state.st_line.amount_currency_str"> (<t t-esc="state.st_line.amount_currency_str"/>)</t></td></tr> <tr><td>Amount</td><td><t t-raw="state.st_line.amount_str"/><t t-if="state.st_line.amount_currency_str"> (<t t-esc="state.st_line.amount_currency_str"/>)</t></td></tr>
<tr><td>Account</td><td><t t-esc="state.st_line.account_code"/> <t t-esc="state.st_line.account_name"/></td></tr> <tr><td>Account</td><td><t t-esc="state.st_line.account_code"/> <t t-esc="state.st_line.account_name"/></td></tr>
<tr t-if="state.st_line.note"><td>Note</td><td><t t-esc="state.st_line.note"/></td></tr> <tr t-if="state.st_line.note"><td>Note</td><td style="white-space: pre;"><t t-esc="state.st_line.note"/></td></tr>
</table> </table>
</t> </t>

View File

@ -1019,8 +1019,8 @@ QUnit.module('account', {
widget.$('.create .create_label input').val('test1').trigger('input'); widget.$('.create .create_label input').val('test1').trigger('input');
assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$ 200.00", "should display the value 200.00 in left column"); assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$ 200.00", "should display the value 200.00 in left column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Create Write-off' with 25.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Open balance' with 25.00 in left column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines");
clientAction.destroy(); clientAction.destroy();
@ -1126,8 +1126,8 @@ QUnit.module('account', {
$('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').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('$_', ''), "$\u00a01100.00$\u00a0220.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_label').text(), "Open balance", "should display 'Open balance'");
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 tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Open balance' with 145.00 in right column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
clientAction.destroy(); clientAction.destroy();
@ -1151,7 +1151,7 @@ QUnit.module('account', {
assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '), assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '),
" ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines"); " ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '),
"Create Write-off229.12", "should display the 'Create Write-off' line with value in left column"); "Open balance229.12", "should display the 'Open balance' line with value in left column");
widget.$('.create .create_amount input').val('100').trigger('input'); widget.$('.create .create_amount input').val('100').trigger('input');
@ -1159,7 +1159,7 @@ QUnit.module('account', {
" 101120 ATOS Banque 1075.00 101120 Tax 20.00% 215.00 101130 ATOS Frais 90.91 101300 Tax 10.00% include 9.09 ", " 101120 ATOS Banque 1075.00 101120 Tax 20.00% 215.00 101130 ATOS Frais 90.91 101300 Tax 10.00% include 9.09 ",
"should update the value of the 4 lines (because the line must have 100% of the value)"); "should update the value of the 4 lines (because the line must have 100% of the value)");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '),
"Create Write-off215.00", "should change the 'Create Write-off' line because the 20.00% tax is not an include tax"); "Open balance215.00", "should change the 'Open balance' line because the 20.00% tax is not an include tax");
widget.$('.accounting_view tbody .cell_account_code:first').trigger('click'); widget.$('.accounting_view tbody .cell_account_code:first').trigger('click');
widget.$('.accounting_view tbody .cell_label:first').trigger('click'); widget.$('.accounting_view tbody .cell_label:first').trigger('click');

View File

@ -1,6 +1,8 @@
from flectra.addons.account.tests.account_test_classes import AccountingTestCase from flectra.addons.account.tests.account_test_classes import AccountingTestCase
from flectra.osv.orm import except_orm from flectra.osv.orm import except_orm
from datetime import datetime, timedelta from datetime import datetime
from dateutil.relativedelta import relativedelta
from calendar import monthrange
from flectra.tools import DEFAULT_SERVER_DATE_FORMAT from flectra.tools import DEFAULT_SERVER_DATE_FORMAT
class TestPeriodState(AccountingTestCase): class TestPeriodState(AccountingTestCase):
@ -11,14 +13,16 @@ class TestPeriodState(AccountingTestCase):
def setUp(self): def setUp(self):
super(TestPeriodState, self).setUp() super(TestPeriodState, self).setUp()
self.user_id = self.env.user self.user_id = self.env.user
self.day_before_yesterday = datetime.now() - timedelta(2)
self.yesterday = datetime.now() - timedelta(1) last_day_month = datetime.now() - relativedelta(months=1)
self.yesterday_str = self.yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT) last_day_month = last_day_month.replace(day=monthrange(last_day_month.year, last_day_month.month)[1])
self.last_day_month_str = last_day_month.strftime(DEFAULT_SERVER_DATE_FORMAT)
#make sure there is no unposted entry #make sure there is no unposted entry
draft_entries = self.env['account.move'].search([('date', '<=', self.yesterday_str), ('state', '=', 'draft')]) draft_entries = self.env['account.move'].search([('date', '<=', self.last_day_month_str), ('state', '=', 'draft')])
if draft_entries: if draft_entries:
draft_entries.post() draft_entries.post()
self.user_id.company_id.write({'fiscalyear_lock_date': self.yesterday_str}) self.user_id.company_id.fiscalyear_lock_date = self.last_day_month_str
self.sale_journal_id = self.env['account.journal'].search([('type', '=', 'sale')])[0] self.sale_journal_id = self.env['account.journal'].search([('type', '=', 'sale')])[0]
self.account_id = self.env['account.account'].search([('internal_type', '=', 'receivable')])[0] self.account_id = self.env['account.account'].search([('internal_type', '=', 'receivable')])[0]
@ -27,7 +31,7 @@ class TestPeriodState(AccountingTestCase):
move = self.env['account.move'].create({ move = self.env['account.move'].create({
'name': '/', 'name': '/',
'journal_id': self.sale_journal_id.id, 'journal_id': self.sale_journal_id.id,
'date': self.day_before_yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT), 'date': self.last_day_month_str,
'line_ids': [(0, 0, { 'line_ids': [(0, 0, {
'name': 'foo', 'name': 'foo',
'debit': 10, 'debit': 10,

View File

@ -40,6 +40,12 @@ class TestReconciliation(AccountingTestCase):
self.diff_income_account = self.env['res.users'].browse(self.env.uid).company_id.income_currency_exchange_account_id self.diff_income_account = self.env['res.users'].browse(self.env.uid).company_id.income_currency_exchange_account_id
self.diff_expense_account = self.env['res.users'].browse(self.env.uid).company_id.expense_currency_exchange_account_id self.diff_expense_account = self.env['res.users'].browse(self.env.uid).company_id.expense_currency_exchange_account_id
self.inbound_payment_method = self.env['account.payment.method'].create({
'name': 'inbound',
'code': 'IN',
'payment_type': 'inbound',
})
def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None): def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None):
#we create an invoice in given currency #we create an invoice in given currency
invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
@ -657,3 +663,151 @@ class TestReconciliation(AccountingTestCase):
credit_aml.with_context(invoice_id=inv2.id).remove_move_reconcile() credit_aml.with_context(invoice_id=inv2.id).remove_move_reconcile()
self.assertAlmostEquals(inv1.residual, 10) self.assertAlmostEquals(inv1.residual, 10)
self.assertAlmostEquals(inv2.residual, 20) self.assertAlmostEquals(inv2.residual, 20)
def test_unreconcile_exchange(self):
# Use case:
# - Company currency in EUR
# - Create 2 rates for USD:
# 1.0 on 2018-01-01
# 0.5 on 2018-02-01
# - Create an invoice on 2018-01-02 of 111 USD
# - Register a payment on 2018-02-02 of 111 USD
# - Unreconcile the payment
self.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-07-01',
'rate': 1.0,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id
})
self.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-08-01',
'rate': 0.5,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id
})
inv = self.create_invoice(invoice_amount=111, 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': 111,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_usd.id,
'payment_date': time.strftime('%Y') + '-08-01',
})
payment.post()
credit_aml = payment.move_line_ids.filtered('credit')
# Check residual before assignation
self.assertAlmostEquals(inv.residual, 111)
# Assign credit, check exchange move and residual
inv.assign_outstanding_credit(credit_aml.id)
self.assertEqual(len(payment.move_line_ids.mapped('full_reconcile_id').exchange_move_id), 1)
self.assertAlmostEquals(inv.residual, 0)
# Unreconcile invoice and check residual
credit_aml.with_context(invoice_id=inv.id).remove_move_reconcile()
self.assertAlmostEquals(inv.residual, 111)
def test_revert_payment_and_reconcile(self):
payment = self.env['account.payment'].create({
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner_agrolait_id,
'journal_id': self.bank_journal_usd.id,
'payment_date': '2018-06-04',
'amount': 666,
})
payment.post()
self.assertEqual(len(payment.move_line_ids), 2)
bank_line = payment.move_line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.default_debit_account_id.id)
customer_line = payment.move_line_ids - bank_line
self.assertEqual(len(bank_line), 1)
self.assertEqual(len(customer_line), 1)
self.assertNotEqual(bank_line.id, customer_line.id)
self.assertEqual(bank_line.move_id.id, customer_line.move_id.id)
move = bank_line.move_id
# Reversing the payment's move
reversed_move_list = move.reverse_moves('2018-06-04')
self.assertEqual(len(reversed_move_list), 1)
reversed_move = self.env['account.move'].browse(reversed_move_list[0])
self.assertEqual(len(reversed_move.line_ids), 2)
# Testing the reconciliation matching between the move lines and their reversed counterparts
reversed_bank_line = reversed_move.line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.default_debit_account_id.id)
reversed_customer_line = reversed_move.line_ids - reversed_bank_line
self.assertEqual(len(reversed_bank_line), 1)
self.assertEqual(len(reversed_customer_line), 1)
self.assertNotEqual(reversed_bank_line.id, reversed_customer_line.id)
self.assertEqual(reversed_bank_line.move_id.id, reversed_customer_line.move_id.id)
self.assertEqual(reversed_bank_line.full_reconcile_id.id, bank_line.full_reconcile_id.id)
self.assertEqual(reversed_customer_line.full_reconcile_id.id, customer_line.full_reconcile_id.id)
def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False):
#we create an invoice in given currency
invoice = self.account_invoice_model.create({'partner_id': partner_id,
'reference_type': 'none',
'currency_id': currency_id,
'name': type == 'out_invoice' and 'invoice to client' or 'invoice to vendor',
'account_id': self.account_rcv.id,
'type': type,
'date_invoice': time.strftime('%Y') + '-07-01',
})
self.account_invoice_line_model.create({'product_id': self.product.id,
'quantity': 1,
'price_unit': invoice_amount,
'invoice_id': invoice.id,
'name': 'product that cost ' + str(invoice_amount),
'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id,
})
#validate invoice
invoice.action_invoice_open()
return invoice
def test_aged_report(self):
AgedReport = self.env['report.account.report_agedpartnerbalance'].with_context(include_nullified_amount=True)
account_type = ['receivable']
report_date_to = time.strftime('%Y') + '-07-15'
partner = self.env['res.partner'].create({'name': 'AgedPartner'})
currency = self.env.user.company_id.currency_id
invoice = self.create_invoice_partner(currency_id=currency.id, partner_id=partner.id)
journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE', 'currency_id': currency.id})
statement = self.make_payment(invoice, journal, 50)
# Case 1: The invoice and payment are reconciled: Nothing should appear
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30)
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
self.assertEqual(partner_lines, [], 'The aged receivable shouldn\'t have lines at this point')
self.assertFalse(partner.id in amls, 'The aged receivable should not have amls either')
# Case 2: The invoice and payment are not reconciled: we should have one line on the report
# and 2 amls
invoice.move_id.line_ids.with_context(invoice_id=invoice.id).remove_move_reconcile()
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30)
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
self.assertEqual(partner_lines, [{'trust': 'normal', '1': 0.0, '0': 0.0, 'direction': 0.0, 'partner_id': partner.id, '3': 0.0, 'total': 0.0, 'name': 'AgedPartner', '4': 0.0, '2': 0.0}],
'We should have a line in the report for the partner')
self.assertEqual(len(amls[partner.id]), 2, 'We should have 2 account move lines for the partner')
positive_line = [line for line in amls[partner.id] if line['line'].balance > 0]
negative_line = [line for line in amls[partner.id] if line['line'].balance < 0]
self.assertEqual(positive_line[0]['amount'], 50.0, 'The amount of the amls should be 50')
self.assertEqual(negative_line[0]['amount'], -50.0, 'The amount of the amls should be -50')

View File

@ -73,7 +73,7 @@
</group> </group>
<group> <group>
<field domain="[('company_id', '=', parent.company_id)]" name="account_id" groups="account.group_account_user"/> <field domain="[('company_id', '=', parent.company_id)]" name="account_id" groups="account.group_account_user"/>
<field name="invoice_line_tax_ids" context="{'type':parent.get('type')}" domain="[('type_tax_use','!=','none'),('company_id', '=', parent.company_id)]" widget="many2many_tags" options="{'no_create': True}"/> <field name="invoice_line_tax_ids" context="{'type':parent.type}" domain="[('type_tax_use','!=','none'),('company_id', '=', parent.company_id)]" widget="many2many_tags" options="{'no_create': True}"/>
<field domain="[('company_id', '=', parent.company_id)]" name="account_analytic_id" groups="analytic.group_analytic_accounting"/> <field domain="[('company_id', '=', parent.company_id)]" name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
<field name="company_id" groups="base.group_multi_company" readonly="1"/> <field name="company_id" groups="base.group_multi_company" readonly="1"/>
</group> </group>

View File

@ -248,14 +248,16 @@
<span><t t-esc="dashboard.account_balance"/></span> <span><t t-esc="dashboard.account_balance"/></span>
</div> </div>
</div> </div>
<div class="row" name="latest_statement" t-if="dashboard.last_balance != dashboard.account_balance"> <t t-if="dashboard.last_balance != dashboard.account_balance">
<div class="col-xs-6"> <div class="row" name="latest_statement">
<span title="Latest Statement">Latest Statement</span> <div class="col-xs-6">
<span title="Latest Statement">Latest Statement</span>
</div>
<div class="col-xs-6 text-right">
<span><t t-esc="dashboard.last_balance"/></span>
</div>
</div> </div>
<div class="col-xs-6 text-right"> </t>
<span><t t-esc="dashboard.last_balance"/></span>
</div>
</div>
</div> </div>
</t> </t>
<t t-name="JournalBodySalePurchase" id="account.JournalBodySalePurchase"> <t t-name="JournalBodySalePurchase" id="account.JournalBodySalePurchase">

View File

@ -146,8 +146,8 @@
<group> <group>
<group> <group>
<field name="payment_type" widget="radio" attrs="{'readonly': [('state', '!=', 'draft')]}"/> <field name="payment_type" widget="radio" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="partner_type" widget="selection" attrs="{'required': [('payment_type', 'in', ('inbound', 'outbound'))], 'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))], 'readonly': [('state', '!=', 'draft')]}"/> <field name="partner_type" widget="selection" attrs="{'required': [('state', '=', 'draft'), ('payment_type', 'in', ('inbound', 'outbound'))], 'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))], 'readonly': [('state', '!=', 'draft')]}"/>
<field name="partner_id" attrs="{'required': [('payment_type', 'in', ('inbound', 'outbound'))], 'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))], 'readonly': [('state', '!=', 'draft')]}" context="{'default_is_company': True, 'default_supplier': payment_type == 'outbound', 'default_customer': payment_type == 'inbound'}"/> <field name="partner_id" attrs="{'required': [('state', '=', 'draft'), ('payment_type', 'in', ('inbound', 'outbound'))], 'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))], 'readonly': [('state', '!=', 'draft')]}" context="{'default_is_company': True, 'default_supplier': payment_type == 'outbound', 'default_customer': payment_type == 'inbound'}"/>
<label for="amount"/> <label for="amount"/>
<div name="amount_div" class="o_row"> <div name="amount_div" class="o_row">
<field name="amount" attrs="{'readonly': [('state', '!=', 'draft')]}"/> <field name="amount" attrs="{'readonly': [('state', '!=', 'draft')]}"/>

View File

@ -57,7 +57,7 @@
<span class="label label-default"><i class="fa fa-fw fa-remove"></i><span class="hidden-xs"> Cancelled</span></span> <span class="label label-default"><i class="fa fa-fw fa-remove"></i><span class="hidden-xs"> Cancelled</span></span>
</t> </t>
</td> </td>
<td><span t-field="invoice.residual" t-options='{"widget": "monetary", "display_currency": invoice.currency_id}'/></td> <td><span t-esc="-invoice.residual if invoice.type == 'out_refund' else invoice.residual" t-options='{"widget": "monetary", "display_currency": invoice.currency_id}'/></td>
</tr> </tr>
</t> </t>
</table></div> </table></div>

View File

@ -11,6 +11,7 @@
file="account.report_invoice_with_payments" file="account.report_invoice_with_payments"
attachment="(object.state in ('open','paid')) and ('INV'+(object.number or '').replace('/','')+'.pdf')" attachment="(object.state in ('open','paid')) and ('INV'+(object.number or '').replace('/','')+'.pdf')"
print_report_name="(object._get_printed_report_name())" print_report_name="(object._get_printed_report_name())"
groups="account.group_account_invoice"
/> />
<report <report

View File

@ -1143,7 +1143,8 @@
<group> <group>
<field name="name"/> <field name="name"/>
<field name="partner_id" <field name="partner_id"
domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]"/> domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]"
attrs="{'readonly': [('parent_state', '=', 'posted')]}"/>
</group> </group>
<notebook colspan="4"> <notebook colspan="4">
<page string="Information"> <page string="Information">
@ -1475,7 +1476,7 @@
<field name="name"/> <field name="name"/>
<field name="branch_id" groups="base_branch_company.group_multi_branch"/> <field name="branch_id" groups="base_branch_company.group_multi_branch"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/> <field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting"/> <field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_accounting"/>
<field name="amount_currency" groups="base.group_multi_currency"/> <field name="amount_currency" groups="base.group_multi_currency"/>
<field name="company_currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/>
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>

View File

@ -95,6 +95,7 @@
<field name="priority" eval="20"/> <field name="priority" eval="20"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="email" position="after"> <field name="email" position="after">
<field name="company_id" invisible="1"/>
<field name="property_payment_term_id" widget="selection"/> <field name="property_payment_term_id" widget="selection"/>
<field name="property_account_position_id" options="{'no_create': True, 'no_open': True}"/> <field name="property_account_position_id" options="{'no_create': True, 'no_open': True}"/>
</field> </field>
@ -111,7 +112,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<div name="button_box" position="inside"> <div name="button_box" position="inside">
<button type="object" class="oe_stat_button" icon="fa-pencil-square-o" name="action_view_partner_invoices" <button type="object" class="oe_stat_button" icon="fa-pencil-square-o" name="action_view_partner_invoices"
attrs="{'invisible': [('customer', '=', False)]}"> attrs="{'invisible': [('customer', '=', False)]}" context="{'default_partner_id': active_id}">
<div class="o_form_field o_stat_info"> <div class="o_form_field o_stat_info">
<span class="o_stat_value"> <span class="o_stat_value">
<field name="total_invoiced" widget='monetary' options="{'currency_field': 'currency_id'}"/> <field name="total_invoiced" widget='monetary' options="{'currency_field': 'currency_id'}"/>

View File

@ -50,7 +50,7 @@
<td><span t-field="aml.date"/></td> <td><span t-field="aml.date"/></td>
<td><span t-field="aml.account_id.code"/></td> <td><span t-field="aml.account_id.code"/></td>
<td><span t-esc="aml.sudo().partner_id and aml.sudo().partner_id.name and aml.sudo().partner_id.name[:23] or ''"/></td> <td><span t-esc="aml.sudo().partner_id and aml.sudo().partner_id.name and aml.sudo().partner_id.name[:23] or ''"/></td>
<td><span t-esc="aml.name[:35]"/></td> <td><span t-esc="aml.name and aml.name[:35]"/></td>
<td><span t-esc="aml.debit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td> <td><span t-esc="aml.debit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td>
<td><span t-esc="aml.credit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td> <td><span t-esc="aml.credit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td>
<td t-if="data['form']['amount_currency'] and aml.amount_currency"> <td t-if="data['form']['amount_currency'] and aml.amount_currency">

View File

@ -18,24 +18,26 @@ class TaxAdjustments(models.TransientModel):
debit_account_id = fields.Many2one('account.account', string='Debit account', required=True, domain=[('deprecated', '=', False)]) debit_account_id = fields.Many2one('account.account', string='Debit account', required=True, domain=[('deprecated', '=', False)])
credit_account_id = fields.Many2one('account.account', string='Credit account', required=True, domain=[('deprecated', '=', False)]) credit_account_id = fields.Many2one('account.account', string='Credit account', required=True, domain=[('deprecated', '=', False)])
amount = fields.Monetary(currency_field='company_currency_id', required=True) amount = fields.Monetary(currency_field='company_currency_id', required=True)
adjustment_type = fields.Selection([('debit', 'Applied on debit journal item'), ('credit', 'Applied on credit journal item')], string="Adjustment Type", store=False, required=True)
company_currency_id = fields.Many2one('res.currency', readonly=True, default=lambda self: self.env.user.company_id.currency_id) company_currency_id = fields.Many2one('res.currency', readonly=True, default=lambda self: self.env.user.company_id.currency_id)
tax_id = fields.Many2one('account.tax', string='Adjustment Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'none'), ('tax_adjustment', '=', True)], required=True) tax_id = fields.Many2one('account.tax', string='Adjustment Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'none'), ('tax_adjustment', '=', True)], required=True)
@api.multi @api.multi
def _create_move(self): def _create_move(self):
adjustment_type = self.env.context.get('adjustment_type', (self.amount > 0.0 and 'debit' or 'credit'))
debit_vals = { debit_vals = {
'name': self.reason, 'name': self.reason,
'debit': self.amount, 'debit': abs(self.amount),
'credit': 0.0, 'credit': 0.0,
'account_id': self.debit_account_id.id, 'account_id': self.debit_account_id.id,
'tax_line_id': self.tax_id.id, 'tax_line_id': adjustment_type == 'debit' and self.tax_id.id or False,
} }
credit_vals = { credit_vals = {
'name': self.reason, 'name': self.reason,
'debit': 0.0, 'debit': 0.0,
'credit': self.amount, 'credit': abs(self.amount),
'account_id': self.credit_account_id.id, 'account_id': self.credit_account_id.id,
'tax_line_id': self.tax_id.id, 'tax_line_id': adjustment_type == 'credit' and self.tax_id.id or False,
} }
vals = { vals = {
'journal_id': self.journal_id.id, 'journal_id': self.journal_id.id,
@ -48,6 +50,13 @@ class TaxAdjustments(models.TransientModel):
return move.id return move.id
@api.multi @api.multi
def create_move_debit(self):
return self.with_context(adjustment_type='debit').create_move()
@api.multi
def create_move_credit(self):
return self.with_context(adjustment_type='credit').create_move()
def create_move(self): def create_move(self):
#create the adjustment move #create the adjustment move
move_id = self._create_move() move_id = self._create_move()

View File

@ -12,6 +12,7 @@
<group> <group>
<group> <group>
<field name="amount"/> <field name="amount"/>
<field name="adjustment_type"/>
</group> </group>
<group> <group>
<field name="tax_id" widget="selection"/> <field name="tax_id" widget="selection"/>
@ -26,8 +27,14 @@
</group> </group>
</group> </group>
<footer> <footer>
<button name="create_move" string="Create and post move" type="object" default_focus="1" class="oe_highlight"/> <div attrs="{'invisible': [('adjustment_type', '=', 'credit')]}">
<button name="create_move_debit" string="Create and post move" type="object" default_focus="1" class="oe_highlight"/>
<button string="Cancel" class="btn btn-default" special="cancel" /> <button string="Cancel" class="btn btn-default" special="cancel" />
</div>
<div attrs="{'invisible': [('adjustment_type', '!=', 'credit')]}">
<button name="create_move_credit" string="Create and post move" type="object" default_focus="1" class="oe_highlight"/>
<button string="Cancel" class="btn btn-default" special="cancel" />
</div>
</footer> </footer>
</form> </form>
</field> </field>