mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-22 18:22:03 +02:00
Initial commit: Accounting packages
This commit is contained in:
commit
4ef34c2317
2661 changed files with 1709616 additions and 0 deletions
16
odoo-bringout-oca-ocb-account/account/wizard/__init__.py
Normal file
16
odoo-bringout-oca-ocb-account/account/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from . import account_automatic_entry_wizard
|
||||
from . import account_unreconcile
|
||||
from . import account_validate_account_move
|
||||
from . import account_move_reversal
|
||||
from . import account_resequence
|
||||
from . import setup_wizards
|
||||
from . import account_invoice_send
|
||||
from . import base_document_layout
|
||||
from . import account_payment_register
|
||||
from . import account_tour_upload_bill
|
||||
from . import accrued_orders
|
||||
from . import base_partner_merge
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,504 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.misc import format_date, formatLang
|
||||
|
||||
from collections import defaultdict
|
||||
from odoo.tools import groupby, frozendict
|
||||
import json
|
||||
|
||||
class AutomaticEntryWizard(models.TransientModel):
|
||||
_name = 'account.automatic.entry.wizard'
|
||||
_description = 'Create Automatic Entries'
|
||||
|
||||
# General
|
||||
action = fields.Selection([('change_period', 'Change Period'), ('change_account', 'Change Account')], required=True)
|
||||
move_data = fields.Text(compute="_compute_move_data") # JSON value of the moves to be created
|
||||
preview_move_data = fields.Text(compute="_compute_preview_move_data") # JSON value of the data to be displayed in the previewer
|
||||
move_line_ids = fields.Many2many('account.move.line')
|
||||
date = fields.Date(required=True, default=lambda self: fields.Date.context_today(self))
|
||||
company_id = fields.Many2one('res.company', required=True, readonly=True)
|
||||
company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
|
||||
percentage = fields.Float("Percentage", compute='_compute_percentage', readonly=False, store=True, help="Percentage of each line to execute the action on.")
|
||||
total_amount = fields.Monetary(compute='_compute_total_amount', store=True, readonly=False, currency_field='company_currency_id', help="Total amount impacted by the automatic entry.")
|
||||
journal_id = fields.Many2one('account.journal', required=True, readonly=False, string="Journal",
|
||||
domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
|
||||
compute="_compute_journal_id",
|
||||
inverse="_inverse_journal_id",
|
||||
help="Journal where to create the entry.")
|
||||
|
||||
# change period
|
||||
account_type = fields.Selection([('income', 'Revenue'), ('expense', 'Expense')], compute='_compute_account_type', store=True)
|
||||
expense_accrual_account = fields.Many2one('account.account', readonly=False,
|
||||
domain="[('company_id', '=', company_id),"
|
||||
"('account_type', 'not in', ('asset_receivable', 'liability_payable')),"
|
||||
"('is_off_balance', '=', False)]",
|
||||
compute="_compute_expense_accrual_account",
|
||||
inverse="_inverse_expense_accrual_account",
|
||||
)
|
||||
revenue_accrual_account = fields.Many2one('account.account', readonly=False,
|
||||
domain="[('company_id', '=', company_id),"
|
||||
"('account_type', 'not in', ('asset_receivable', 'liability_payable')),"
|
||||
"('is_off_balance', '=', False)]",
|
||||
compute="_compute_revenue_accrual_account",
|
||||
inverse="_inverse_revenue_accrual_account",
|
||||
)
|
||||
lock_date_message = fields.Char(string="Lock Date Message", compute="_compute_lock_date_message")
|
||||
|
||||
# change account
|
||||
destination_account_id = fields.Many2one(string="To", comodel_name='account.account', help="Account to transfer to.")
|
||||
display_currency_helper = fields.Boolean(string="Currency Conversion Helper", compute='_compute_display_currency_helper')
|
||||
# Technical field. Used to indicate whether or not to display the currency conversion tooltip. The tooltip informs a currency conversion will be performed with the transfer.
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_expense_accrual_account(self):
|
||||
for record in self:
|
||||
record.expense_accrual_account = record.company_id.expense_accrual_account_id
|
||||
|
||||
def _inverse_expense_accrual_account(self):
|
||||
for record in self:
|
||||
record.company_id.sudo().expense_accrual_account_id = record.expense_accrual_account
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_revenue_accrual_account(self):
|
||||
for record in self:
|
||||
record.revenue_accrual_account = record.company_id.revenue_accrual_account_id
|
||||
|
||||
def _inverse_revenue_accrual_account(self):
|
||||
for record in self:
|
||||
record.company_id.sudo().revenue_accrual_account_id = record.revenue_accrual_account
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_journal_id(self):
|
||||
for record in self:
|
||||
record.journal_id = record.company_id.automatic_entry_default_journal_id
|
||||
|
||||
def _inverse_journal_id(self):
|
||||
for record in self:
|
||||
record.company_id.sudo().automatic_entry_default_journal_id = record.journal_id
|
||||
|
||||
@api.constrains('percentage', 'action')
|
||||
def _constraint_percentage(self):
|
||||
for record in self:
|
||||
if not (0.0 < record.percentage <= 100.0) and record.action == 'change_period':
|
||||
raise UserError(_("Percentage must be between 0 and 100"))
|
||||
|
||||
@api.depends('percentage', 'move_line_ids')
|
||||
def _compute_total_amount(self):
|
||||
for record in self:
|
||||
record.total_amount = (record.percentage or 100) * sum(record.move_line_ids.mapped('balance')) / 100
|
||||
|
||||
@api.depends('total_amount', 'move_line_ids')
|
||||
def _compute_percentage(self):
|
||||
for record in self:
|
||||
total = (sum(record.move_line_ids.mapped('balance')) or record.total_amount)
|
||||
if total != 0:
|
||||
record.percentage = min((record.total_amount / total) * 100, 100) # min() to avoid value being slightly over 100 due to rounding error
|
||||
else:
|
||||
record.percentage = 100
|
||||
|
||||
@api.depends('move_line_ids')
|
||||
def _compute_account_type(self):
|
||||
for record in self:
|
||||
record.account_type = 'income' if sum(record.move_line_ids.mapped('balance')) < 0 else 'expense'
|
||||
|
||||
@api.depends('action', 'move_line_ids')
|
||||
def _compute_lock_date_message(self):
|
||||
for record in self:
|
||||
record.lock_date_message = False
|
||||
if record.action == 'change_period':
|
||||
for aml in record.move_line_ids:
|
||||
lock_date_message = aml.move_id._get_lock_date_message(aml.date, False)
|
||||
if lock_date_message:
|
||||
record.lock_date_message = lock_date_message
|
||||
break
|
||||
|
||||
@api.depends('destination_account_id')
|
||||
def _compute_display_currency_helper(self):
|
||||
for record in self:
|
||||
record.display_currency_helper = bool(record.destination_account_id.currency_id)
|
||||
|
||||
@api.constrains('date', 'move_line_ids')
|
||||
def _check_date(self):
|
||||
for wizard in self:
|
||||
if wizard.move_line_ids.move_id._get_violated_lock_dates(wizard.date, False):
|
||||
raise ValidationError(_("The date selected is protected by a lock date"))
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
if not set(fields) & set(['move_line_ids', 'company_id']):
|
||||
return res
|
||||
|
||||
if self.env.context.get('active_model') != 'account.move.line' or not self.env.context.get('active_ids'):
|
||||
raise UserError(_('This can only be used on journal items'))
|
||||
move_line_ids = self.env['account.move.line'].browse(self.env.context['active_ids'])
|
||||
res['move_line_ids'] = [(6, 0, move_line_ids.ids)]
|
||||
|
||||
if any(move.state != 'posted' for move in move_line_ids.mapped('move_id')):
|
||||
raise UserError(_('You can only change the period/account for posted journal items.'))
|
||||
if any(move_line.reconciled for move_line in move_line_ids):
|
||||
raise UserError(_('You can only change the period/account for items that are not yet reconciled.'))
|
||||
if any(line.company_id != move_line_ids[0].company_id for line in move_line_ids):
|
||||
raise UserError(_('You cannot use this wizard on journal entries belonging to different companies.'))
|
||||
res['company_id'] = move_line_ids[0].company_id.id
|
||||
|
||||
allowed_actions = set(dict(self._fields['action'].selection))
|
||||
if self.env.context.get('default_action'):
|
||||
allowed_actions = {self.env.context['default_action']}
|
||||
if any(line.account_id.account_type != move_line_ids[0].account_id.account_type for line in move_line_ids):
|
||||
allowed_actions.discard('change_period')
|
||||
if not allowed_actions:
|
||||
raise UserError(_('No possible action found with the selected lines.'))
|
||||
res['action'] = allowed_actions.pop()
|
||||
return res
|
||||
|
||||
def _get_move_dict_vals_change_account(self):
|
||||
line_vals = []
|
||||
|
||||
# Group data from selected move lines
|
||||
counterpart_balances = defaultdict(lambda: defaultdict(lambda: 0))
|
||||
counterpart_distribution_amount = defaultdict(lambda: defaultdict(lambda: {}))
|
||||
grouped_source_lines = defaultdict(lambda: self.env['account.move.line'])
|
||||
|
||||
for line in self.move_line_ids.filtered(lambda x: x.account_id != self.destination_account_id):
|
||||
counterpart_currency = line.currency_id
|
||||
counterpart_amount_currency = line.amount_currency
|
||||
|
||||
if self.destination_account_id.currency_id and self.destination_account_id.currency_id != self.company_id.currency_id:
|
||||
counterpart_currency = self.destination_account_id.currency_id
|
||||
counterpart_amount_currency = self.company_id.currency_id._convert(line.balance, self.destination_account_id.currency_id, self.company_id, line.date)
|
||||
|
||||
grouping_key = (line.partner_id, counterpart_currency)
|
||||
|
||||
counterpart_balances[grouping_key]['amount_currency'] += counterpart_amount_currency
|
||||
counterpart_balances[grouping_key]['balance'] += line.balance
|
||||
if line.analytic_distribution:
|
||||
for account_id, distribution in line.analytic_distribution.items():
|
||||
# For the counterpart, we will need to make a prorata of the different distribution of the lines
|
||||
# This computes the total balance for each analytic account, for each counterpart line to generate
|
||||
distribution_values = counterpart_distribution_amount[grouping_key]
|
||||
distribution_values[account_id] = (line.balance * distribution + distribution_values.get(account_id, 0) * 100) / 100
|
||||
counterpart_balances[grouping_key]['analytic_distribution'] = counterpart_distribution_amount[grouping_key] or {}
|
||||
grouped_source_lines[(
|
||||
line.partner_id,
|
||||
line.currency_id,
|
||||
line.account_id,
|
||||
line.analytic_distribution and frozendict(line.analytic_distribution),
|
||||
)] += line
|
||||
|
||||
# Generate counterpart lines' vals
|
||||
for (counterpart_partner, counterpart_currency), counterpart_vals in counterpart_balances.items():
|
||||
source_accounts = self.move_line_ids.mapped('account_id')
|
||||
counterpart_label = len(source_accounts) == 1 and _("Transfer from %s", source_accounts.display_name) or _("Transfer counterpart")
|
||||
|
||||
# We divide the amount for each account by the total balance to reflect the lines counter-parted
|
||||
analytic_distribution = {
|
||||
account_id: (
|
||||
100
|
||||
if counterpart_currency.is_zero(counterpart_vals['balance'])
|
||||
else 100 * distribution_amount / counterpart_vals['balance']
|
||||
)
|
||||
for account_id, distribution_amount in counterpart_vals['analytic_distribution'].items()
|
||||
}
|
||||
|
||||
if not counterpart_currency.is_zero(counterpart_vals['amount_currency']) or not self.company_id.currency_id.is_zero(counterpart_vals['balance']):
|
||||
line_vals.append({
|
||||
'name': counterpart_label,
|
||||
'debit': counterpart_vals['balance'] > 0 and self.company_id.currency_id.round(counterpart_vals['balance']) or 0,
|
||||
'credit': counterpart_vals['balance'] < 0 and self.company_id.currency_id.round(-counterpart_vals['balance']) or 0,
|
||||
'account_id': self.destination_account_id.id,
|
||||
'partner_id': counterpart_partner.id or None,
|
||||
'amount_currency': counterpart_currency.round((counterpart_vals['balance'] < 0 and -1 or 1) * abs(counterpart_vals['amount_currency'])) or 0,
|
||||
'currency_id': counterpart_currency.id,
|
||||
'analytic_distribution': analytic_distribution,
|
||||
})
|
||||
|
||||
# Generate change_account lines' vals
|
||||
for (partner, currency, account, analytic_distribution), lines in grouped_source_lines.items():
|
||||
account_balance = sum(line.balance for line in lines)
|
||||
if not self.company_id.currency_id.is_zero(account_balance):
|
||||
account_amount_currency = currency.round(sum(line.amount_currency for line in lines))
|
||||
line_vals.append({
|
||||
'name': _('Transfer to %s', self.destination_account_id.display_name or _('[Not set]')),
|
||||
'debit': account_balance < 0 and self.company_id.currency_id.round(-account_balance) or 0,
|
||||
'credit': account_balance > 0 and self.company_id.currency_id.round(account_balance) or 0,
|
||||
'account_id': account.id,
|
||||
'partner_id': partner.id or None,
|
||||
'currency_id': currency.id,
|
||||
'amount_currency': (account_balance > 0 and -1 or 1) * abs(account_amount_currency),
|
||||
'analytic_distribution': analytic_distribution,
|
||||
})
|
||||
|
||||
return [{
|
||||
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
|
||||
'move_type': 'entry',
|
||||
'journal_id': self.journal_id.id,
|
||||
'date': fields.Date.to_string(self.date),
|
||||
'ref': self.destination_account_id.display_name and _("Transfer entry to %s", self.destination_account_id.display_name or ''),
|
||||
'line_ids': [(0, 0, line) for line in line_vals],
|
||||
}]
|
||||
|
||||
def _get_move_line_dict_vals_change_period(self, aml, date):
|
||||
# account.move.line data
|
||||
accrual_account = self.revenue_accrual_account if self.account_type == 'income' else self.expense_accrual_account
|
||||
reported_debit = aml.company_id.currency_id.round((self.percentage / 100) * aml.debit)
|
||||
reported_credit = aml.company_id.currency_id.round((self.percentage / 100) * aml.credit)
|
||||
reported_amount_currency = aml.currency_id.round((self.percentage / 100) * aml.amount_currency)
|
||||
|
||||
if date == 'new_date':
|
||||
return [
|
||||
(0, 0, {
|
||||
'name': aml.name or '',
|
||||
'debit': reported_debit,
|
||||
'credit': reported_credit,
|
||||
'amount_currency': reported_amount_currency,
|
||||
'currency_id': aml.currency_id.id,
|
||||
'account_id': aml.account_id.id,
|
||||
'partner_id': aml.partner_id.id,
|
||||
'analytic_distribution': aml.analytic_distribution,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': self._format_strings(_('{percent:0.2f}% to recognize on {new_date}'), aml.move_id),
|
||||
'debit': reported_credit,
|
||||
'credit': reported_debit,
|
||||
'amount_currency': -reported_amount_currency,
|
||||
'currency_id': aml.currency_id.id,
|
||||
'account_id': accrual_account.id,
|
||||
'partner_id': aml.partner_id.id,
|
||||
'analytic_distribution': aml.analytic_distribution,
|
||||
}),
|
||||
]
|
||||
return [
|
||||
(0, 0, {
|
||||
'name': aml.name or '',
|
||||
'debit': reported_credit,
|
||||
'credit': reported_debit,
|
||||
'amount_currency': -reported_amount_currency,
|
||||
'currency_id': aml.currency_id.id,
|
||||
'account_id': aml.account_id.id,
|
||||
'partner_id': aml.partner_id.id,
|
||||
'analytic_distribution': aml.analytic_distribution,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': self._format_strings(_('{percent:0.2f}% to recognize on {new_date}'), aml.move_id),
|
||||
'debit': reported_debit,
|
||||
'credit': reported_credit,
|
||||
'amount_currency': reported_amount_currency,
|
||||
'currency_id': aml.currency_id.id,
|
||||
'account_id': accrual_account.id,
|
||||
'partner_id': aml.partner_id.id,
|
||||
'analytic_distribution': aml.analytic_distribution,
|
||||
}),
|
||||
]
|
||||
|
||||
def _get_lock_safe_date(self, date):
|
||||
# Use a reference move in the correct journal because _get_accounting_date depends on the journal sequence.
|
||||
reference_move = self.env['account.move'].new({'journal_id': self.journal_id.id, 'move_type': 'entry', 'invoice_date': date})
|
||||
return reference_move._get_accounting_date(date, False)
|
||||
|
||||
def _get_move_dict_vals_change_period(self):
|
||||
|
||||
def get_lock_safe_date(aml):
|
||||
return self._get_lock_safe_date(aml.date)
|
||||
|
||||
# set the change_period account on the selected journal items
|
||||
|
||||
move_data = {'new_date': {
|
||||
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
|
||||
'move_type': 'entry',
|
||||
'line_ids': [],
|
||||
'ref': self._format_strings(_('{label}: Adjusting Entry of {new_date}'), self.move_line_ids[0].move_id),
|
||||
'date': fields.Date.to_string(self.date),
|
||||
'journal_id': self.journal_id.id,
|
||||
}}
|
||||
# complete the account.move data
|
||||
for date, grouped_lines in groupby(self.move_line_ids, get_lock_safe_date):
|
||||
grouped_lines = list(grouped_lines)
|
||||
amount = sum(l.balance for l in grouped_lines)
|
||||
move_data[date] = {
|
||||
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
|
||||
'move_type': 'entry',
|
||||
'line_ids': [],
|
||||
'ref': self._format_strings(_('{label}: Adjusting Entry of {date}'), grouped_lines[0].move_id, amount),
|
||||
'date': fields.Date.to_string(date),
|
||||
'journal_id': self.journal_id.id,
|
||||
}
|
||||
|
||||
# compute the account.move.lines and the total amount per move
|
||||
for aml in self.move_line_ids:
|
||||
for date in (get_lock_safe_date(aml), 'new_date'):
|
||||
move_data[date]['line_ids'] += self._get_move_line_dict_vals_change_period(aml, date)
|
||||
|
||||
move_vals = [m for m in move_data.values()]
|
||||
return move_vals
|
||||
|
||||
@api.depends('move_line_ids', 'journal_id', 'revenue_accrual_account', 'expense_accrual_account', 'percentage', 'date', 'account_type', 'action', 'destination_account_id')
|
||||
def _compute_move_data(self):
|
||||
for record in self:
|
||||
if record.action == 'change_period':
|
||||
if any(line.account_id.account_type != record.move_line_ids[0].account_id.account_type for line in record.move_line_ids):
|
||||
raise UserError(_('All accounts on the lines must be of the same type.'))
|
||||
if record.action == 'change_period':
|
||||
record.move_data = json.dumps(record._get_move_dict_vals_change_period())
|
||||
elif record.action == 'change_account':
|
||||
record.move_data = json.dumps(record._get_move_dict_vals_change_account())
|
||||
|
||||
@api.depends('move_data')
|
||||
def _compute_preview_move_data(self):
|
||||
for record in self:
|
||||
preview_columns = [
|
||||
{'field': 'account_id', 'label': _('Account')},
|
||||
{'field': 'name', 'label': _('Label')},
|
||||
{'field': 'debit', 'label': _('Debit'), 'class': 'text-end text-nowrap'},
|
||||
{'field': 'credit', 'label': _('Credit'), 'class': 'text-end text-nowrap'},
|
||||
]
|
||||
if record.action == 'change_account':
|
||||
preview_columns[2:2] = [{'field': 'partner_id', 'label': _('Partner')}]
|
||||
|
||||
move_vals = json.loads(record.move_data)
|
||||
preview_vals = []
|
||||
for move in move_vals[:4]:
|
||||
preview_vals += [self.env['account.move']._move_dict_to_preview_vals(move, record.company_id.currency_id)]
|
||||
preview_discarded = max(0, len(move_vals) - len(preview_vals))
|
||||
|
||||
record.preview_move_data = json.dumps({
|
||||
'groups_vals': preview_vals,
|
||||
'options': {
|
||||
'discarded_number': _("%d moves", preview_discarded) if preview_discarded else False,
|
||||
'columns': preview_columns,
|
||||
},
|
||||
})
|
||||
|
||||
def do_action(self):
|
||||
move_vals = json.loads(self.move_data)
|
||||
self = self.with_context(skip_computed_taxes=True)
|
||||
if self.action == 'change_period':
|
||||
return self._do_action_change_period(move_vals)
|
||||
elif self.action == 'change_account':
|
||||
return self._do_action_change_account(move_vals)
|
||||
|
||||
def _do_action_change_period(self, move_vals):
|
||||
accrual_account = self.revenue_accrual_account if self.account_type == 'income' else self.expense_accrual_account
|
||||
|
||||
created_moves = self.env['account.move'].create(move_vals)
|
||||
created_moves._post()
|
||||
|
||||
destination_move = created_moves[0]
|
||||
destination_move_offset = 0
|
||||
destination_messages = []
|
||||
accrual_move_messages = defaultdict(lambda: [])
|
||||
accrual_move_offsets = defaultdict(int)
|
||||
for move in self.move_line_ids.move_id:
|
||||
amount = sum((self.move_line_ids._origin & move.line_ids).mapped('balance'))
|
||||
accrual_move = created_moves[1:].filtered(lambda m: m.date == self._get_lock_safe_date(move.date))
|
||||
|
||||
if accrual_account.reconcile and accrual_move.state == 'posted' and destination_move.state == 'posted':
|
||||
destination_move_lines = destination_move.mapped('line_ids').filtered(lambda line: line.account_id == accrual_account)[destination_move_offset:destination_move_offset+2]
|
||||
destination_move_offset += 2
|
||||
accrual_move_lines = accrual_move.mapped('line_ids').filtered(lambda line: line.account_id == accrual_account)[accrual_move_offsets[accrual_move]:accrual_move_offsets[accrual_move]+2]
|
||||
accrual_move_offsets[accrual_move] += 2
|
||||
(accrual_move_lines + destination_move_lines).filtered(lambda line: not line.currency_id.is_zero(line.balance)).reconcile()
|
||||
move.message_post(body=self._format_strings(_('Adjusting Entries have been created for this invoice:<ul><li>%(link1)s cancelling '
|
||||
'{percent:.2f}%% of {amount}</li><li>%(link0)s postponing it to {new_date}</li></ul>',
|
||||
link0=self._format_move_link(destination_move),
|
||||
link1=self._format_move_link(accrual_move),
|
||||
), move, amount))
|
||||
destination_messages += [self._format_strings(_('Adjusting Entry {link}: {percent:.2f}% of {amount} recognized from {date}'), move, amount)]
|
||||
accrual_move_messages[accrual_move] += [self._format_strings(_('Adjusting Entry for {link}: {percent:.2f}% of {amount} recognized on {new_date}'), move, amount)]
|
||||
|
||||
destination_move.message_post(body='<br/>\n'.join(destination_messages))
|
||||
for accrual_move, messages in accrual_move_messages.items():
|
||||
accrual_move.message_post(body='<br/>\n'.join(messages))
|
||||
|
||||
# open the generated entries
|
||||
action = {
|
||||
'name': _('Generated Entries'),
|
||||
'domain': [('id', 'in', created_moves.ids)],
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'tree,form',
|
||||
'type': 'ir.actions.act_window',
|
||||
'views': [(self.env.ref('account.view_move_tree').id, 'tree'), (False, 'form')],
|
||||
}
|
||||
if len(created_moves) == 1:
|
||||
action.update({'view_mode': 'form', 'res_id': created_moves.id})
|
||||
return action
|
||||
|
||||
def _do_action_change_account(self, move_vals):
|
||||
new_move = self.env['account.move'].create(move_vals)
|
||||
new_move._post()
|
||||
|
||||
# Group lines
|
||||
grouped_lines = defaultdict(lambda: self.env['account.move.line'])
|
||||
destination_lines = self.move_line_ids.filtered(lambda x: x.account_id == self.destination_account_id)
|
||||
for line in self.move_line_ids - destination_lines:
|
||||
grouped_lines[(line.partner_id, line.currency_id, line.account_id)] += line
|
||||
|
||||
# Reconcile
|
||||
for (partner, currency, account), lines in grouped_lines.items():
|
||||
if account.reconcile:
|
||||
to_reconcile = lines + new_move.line_ids.filtered(lambda x: x.account_id == account and x.partner_id == partner and x.currency_id == currency)
|
||||
to_reconcile.reconcile()
|
||||
|
||||
if destination_lines and self.destination_account_id.reconcile:
|
||||
to_reconcile = destination_lines + new_move.line_ids.filtered(lambda x: x.account_id == self.destination_account_id and x.partner_id == partner and x.currency_id == currency)
|
||||
to_reconcile.reconcile()
|
||||
|
||||
# Log the operation on source moves
|
||||
acc_transfer_per_move = defaultdict(lambda: defaultdict(lambda: 0)) # dict(move, dict(account, balance))
|
||||
for line in self.move_line_ids:
|
||||
acc_transfer_per_move[line.move_id][line.account_id] += line.balance
|
||||
|
||||
for move, balances_per_account in acc_transfer_per_move.items():
|
||||
message_to_log = self._format_transfer_source_log(balances_per_account, new_move)
|
||||
if message_to_log:
|
||||
move.message_post(body=message_to_log)
|
||||
|
||||
# Log on target move as well
|
||||
new_move.message_post(body=self._format_new_transfer_move_log(acc_transfer_per_move))
|
||||
|
||||
return {
|
||||
'name': _("Transfer"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'account.move',
|
||||
'res_id': new_move.id,
|
||||
}
|
||||
|
||||
# Transfer utils
|
||||
def _format_new_transfer_move_log(self, acc_transfer_per_move):
|
||||
format = _("<li>{amount} ({debit_credit}) from {link}, <strong>%(account_source_name)s</strong></li>")
|
||||
rslt = _("This entry transfers the following amounts to <strong>%(destination)s</strong> <ul>", destination=self.destination_account_id.display_name)
|
||||
for move, balances_per_account in acc_transfer_per_move.items():
|
||||
for account, balance in balances_per_account.items():
|
||||
if account != self.destination_account_id: # Otherwise, logging it here is confusing for the user
|
||||
rslt += self._format_strings(format % {'account_source_name': account.display_name}, move, balance)
|
||||
|
||||
rslt += '</ul>'
|
||||
return rslt
|
||||
|
||||
def _format_transfer_source_log(self, balances_per_account, transfer_move):
|
||||
transfer_format = _("<li>{amount} ({debit_credit}) from <strong>%s</strong> were transferred to <strong>{account_target_name}</strong> by {link}</li>")
|
||||
content = ''
|
||||
for account, balance in balances_per_account.items():
|
||||
if account != self.destination_account_id:
|
||||
content += self._format_strings(transfer_format % account.display_name, transfer_move, balance)
|
||||
return content and '<ul>' + content + '</ul>' or None
|
||||
|
||||
def _format_move_link(self, move):
|
||||
return move._get_html_link()
|
||||
|
||||
def _format_strings(self, string, move, amount=None):
|
||||
return string.format(
|
||||
label=move.name or 'Adjusting Entry',
|
||||
percent=self.percentage,
|
||||
name=move.name,
|
||||
id=move.id,
|
||||
amount=formatLang(self.env, abs(amount), currency_obj=self.company_id.currency_id) if amount else '',
|
||||
debit_credit=amount < 0 and _('C') or _('D') if amount else None,
|
||||
link=self._format_move_link(move),
|
||||
date=format_date(self.env, move.date),
|
||||
new_date=self.date and format_date(self.env, self.date) or _('[Not set]'),
|
||||
account_target_name=self.destination_account_id.display_name,
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="account_automatic_entry_wizard_form" model="ir.ui.view">
|
||||
<field name="name">account.automatic.entry.wizard.form</field>
|
||||
<field name="model">account.automatic.entry.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="account_type" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="move_line_ids" invisible="1"/>
|
||||
<field name="display_currency_helper" invisible="1"/>
|
||||
<div attrs="{'invisible': [('display_currency_helper', '=', False)]}" class="alert alert-info text-center" role="status">
|
||||
The selected destination account is set to use a specific currency. Every entry transferred to it will be converted into this currency, causing
|
||||
the loss of any pre-existing foreign currency amount.
|
||||
</div>
|
||||
<div attrs="{'invisible': [('lock_date_message', '=', False)]}" class="alert alert-warning text-center" role="status">
|
||||
<field name="lock_date_message" nolabel="1"/>
|
||||
</div>
|
||||
<field name="action" invisible="context.get('hide_automatic_options')" widget="radio" options="{'horizontal': true}"/>
|
||||
<group>
|
||||
<group attrs="{'invisible': [('action', '!=', 'change_period')]}">
|
||||
<field name="date" string="Recognition Date"/>
|
||||
<field name="expense_accrual_account" string="Accrued Account"
|
||||
attrs="{'invisible': [('account_type', '!=', 'expense')], 'required': [('account_type', '=', 'expense'), ('action', '=', 'change_period')]}"/>
|
||||
<field name="revenue_accrual_account" string="Accrued Account"
|
||||
attrs="{'invisible': [('account_type', '!=', 'income')], 'required': [('account_type', '=', 'income'), ('action', '=', 'change_period')]}"/>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('action', '!=', 'change_account')]}">
|
||||
<field name="date" string="Transfer Date"/>
|
||||
<field name="destination_account_id" attrs="{'required': [('action', '=', 'change_account')]}" domain="[('company_id', '=', company_id)]"/>
|
||||
</group>
|
||||
<group>
|
||||
<label for="total_amount" string="Adjusting Amount" attrs="{'invisible': [('action', '!=', 'change_period')]}"/>
|
||||
<div attrs="{'invisible': [('action', '!=', 'change_period')]}">
|
||||
<field name="percentage" style="width:40% !important" attrs="{'readonly': [('action', '!=', 'change_period')]}"/>%<span class="px-3"></span>(<field name="total_amount" class="oe_inline"/>)
|
||||
</div>
|
||||
<field name="total_amount" readonly="1" attrs="{'invisible': [('action', '=', 'change_period')]}"/>
|
||||
<field name="journal_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<label for="preview_move_data" string="The following Journal Entries will be generated"/>
|
||||
<field name="preview_move_data" widget="grouped_view_widget" class="d-block"/>
|
||||
<footer>
|
||||
<button string="Create Journal Entries" name="do_action" type="object" class="oe_highlight" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_automatic_entry_wizard_action" model="ir.actions.act_window">
|
||||
<field name="name">Create Automatic Entries for selected Journal Items</field>
|
||||
<field name="res_model">account.automatic.entry.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.mail.wizard.mail_compose_message import _reopen
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import get_lang
|
||||
|
||||
|
||||
class AccountInvoiceSend(models.TransientModel):
|
||||
_name = 'account.invoice.send'
|
||||
_inherits = {'mail.compose.message':'composer_id'}
|
||||
_description = 'Account Invoice Send'
|
||||
|
||||
is_email = fields.Boolean('Email', default=lambda self: self.env.company.invoice_is_email)
|
||||
invoice_without_email = fields.Text(compute='_compute_invoice_without_email', string='invoice(s) that will not be sent')
|
||||
is_print = fields.Boolean('Print', default=lambda self: self.env.company.invoice_is_print)
|
||||
printed = fields.Boolean('Is Printed', default=False)
|
||||
invoice_ids = fields.Many2many('account.move', 'account_move_account_invoice_send_rel', string='Invoices')
|
||||
composer_id = fields.Many2one('mail.compose.message', string='Composer', required=True, ondelete='cascade')
|
||||
template_id = fields.Many2one(
|
||||
'mail.template', 'Use template',
|
||||
domain="[('model', '=', 'account.move')]"
|
||||
)
|
||||
|
||||
# Technical field containing a textual representation of the selected move types,
|
||||
# if multiple. It is used to inform the user in the window in such case.
|
||||
move_types = fields.Char(
|
||||
string='Move types',
|
||||
compute='_compute_move_types',
|
||||
readonly=True)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(AccountInvoiceSend, self).default_get(fields)
|
||||
res_ids = self._context.get('active_ids')
|
||||
|
||||
invoices = self.env['account.move'].browse(res_ids).filtered(lambda move: move.is_invoice(include_receipts=True))
|
||||
if not invoices:
|
||||
raise UserError(_("You can only send invoices."))
|
||||
|
||||
composer = self.env['mail.compose.message'].create({
|
||||
'composition_mode': 'comment' if len(res_ids) == 1 else 'mass_mail',
|
||||
})
|
||||
res.update({
|
||||
'invoice_ids': res_ids,
|
||||
'composer_id': composer.id,
|
||||
})
|
||||
return res
|
||||
|
||||
@api.onchange('invoice_ids')
|
||||
def _compute_composition_mode(self):
|
||||
for wizard in self:
|
||||
wizard.composer_id.composition_mode = 'comment' if len(wizard.invoice_ids) == 1 else 'mass_mail'
|
||||
|
||||
@api.onchange('invoice_ids')
|
||||
def _compute_move_types(self):
|
||||
for wizard in self:
|
||||
move_types = False
|
||||
|
||||
if len(wizard.invoice_ids) > 1:
|
||||
moves = self.env['account.move'].browse(self.env.context.get('active_ids'))
|
||||
|
||||
# Get the move types of all selected moves and see if there is more than one of them.
|
||||
# If so, we'll display a warning on the next window about it.
|
||||
move_types_set = set(m.type_name for m in moves)
|
||||
|
||||
if len(move_types_set) > 1:
|
||||
move_types = ', '.join(move_types_set)
|
||||
|
||||
wizard.move_types = move_types
|
||||
|
||||
|
||||
@api.onchange('template_id')
|
||||
def onchange_template_id(self):
|
||||
for wizard in self:
|
||||
if wizard.composer_id:
|
||||
wizard.composer_id.template_id = wizard.template_id.id
|
||||
wizard._compute_composition_mode()
|
||||
wizard.composer_id._onchange_template_id_wrapper()
|
||||
|
||||
@api.onchange('is_email')
|
||||
def onchange_is_email(self):
|
||||
if self.is_email:
|
||||
res_ids = self._context.get('active_ids')
|
||||
if not self.composer_id:
|
||||
self.composer_id = self.env['mail.compose.message'].create({
|
||||
'composition_mode': 'comment' if len(res_ids) == 1 else 'mass_mail',
|
||||
'template_id': self.template_id.id
|
||||
})
|
||||
else:
|
||||
self.composer_id.composition_mode = 'comment' if len(res_ids) == 1 else 'mass_mail'
|
||||
self.composer_id.template_id = self.template_id.id
|
||||
self._compute_composition_mode()
|
||||
self.composer_id._onchange_template_id_wrapper()
|
||||
|
||||
@api.onchange('is_email')
|
||||
def _compute_invoice_without_email(self):
|
||||
for wizard in self:
|
||||
if wizard.is_email and len(wizard.invoice_ids) > 1:
|
||||
invoices = self.env['account.move'].search([
|
||||
('id', 'in', self.env.context.get('active_ids')),
|
||||
('partner_id.email', '=', False)
|
||||
])
|
||||
if invoices:
|
||||
wizard.invoice_without_email = "%s\n%s" % (
|
||||
_("The following invoice(s) will not be sent by email, because the customers don't have email address."),
|
||||
"\n".join([i.name for i in invoices])
|
||||
)
|
||||
else:
|
||||
wizard.invoice_without_email = False
|
||||
else:
|
||||
wizard.invoice_without_email = False
|
||||
|
||||
def _send_email(self):
|
||||
if self.is_email:
|
||||
# with_context : we don't want to reimport the file we just exported.
|
||||
self.composer_id.with_context(no_new_invoice=True,
|
||||
mail_notify_author=self.env.user.partner_id in self.composer_id.partner_ids,
|
||||
mailing_document_based=True,
|
||||
)._action_send_mail()
|
||||
if self.env.context.get('mark_invoice_as_sent'):
|
||||
#Salesman send posted invoice, without the right to write
|
||||
#but they should have the right to change this flag
|
||||
self.mapped('invoice_ids').sudo().write({'is_move_sent': True})
|
||||
for invoice in self.invoice_ids:
|
||||
prioritary_attachments = False
|
||||
if self.composition_mode == 'comment':
|
||||
# With a single invoice we take the attachment directly from the composer
|
||||
prioritary_attachments = self.attachment_ids.filtered(lambda x: x.mimetype.endswith('pdf')).sorted('id')
|
||||
elif self.composition_mode == 'mass_mail':
|
||||
# In mass mail mode we need to look for attachment in the invoice record
|
||||
prioritary_attachments = invoice.attachment_ids.filtered(lambda x: x.mimetype.endswith('pdf'))
|
||||
if prioritary_attachments:
|
||||
main_attachment = prioritary_attachments[0]
|
||||
invoice.with_context(tracking_disable=True).sudo().write({'message_main_attachment_id': main_attachment.id})
|
||||
|
||||
|
||||
def _print_document(self):
|
||||
""" to override for each type of models that will use this composer."""
|
||||
self.ensure_one()
|
||||
action = self.invoice_ids.action_invoice_print()
|
||||
action.update({'close_on_report_download': True})
|
||||
return action
|
||||
|
||||
def send_and_print_action(self):
|
||||
self.ensure_one()
|
||||
# Send the mails in the correct language by splitting the ids per lang.
|
||||
# This should ideally be fixed in mail_compose_message, so when a fix is made there this whole commit should be reverted.
|
||||
# basically self.body (which could be manually edited) extracts self.template_id,
|
||||
# which is then not translated for each customer.
|
||||
if self.composition_mode == 'mass_mail' and self.template_id:
|
||||
active_ids = self.env.context.get('active_ids', self.res_id)
|
||||
active_records = self.env[self.model].browse(active_ids)
|
||||
langs = set(active_records.mapped('partner_id.lang'))
|
||||
for lang in langs:
|
||||
active_ids_lang = active_records.filtered(lambda r: r.partner_id.lang == lang).ids
|
||||
self_lang = self.with_context(active_ids=active_ids_lang, lang=get_lang(self.env, lang).code)
|
||||
self_lang.onchange_template_id()
|
||||
self_lang._send_email()
|
||||
else:
|
||||
active_record = self.env[self.model].browse(self.res_id)
|
||||
lang = get_lang(self.env, active_record.partner_id.lang).code
|
||||
self.with_context(lang=lang)._send_email()
|
||||
if self.is_print:
|
||||
return self._print_document()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def save_as_template(self):
|
||||
self.ensure_one()
|
||||
self.composer_id.action_save_as_template()
|
||||
self.template_id = self.composer_id.template_id.id
|
||||
action = _reopen(self, self.id, self.model, context=self._context)
|
||||
action.update({'name': _('Send Invoice')})
|
||||
return action
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="account_invoice_send_wizard_form" model="ir.ui.view">
|
||||
<field name="name">account.invoice.send.form</field>
|
||||
<field name="model">account.invoice.send</field>
|
||||
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Invoice send & Print">
|
||||
<div class="alert alert-warning" role="alert"
|
||||
attrs="{'invisible': [('move_types', '=', False)]}">
|
||||
You have selected the following document types at the same time:
|
||||
<field name="move_types"/>
|
||||
</div>
|
||||
<!-- truly invisible fields for control and options -->
|
||||
<field name="composition_mode" invisible="1"/>
|
||||
<field name="invoice_ids" invisible="1"/>
|
||||
<field name="email_from" invisible="1" />
|
||||
<field name="mail_server_id" invisible="1"/>
|
||||
<div name="option_print">
|
||||
<field name="is_print" />
|
||||
<b><label for="is_print"/></b>
|
||||
<div name="info_form"
|
||||
attrs="{'invisible': ['|', ('is_print', '=', False), ('composition_mode', '=', 'mass_mail')]}"
|
||||
class="text-center text-muted d-inline-block ms-2">
|
||||
Preview as a PDF
|
||||
</div>
|
||||
</div>
|
||||
<div name="option_email">
|
||||
<field name="is_email" />
|
||||
<b><label for="is_email"/></b>
|
||||
</div>
|
||||
<div class="text-start d-inline-block mr8" attrs="{'invisible': ['|', ('is_email','=', False), ('invoice_without_email', '=', False)]}">
|
||||
<field name="invoice_without_email" class="mr4"/>
|
||||
</div>
|
||||
<div name="mail_form" attrs="{'invisible': [('is_email', '=', False)]}">
|
||||
<!-- visible wizard -->
|
||||
<div attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
|
||||
<group>
|
||||
<label for="partner_ids" string="Recipients" groups="base.group_user"/>
|
||||
<div groups="base.group_user">
|
||||
<span attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}">
|
||||
<strong>Email mass mailing</strong> on
|
||||
<span>the selected records</span>
|
||||
</span>
|
||||
<span>Followers of the document and</span>
|
||||
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
|
||||
context="{'force_email':True, 'show_email':True}" attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}"/>
|
||||
</div>
|
||||
<field name="subject" placeholder="Subject..." attrs="{'required': [('is_email', '=', True), ('composition_mode', '=', 'comment')]}"/>
|
||||
</group>
|
||||
<field name="body" class="oe-bordered-editor" options="{'style-inline': true}"/>
|
||||
</div>
|
||||
<group>
|
||||
<group attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
|
||||
<field name="attachment_ids" widget="many2many_binary" string="Attach a file" nolabel="1" colspan="2" attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="template_id" options="{'no_create': True, 'no_edit': True}"
|
||||
context="{'default_model': 'account.move'}"/>
|
||||
</group>
|
||||
</group>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button string="Send & Print"
|
||||
attrs="{'invisible': ['|', ('is_email', '=', False), ('is_print', '=', False)]}" data-hotkey="q"
|
||||
name="send_and_print_action" type="object" class="send_and_print btn-primary o_mail_send"/>
|
||||
<button string="Send" data-hotkey="q"
|
||||
attrs="{'invisible': ['|', ('is_print', '=', True), ('is_email', '=', False)]}"
|
||||
name="send_and_print_action" type="object" class="send btn-primary o_mail_send"/>
|
||||
<button string="Print" data-hotkey="q"
|
||||
attrs="{'invisible': ['|', ('is_print', '=', False), ('is_email', '=', True)]}"
|
||||
name="send_and_print_action" type="object" class="print btn-primary o_mail_send"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
|
||||
<button icon="fa-lg fa-save" type="object" name="save_as_template" string="Save as new template"
|
||||
attrs="{'invisible': ['|', ('composition_mode', '=', 'mass_mail'), ('is_email', '=', False)]}"
|
||||
class="float-end btn-secondary" help="Save as a new template" data-hotkey="w" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="invoice_send" model="ir.actions.server">
|
||||
<field name="name">Send & print</field>
|
||||
<field name="state">code</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="model_id" ref="model_account_move"/>
|
||||
<field name="binding_model_id" ref="model_account_move"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records.action_send_and_print()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
from odoo.tools.translate import _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountMoveReversal(models.TransientModel):
|
||||
"""
|
||||
Account move reversal wizard, it cancel an account move by reversing it.
|
||||
"""
|
||||
_name = 'account.move.reversal'
|
||||
_description = 'Account Move Reversal'
|
||||
_check_company_auto = True
|
||||
|
||||
move_ids = fields.Many2many('account.move', 'account_move_reversal_move', 'reversal_id', 'move_id', domain=[('state', '=', 'posted')])
|
||||
new_move_ids = fields.Many2many('account.move', 'account_move_reversal_new_move', 'reversal_id', 'new_move_id')
|
||||
date_mode = fields.Selection(selection=[
|
||||
('custom', 'Specific'),
|
||||
('entry', 'Journal Entry Date')
|
||||
], required=True, default='custom')
|
||||
date = fields.Date(string='Reversal date', default=fields.Date.context_today)
|
||||
reason = fields.Char(string='Reason')
|
||||
refund_method = fields.Selection(selection=[
|
||||
('refund', 'Partial Refund'),
|
||||
('cancel', 'Full Refund'),
|
||||
('modify', 'Full refund and new draft invoice')
|
||||
], string='Credit Method', required=True,
|
||||
help='Choose how you want to credit this invoice. You cannot "modify" nor "cancel" if the invoice is already reconciled.')
|
||||
journal_id = fields.Many2one(
|
||||
comodel_name='account.journal',
|
||||
string='Use Specific Journal',
|
||||
required=True,
|
||||
compute='_compute_journal_id',
|
||||
readonly=False,
|
||||
store=True,
|
||||
check_company=True,
|
||||
help='If empty, uses the journal of the journal entry to be reversed.',
|
||||
)
|
||||
company_id = fields.Many2one('res.company', required=True, readonly=True)
|
||||
available_journal_ids = fields.Many2many('account.journal', compute='_compute_available_journal_ids')
|
||||
country_code = fields.Char(related='company_id.country_id.code')
|
||||
|
||||
# computed fields
|
||||
residual = fields.Monetary(compute="_compute_from_moves")
|
||||
currency_id = fields.Many2one('res.currency', compute="_compute_from_moves")
|
||||
move_type = fields.Char(compute="_compute_from_moves")
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_journal_id(self):
|
||||
for record in self:
|
||||
if record.journal_id:
|
||||
record.journal_id = record.journal_id
|
||||
else:
|
||||
journals = record.move_ids.journal_id.filtered(lambda x: x.active)
|
||||
record.journal_id = journals[0] if journals else None
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_available_journal_ids(self):
|
||||
for record in self:
|
||||
if record.move_ids:
|
||||
record.available_journal_ids = self.env['account.journal'].search([
|
||||
('company_id', '=', record.company_id.id),
|
||||
('type', 'in', record.move_ids.journal_id.mapped('type')),
|
||||
])
|
||||
else:
|
||||
record.available_journal_ids = self.env['account.journal'].search([('company_id', '=', record.company_id.id)])
|
||||
|
||||
@api.constrains('journal_id', 'move_ids')
|
||||
def _check_journal_type(self):
|
||||
for record in self:
|
||||
if record.journal_id.type not in record.move_ids.journal_id.mapped('type'):
|
||||
raise UserError(_('Journal should be the same type as the reversed entry.'))
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(AccountMoveReversal, self).default_get(fields)
|
||||
move_ids = self.env['account.move'].browse(self.env.context['active_ids']) if self.env.context.get('active_model') == 'account.move' else self.env['account.move']
|
||||
|
||||
if any(move.state != "posted" for move in move_ids):
|
||||
raise UserError(_('You can only reverse posted moves.'))
|
||||
if 'company_id' in fields:
|
||||
res['company_id'] = move_ids.company_id.id or self.env.company.id
|
||||
if 'move_ids' in fields:
|
||||
res['move_ids'] = [(6, 0, move_ids.ids)]
|
||||
if 'refund_method' in fields:
|
||||
res['refund_method'] = (len(move_ids) > 1 or move_ids.move_type == 'entry') and 'cancel' or 'refund'
|
||||
return res
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_from_moves(self):
|
||||
for record in self:
|
||||
move_ids = record.move_ids._origin
|
||||
record.residual = len(move_ids) == 1 and move_ids.amount_residual or 0
|
||||
record.currency_id = len(move_ids.currency_id) == 1 and move_ids.currency_id or False
|
||||
record.move_type = move_ids.move_type if len(move_ids) == 1 else (any(move.move_type in ('in_invoice', 'out_invoice') for move in move_ids) and 'some_invoice' or False)
|
||||
|
||||
def _prepare_default_reversal(self, move):
|
||||
reverse_date = self.date if self.date_mode == 'custom' else move.date
|
||||
mixed_payment_term = move.invoice_payment_term_id.id if move.invoice_payment_term_id and move.company_id.early_pay_discount_computation == 'mixed' else None
|
||||
return {
|
||||
'ref': _('Reversal of: %(move_name)s, %(reason)s', move_name=move.name, reason=self.reason)
|
||||
if self.reason
|
||||
else _('Reversal of: %s', move.name),
|
||||
'date': reverse_date,
|
||||
'invoice_date_due': reverse_date,
|
||||
'invoice_date': move.is_invoice(include_receipts=True) and reverse_date or False,
|
||||
'journal_id': self.journal_id.id,
|
||||
'invoice_payment_term_id': mixed_payment_term,
|
||||
'invoice_user_id': move.invoice_user_id.id,
|
||||
'auto_post': 'at_date' if reverse_date > fields.Date.context_today(self) else 'no',
|
||||
}
|
||||
|
||||
def reverse_moves(self):
|
||||
self.ensure_one()
|
||||
moves = self.move_ids
|
||||
|
||||
# Create default values.
|
||||
partners = moves.company_id.partner_id + moves.commercial_partner_id
|
||||
|
||||
bank_ids = self.env['res.partner.bank'].search([
|
||||
('partner_id', 'in', partners.ids),
|
||||
('company_id', 'in', moves.company_id.ids + [False]),
|
||||
], order='sequence DESC')
|
||||
partner_to_bank = {bank.partner_id: bank for bank in bank_ids}
|
||||
default_values_list = []
|
||||
for move in moves:
|
||||
if move.is_outbound():
|
||||
partner = move.company_id.partner_id
|
||||
else:
|
||||
partner = move.commercial_partner_id
|
||||
default_values_list.append({
|
||||
'partner_bank_id': partner_to_bank.get(partner, self.env['res.partner.bank']).id,
|
||||
**self._prepare_default_reversal(move),
|
||||
})
|
||||
|
||||
batches = [
|
||||
[self.env['account.move'], [], True], # Moves to be cancelled by the reverses.
|
||||
[self.env['account.move'], [], False], # Others.
|
||||
]
|
||||
for move, default_vals in zip(moves, default_values_list):
|
||||
is_auto_post = default_vals.get('auto_post') != 'no'
|
||||
is_cancel_needed = not is_auto_post and self.refund_method in ('cancel', 'modify')
|
||||
batch_index = 0 if is_cancel_needed else 1
|
||||
batches[batch_index][0] |= move
|
||||
batches[batch_index][1].append(default_vals)
|
||||
|
||||
# Handle reverse method.
|
||||
moves_to_redirect = self.env['account.move']
|
||||
for moves, default_values_list, is_cancel_needed in batches:
|
||||
new_moves = moves._reverse_moves(default_values_list, cancel=is_cancel_needed)
|
||||
|
||||
if self.refund_method == 'modify':
|
||||
moves_vals_list = []
|
||||
for move in moves.with_context(include_business_fields=True):
|
||||
moves_vals_list.append(move.copy_data({'date': self.date if self.date_mode == 'custom' else move.date})[0])
|
||||
new_moves = self.env['account.move'].create(moves_vals_list)
|
||||
|
||||
moves_to_redirect |= new_moves
|
||||
|
||||
self.new_move_ids = moves_to_redirect
|
||||
|
||||
# Create action.
|
||||
action = {
|
||||
'name': _('Reverse Moves'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move',
|
||||
}
|
||||
if len(moves_to_redirect) == 1:
|
||||
action.update({
|
||||
'view_mode': 'form',
|
||||
'res_id': moves_to_redirect.id,
|
||||
'context': {'default_move_type': moves_to_redirect.move_type},
|
||||
})
|
||||
else:
|
||||
action.update({
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('id', 'in', moves_to_redirect.ids)],
|
||||
})
|
||||
if len(set(moves_to_redirect.mapped('move_type'))) == 1:
|
||||
action['context'] = {'default_move_type': moves_to_redirect.mapped('move_type').pop()}
|
||||
return action
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_account_move_reversal" model="ir.ui.view">
|
||||
<field name="name">account.move.reversal.form</field>
|
||||
<field name="model">account.move.reversal</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reverse Journal Entry">
|
||||
<field name="residual" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="move_ids" invisible="1"/>
|
||||
<field name="move_type" invisible="1"/>
|
||||
<field name="available_journal_ids" invisible="1"/>
|
||||
<group>
|
||||
<group attrs="{'invisible': ['|',('move_type', 'not in', ('out_invoice', 'in_invoice')),('residual', '=', 0)]}">
|
||||
<field name="refund_method" widget="radio" attrs="{'readonly': [('residual', '=', 0)]}"/>
|
||||
</group>
|
||||
<group attrs="{'invisible': ['|', ('move_type', 'not in', ('out_invoice', 'in_invoice', 'some_invoice')), ('residual', '=', 0)]}">
|
||||
<div attrs="{'invisible':[('refund_method', '!=', 'refund')]}" class="oe_grey" colspan="2">
|
||||
The credit note is created in draft and can be edited before being issued.
|
||||
</div>
|
||||
<div attrs="{'invisible':[('refund_method', '!=', 'cancel')]}" class="oe_grey" colspan="2">
|
||||
The credit note is auto-validated and reconciled with the invoice.
|
||||
</div>
|
||||
<div attrs="{'invisible':[('refund_method', '!=', 'modify')]}" class="oe_grey" colspan="2">
|
||||
The credit note is auto-validated and reconciled with the invoice.
|
||||
The original invoice is duplicated as a new draft.
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="reason" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
|
||||
<field name="date_mode" string="Reversal Date" widget="radio"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="journal_id" domain="[('id', 'in', available_journal_ids)]"/>
|
||||
<field name="date" string="Refund Date" attrs="{'invisible': ['|', ('move_type', 'not in', ('out_invoice', 'in_invoice')), ('date_mode', '!=', 'custom')], 'required':[('date_mode', '=', 'custom')]}"/>
|
||||
<field name="date" attrs="{'invisible': ['|', ('move_type', 'in', ('out_invoice', 'in_invoice')), ('date_mode', '!=', 'custom')], 'required':[('date_mode', '=', 'custom')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button string='Reverse' name="reverse_moves" type="object" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_account_move_reversal" model="ir.actions.act_window">
|
||||
<field name="name">Reverse</field>
|
||||
<field name="res_model">account.move.reversal</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_account_move_reversal"/>
|
||||
<field name="target">new</field>
|
||||
<field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/>
|
||||
<field name="binding_model_id" ref="account.model_account_move" />
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,897 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import Command, models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import frozendict
|
||||
|
||||
|
||||
class AccountPaymentRegister(models.TransientModel):
|
||||
_name = 'account.payment.register'
|
||||
_description = 'Register Payment'
|
||||
|
||||
# == Business fields ==
|
||||
payment_date = fields.Date(string="Payment Date", required=True,
|
||||
default=fields.Date.context_today)
|
||||
amount = fields.Monetary(currency_field='currency_id', store=True, readonly=False,
|
||||
compute='_compute_amount')
|
||||
hide_writeoff_section = fields.Boolean(compute="_compute_hide_writeoff_section")
|
||||
communication = fields.Char(string="Memo", store=True, readonly=False,
|
||||
compute='_compute_communication')
|
||||
group_payment = fields.Boolean(string="Group Payments", store=True, readonly=False,
|
||||
compute='_compute_group_payment',
|
||||
help="Only one payment will be created by partner (bank), instead of one per bill.")
|
||||
early_payment_discount_mode = fields.Boolean(compute='_compute_early_payment_discount_mode')
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
string='Currency',
|
||||
compute='_compute_currency_id', store=True, readonly=False, precompute=True,
|
||||
help="The payment's currency.")
|
||||
journal_id = fields.Many2one(
|
||||
comodel_name='account.journal',
|
||||
compute='_compute_journal_id', store=True, readonly=False, precompute=True,
|
||||
domain="[('id', 'in', available_journal_ids)]")
|
||||
available_journal_ids = fields.Many2many(
|
||||
comodel_name='account.journal',
|
||||
compute='_compute_available_journal_ids'
|
||||
)
|
||||
available_partner_bank_ids = fields.Many2many(
|
||||
comodel_name='res.partner.bank',
|
||||
compute='_compute_available_partner_bank_ids',
|
||||
)
|
||||
partner_bank_id = fields.Many2one(
|
||||
comodel_name='res.partner.bank',
|
||||
string="Recipient Bank Account",
|
||||
readonly=False,
|
||||
store=True,
|
||||
compute='_compute_partner_bank_id',
|
||||
domain="[('id', 'in', available_partner_bank_ids)]",
|
||||
)
|
||||
company_currency_id = fields.Many2one('res.currency', string="Company Currency",
|
||||
related='company_id.currency_id')
|
||||
|
||||
# == Fields given through the context ==
|
||||
line_ids = fields.Many2many('account.move.line', 'account_payment_register_move_line_rel', 'wizard_id', 'line_id',
|
||||
string="Journal items", readonly=True, copy=False,)
|
||||
payment_type = fields.Selection([
|
||||
('outbound', 'Send Money'),
|
||||
('inbound', 'Receive Money'),
|
||||
], string='Payment Type', store=True, copy=False,
|
||||
compute='_compute_from_lines')
|
||||
partner_type = fields.Selection([
|
||||
('customer', 'Customer'),
|
||||
('supplier', 'Vendor'),
|
||||
], store=True, copy=False,
|
||||
compute='_compute_from_lines')
|
||||
source_amount = fields.Monetary(
|
||||
string="Amount to Pay (company currency)", store=True, copy=False,
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_from_lines')
|
||||
source_amount_currency = fields.Monetary(
|
||||
string="Amount to Pay (foreign currency)", store=True, copy=False,
|
||||
currency_field='source_currency_id',
|
||||
compute='_compute_from_lines')
|
||||
source_currency_id = fields.Many2one('res.currency',
|
||||
string='Source Currency', store=True, copy=False,
|
||||
compute='_compute_from_lines')
|
||||
can_edit_wizard = fields.Boolean(store=True, copy=False,
|
||||
compute='_compute_from_lines') # used to check if user can edit info such as the amount
|
||||
can_group_payments = fields.Boolean(store=True, copy=False,
|
||||
compute='_compute_from_lines') # can the user see the 'group_payments' box
|
||||
company_id = fields.Many2one('res.company', store=True, copy=False,
|
||||
compute='_compute_from_lines')
|
||||
partner_id = fields.Many2one('res.partner',
|
||||
string="Customer/Vendor", store=True, copy=False, ondelete='restrict',
|
||||
compute='_compute_from_lines')
|
||||
|
||||
# == Payment methods fields ==
|
||||
payment_method_line_id = fields.Many2one('account.payment.method.line', string='Payment Method',
|
||||
readonly=False, store=True,
|
||||
compute='_compute_payment_method_line_id',
|
||||
domain="[('id', 'in', available_payment_method_line_ids)]",
|
||||
help="Manual: Pay or Get paid by any method outside of Odoo.\n"
|
||||
"Payment Providers: Each payment provider has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n"
|
||||
"Check: Pay bills by check and print it from Odoo.\n"
|
||||
"Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n"
|
||||
"SEPA Credit Transfer: Pay in the SEPA zone by submitting a SEPA Credit Transfer file to your bank. Module account_sepa is necessary.\n"
|
||||
"SEPA Direct Debit: Get paid in the SEPA zone thanks to a mandate your partner will have granted to you. Module account_sepa is necessary.\n")
|
||||
available_payment_method_line_ids = fields.Many2many('account.payment.method.line', compute='_compute_payment_method_line_fields')
|
||||
|
||||
# == Payment difference fields ==
|
||||
payment_difference = fields.Monetary(
|
||||
compute='_compute_payment_difference')
|
||||
payment_difference_handling = fields.Selection(
|
||||
string="Payment Difference Handling",
|
||||
selection=[('open', 'Keep open'), ('reconcile', 'Mark as fully paid')],
|
||||
compute='_compute_payment_difference_handling',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
writeoff_account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
string="Difference Account",
|
||||
copy=False,
|
||||
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
|
||||
)
|
||||
writeoff_label = fields.Char(string='Journal Item Label', default='Write-Off',
|
||||
help='Change label of the counterpart that will hold the payment difference')
|
||||
|
||||
# == Display purpose fields ==
|
||||
show_partner_bank_account = fields.Boolean(
|
||||
compute='_compute_show_require_partner_bank') # Used to know whether the field `partner_bank_id` should be displayed
|
||||
require_partner_bank_account = fields.Boolean(
|
||||
compute='_compute_show_require_partner_bank') # used to know whether the field `partner_bank_id` should be required
|
||||
country_code = fields.Char(related='company_id.account_fiscal_country_id.code', readonly=True)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPERS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _get_batch_communication(self, batch_result):
|
||||
''' Helper to compute the communication based on the batch.
|
||||
:param batch_result: A batch returned by '_get_batches'.
|
||||
:return: A string representing a communication to be set on payment.
|
||||
'''
|
||||
labels = set(line.move_id.payment_reference or line.name or line.move_id.ref or line.move_id.name for line in batch_result['lines'])
|
||||
return ' '.join(sorted(labels))
|
||||
|
||||
@api.model
|
||||
def _get_batch_available_journals(self, batch_result):
|
||||
""" Helper to compute the available journals based on the batch.
|
||||
|
||||
:param batch_result: A batch returned by '_get_batches'.
|
||||
:return: A recordset of account.journal.
|
||||
"""
|
||||
payment_type = batch_result['payment_values']['payment_type']
|
||||
company = batch_result['lines'].company_id
|
||||
journals = self.env['account.journal'].search([('company_id', '=', company.id), ('type', 'in', ('bank', 'cash'))])
|
||||
if payment_type == 'inbound':
|
||||
return journals.filtered('inbound_payment_method_line_ids')
|
||||
else:
|
||||
return journals.filtered('outbound_payment_method_line_ids')
|
||||
|
||||
@api.model
|
||||
def _get_batch_journal(self, batch_result):
|
||||
""" Helper to compute the journal based on the batch.
|
||||
|
||||
:param batch_result: A batch returned by '_get_batches'.
|
||||
:return: An account.journal record.
|
||||
"""
|
||||
payment_values = batch_result['payment_values']
|
||||
foreign_currency_id = payment_values['currency_id']
|
||||
partner_bank_id = payment_values['partner_bank_id']
|
||||
|
||||
currency_domain = [('currency_id', '=', foreign_currency_id)]
|
||||
partner_bank_domain = [('bank_account_id', '=', partner_bank_id)]
|
||||
|
||||
default_domain = [
|
||||
('type', 'in', ('bank', 'cash')),
|
||||
('company_id', '=', batch_result['lines'].company_id.id),
|
||||
('id', 'in', self.available_journal_ids.ids)
|
||||
]
|
||||
|
||||
if partner_bank_id:
|
||||
extra_domains = (
|
||||
currency_domain + partner_bank_domain,
|
||||
partner_bank_domain,
|
||||
currency_domain,
|
||||
[],
|
||||
)
|
||||
else:
|
||||
extra_domains = (
|
||||
currency_domain,
|
||||
[],
|
||||
)
|
||||
|
||||
for extra_domain in extra_domains:
|
||||
journal = self.env['account.journal'].search(default_domain + extra_domain, limit=1)
|
||||
if journal:
|
||||
return journal
|
||||
|
||||
return self.env['account.journal']
|
||||
|
||||
@api.model
|
||||
def _get_batch_available_partner_banks(self, batch_result, journal):
|
||||
payment_values = batch_result['payment_values']
|
||||
company = batch_result['lines'].company_id
|
||||
|
||||
# A specific bank account is set on the journal. The user must use this one.
|
||||
if payment_values['payment_type'] == 'inbound':
|
||||
# Receiving money on a bank account linked to the journal.
|
||||
return journal.bank_account_id
|
||||
else:
|
||||
# Sending money to a bank account owned by a partner.
|
||||
return batch_result['lines'].partner_id.bank_ids.filtered(lambda x: x.company_id.id in (False, company.id))._origin
|
||||
|
||||
@api.model
|
||||
def _get_line_batch_key(self, line):
|
||||
''' Turn the line passed as parameter to a dictionary defining on which way the lines
|
||||
will be grouped together.
|
||||
:return: A python dictionary.
|
||||
'''
|
||||
move = line.move_id
|
||||
|
||||
partner_bank_account = self.env['res.partner.bank']
|
||||
if move.is_invoice(include_receipts=True):
|
||||
partner_bank_account = move.partner_bank_id._origin
|
||||
|
||||
return {
|
||||
'partner_id': line.partner_id.id,
|
||||
'account_id': line.account_id.id,
|
||||
'currency_id': line.currency_id.id,
|
||||
'partner_bank_id': partner_bank_account.id,
|
||||
'partner_type': 'customer' if line.account_type == 'asset_receivable' else 'supplier',
|
||||
}
|
||||
|
||||
def _get_batches(self):
|
||||
''' Group the account.move.line linked to the wizard together.
|
||||
Lines are grouped if they share 'partner_id','account_id','currency_id' & 'partner_type' and if
|
||||
0 or 1 partner_bank_id can be determined for the group.
|
||||
:return: A list of batches, each one containing:
|
||||
* payment_values: A dictionary of payment values.
|
||||
* moves: An account.move recordset.
|
||||
'''
|
||||
self.ensure_one()
|
||||
|
||||
lines = self.line_ids._origin
|
||||
|
||||
if len(lines.company_id) > 1:
|
||||
raise UserError(_("You can't create payments for entries belonging to different companies."))
|
||||
if not lines:
|
||||
raise UserError(_("You can't open the register payment wizard without at least one receivable/payable line."))
|
||||
|
||||
batches = defaultdict(lambda: {'lines': self.env['account.move.line']})
|
||||
banks_per_partner = defaultdict(lambda: {'inbound': set(), 'outbound': set()})
|
||||
for line in lines:
|
||||
batch_key = self._get_line_batch_key(line)
|
||||
vals = batches[frozendict(batch_key)]
|
||||
vals['payment_values'] = batch_key
|
||||
vals['lines'] += line
|
||||
banks_per_partner[batch_key['partner_id']]['inbound' if line.balance > 0.0 else 'outbound'].add(
|
||||
batch_key['partner_bank_id']
|
||||
)
|
||||
|
||||
partner_unique_inbound = {p for p, b in banks_per_partner.items() if len(b['inbound']) == 1}
|
||||
partner_unique_outbound = {p for p, b in banks_per_partner.items() if len(b['outbound']) == 1}
|
||||
|
||||
# Compute 'payment_type'.
|
||||
batch_vals = []
|
||||
seen_keys = set()
|
||||
for i, key in enumerate(list(batches)):
|
||||
if key in seen_keys:
|
||||
continue
|
||||
vals = batches[key]
|
||||
lines = vals['lines']
|
||||
merge = (
|
||||
batch_key['partner_id'] in partner_unique_inbound
|
||||
and batch_key['partner_id'] in partner_unique_outbound
|
||||
)
|
||||
if merge:
|
||||
for other_key in list(batches)[i+1:]:
|
||||
if other_key in seen_keys:
|
||||
continue
|
||||
other_vals = batches[other_key]
|
||||
if all(
|
||||
other_vals['payment_values'][k] == v
|
||||
for k, v in vals['payment_values'].items()
|
||||
if k not in ('partner_bank_id', 'payment_type')
|
||||
):
|
||||
# add the lines in this batch and mark as seen
|
||||
lines += other_vals['lines']
|
||||
seen_keys.add(other_key)
|
||||
balance = sum(lines.mapped('balance'))
|
||||
vals['payment_values']['payment_type'] = 'inbound' if balance > 0.0 else 'outbound'
|
||||
if merge:
|
||||
partner_banks = banks_per_partner[batch_key['partner_id']]
|
||||
vals['partner_bank_id'] = partner_banks[vals['payment_values']['payment_type']]
|
||||
vals['lines'] = lines
|
||||
batch_vals.append(vals)
|
||||
return batch_vals
|
||||
|
||||
@api.model
|
||||
def _get_wizard_values_from_batch(self, batch_result):
|
||||
''' Extract values from the batch passed as parameter (see '_get_batches')
|
||||
to be mounted in the wizard view.
|
||||
:param batch_result: A batch returned by '_get_batches'.
|
||||
:return: A dictionary containing valid fields
|
||||
'''
|
||||
payment_values = batch_result['payment_values']
|
||||
lines = batch_result['lines']
|
||||
company = lines[0].company_id
|
||||
|
||||
source_amount = abs(sum(lines.mapped('amount_residual')))
|
||||
if payment_values['currency_id'] == company.currency_id.id:
|
||||
source_amount_currency = source_amount
|
||||
else:
|
||||
source_amount_currency = abs(sum(lines.mapped('amount_residual_currency')))
|
||||
|
||||
return {
|
||||
'company_id': company.id,
|
||||
'partner_id': payment_values['partner_id'],
|
||||
'partner_type': payment_values['partner_type'],
|
||||
'payment_type': payment_values['payment_type'],
|
||||
'source_currency_id': payment_values['currency_id'],
|
||||
'source_amount': source_amount,
|
||||
'source_amount_currency': source_amount_currency,
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('line_ids')
|
||||
def _compute_from_lines(self):
|
||||
''' Load initial values from the account.moves passed through the context. '''
|
||||
for wizard in self:
|
||||
batches = wizard._get_batches()
|
||||
batch_result = batches[0]
|
||||
wizard_values_from_batch = wizard._get_wizard_values_from_batch(batch_result)
|
||||
|
||||
if len(batches) == 1:
|
||||
# == Single batch to be mounted on the view ==
|
||||
wizard.update(wizard_values_from_batch)
|
||||
|
||||
wizard.can_edit_wizard = True
|
||||
wizard.can_group_payments = len(batch_result['lines']) != 1
|
||||
else:
|
||||
# == Multiple batches: The wizard is not editable ==
|
||||
wizard.update({
|
||||
'company_id': batches[0]['lines'][0].company_id.id,
|
||||
'partner_id': False,
|
||||
'partner_type': False,
|
||||
'payment_type': wizard_values_from_batch['payment_type'],
|
||||
'source_currency_id': False,
|
||||
'source_amount': False,
|
||||
'source_amount_currency': False,
|
||||
})
|
||||
|
||||
wizard.can_edit_wizard = False
|
||||
wizard.can_group_payments = any(len(batch_result['lines']) != 1 for batch_result in batches)
|
||||
|
||||
@api.depends('can_edit_wizard')
|
||||
def _compute_communication(self):
|
||||
# The communication can't be computed in '_compute_from_lines' because
|
||||
# it's a compute editable field and then, should be computed in a separated method.
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
batches = wizard._get_batches()
|
||||
wizard.communication = wizard._get_batch_communication(batches[0])
|
||||
else:
|
||||
wizard.communication = False
|
||||
|
||||
@api.depends('can_edit_wizard')
|
||||
def _compute_group_payment(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
batches = wizard._get_batches()
|
||||
wizard.group_payment = len(batches[0]['lines'].move_id) == 1
|
||||
else:
|
||||
wizard.group_payment = False
|
||||
|
||||
@api.depends('journal_id')
|
||||
def _compute_currency_id(self):
|
||||
for wizard in self:
|
||||
wizard.currency_id = wizard.journal_id.currency_id or wizard.source_currency_id or wizard.company_id.currency_id
|
||||
|
||||
@api.depends('payment_type', 'company_id', 'can_edit_wizard')
|
||||
def _compute_available_journal_ids(self):
|
||||
for wizard in self:
|
||||
available_journals = self.env['account.journal']
|
||||
for batch in wizard._get_batches():
|
||||
available_journals |= wizard._get_batch_available_journals(batch)
|
||||
wizard.available_journal_ids = [Command.set(available_journals.ids)]
|
||||
|
||||
@api.depends('available_journal_ids')
|
||||
def _compute_journal_id(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
batch = wizard._get_batches()[0]
|
||||
wizard.journal_id = wizard._get_batch_journal(batch)
|
||||
else:
|
||||
wizard.journal_id = self.env['account.journal'].search([
|
||||
('type', 'in', ('bank', 'cash')),
|
||||
('company_id', '=', wizard.company_id.id),
|
||||
('id', 'in', self.available_journal_ids.ids)
|
||||
], limit=1)
|
||||
|
||||
@api.depends('can_edit_wizard', 'journal_id')
|
||||
def _compute_available_partner_bank_ids(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
batch = wizard._get_batches()[0]
|
||||
wizard.available_partner_bank_ids = wizard._get_batch_available_partner_banks(batch, wizard.journal_id)
|
||||
else:
|
||||
wizard.available_partner_bank_ids = None
|
||||
|
||||
@api.depends('journal_id', 'available_partner_bank_ids')
|
||||
def _compute_partner_bank_id(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
batch = wizard._get_batches()[0]
|
||||
partner_bank_id = batch['payment_values']['partner_bank_id']
|
||||
available_partner_banks = wizard.available_partner_bank_ids._origin
|
||||
if partner_bank_id and partner_bank_id in available_partner_banks.ids:
|
||||
wizard.partner_bank_id = self.env['res.partner.bank'].browse(partner_bank_id)
|
||||
else:
|
||||
wizard.partner_bank_id = available_partner_banks[:1]
|
||||
else:
|
||||
wizard.partner_bank_id = None
|
||||
|
||||
@api.depends('payment_type', 'journal_id', 'currency_id')
|
||||
def _compute_payment_method_line_fields(self):
|
||||
for wizard in self:
|
||||
if wizard.journal_id:
|
||||
wizard.available_payment_method_line_ids = wizard.journal_id._get_available_payment_method_lines(wizard.payment_type)
|
||||
else:
|
||||
wizard.available_payment_method_line_ids = False
|
||||
|
||||
@api.depends('payment_type', 'journal_id')
|
||||
def _compute_payment_method_line_id(self):
|
||||
for wizard in self:
|
||||
if wizard.journal_id:
|
||||
available_payment_method_lines = wizard.journal_id._get_available_payment_method_lines(wizard.payment_type)
|
||||
else:
|
||||
available_payment_method_lines = False
|
||||
|
||||
# Select the first available one by default.
|
||||
if available_payment_method_lines:
|
||||
wizard.payment_method_line_id = available_payment_method_lines[0]._origin
|
||||
else:
|
||||
wizard.payment_method_line_id = False
|
||||
|
||||
@api.depends('payment_method_line_id')
|
||||
def _compute_show_require_partner_bank(self):
|
||||
""" Computes if the destination bank account must be displayed in the payment form view. By default, it
|
||||
won't be displayed but some modules might change that, depending on the payment type."""
|
||||
for wizard in self:
|
||||
if wizard.journal_id.type == 'cash':
|
||||
wizard.show_partner_bank_account = False
|
||||
else:
|
||||
wizard.show_partner_bank_account = wizard.payment_method_line_id.code in self.env['account.payment']._get_method_codes_using_bank_account()
|
||||
wizard.require_partner_bank_account = wizard.payment_method_line_id.code in self.env['account.payment']._get_method_codes_needing_bank_account()
|
||||
|
||||
def _get_total_amount_using_same_currency(self, batch_result, early_payment_discount=True):
|
||||
self.ensure_one()
|
||||
amount = 0.0
|
||||
mode = False
|
||||
for aml in batch_result['lines']:
|
||||
if early_payment_discount and aml._is_eligible_for_early_payment_discount(aml.currency_id, self.payment_date):
|
||||
amount += aml.discount_amount_currency
|
||||
mode = 'early_payment'
|
||||
else:
|
||||
amount += aml.amount_residual_currency
|
||||
return abs(amount), mode
|
||||
|
||||
def _get_total_amount_in_wizard_currency_to_full_reconcile(self, batch_result, early_payment_discount=True):
|
||||
""" Compute the total amount needed in the currency of the wizard to fully reconcile the batch of journal
|
||||
items passed as parameter.
|
||||
|
||||
:param batch_result: A batch returned by '_get_batches'.
|
||||
:return: An amount in the currency of the wizard.
|
||||
"""
|
||||
self.ensure_one()
|
||||
comp_curr = self.company_id.currency_id
|
||||
if self.source_currency_id == self.currency_id:
|
||||
# Same currency (manage the early payment discount).
|
||||
return self._get_total_amount_using_same_currency(batch_result, early_payment_discount=early_payment_discount)
|
||||
elif self.source_currency_id != comp_curr and self.currency_id == comp_curr:
|
||||
# Foreign currency on source line but the company currency one on the opposite line.
|
||||
return self.source_currency_id._convert(
|
||||
self.source_amount_currency,
|
||||
comp_curr,
|
||||
self.company_id,
|
||||
self.payment_date,
|
||||
), False
|
||||
elif self.source_currency_id == comp_curr and self.currency_id != comp_curr:
|
||||
# Company currency on source line but a foreign currency one on the opposite line.
|
||||
residual_amount = 0.0
|
||||
for aml in batch_result['lines']:
|
||||
if not aml.move_id.payment_id and not aml.move_id.statement_line_id:
|
||||
conversion_date = self.payment_date
|
||||
else:
|
||||
conversion_date = aml.date
|
||||
residual_amount += comp_curr._convert(
|
||||
aml.amount_residual,
|
||||
self.currency_id,
|
||||
self.company_id,
|
||||
conversion_date,
|
||||
)
|
||||
return abs(residual_amount), False
|
||||
else:
|
||||
# Foreign currency on payment different than the one set on the journal entries.
|
||||
return comp_curr._convert(
|
||||
self.source_amount,
|
||||
self.currency_id,
|
||||
self.company_id,
|
||||
self.payment_date,
|
||||
), False
|
||||
|
||||
@api.depends('can_edit_wizard', 'source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date')
|
||||
def _compute_amount(self):
|
||||
for wizard in self:
|
||||
if wizard.source_currency_id and wizard.can_edit_wizard:
|
||||
batch_result = wizard._get_batches()[0]
|
||||
wizard.amount = wizard._get_total_amount_in_wizard_currency_to_full_reconcile(batch_result)[0]
|
||||
else:
|
||||
# The wizard is not editable so no partial payment allowed and then, 'amount' is not used.
|
||||
wizard.amount = None
|
||||
|
||||
@api.depends('can_edit_wizard', 'payment_date', 'currency_id', 'amount')
|
||||
def _compute_early_payment_discount_mode(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard and wizard.currency_id:
|
||||
batch_result = wizard._get_batches()[0]
|
||||
total_amount_residual_in_wizard_currency, mode = wizard._get_total_amount_in_wizard_currency_to_full_reconcile(batch_result)
|
||||
wizard.early_payment_discount_mode = \
|
||||
wizard.currency_id.compare_amounts(wizard.amount, total_amount_residual_in_wizard_currency) == 0 \
|
||||
and mode == 'early_payment'
|
||||
else:
|
||||
wizard.early_payment_discount_mode = False
|
||||
|
||||
@api.depends('can_edit_wizard', 'amount')
|
||||
def _compute_payment_difference(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
batch_result = wizard._get_batches()[0]
|
||||
total_amount_residual_in_wizard_currency = wizard\
|
||||
._get_total_amount_in_wizard_currency_to_full_reconcile(batch_result, early_payment_discount=False)[0]
|
||||
wizard.payment_difference = total_amount_residual_in_wizard_currency - wizard.amount
|
||||
else:
|
||||
wizard.payment_difference = 0.0
|
||||
|
||||
@api.depends('early_payment_discount_mode')
|
||||
def _compute_payment_difference_handling(self):
|
||||
for wizard in self:
|
||||
if wizard.can_edit_wizard:
|
||||
wizard.payment_difference_handling = 'reconcile' if wizard.early_payment_discount_mode else 'open'
|
||||
else:
|
||||
wizard.payment_difference_handling = False
|
||||
|
||||
@api.depends('early_payment_discount_mode')
|
||||
def _compute_hide_writeoff_section(self):
|
||||
for wizard in self:
|
||||
wizard.hide_writeoff_section = wizard.early_payment_discount_mode
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# LOW-LEVEL METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
# OVERRIDE
|
||||
res = super().default_get(fields_list)
|
||||
|
||||
if 'line_ids' in fields_list and 'line_ids' not in res:
|
||||
|
||||
# Retrieve moves to pay from the context.
|
||||
|
||||
if self._context.get('active_model') == 'account.move':
|
||||
lines = self.env['account.move'].browse(self._context.get('active_ids', [])).line_ids
|
||||
elif self._context.get('active_model') == 'account.move.line':
|
||||
lines = self.env['account.move.line'].browse(self._context.get('active_ids', []))
|
||||
else:
|
||||
raise UserError(_(
|
||||
"The register payment wizard should only be called on account.move or account.move.line records."
|
||||
))
|
||||
|
||||
if 'journal_id' in res and not self.env['account.journal'].browse(res['journal_id'])\
|
||||
.filtered_domain([('company_id', '=', lines.company_id.id), ('type', 'in', ('bank', 'cash'))]):
|
||||
# default can be inherited from the list view, should be computed instead
|
||||
del res['journal_id']
|
||||
|
||||
# Keep lines having a residual amount to pay.
|
||||
available_lines = self.env['account.move.line']
|
||||
for line in lines:
|
||||
if line.move_id.state != 'posted':
|
||||
raise UserError(_("You can only register payment for posted journal entries."))
|
||||
|
||||
if line.account_type not in ('asset_receivable', 'liability_payable'):
|
||||
continue
|
||||
if line.currency_id:
|
||||
if line.currency_id.is_zero(line.amount_residual_currency):
|
||||
continue
|
||||
else:
|
||||
if line.company_currency_id.is_zero(line.amount_residual):
|
||||
continue
|
||||
available_lines |= line
|
||||
|
||||
# Check.
|
||||
if not available_lines:
|
||||
raise UserError(_("You can't register a payment because there is nothing left to pay on the selected journal items."))
|
||||
if len(lines.company_id) > 1:
|
||||
raise UserError(_("You can't create payments for entries belonging to different companies."))
|
||||
if len(set(available_lines.mapped('account_type'))) > 1:
|
||||
raise UserError(_("You can't register payments for both inbound and outbound moves at the same time."))
|
||||
|
||||
res['line_ids'] = [(6, 0, available_lines.ids)]
|
||||
|
||||
return res
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# BUSINESS METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _create_payment_vals_from_wizard(self, batch_result):
|
||||
payment_vals = {
|
||||
'date': self.payment_date,
|
||||
'amount': self.amount,
|
||||
'payment_type': self.payment_type,
|
||||
'partner_type': self.partner_type,
|
||||
'ref': self.communication,
|
||||
'journal_id': self.journal_id.id,
|
||||
'currency_id': self.currency_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'partner_bank_id': self.partner_bank_id.id,
|
||||
'payment_method_line_id': self.payment_method_line_id.id,
|
||||
'destination_account_id': self.line_ids[0].account_id.id,
|
||||
'write_off_line_vals': [],
|
||||
}
|
||||
|
||||
conversion_rate = self.env['res.currency']._get_conversion_rate(
|
||||
self.currency_id,
|
||||
self.company_id.currency_id,
|
||||
self.company_id,
|
||||
self.payment_date,
|
||||
)
|
||||
|
||||
if self.payment_difference_handling == 'reconcile':
|
||||
|
||||
if self.early_payment_discount_mode:
|
||||
epd_aml_values_list = []
|
||||
for aml in batch_result['lines']:
|
||||
if aml._is_eligible_for_early_payment_discount(self.currency_id, self.payment_date):
|
||||
epd_aml_values_list.append({
|
||||
'aml': aml,
|
||||
'amount_currency': -aml.amount_residual_currency,
|
||||
'balance': aml.company_currency_id.round(-aml.amount_residual_currency * conversion_rate),
|
||||
})
|
||||
|
||||
open_amount_currency = self.payment_difference * (-1 if self.payment_type == 'outbound' else 1)
|
||||
open_balance = self.company_id.currency_id.round(open_amount_currency * conversion_rate)
|
||||
early_payment_values = self.env['account.move']._get_invoice_counterpart_amls_for_early_payment_discount(epd_aml_values_list, open_balance)
|
||||
for aml_values_list in early_payment_values.values():
|
||||
payment_vals['write_off_line_vals'] += aml_values_list
|
||||
|
||||
elif not self.currency_id.is_zero(self.payment_difference):
|
||||
if self.payment_type == 'inbound':
|
||||
# Receive money.
|
||||
write_off_amount_currency = self.payment_difference
|
||||
else: # if self.payment_type == 'outbound':
|
||||
# Send money.
|
||||
write_off_amount_currency = -self.payment_difference
|
||||
|
||||
write_off_balance = self.company_id.currency_id.round(write_off_amount_currency * conversion_rate)
|
||||
payment_vals['write_off_line_vals'].append({
|
||||
'name': self.writeoff_label,
|
||||
'account_id': self.writeoff_account_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'currency_id': self.currency_id.id,
|
||||
'amount_currency': write_off_amount_currency,
|
||||
'balance': write_off_balance,
|
||||
})
|
||||
return payment_vals
|
||||
|
||||
def _create_payment_vals_from_batch(self, batch_result):
|
||||
batch_values = self._get_wizard_values_from_batch(batch_result)
|
||||
|
||||
if batch_values['payment_type'] == 'inbound':
|
||||
partner_bank_id = self.journal_id.bank_account_id.id
|
||||
else:
|
||||
partner_bank_id = batch_result['payment_values']['partner_bank_id']
|
||||
|
||||
payment_method_line = self.payment_method_line_id
|
||||
|
||||
if batch_values['payment_type'] != payment_method_line.payment_type:
|
||||
payment_method_line = self.journal_id._get_available_payment_method_lines(batch_values['payment_type'])[:1]
|
||||
|
||||
payment_vals = {
|
||||
'date': self.payment_date,
|
||||
'amount': batch_values['source_amount_currency'],
|
||||
'payment_type': batch_values['payment_type'],
|
||||
'partner_type': batch_values['partner_type'],
|
||||
'ref': self._get_batch_communication(batch_result),
|
||||
'journal_id': self.journal_id.id,
|
||||
'currency_id': batch_values['source_currency_id'],
|
||||
'partner_id': batch_values['partner_id'],
|
||||
'partner_bank_id': partner_bank_id,
|
||||
'payment_method_line_id': payment_method_line.id,
|
||||
'destination_account_id': batch_result['lines'][0].account_id.id,
|
||||
'write_off_line_vals': [],
|
||||
}
|
||||
|
||||
total_amount, mode = self._get_total_amount_using_same_currency(batch_result)
|
||||
currency = self.env['res.currency'].browse(batch_values['source_currency_id'])
|
||||
if mode == 'early_payment':
|
||||
payment_vals['amount'] = total_amount
|
||||
|
||||
conversion_rate = self.env['res.currency']._get_conversion_rate(
|
||||
currency,
|
||||
self.company_id.currency_id,
|
||||
self.company_id,
|
||||
self.payment_date,
|
||||
)
|
||||
|
||||
epd_aml_values_list = []
|
||||
for aml in batch_result['lines']:
|
||||
if aml._is_eligible_for_early_payment_discount(currency, self.payment_date):
|
||||
epd_aml_values_list.append({
|
||||
'aml': aml,
|
||||
'amount_currency': -aml.amount_residual_currency,
|
||||
'balance': aml.company_currency_id.round(-aml.amount_residual_currency * conversion_rate),
|
||||
})
|
||||
|
||||
open_amount_currency = (batch_values['source_amount_currency'] - total_amount) * (-1 if batch_values['payment_type'] == 'outbound' else 1)
|
||||
open_balance = self.company_id.currency_id.round(open_amount_currency * conversion_rate)
|
||||
early_payment_values = self.env['account.move']\
|
||||
._get_invoice_counterpart_amls_for_early_payment_discount(epd_aml_values_list, open_balance)
|
||||
for aml_values_list in early_payment_values.values():
|
||||
payment_vals['write_off_line_vals'] += aml_values_list
|
||||
|
||||
return payment_vals
|
||||
|
||||
def _init_payments(self, to_process, edit_mode=False):
|
||||
""" Create the payments.
|
||||
|
||||
:param to_process: A list of python dictionary, one for each payment to create, containing:
|
||||
* create_vals: The values used for the 'create' method.
|
||||
* to_reconcile: The journal items to perform the reconciliation.
|
||||
* batch: A python dict containing everything you want about the source journal items
|
||||
to which a payment will be created (see '_get_batches').
|
||||
:param edit_mode: Is the wizard in edition mode.
|
||||
"""
|
||||
|
||||
payments = self.env['account.payment']\
|
||||
.with_context(skip_invoice_sync=True)\
|
||||
.create([x['create_vals'] for x in to_process])
|
||||
|
||||
for payment, vals in zip(payments, to_process):
|
||||
vals['payment'] = payment
|
||||
|
||||
# If payments are made using a currency different than the source one, ensure the balance match exactly in
|
||||
# order to fully paid the source journal items.
|
||||
# For example, suppose a new currency B having a rate 100:1 regarding the company currency A.
|
||||
# If you try to pay 12.15A using 0.12B, the computed balance will be 12.00A for the payment instead of 12.15A.
|
||||
if edit_mode:
|
||||
lines = vals['to_reconcile']
|
||||
|
||||
# Batches are made using the same currency so making 'lines.currency_id' is ok.
|
||||
if payment.currency_id != lines.currency_id:
|
||||
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
|
||||
source_balance = abs(sum(lines.mapped('amount_residual')))
|
||||
if liquidity_lines[0].balance:
|
||||
payment_rate = liquidity_lines[0].amount_currency / liquidity_lines[0].balance
|
||||
else:
|
||||
payment_rate = 0.0
|
||||
source_balance_converted = abs(source_balance) * payment_rate
|
||||
|
||||
# Translate the balance into the payment currency is order to be able to compare them.
|
||||
# In case in both have the same value (12.15 * 0.01 ~= 0.12 in our example), it means the user
|
||||
# attempt to fully paid the source lines and then, we need to manually fix them to get a perfect
|
||||
# match.
|
||||
payment_balance = abs(sum(counterpart_lines.mapped('balance')))
|
||||
payment_amount_currency = abs(sum(counterpart_lines.mapped('amount_currency')))
|
||||
if not payment.currency_id.is_zero(source_balance_converted - payment_amount_currency):
|
||||
continue
|
||||
|
||||
delta_balance = source_balance - payment_balance
|
||||
|
||||
# Balance are already the same.
|
||||
if self.company_currency_id.is_zero(delta_balance):
|
||||
continue
|
||||
|
||||
# Fix the balance but make sure to peek the liquidity and counterpart lines first.
|
||||
debit_lines = (liquidity_lines + counterpart_lines).filtered('debit')
|
||||
credit_lines = (liquidity_lines + counterpart_lines).filtered('credit')
|
||||
|
||||
if debit_lines and credit_lines:
|
||||
payment.move_id.write({'line_ids': [
|
||||
(1, debit_lines[0].id, {'debit': debit_lines[0].debit + delta_balance}),
|
||||
(1, credit_lines[0].id, {'credit': credit_lines[0].credit + delta_balance}),
|
||||
]})
|
||||
return payments
|
||||
|
||||
def _post_payments(self, to_process, edit_mode=False):
|
||||
""" Post the newly created payments.
|
||||
|
||||
:param to_process: A list of python dictionary, one for each payment to create, containing:
|
||||
* create_vals: The values used for the 'create' method.
|
||||
* to_reconcile: The journal items to perform the reconciliation.
|
||||
* batch: A python dict containing everything you want about the source journal items
|
||||
to which a payment will be created (see '_get_batches').
|
||||
:param edit_mode: Is the wizard in edition mode.
|
||||
"""
|
||||
payments = self.env['account.payment']
|
||||
for vals in to_process:
|
||||
payments |= vals['payment']
|
||||
payments.action_post()
|
||||
|
||||
def _reconcile_payments(self, to_process, edit_mode=False):
|
||||
""" Reconcile the payments.
|
||||
|
||||
:param to_process: A list of python dictionary, one for each payment to create, containing:
|
||||
* create_vals: The values used for the 'create' method.
|
||||
* to_reconcile: The journal items to perform the reconciliation.
|
||||
* batch: A python dict containing everything you want about the source journal items
|
||||
to which a payment will be created (see '_get_batches').
|
||||
:param edit_mode: Is the wizard in edition mode.
|
||||
"""
|
||||
domain = [
|
||||
('parent_state', '=', 'posted'),
|
||||
('account_type', 'in', ('asset_receivable', 'liability_payable')),
|
||||
('reconciled', '=', False),
|
||||
]
|
||||
for vals in to_process:
|
||||
payment_lines = vals['payment'].line_ids.filtered_domain(domain)
|
||||
lines = vals['to_reconcile']
|
||||
|
||||
for account in payment_lines.account_id:
|
||||
(payment_lines + lines)\
|
||||
.filtered_domain([('account_id', '=', account.id), ('reconciled', '=', False)])\
|
||||
.reconcile()
|
||||
|
||||
def _create_payments(self):
|
||||
self.ensure_one()
|
||||
batches = self._get_batches()
|
||||
first_batch_result = batches[0]
|
||||
edit_mode = self.can_edit_wizard and (len(first_batch_result['lines']) == 1 or self.group_payment)
|
||||
to_process = []
|
||||
|
||||
if edit_mode:
|
||||
payment_vals = self._create_payment_vals_from_wizard(first_batch_result)
|
||||
to_process.append({
|
||||
'create_vals': payment_vals,
|
||||
'to_reconcile': first_batch_result['lines'],
|
||||
'batch': first_batch_result,
|
||||
})
|
||||
else:
|
||||
# Don't group payments: Create one batch per move.
|
||||
if not self.group_payment:
|
||||
new_batches = []
|
||||
for batch_result in batches:
|
||||
for line in batch_result['lines']:
|
||||
new_batches.append({
|
||||
**batch_result,
|
||||
'payment_values': {
|
||||
**batch_result['payment_values'],
|
||||
'payment_type': 'inbound' if line.balance > 0 else 'outbound'
|
||||
},
|
||||
'lines': line,
|
||||
})
|
||||
batches = new_batches
|
||||
|
||||
for batch_result in batches:
|
||||
to_process.append({
|
||||
'create_vals': self._create_payment_vals_from_batch(batch_result),
|
||||
'to_reconcile': batch_result['lines'],
|
||||
'batch': batch_result,
|
||||
})
|
||||
|
||||
payments = self._init_payments(to_process, edit_mode=edit_mode)
|
||||
self._post_payments(to_process, edit_mode=edit_mode)
|
||||
self._reconcile_payments(to_process, edit_mode=edit_mode)
|
||||
return payments
|
||||
|
||||
def action_create_payments(self):
|
||||
payments = self._create_payments()
|
||||
|
||||
if self._context.get('dont_redirect_to_payments'):
|
||||
return True
|
||||
|
||||
action = {
|
||||
'name': _('Payments'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.payment',
|
||||
'context': {'create': False},
|
||||
}
|
||||
if len(payments) == 1:
|
||||
action.update({
|
||||
'view_mode': 'form',
|
||||
'res_id': payments.id,
|
||||
})
|
||||
else:
|
||||
action.update({
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('id', 'in', payments.ids)],
|
||||
})
|
||||
return action
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="view_account_payment_register_form" model="ir.ui.view">
|
||||
<field name="name">account.payment.register.form</field>
|
||||
<field name="model">account.payment.register</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Register Payment">
|
||||
<!-- Invisible fields -->
|
||||
<field name="line_ids" invisible="1"/>
|
||||
<field name="can_edit_wizard" invisible="1" force_save="1"/>
|
||||
<field name="can_group_payments" invisible="1" force_save="1"/>
|
||||
<field name="early_payment_discount_mode" invisible="1" force_save="1"/>
|
||||
<field name="payment_type" invisible="1" force_save="1"/>
|
||||
<field name="partner_type" invisible="1" force_save="1"/>
|
||||
<field name="source_amount" invisible="1" force_save="1"/>
|
||||
<field name="source_amount_currency" invisible="1" force_save="1"/>
|
||||
<field name="source_currency_id" invisible="1" force_save="1"/>
|
||||
<field name="company_id" invisible="1" force_save="1"/>
|
||||
<field name="partner_id" invisible="1" force_save="1"/>
|
||||
<field name="country_code" invisible="1" force_save="1"/>
|
||||
|
||||
<field name="show_partner_bank_account" invisible="1"/>
|
||||
<field name="require_partner_bank_account" invisible="1"/>
|
||||
<field name="available_journal_ids" invisible="1"/>
|
||||
<field name="available_payment_method_line_ids" invisible="1"/>
|
||||
<field name="available_partner_bank_ids" invisible="1"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<field name="hide_writeoff_section" invisible="1"/>
|
||||
|
||||
<div role="alert" class="alert alert-info" attrs="{'invisible': [('hide_writeoff_section', '=', False)]}">
|
||||
<p><b>Early Payment Discount of <field name="payment_difference"/> has been applied.</b></p>
|
||||
</div>
|
||||
<group>
|
||||
<group name="group1">
|
||||
<field name="journal_id" options="{'no_open': True, 'no_create': True}" required="1"/>
|
||||
<field name="payment_method_line_id"
|
||||
required="1" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="partner_bank_id"
|
||||
attrs="{'invisible': ['|', ('show_partner_bank_account', '=', False), '|', ('can_edit_wizard', '=', False), '&', ('can_group_payments', '=', True), ('group_payment', '=', False)],
|
||||
'required': [('require_partner_bank_account', '=', True), ('can_edit_wizard', '=', True), '|', ('can_group_payments', '=', False), ('group_payment', '=', False)], 'readonly': [('payment_type', '=', 'inbound')]}"
|
||||
context="{'default_allow_out_payment': True}"/>
|
||||
<field name="group_payment"
|
||||
attrs="{'invisible': [('can_group_payments', '=', False)]}"/>
|
||||
</group>
|
||||
<group name="group2">
|
||||
<label for="amount"
|
||||
attrs="{'invisible': ['|', ('can_edit_wizard', '=', False), '&', ('can_group_payments', '=', True), ('group_payment', '=', False)]}"/>
|
||||
<div name="amount_div" class="o_row"
|
||||
attrs="{'invisible': ['|', ('can_edit_wizard', '=', False), '&', ('can_group_payments', '=', True), ('group_payment', '=', False)]}">
|
||||
<field name="amount"/>
|
||||
<field name="currency_id"
|
||||
required="1"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
groups="base.group_multi_currency"/>
|
||||
</div>
|
||||
<field name="payment_date"/>
|
||||
<field name="communication"
|
||||
attrs="{'invisible': ['|', ('can_edit_wizard', '=', False), '&', ('can_group_payments', '=', True), ('group_payment', '=', False)]}"/>
|
||||
</group>
|
||||
<group name="group3"
|
||||
attrs="{'invisible': ['|', ('payment_difference', '=', 0.0), '|', ('early_payment_discount_mode', '=', True), '|', ('can_edit_wizard', '=', False), '&', ('can_group_payments', '=', True), ('group_payment', '=', False)]}">
|
||||
<label for="payment_difference"/>
|
||||
<div>
|
||||
<field name="payment_difference"/>
|
||||
<field name="payment_difference_handling" widget="radio" nolabel="1"/>
|
||||
<div attrs="{'invisible': ['|', ('hide_writeoff_section', '=', True), ('payment_difference_handling','=','open')]}">
|
||||
<label for="writeoff_account_id" string="Post Difference In" class="oe_edit_only"/>
|
||||
<field name="writeoff_account_id"
|
||||
string="Post Difference In"
|
||||
options="{'no_create': True}"
|
||||
attrs="{'required': [('payment_difference_handling', '=', 'reconcile'), ('early_payment_discount_mode', '=', False)]}"/>
|
||||
<label for="writeoff_label" class="oe_edit_only" string="Label"/>
|
||||
<field name="writeoff_label" attrs="{'required': [('payment_difference_handling', '=', 'reconcile')]}"/>
|
||||
</div>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Create Payment" name="action_create_payments" type="object" class="oe_highlight" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.date_utils import get_month, get_fiscal_year
|
||||
from odoo.tools.misc import format_date
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
import json
|
||||
|
||||
|
||||
class ReSequenceWizard(models.TransientModel):
|
||||
_name = 'account.resequence.wizard'
|
||||
_description = 'Remake the sequence of Journal Entries.'
|
||||
|
||||
sequence_number_reset = fields.Char(compute='_compute_sequence_number_reset')
|
||||
first_date = fields.Date(help="Date (inclusive) from which the numbers are resequenced.")
|
||||
end_date = fields.Date(help="Date (inclusive) to which the numbers are resequenced. If not set, all Journal Entries up to the end of the period are resequenced.")
|
||||
first_name = fields.Char(compute="_compute_first_name", readonly=False, store=True, required=True, string="First New Sequence")
|
||||
ordering = fields.Selection([('keep', 'Keep current order'), ('date', 'Reorder by accounting date')], required=True, default='keep')
|
||||
move_ids = fields.Many2many('account.move')
|
||||
new_values = fields.Text(compute='_compute_new_values')
|
||||
preview_moves = fields.Text(compute='_compute_preview_moves')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
values = super(ReSequenceWizard, self).default_get(fields_list)
|
||||
if 'move_ids' not in fields_list:
|
||||
return values
|
||||
active_move_ids = self.env['account.move']
|
||||
if self.env.context['active_model'] == 'account.move' and 'active_ids' in self.env.context:
|
||||
active_move_ids = self.env['account.move'].browse(self.env.context['active_ids'])
|
||||
if len(active_move_ids.journal_id) > 1:
|
||||
raise UserError(_('You can only resequence items from the same journal'))
|
||||
move_types = set(active_move_ids.mapped('move_type'))
|
||||
if (
|
||||
active_move_ids.journal_id.refund_sequence
|
||||
and ('in_refund' in move_types or 'out_refund' in move_types)
|
||||
and len(move_types) > 1
|
||||
):
|
||||
raise UserError(_('The sequences of this journal are different for Invoices and Refunds but you selected some of both types.'))
|
||||
is_payment = set(active_move_ids.mapped(lambda x: bool(x.payment_id)))
|
||||
if (
|
||||
active_move_ids.journal_id.payment_sequence
|
||||
and len(is_payment) > 1
|
||||
):
|
||||
raise UserError(_('The sequences of this journal are different for Payments and non-Payments but you selected some of both types.'))
|
||||
values['move_ids'] = [(6, 0, active_move_ids.ids)]
|
||||
return values
|
||||
|
||||
@api.depends('first_name')
|
||||
def _compute_sequence_number_reset(self):
|
||||
for record in self:
|
||||
record.sequence_number_reset = record.move_ids[0]._deduce_sequence_number_reset(record.first_name)
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_first_name(self):
|
||||
self.first_name = ""
|
||||
for record in self:
|
||||
if record.move_ids:
|
||||
record.first_name = min(record.move_ids._origin.mapped(lambda move: move.name or ""))
|
||||
|
||||
@api.depends('new_values', 'ordering')
|
||||
def _compute_preview_moves(self):
|
||||
"""Reduce the computed new_values to a smaller set to display in the preview."""
|
||||
for record in self:
|
||||
new_values = sorted(json.loads(record.new_values).values(), key=lambda x: x['server-date'], reverse=True)
|
||||
changeLines = []
|
||||
in_elipsis = 0
|
||||
previous_line = None
|
||||
for i, line in enumerate(new_values):
|
||||
if i < 3 or i == len(new_values) - 1 or line['new_by_name'] != line['new_by_date'] \
|
||||
or (self.sequence_number_reset == 'year' and line['server-date'][0:4] != previous_line['server-date'][0:4])\
|
||||
or (self.sequence_number_reset == 'year_range' and line['server-year-start-date'][0:4] != previous_line['server-year-start-date'][0:4])\
|
||||
or (self.sequence_number_reset == 'month' and line['server-date'][0:7] != previous_line['server-date'][0:7]):
|
||||
if in_elipsis:
|
||||
changeLines.append({'id': 'other_' + str(line['id']), 'current_name': _('... (%s other)', in_elipsis), 'new_by_name': '...', 'new_by_date': '...', 'date': '...'})
|
||||
in_elipsis = 0
|
||||
changeLines.append(line)
|
||||
else:
|
||||
in_elipsis += 1
|
||||
previous_line = line
|
||||
|
||||
record.preview_moves = json.dumps({
|
||||
'ordering': record.ordering,
|
||||
'changeLines': changeLines,
|
||||
})
|
||||
|
||||
@api.depends('first_name', 'move_ids', 'sequence_number_reset')
|
||||
def _compute_new_values(self):
|
||||
"""Compute the proposed new values.
|
||||
|
||||
Sets a json string on new_values representing a dictionary thats maps account.move
|
||||
ids to a disctionay containing the name if we execute the action, and information
|
||||
relative to the preview widget.
|
||||
"""
|
||||
def _get_move_key(move_id):
|
||||
company = move_id.company_id
|
||||
year_start, year_end = get_fiscal_year(move_id.date, day=company.fiscalyear_last_day, month=int(company.fiscalyear_last_month))
|
||||
if self.sequence_number_reset == 'year':
|
||||
return move_id.date.year
|
||||
elif self.sequence_number_reset == 'year_range':
|
||||
return "%s-%s"%(year_start.year, year_end.year)
|
||||
elif self.sequence_number_reset == 'month':
|
||||
return (move_id.date.year, move_id.date.month)
|
||||
return 'default'
|
||||
|
||||
self.new_values = "{}"
|
||||
for record in self.filtered('first_name'):
|
||||
moves_by_period = defaultdict(lambda: record.env['account.move'])
|
||||
for move in record.move_ids._origin: # Sort the moves by period depending on the sequence number reset
|
||||
moves_by_period[_get_move_key(move)] += move
|
||||
|
||||
seq_format, format_values = record.move_ids[0]._get_sequence_format_param(record.first_name)
|
||||
sequence_number_reset = record.move_ids[0]._deduce_sequence_number_reset(record.first_name)
|
||||
|
||||
new_values = {}
|
||||
for j, period_recs in enumerate(moves_by_period.values()):
|
||||
# compute the new values period by period
|
||||
year_start, year_end = period_recs[0]._get_sequence_date_range(sequence_number_reset)
|
||||
for move in period_recs:
|
||||
new_values[move.id] = {
|
||||
'id': move.id,
|
||||
'current_name': move.name,
|
||||
'state': move.state,
|
||||
'date': format_date(self.env, move.date),
|
||||
'server-date': str(move.date),
|
||||
'server-year-start-date': str(year_start),
|
||||
}
|
||||
|
||||
new_name_list = [seq_format.format(**{
|
||||
**format_values,
|
||||
'month': year_start.month,
|
||||
'year_end': year_end.year % (10 ** format_values['year_end_length']),
|
||||
'year': year_start.year % (10 ** format_values['year_length']),
|
||||
'seq': i + (format_values['seq'] if j == (len(moves_by_period)-1) else 1),
|
||||
}) for i in range(len(period_recs))]
|
||||
|
||||
# For all the moves of this period, assign the name by increasing initial name
|
||||
for move, new_name in zip(period_recs.sorted(lambda m: (m.sequence_prefix, m.sequence_number)), new_name_list):
|
||||
new_values[move.id]['new_by_name'] = new_name
|
||||
# For all the moves of this period, assign the name by increasing date
|
||||
for move, new_name in zip(period_recs.sorted(lambda m: (m.date, m.name or "", m.id)), new_name_list):
|
||||
new_values[move.id]['new_by_date'] = new_name
|
||||
|
||||
record.new_values = json.dumps(new_values)
|
||||
|
||||
def resequence(self):
|
||||
new_values = json.loads(self.new_values)
|
||||
if self.move_ids.journal_id and self.move_ids.journal_id.restrict_mode_hash_table:
|
||||
if self.ordering == 'date':
|
||||
raise UserError(_('You can not reorder sequence by date when the journal is locked with a hash.'))
|
||||
moves_to_rename = self.env['account.move'].browse(int(k) for k in new_values.keys())
|
||||
moves_to_rename.name = '/'
|
||||
moves_to_rename.flush_recordset(["name"])
|
||||
# If the db is not forcibly updated, the temporary renaming could only happen in cache and still trigger the constraint
|
||||
|
||||
for move_id in self.move_ids:
|
||||
if str(move_id.id) in new_values:
|
||||
if self.ordering == 'keep':
|
||||
move_id.name = new_values[str(move_id.id)]['new_by_name']
|
||||
else:
|
||||
move_id.name = new_values[str(move_id.id)]['new_by_date']
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="account_resequence_view" model="ir.ui.view">
|
||||
<field name="name">Re-sequence Journal Entries</field>
|
||||
<field name="model">account.resequence.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Re-Sequence">
|
||||
<field name="move_ids" invisible="1"/>
|
||||
<field name="new_values" invisible="1"/>
|
||||
<field name="sequence_number_reset" invisible="1"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="ordering" widget="radio"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="first_name"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<label for="preview_moves" string="Preview Modifications" colspan="2"/>
|
||||
<field name="preview_moves" widget="account_resequence_widget" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Confirm" name="resequence" type="object" default_focus="1" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="action_account_resequence" model="ir.actions.act_window">
|
||||
<field name="name">Resequence</field>
|
||||
<field name="res_model">account.resequence.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="account_resequence_view"/>
|
||||
<field name="target">new</field>
|
||||
<field name="groups_id" eval="[(6, 0, [ref('base.group_no_one')])]"/>
|
||||
<field name="binding_model_id" ref="account.model_account_move" />
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _, Command, tools
|
||||
import base64
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class AccountTourUploadBill(models.TransientModel):
|
||||
_name = 'account.tour.upload.bill'
|
||||
_description = 'Account tour upload bill'
|
||||
|
||||
attachment_ids = fields.Many2many(
|
||||
comodel_name='ir.attachment',
|
||||
relation='account_tour_upload_bill_ir_attachments_rel',
|
||||
string='Attachments')
|
||||
|
||||
selection = fields.Selection(
|
||||
selection=lambda self: self._selection_values(),
|
||||
default="sample"
|
||||
)
|
||||
|
||||
preview_invoice = fields.Html(
|
||||
compute="_compute_preview_invoice",
|
||||
string="Invoice Preview",
|
||||
translate=True,
|
||||
)
|
||||
|
||||
def _compute_preview_invoice(self):
|
||||
invoice_date = fields.Date.today() - timedelta(days=12)
|
||||
addr = [x for x in [
|
||||
self.env.company.street,
|
||||
self.env.company.street2,
|
||||
' '.join([x for x in [self.env.company.state_id.name, self.env.company.zip] if x]),
|
||||
self.env.company.country_id.name,
|
||||
] if x]
|
||||
ref = 'INV/%s/0001' % invoice_date.strftime('%Y/%m')
|
||||
html = self.env['ir.qweb']._render('account.bill_preview', {
|
||||
'company_name': self.env.company.name,
|
||||
'company_street_address': addr,
|
||||
'invoice_name': 'Invoice ' + ref,
|
||||
'invoice_ref': ref,
|
||||
'invoice_date': invoice_date,
|
||||
'invoice_due_date': invoice_date + timedelta(days=30),
|
||||
})
|
||||
for record in self:
|
||||
record.preview_invoice = html
|
||||
|
||||
def _selection_values(self):
|
||||
journal_alias = self.env['account.journal'] \
|
||||
.search([('type', '=', 'purchase'), ('company_id', '=', self.env.company.id)], limit=1)
|
||||
|
||||
values = [('sample', _('Try a sample vendor bill')), ('upload', _('Upload your own bill'))]
|
||||
if journal_alias.alias_name and journal_alias.alias_domain:
|
||||
values.append(('email', _('Or send a bill to %s@%s', journal_alias.alias_name, journal_alias.alias_domain)))
|
||||
return values
|
||||
|
||||
def _action_list_view_bill(self, bill_ids=[]):
|
||||
context = dict(self._context)
|
||||
context['default_move_type'] = 'in_invoice'
|
||||
return {
|
||||
'name': _('Generated Documents'),
|
||||
'domain': [('id', 'in', bill_ids)],
|
||||
'view_mode': 'tree,form',
|
||||
'res_model': 'account.move',
|
||||
'views': [[False, "tree"], [False, "form"]],
|
||||
'type': 'ir.actions.act_window',
|
||||
'context': context
|
||||
}
|
||||
|
||||
def apply(self):
|
||||
if self._context.get('active_model') == 'account.journal' and self._context.get('active_ids'):
|
||||
purchase_journal = self.env['account.journal'].browse(self._context['active_ids'])
|
||||
else:
|
||||
purchase_journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1)
|
||||
|
||||
if self.selection == 'upload':
|
||||
return purchase_journal.with_context(default_journal_id=purchase_journal.id, default_move_type='in_invoice').create_document_from_attachment(attachment_ids=self.attachment_ids.ids)
|
||||
elif self.selection == 'sample':
|
||||
invoice_date = fields.Date.today() - timedelta(days=12)
|
||||
partner = self.env['res.partner'].search([('name', '=', 'Deco Addict')], limit=1)
|
||||
if not partner:
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Deco Addict',
|
||||
'is_company': True,
|
||||
})
|
||||
bill = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': partner.id,
|
||||
'ref': 'INV/%s/0001' % invoice_date.strftime('%Y/%m'),
|
||||
'invoice_date': invoice_date,
|
||||
'invoice_date_due': invoice_date + timedelta(days=30),
|
||||
'journal_id': purchase_journal.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': "[FURN_8999] Three-Seat Sofa",
|
||||
'quantity': 5,
|
||||
'price_unit': 1500,
|
||||
}),
|
||||
Command.create({
|
||||
'name': "[FURN_8220] Four Person Desk",
|
||||
'quantity': 5,
|
||||
'price_unit': 2350,
|
||||
})
|
||||
],
|
||||
})
|
||||
# In case of test environment, don't create the pdf
|
||||
if tools.config['test_enable'] or tools.config['test_file']:
|
||||
bill.with_context(no_new_invoice=True).message_post()
|
||||
else:
|
||||
bodies = self.env['ir.actions.report']._prepare_html(self.preview_invoice)[0]
|
||||
content = self.env['ir.actions.report']._run_wkhtmltopdf(bodies)
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'type': 'binary',
|
||||
'name': 'INV-%s-0001.pdf' % invoice_date.strftime('%Y-%m'),
|
||||
'res_model': 'mail.compose.message',
|
||||
'datas': base64.encodebytes(content),
|
||||
})
|
||||
bill.with_context(no_new_invoice=True).message_post(attachment_ids=[attachment.id])
|
||||
|
||||
return self._action_list_view_bill(bill.ids)
|
||||
else:
|
||||
email_alias = '%s@%s' % (purchase_journal.alias_name, purchase_journal.alias_domain)
|
||||
new_wizard = self.env['account.tour.upload.bill.email.confirm'].create({'email_alias': email_alias})
|
||||
view_id = self.env.ref('account.account_tour_upload_bill_email_confirm').id
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Confirm'),
|
||||
'view_mode': 'form',
|
||||
'res_model': 'account.tour.upload.bill.email.confirm',
|
||||
'target': 'new',
|
||||
'res_id': new_wizard.id,
|
||||
'views': [[view_id, 'form']],
|
||||
}
|
||||
|
||||
|
||||
class AccountTourUploadBillEmailConfirm(models.TransientModel):
|
||||
_name = 'account.tour.upload.bill.email.confirm'
|
||||
_description = 'Account tour upload bill email confirm'
|
||||
|
||||
email_alias = fields.Char(readonly=True)
|
||||
|
||||
def apply(self):
|
||||
purchase_journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1)
|
||||
bill_ids = self.env['account.move'].search([('journal_id', '=', purchase_journal.id)]).ids
|
||||
return self.env['account.tour.upload.bill']._action_list_view_bill(bill_ids)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="account_tour_upload_bill" model="ir.ui.view">
|
||||
<field name="name">account.tour.upload.bill</field>
|
||||
<field name="model">account.tour.upload.bill</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<h2>With Odoo, you won't have to record bills manually</h2>
|
||||
<p>We process bills automatically so that you only have to validate them. Choose how you want to test our artificial intelligence engine:</p>
|
||||
<group col="6">
|
||||
<group colspan="2">
|
||||
<field name="selection" widget="radio" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<group colspan="4" attrs="{'invisible': [('selection', '!=', 'sample')]}">
|
||||
<field name="preview_invoice" widget="html" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<group colspan="4" attrs="{'invisible': [('selection', '!=', 'upload')]}">
|
||||
<field name="attachment_ids" widget="many2many_binary" string="Attach a file" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Continue" type="object" name="apply" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_tour_upload_bill_email_confirm" model="ir.ui.view">
|
||||
<field name="name">account.tour.upload.bill.email.confirm</field>
|
||||
<field name="model">account.tour.upload.bill.email.confirm</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<p>Send your email to <field name="email_alias" class="oe_inline"/> with a pdf of an invoice as attachment.</p>
|
||||
<p>Once done, press continue.</p>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Continue" type="object" name="apply" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
from odoo import models, api
|
||||
|
||||
|
||||
class AccountUnreconcile(models.TransientModel):
|
||||
_name = "account.unreconcile"
|
||||
_description = "Account Unreconcile"
|
||||
|
||||
def trans_unrec(self):
|
||||
context = dict(self._context or {})
|
||||
if context.get('active_ids', False):
|
||||
self.env['account.move.line'].browse(context.get('active_ids')).remove_move_reconcile()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="account_unreconcile_view" model="ir.ui.view">
|
||||
<field name="name">Unreconcile Entries</field>
|
||||
<field name="model">account.unreconcile</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Unreconcile">
|
||||
<separator string="Unreconcile Transactions"/>
|
||||
<form class="o_form_label">If you unreconcile transactions, you must also verify all the actions that are linked to those transactions because they will not be disabled</form>
|
||||
<footer>
|
||||
<button string="Unreconcile" name="trans_unrec" type="object" default_focus="1" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_unreconcile" model="ir.actions.act_window">
|
||||
<field name="name">Unreconcile</field>
|
||||
<field name="groups_id" eval="[(4, ref('account.group_account_user'))]"/>
|
||||
<field name="res_model">account.unreconcile</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="account_unreconcile_view"/>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="account.model_account_move_line" />
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from odoo import models, fields, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ValidateAccountMove(models.TransientModel):
|
||||
_name = "validate.account.move"
|
||||
_description = "Validate Account Move"
|
||||
|
||||
force_post = fields.Boolean(string="Force", help="Entries in the future are set to be auto-posted by default. Check this checkbox to post them now.")
|
||||
|
||||
def validate_move(self):
|
||||
if self._context.get('active_model') == 'account.move':
|
||||
domain = [('id', 'in', self._context.get('active_ids', [])), ('state', '=', 'draft')]
|
||||
elif self._context.get('active_model') == 'account.journal':
|
||||
domain = [('journal_id', '=', self._context.get('active_id')), ('state', '=', 'draft')]
|
||||
else:
|
||||
raise UserError(_("Missing 'active_model' in context."))
|
||||
|
||||
moves = self.env['account.move'].search(domain).filtered('line_ids')
|
||||
if not moves:
|
||||
raise UserError(_('There are no journal items in the draft state to post.'))
|
||||
if self.force_post:
|
||||
moves.auto_post = 'no'
|
||||
moves._post(not self.force_post)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!--Account Move lines-->
|
||||
<record id="validate_account_move_view" model="ir.ui.view">
|
||||
<field name="name">Post Journal Entries</field>
|
||||
<field name="model">validate.account.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Post Journal Entries">
|
||||
<group>
|
||||
<field name="force_post"/>
|
||||
</group>
|
||||
<span class="o_form_label">All selected journal entries will be validated and posted. You won't be able to modify them afterwards.</span>
|
||||
<footer>
|
||||
<button string="Post Journal Entries"
|
||||
name="validate_move"
|
||||
type="object"
|
||||
default_focus="1"
|
||||
class="btn-primary"
|
||||
data-hotkey="q"
|
||||
context="{'validate_analytic': True}"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_validate_account_move" model="ir.actions.act_window">
|
||||
<field name="name">Post entries</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">validate.account.move</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="validate_account_move_view"/>
|
||||
<field name="context">{}</field>
|
||||
<field name="target">new</field>
|
||||
<field name="help">This wizard will validate all journal entries selected. Once journal entries are validated, you can not update them anymore.</field>
|
||||
<field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/>
|
||||
<field name="binding_model_id" ref="account.model_account_move" />
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
279
odoo-bringout-oca-ocb-account/account/wizard/accrued_orders.py
Normal file
279
odoo-bringout-oca-ocb-account/account/wizard/accrued_orders.py
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import json
|
||||
from odoo import models, fields, api, _, Command
|
||||
from odoo.tools import format_date
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import date_utils
|
||||
from odoo.tools.misc import formatLang
|
||||
|
||||
class AccruedExpenseRevenue(models.TransientModel):
|
||||
_name = 'account.accrued.orders.wizard'
|
||||
_description = 'Accrued Orders Wizard'
|
||||
|
||||
def _get_account_domain(self):
|
||||
if self.env.context.get('active_model') == 'purchase.order':
|
||||
return [('account_type', '=', 'liability_current'), ('company_id', '=', self._get_default_company())]
|
||||
else:
|
||||
return [('account_type', '=', 'asset_current'), ('company_id', '=', self._get_default_company())]
|
||||
|
||||
def _get_default_company(self):
|
||||
if not self._context.get('active_model'):
|
||||
return
|
||||
orders = self.env[self._context['active_model']].browse(self._context['active_ids'])
|
||||
return orders and orders[0].company_id.id
|
||||
|
||||
def _get_default_journal(self):
|
||||
return self.env['account.journal'].search([('company_id', '=', self.env.company.id), ('type', '=', 'general')], limit=1)
|
||||
|
||||
def _get_default_date(self):
|
||||
return date_utils.get_month(fields.Date.context_today(self))[0] - relativedelta(days=1)
|
||||
|
||||
company_id = fields.Many2one('res.company', default=_get_default_company)
|
||||
journal_id = fields.Many2one(
|
||||
comodel_name='account.journal',
|
||||
compute='_compute_journal_id',
|
||||
domain="[('type', '=', 'general'), ('company_id', '=', company_id)]",
|
||||
readonly=False,
|
||||
required=True,
|
||||
default=_get_default_journal,
|
||||
check_company=True,
|
||||
company_dependent=True,
|
||||
string='Journal',
|
||||
)
|
||||
date = fields.Date(default=_get_default_date, required=True)
|
||||
reversal_date = fields.Date(
|
||||
compute="_compute_reversal_date",
|
||||
required=True,
|
||||
readonly=False,
|
||||
store=True,
|
||||
precompute=True,
|
||||
)
|
||||
amount = fields.Monetary(string='Amount', help="Specify an arbitrary value that will be accrued on a \
|
||||
default account for the entire order, regardless of the products on the different lines.")
|
||||
currency_id = fields.Many2one(related='company_id.currency_id', string='Company Currency',
|
||||
readonly=True, store=True,
|
||||
help='Utility field to express amount currency')
|
||||
account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
required=True,
|
||||
string='Accrual Account',
|
||||
check_company=True,
|
||||
domain=_get_account_domain,
|
||||
)
|
||||
preview_data = fields.Text(compute='_compute_preview_data')
|
||||
display_amount = fields.Boolean(compute='_compute_display_amount')
|
||||
|
||||
@api.depends('date', 'amount')
|
||||
def _compute_display_amount(self):
|
||||
single_order = len(self._context['active_ids']) == 1
|
||||
for record in self:
|
||||
preview_data = json.loads(self.preview_data)
|
||||
lines = preview_data.get('groups_vals', [])[0].get('items_vals', [])
|
||||
record.display_amount = record.amount or (single_order and not lines)
|
||||
|
||||
@api.depends('date')
|
||||
def _compute_reversal_date(self):
|
||||
for record in self:
|
||||
if not record.reversal_date or record.reversal_date <= record.date:
|
||||
record.reversal_date = record.date + relativedelta(days=1)
|
||||
else:
|
||||
record.reversal_date = record.reversal_date
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_journal_id(self):
|
||||
journal = self.env['account.journal'].search(
|
||||
[('type', '=', 'general'), ('company_id', '=', self.company_id.id)], limit=1
|
||||
)
|
||||
for record in self:
|
||||
record.journal_id = journal
|
||||
|
||||
@api.depends('date', 'journal_id', 'account_id', 'amount')
|
||||
def _compute_preview_data(self):
|
||||
for record in self:
|
||||
preview_vals = [self.env['account.move']._move_dict_to_preview_vals(
|
||||
record._compute_move_vals()[0],
|
||||
record.company_id.currency_id,
|
||||
)]
|
||||
preview_columns = [
|
||||
{'field': 'account_id', 'label': _('Account')},
|
||||
{'field': 'name', 'label': _('Label')},
|
||||
{'field': 'debit', 'label': _('Debit'), 'class': 'text-end text-nowrap'},
|
||||
{'field': 'credit', 'label': _('Credit'), 'class': 'text-end text-nowrap'},
|
||||
]
|
||||
record.preview_data = json.dumps({
|
||||
'groups_vals': preview_vals,
|
||||
'options': {
|
||||
'columns': preview_columns,
|
||||
},
|
||||
})
|
||||
|
||||
def _get_computed_account(self, order, product, is_purchase):
|
||||
accounts = product.with_company(order.company_id).product_tmpl_id.get_product_accounts(fiscal_pos=order.fiscal_position_id)
|
||||
if is_purchase:
|
||||
return accounts['expense']
|
||||
else:
|
||||
return accounts['income']
|
||||
|
||||
def _compute_move_vals(self):
|
||||
def _get_aml_vals(order, balance, amount_currency, account_id, label="", analytic_distribution=None):
|
||||
if not is_purchase:
|
||||
balance *= -1
|
||||
amount_currency *= -1
|
||||
values = {
|
||||
'name': label,
|
||||
'debit': balance if balance > 0 else 0.0,
|
||||
'credit': balance * -1 if balance < 0 else 0.0,
|
||||
'account_id': account_id,
|
||||
}
|
||||
if analytic_distribution:
|
||||
values.update({
|
||||
'analytic_distribution': analytic_distribution,
|
||||
})
|
||||
if len(order) == 1 and self.company_id.currency_id != order.currency_id:
|
||||
values.update({
|
||||
'amount_currency': amount_currency,
|
||||
'currency_id': order.currency_id.id,
|
||||
})
|
||||
return values
|
||||
|
||||
def _ellipsis(string, size):
|
||||
if len(string) > size:
|
||||
return string[0:size - 3] + '...'
|
||||
return string
|
||||
|
||||
self.ensure_one()
|
||||
move_lines = []
|
||||
is_purchase = self.env.context.get('active_model') == 'purchase.order'
|
||||
orders = self.env[self._context['active_model']].with_company(self.company_id).browse(self._context['active_ids'])
|
||||
|
||||
if orders.filtered(lambda o: o.company_id != self.company_id):
|
||||
raise UserError(_('Entries can only be created for a single company at a time.'))
|
||||
if orders.currency_id and len(orders.currency_id) > 1:
|
||||
raise UserError(_('Cannot create an accrual entry with orders in different currencies.'))
|
||||
orders_with_entries = []
|
||||
fnames = []
|
||||
total_balance = 0.0
|
||||
for order in orders:
|
||||
product_lines = order.order_line.filtered(lambda x: x.product_id)
|
||||
if len(orders) == 1 and product_lines and self.amount and order.order_line:
|
||||
total_balance = self.amount
|
||||
order_line = product_lines[0]
|
||||
account = self._get_computed_account(order, order_line.product_id, is_purchase)
|
||||
distribution = order_line.analytic_distribution if order_line.analytic_distribution else {}
|
||||
if not is_purchase and order.analytic_account_id:
|
||||
analytic_account_id = str(order.analytic_account_id.id)
|
||||
distribution[analytic_account_id] = distribution.get(analytic_account_id, 0) + 100.0
|
||||
values = _get_aml_vals(order, self.amount, 0, account.id, label=_('Manual entry'), analytic_distribution=distribution)
|
||||
move_lines.append(Command.create(values))
|
||||
else:
|
||||
other_currency = self.company_id.currency_id != order.currency_id
|
||||
rate = order.currency_id._get_rates(self.company_id, self.date).get(order.currency_id.id) if other_currency else 1.0
|
||||
# create a virtual order that will allow to recompute the qty delivered/received (and dependancies)
|
||||
# without actually writing anything on the real record (field is computed and stored)
|
||||
o = order.new(origin=order)
|
||||
if is_purchase:
|
||||
o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_received()
|
||||
o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_invoiced()
|
||||
else:
|
||||
o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_delivered()
|
||||
o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_invoiced()
|
||||
o.order_line.with_context(accrual_entry_date=self.date)._compute_untaxed_amount_invoiced()
|
||||
o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_to_invoice()
|
||||
lines = o.order_line.filtered(
|
||||
lambda l: l.display_type not in ['line_section', 'line_note'] and
|
||||
fields.Float.compare(
|
||||
l.qty_to_invoice,
|
||||
0,
|
||||
precision_rounding=l.product_uom.rounding,
|
||||
) != 0
|
||||
)
|
||||
for order_line in lines:
|
||||
if is_purchase:
|
||||
account = self._get_computed_account(order, order_line.product_id, is_purchase)
|
||||
if any(tax.price_include for tax in order_line.taxes_id):
|
||||
# As included taxes are not taken into account in the price_unit, we need to compute the price_subtotal
|
||||
price_subtotal = order_line.taxes_id.compute_all(
|
||||
order_line.price_unit,
|
||||
currency=order_line.order_id.currency_id,
|
||||
quantity=order_line.qty_to_invoice,
|
||||
product=order_line.product_id,
|
||||
partner=order_line.order_id.partner_id)['total_excluded']
|
||||
else:
|
||||
price_subtotal = order_line.qty_to_invoice * order_line.price_unit
|
||||
amount = self.company_id.currency_id.round(price_subtotal / rate)
|
||||
amount_currency = order_line.currency_id.round(price_subtotal)
|
||||
fnames = ['qty_to_invoice', 'qty_received', 'qty_invoiced', 'invoice_lines']
|
||||
label = _('%s - %s; %s Billed, %s Received at %s each', order.name, _ellipsis(order_line.name, 20), order_line.qty_invoiced, order_line.qty_received, formatLang(self.env, order_line.price_unit, currency_obj=order.currency_id))
|
||||
else:
|
||||
account = self._get_computed_account(order, order_line.product_id, is_purchase)
|
||||
amount = self.company_id.currency_id.round(order_line.untaxed_amount_to_invoice / rate)
|
||||
amount_currency = order_line.untaxed_amount_to_invoice
|
||||
fnames = ['qty_to_invoice', 'untaxed_amount_to_invoice', 'qty_invoiced', 'qty_delivered', 'invoice_lines']
|
||||
label = _('%s - %s; %s Invoiced, %s Delivered at %s each', order.name, _ellipsis(order_line.name, 20), order_line.qty_invoiced, order_line.qty_delivered, formatLang(self.env, order_line.price_unit, currency_obj=order.currency_id))
|
||||
distribution = order_line.analytic_distribution if order_line.analytic_distribution else {}
|
||||
if not is_purchase and order.analytic_account_id:
|
||||
analytic_account_id = str(order.analytic_account_id.id)
|
||||
distribution[analytic_account_id] = distribution.get(analytic_account_id, 0) + 100.0
|
||||
values = _get_aml_vals(order, amount, amount_currency, account.id, label=label, analytic_distribution=distribution)
|
||||
move_lines.append(Command.create(values))
|
||||
total_balance += amount
|
||||
# must invalidate cache or o can mess when _create_invoices().action_post() of original order after this
|
||||
order.order_line.invalidate_model(fnames)
|
||||
|
||||
if not self.company_id.currency_id.is_zero(total_balance):
|
||||
# globalized counterpart for the whole orders selection
|
||||
analytic_distribution = {}
|
||||
total = sum(order.amount_total for order in orders)
|
||||
for line in orders.order_line:
|
||||
ratio = line.price_total / total
|
||||
if not is_purchase and line.order_id.analytic_account_id:
|
||||
account_id = str(line.order_id.analytic_account_id.id)
|
||||
analytic_distribution.update({account_id: analytic_distribution.get(account_id, 0) +100.0*ratio})
|
||||
if not line.analytic_distribution:
|
||||
continue
|
||||
for account_id, distribution in line.analytic_distribution.items():
|
||||
analytic_distribution.update({account_id : analytic_distribution.get(account_id, 0) + distribution*ratio})
|
||||
values = _get_aml_vals(orders, -total_balance, 0.0, self.account_id.id, label=_('Accrued total'), analytic_distribution=analytic_distribution)
|
||||
move_lines.append(Command.create(values))
|
||||
|
||||
move_type = _('Expense') if is_purchase else _('Revenue')
|
||||
move_vals = {
|
||||
'ref': _('Accrued %s entry as of %s', move_type, format_date(self.env, self.date)),
|
||||
'journal_id': self.journal_id.id,
|
||||
'date': self.date,
|
||||
'line_ids': move_lines,
|
||||
'currency_id': orders.currency_id.id or self.company_id.currency_id.id,
|
||||
}
|
||||
return move_vals, orders_with_entries
|
||||
|
||||
def create_entries(self):
|
||||
self.ensure_one()
|
||||
|
||||
if self.reversal_date <= self.date:
|
||||
raise UserError(_('Reversal date must be posterior to date.'))
|
||||
move_vals, orders_with_entries = self._compute_move_vals()
|
||||
move = self.env['account.move'].create(move_vals)
|
||||
move._post()
|
||||
reverse_move = move._reverse_moves(default_values_list=[{
|
||||
'ref': _('Reversal of: %s', move.ref),
|
||||
'date': self.reversal_date,
|
||||
}])
|
||||
reverse_move._post()
|
||||
for order in orders_with_entries:
|
||||
body = _(
|
||||
'Accrual entry created on %(date)s: %(accrual_entry)s.\
|
||||
And its reverse entry: %(reverse_entry)s.',
|
||||
date=self.date,
|
||||
accrual_entry=move._get_html_link(),
|
||||
reverse_entry=reverse_move._get_html_link(),
|
||||
)
|
||||
order.message_post(body=body)
|
||||
return {
|
||||
'name': _('Accrual Moves'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('id', 'in', (move.id, reverse_move.id))],
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_account_accrued_orders_wizard" model="ir.ui.view">
|
||||
<field name="name">account.accrued.orders.wizard.view</field>
|
||||
<field name="model">account.accrued.orders.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Make Accrual Entries">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<group>
|
||||
<div class="alert alert-info" colspan="4" role="alert" attrs="{'invisible': [('display_amount', '!=', True)]}">
|
||||
There doesn't appear to be anything to invoice for the selected order. However, you can use the amount field to force an accrual entry.
|
||||
</div>
|
||||
<group>
|
||||
<field name="journal_id"/>
|
||||
<field name="account_id"/>
|
||||
<field name="amount" attrs="{'invisible': [('display_amount', '!=', True)]}"/>
|
||||
<field name="display_amount" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
<field name="reversal_date"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="preview_data" widget="grouped_view_widget" class="w-100"/>
|
||||
<footer>
|
||||
<button string='Create Entry' name="create_entries" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class BaseDocumentLayout(models.TransientModel):
|
||||
_inherit = 'base.document.layout'
|
||||
|
||||
def document_layout_save(self):
|
||||
res = super(BaseDocumentLayout, self).document_layout_save()
|
||||
for wizard in self:
|
||||
wizard.company_id.action_save_onboarding_invoice_layout()
|
||||
return res
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class MergePartnerAutomatic(models.TransientModel):
|
||||
_inherit = 'base.partner.merge.automatic.wizard'
|
||||
|
||||
def _get_summable_fields(self):
|
||||
"""Add to summable fields list, fields created in this module.
|
||||
- customer_rank and supplier_rank will have a better ranking for the merged partner
|
||||
"""
|
||||
return super()._get_summable_fields() + ['customer_rank', 'supplier_rank']
|
||||
149
odoo-bringout-oca-ocb-account/account/wizard/setup_wizards.py
Normal file
149
odoo-bringout-oca-ocb-account/account/wizard/setup_wizards.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class FinancialYearOpeningWizard(models.TransientModel):
|
||||
_name = 'account.financial.year.op'
|
||||
_description = 'Opening Balance of Financial Year'
|
||||
|
||||
company_id = fields.Many2one(comodel_name='res.company', required=True)
|
||||
opening_move_posted = fields.Boolean(string='Opening Move Posted', compute='_compute_opening_move_posted')
|
||||
opening_date = fields.Date(string='Opening Date', required=True, related='company_id.account_opening_date', help="Date from which the accounting is managed in Odoo. It is the date of the opening entry.", readonly=False)
|
||||
fiscalyear_last_day = fields.Integer(related="company_id.fiscalyear_last_day", required=True, readonly=False,
|
||||
help="The last day of the month will be used if the chosen day doesn't exist.")
|
||||
fiscalyear_last_month = fields.Selection(related="company_id.fiscalyear_last_month", readonly=False,
|
||||
required=True,
|
||||
help="The last day of the month will be used if the chosen day doesn't exist.")
|
||||
|
||||
@api.depends('company_id.account_opening_move_id')
|
||||
def _compute_opening_move_posted(self):
|
||||
for record in self:
|
||||
record.opening_move_posted = record.company_id.opening_move_posted()
|
||||
|
||||
@api.constrains('fiscalyear_last_day', 'fiscalyear_last_month')
|
||||
def _check_fiscalyear(self):
|
||||
# We try if the date exists in 2020, which is a leap year.
|
||||
# We do not define the constrain on res.company, since the recomputation of the related
|
||||
# fields is done one field at a time.
|
||||
for wiz in self:
|
||||
try:
|
||||
date(2020, int(wiz.fiscalyear_last_month), wiz.fiscalyear_last_day)
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
_('Incorrect fiscal year date: day is out of range for month. Month: %s; Day: %s') %
|
||||
(wiz.fiscalyear_last_month, wiz.fiscalyear_last_day)
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
# Amazing workaround: non-stored related fields on company are a BAD idea since the 3 fields
|
||||
# must follow the constraint '_check_fiscalyear_last_day'. The thing is, in case of related
|
||||
# fields, the inverse write is done one value at a time, and thus the constraint is verified
|
||||
# one value at a time... so it is likely to fail.
|
||||
for wiz in self:
|
||||
wiz.company_id.write({
|
||||
'fiscalyear_last_day': vals.get('fiscalyear_last_day') or wiz.company_id.fiscalyear_last_day,
|
||||
'fiscalyear_last_month': vals.get('fiscalyear_last_month') or wiz.company_id.fiscalyear_last_month,
|
||||
'account_opening_date': vals.get('opening_date') or wiz.company_id.account_opening_date,
|
||||
})
|
||||
wiz.company_id.account_opening_move_id.write({
|
||||
'date': fields.Date.from_string(vals.get('opening_date') or wiz.company_id.account_opening_date) - timedelta(days=1),
|
||||
})
|
||||
|
||||
vals.pop('opening_date', None)
|
||||
vals.pop('fiscalyear_last_day', None)
|
||||
vals.pop('fiscalyear_last_month', None)
|
||||
return super().write(vals)
|
||||
|
||||
def action_save_onboarding_fiscal_year(self):
|
||||
self.env.company.sudo().set_onboarding_step_done('account_setup_fy_data_state')
|
||||
|
||||
|
||||
class SetupBarBankConfigWizard(models.TransientModel):
|
||||
_inherits = {'res.partner.bank': 'res_partner_bank_id'}
|
||||
_name = 'account.setup.bank.manual.config'
|
||||
_description = 'Bank setup manual config'
|
||||
_check_company_auto = True
|
||||
|
||||
res_partner_bank_id = fields.Many2one(comodel_name='res.partner.bank', ondelete='cascade', required=True)
|
||||
new_journal_name = fields.Char(default=lambda self: self.linked_journal_id.name, inverse='set_linked_journal_id', required=True, help='Will be used to name the Journal related to this bank account')
|
||||
linked_journal_id = fields.Many2one(string="Journal",
|
||||
comodel_name='account.journal', inverse='set_linked_journal_id',
|
||||
compute="_compute_linked_journal_id",
|
||||
domain=lambda self: [('type', '=', 'bank'), ('bank_account_id', '=', False), ('company_id', '=', self.env.company.id)])
|
||||
bank_bic = fields.Char(related='bank_id.bic', readonly=False, string="Bic")
|
||||
num_journals_without_account = fields.Integer(default=lambda self: self._number_unlinked_journal())
|
||||
|
||||
def _number_unlinked_journal(self):
|
||||
return self.env['account.journal'].search([('type', '=', 'bank'), ('bank_account_id', '=', False),
|
||||
('id', '!=', self.default_linked_journal_id())], count=True)
|
||||
|
||||
@api.onchange('acc_number')
|
||||
def _onchange_acc_number(self):
|
||||
for record in self:
|
||||
record.new_journal_name = record.acc_number
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" This wizard is only used to setup an account for the current active
|
||||
company, so we always inject the corresponding partner when creating
|
||||
the model.
|
||||
"""
|
||||
for vals in vals_list:
|
||||
vals['partner_id'] = self.env.company.partner_id.id
|
||||
vals['new_journal_name'] = vals['acc_number']
|
||||
|
||||
# If no bank has been selected, but we have a bic, we are using it to find or create the bank
|
||||
if not vals['bank_id'] and vals['bank_bic']:
|
||||
vals['bank_id'] = self.env['res.bank'].search([('bic', '=', vals['bank_bic'])], limit=1).id \
|
||||
or self.env['res.bank'].create({'name': vals['bank_bic'], 'bic': vals['bank_bic']}).id
|
||||
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.onchange('linked_journal_id')
|
||||
def _onchange_new_journal_related_data(self):
|
||||
for record in self:
|
||||
if record.linked_journal_id:
|
||||
record.new_journal_name = record.linked_journal_id.name
|
||||
|
||||
@api.depends('journal_id') # Despite its name, journal_id is actually a One2many field
|
||||
def _compute_linked_journal_id(self):
|
||||
for record in self:
|
||||
record.linked_journal_id = record.journal_id and record.journal_id[0] or record.default_linked_journal_id()
|
||||
|
||||
def default_linked_journal_id(self):
|
||||
for journal_id in self.env['account.journal'].search([('type', '=', 'bank'), ('bank_account_id', '=', False)]):
|
||||
empty_journal_count = self.env['account.move'].search_count([('journal_id', '=', journal_id.id)])
|
||||
if empty_journal_count == 0:
|
||||
return journal_id.id
|
||||
return False
|
||||
|
||||
def set_linked_journal_id(self):
|
||||
""" Called when saving the wizard.
|
||||
"""
|
||||
for record in self:
|
||||
selected_journal = record.linked_journal_id
|
||||
if not selected_journal:
|
||||
new_journal_code = self.env['account.journal'].get_next_bank_cash_default_code('bank', self.env.company)
|
||||
company = self.env.company
|
||||
selected_journal = self.env['account.journal'].create({
|
||||
'name': record.new_journal_name,
|
||||
'code': new_journal_code,
|
||||
'type': 'bank',
|
||||
'company_id': company.id,
|
||||
'bank_account_id': record.res_partner_bank_id.id,
|
||||
})
|
||||
else:
|
||||
selected_journal.bank_account_id = record.res_partner_bank_id.id
|
||||
selected_journal.name = record.new_journal_name
|
||||
|
||||
def validate(self):
|
||||
""" Called by the validation button of this wizard. Serves as an
|
||||
extension hook in account_bank_statement_import.
|
||||
"""
|
||||
self.linked_journal_id.mark_bank_setup_as_done_action()
|
||||
return {'type': 'ir.actions.client', 'tag': 'soft_reload'}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="setup_financial_year_opening_form" model="ir.ui.view">
|
||||
<field name="name">account.financial.year.op.setup.wizard.form</field>
|
||||
<field name="model">account.financial.year.op</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Fiscal Years">
|
||||
<field name="opening_move_posted" invisible="1"/>
|
||||
<field name="opening_date" attrs="{'readonly': [('opening_move_posted', '=', True)]}"/>
|
||||
|
||||
<label for="fiscalyear_last_month" string="Fiscal Year End"/>
|
||||
<div>
|
||||
<field name="fiscalyear_last_day" class="text-center me-2" style="width: 20% !important;"/>
|
||||
<field name="fiscalyear_last_month" class="w-75"/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_save_onboarding_fiscal_year" string="Apply"
|
||||
class="oe_highlight" type="object" data-hotkey="q" />
|
||||
<button special="cancel" data-hotkey="z" string="Cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="setup_bank_account_wizard" model="ir.ui.view">
|
||||
<field name="name">account.online.sync.res.partner.bank.setup.form</field>
|
||||
<field name="model">account.setup.bank.manual.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="num_journals_without_account" invisible="1"/>
|
||||
<field name="journal_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="linked_journal_id" invisible="1"/>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="acc_number" placeholder="e.g BE15001559627230"/>
|
||||
<field name="bank_id" placeholder="e.g Bank of America"/>
|
||||
<field name="bank_bic" placeholder="e.g GEBABEBB" string="Bank Identifier Code"/>
|
||||
</group>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('num_journals_without_account', '=', 0)]}">
|
||||
<group>
|
||||
<field name="linked_journal_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<span class="text-muted" colspan="2">
|
||||
Leave empty to create a new journal for this bank account, or select a journal to link it with the bank account.
|
||||
</span>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Create" class="oe_highlight" type="object" name="validate" data-hotkey="q"/>
|
||||
<button string="Cancel" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="init_accounts_tree" model="ir.ui.view">
|
||||
<field name="name">account.setup.opening.move.line.tree</field>
|
||||
<field name="model">account.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="top" create="1" delete="1" decoration-muted="opening_debit == 0 and opening_credit == 0">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="account_type" widget="account_type_selection"/>
|
||||
<field name="reconcile" widget="boolean_toggle"/>
|
||||
<field name="opening_debit" options="{'no_symbol': True}"/>
|
||||
<field name="opening_credit" options="{'no_symbol': True}"/>
|
||||
<field name="opening_balance" optional="hide" options="{'no_symbol': True}"/>
|
||||
<field name="tax_ids" optional="hide" widget="many2many_tags"/>
|
||||
<field name="tag_ids" optional="hide" widget="many2many_tags"/>
|
||||
<field name="allowed_journal_ids" optional="hide" widget="many2many_tags"/>
|
||||
<button name="action_read_account" type="object" string="Setup" class="float-end btn-secondary"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue