forked from Mapan/odoo17e
522 lines
26 KiB
Python
522 lines
26 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import date
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import fields, models, api, _
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.osv import expression
|
|
from odoo.tools import SQL
|
|
from odoo.tools.float_utils import float_compare, float_is_zero
|
|
|
|
|
|
class TransferModel(models.Model):
|
|
_name = "account.transfer.model"
|
|
_description = "Account Transfer Model"
|
|
|
|
# DEFAULTS
|
|
def _get_default_date_start(self):
|
|
company = self.env.company
|
|
return company.compute_fiscalyear_dates(date.today())['date_from'] if company else None
|
|
|
|
def _get_default_journal(self):
|
|
return self.env['account.journal'].search([
|
|
*self.env['account.journal']._check_company_domain(self.env.company),
|
|
('type', '=', 'general'),
|
|
], limit=1)
|
|
|
|
name = fields.Char(required=True)
|
|
journal_id = fields.Many2one('account.journal', required=True, string="Destination Journal", default=_get_default_journal)
|
|
company_id = fields.Many2one('res.company', readonly=True, related='journal_id.company_id')
|
|
date_start = fields.Date(string="Start Date", required=True, default=_get_default_date_start)
|
|
date_stop = fields.Date(string="Stop Date", required=False)
|
|
frequency = fields.Selection([('month', 'Monthly'), ('quarter', 'Quarterly'), ('year', 'Yearly')],
|
|
required=True, default='month')
|
|
account_ids = fields.Many2many('account.account', 'account_model_rel', string="Origin Accounts", domain="[('account_type', '!=', 'off_balance')]")
|
|
line_ids = fields.One2many('account.transfer.model.line', 'transfer_model_id', string="Destination Accounts")
|
|
move_ids = fields.One2many('account.move', 'transfer_model_id', string="Generated Moves")
|
|
move_ids_count = fields.Integer(compute="_compute_move_ids_count")
|
|
total_percent = fields.Float(compute="_compute_total_percent", string="Total Percent", readonly=True)
|
|
state = fields.Selection([('disabled', 'Disabled'), ('in_progress', 'Running')], default='disabled', required=True)
|
|
|
|
def copy(self, default=None):
|
|
default = default or {}
|
|
res = super(TransferModel, self).copy(default)
|
|
res.account_ids += self.account_ids
|
|
for line in self.line_ids:
|
|
line.copy({'transfer_model_id': res.id})
|
|
return res
|
|
|
|
@api.ondelete(at_uninstall=False)
|
|
def _unlink_with_check_moves(self):
|
|
# Only unlink a transfer that has no posted/draft moves attached.
|
|
for transfer in self:
|
|
if transfer.move_ids_count > 0:
|
|
posted_moves = any(move.state == 'posted' for move in transfer.move_ids)
|
|
if posted_moves:
|
|
raise UserError(_("You cannot delete an automatic transfer that has posted moves attached ('%s').", transfer.name))
|
|
draft_moves = any(move.state == 'draft' for move in transfer.move_ids)
|
|
if draft_moves:
|
|
raise UserError(_("You cannot delete an automatic transfer that has draft moves attached ('%s'). "
|
|
"Please delete them before deleting this transfer.", transfer.name))
|
|
|
|
# COMPUTEDS / CONSTRAINS
|
|
@api.depends('move_ids')
|
|
def _compute_move_ids_count(self):
|
|
""" Compute the amount of move ids have been generated by this transfer model. """
|
|
for record in self:
|
|
record.move_ids_count = len(record.move_ids)
|
|
|
|
@api.constrains('line_ids')
|
|
def _check_line_ids_percent(self):
|
|
""" Check that the total percent is not bigger than 100.0 """
|
|
for record in self:
|
|
if not (0 < record.total_percent <= 100.0):
|
|
raise ValidationError(_('The total percentage (%s) should be less or equal to 100!', record.total_percent))
|
|
|
|
@api.constrains('line_ids')
|
|
def _check_line_ids_filters(self):
|
|
""" Check that the filters on the lines make sense """
|
|
for record in self:
|
|
combinations = []
|
|
for line in record.line_ids:
|
|
if line.partner_ids and line.analytic_account_ids:
|
|
for p in line.partner_ids:
|
|
for a in line.analytic_account_ids:
|
|
combination = (p.id, a.id)
|
|
if combination in combinations:
|
|
raise ValidationError(_("The partner filter %s in combination with the analytic filter %s is duplicated", p.display_name, a.display_name))
|
|
combinations.append(combination)
|
|
elif line.partner_ids:
|
|
for p in line.partner_ids:
|
|
combination = (p.id, None)
|
|
if combination in combinations:
|
|
raise ValidationError(_("The partner filter %s is duplicated", p.display_name))
|
|
combinations.append(combination)
|
|
elif line.analytic_account_ids:
|
|
for a in line.analytic_account_ids:
|
|
combination = (None, a.id)
|
|
if combination in combinations:
|
|
raise ValidationError(_("The analytic filter %s is duplicated", a.display_name))
|
|
combinations.append(combination)
|
|
|
|
@api.depends('line_ids')
|
|
def _compute_total_percent(self):
|
|
""" Compute the total percentage of all lines linked to this model. """
|
|
for record in self:
|
|
non_filtered_lines = record.line_ids.filtered(lambda l: not l.partner_ids and not l.analytic_account_ids)
|
|
if record.line_ids and not non_filtered_lines:
|
|
# Lines are only composed of filtered ones thus percentage does not matter, make it 100
|
|
record.total_percent = 100.0
|
|
else:
|
|
total_percent = sum(non_filtered_lines.mapped('percent'))
|
|
if float_compare(total_percent, 100.0, precision_digits=6) == 0:
|
|
total_percent = 100.0
|
|
record.total_percent = total_percent
|
|
|
|
# ACTIONS
|
|
def action_activate(self):
|
|
""" Put this move model in "in progress" state. """
|
|
return self.write({'state': 'in_progress'})
|
|
|
|
def action_disable(self):
|
|
""" Put this move model in "disabled" state. """
|
|
return self.write({'state': 'disabled'})
|
|
|
|
@api.model
|
|
def action_cron_auto_transfer(self):
|
|
""" Perform the automatic transfer for the all active move models. """
|
|
self.search([('state', '=', 'in_progress')]).action_perform_auto_transfer()
|
|
|
|
def action_perform_auto_transfer(self):
|
|
""" Perform the automatic transfer for the current recordset of models """
|
|
for record in self:
|
|
# If no account to ventilate or no account to ventilate into : nothing to do
|
|
if record.account_ids and record.line_ids:
|
|
today = date.today()
|
|
max_date = record.date_stop and min(today, record.date_stop) or today
|
|
start_date = record._determine_start_date()
|
|
next_move_date = record._get_next_move_date(start_date)
|
|
|
|
# (Re)Generate moves in draft untill today
|
|
# Journal entries will be recomputed everyday until posted.
|
|
while next_move_date <= max_date:
|
|
record._create_or_update_move_for_period(start_date, next_move_date)
|
|
start_date = next_move_date + relativedelta(days=1)
|
|
next_move_date = record._get_next_move_date(start_date)
|
|
|
|
# (Re)Generate move for one more period if needed
|
|
if not record.date_stop:
|
|
record._create_or_update_move_for_period(start_date, next_move_date)
|
|
elif today < record.date_stop:
|
|
record._create_or_update_move_for_period(start_date, min(next_move_date, record.date_stop))
|
|
return False
|
|
|
|
def _get_move_lines_base_domain(self, start_date, end_date):
|
|
"""
|
|
Determine the domain to get all account move lines posted in a given period, for an account in origin accounts
|
|
:param start_date: the start date of the period
|
|
:param end_date: the end date of the period
|
|
:return: the computed domain
|
|
:rtype: list
|
|
"""
|
|
self.ensure_one()
|
|
return [
|
|
('account_id', 'in', self.account_ids.ids),
|
|
('date', '>=', start_date),
|
|
('date', '<=', end_date),
|
|
('parent_state', '=', 'posted')
|
|
]
|
|
|
|
# PROTECTEDS
|
|
|
|
def _create_or_update_move_for_period(self, start_date, end_date):
|
|
"""
|
|
Create or update a move for a given period. This means (re)generates all the needed moves to execute the
|
|
transfers
|
|
:param start_date: the start date of the targeted period
|
|
:param end_date: the end date of the targeted period
|
|
:return: the created (or updated) move
|
|
"""
|
|
self.ensure_one()
|
|
current_move = self._get_move_for_period(end_date)
|
|
line_values = self._get_auto_transfer_move_line_values(start_date, end_date)
|
|
if line_values:
|
|
if current_move is None:
|
|
current_move = self.env['account.move'].create({
|
|
'ref': '%s: %s --> %s' % (self.name, str(start_date), str(end_date)),
|
|
'date': end_date,
|
|
'journal_id': self.journal_id.id,
|
|
'transfer_model_id': self.id,
|
|
})
|
|
|
|
line_ids_values = [(0, 0, value) for value in line_values]
|
|
# unlink all old line ids
|
|
current_move.line_ids.unlink()
|
|
# recreate line ids
|
|
current_move.write({'line_ids': line_ids_values})
|
|
return current_move
|
|
|
|
def _get_move_for_period(self, end_date):
|
|
""" Get the generated move for a given period
|
|
:param end_date: the end date of the wished period, do not need the start date as the move will always be
|
|
generated with end date of a period as date
|
|
:return: a recordset containing the move found if any, else None
|
|
"""
|
|
self.ensure_one()
|
|
# Move will always be generated with end_date of a period as date
|
|
domain = [
|
|
('date', '=', end_date),
|
|
('state', '=', 'draft'),
|
|
('transfer_model_id', '=', self.id)
|
|
]
|
|
current_moves = self.env['account.move'].search(domain, limit=1, order="date desc")
|
|
return current_moves[0] if current_moves else None
|
|
|
|
def _determine_start_date(self):
|
|
""" Determine the automatic transfer start date which is the last created move if any or the start date of the model """
|
|
self.ensure_one()
|
|
# Get last generated move date if any (to know when to start)
|
|
last_move_domain = [('transfer_model_id', '=', self.id), ('state', '=', 'posted'), ('company_id', '=', self.company_id.id)]
|
|
move_ids = self.env['account.move'].search(last_move_domain, order='date desc', limit=1)
|
|
return (move_ids[0].date + relativedelta(days=1)) if move_ids else self.date_start
|
|
|
|
def _get_next_move_date(self, date):
|
|
""" Compute the following date of automated transfer move, based on a date and the frequency """
|
|
self.ensure_one()
|
|
if self.frequency == 'month':
|
|
delta = relativedelta(months=1)
|
|
elif self.frequency == 'quarter':
|
|
delta = relativedelta(months=3)
|
|
else:
|
|
delta = relativedelta(years=1)
|
|
return date + delta - relativedelta(days=1)
|
|
|
|
def _get_auto_transfer_move_line_values(self, start_date, end_date):
|
|
""" Get all the transfer move lines values for a given period
|
|
:param start_date: the start date of the period
|
|
:param end_date: the end date of the period
|
|
:return: a list of dict representing the values of lines to create
|
|
:rtype: list
|
|
"""
|
|
self.ensure_one()
|
|
values = []
|
|
# Get the balance of all moves from all selected accounts, grouped by accounts
|
|
filtered_lines = self.line_ids.filtered(lambda x: x.analytic_account_ids or x.partner_ids)
|
|
if filtered_lines:
|
|
values += filtered_lines._get_transfer_move_lines_values(start_date, end_date)
|
|
|
|
non_filtered_lines = self.line_ids - filtered_lines
|
|
if non_filtered_lines:
|
|
values += self._get_non_filtered_auto_transfer_move_line_values(non_filtered_lines, start_date, end_date)
|
|
|
|
return values
|
|
|
|
def _get_non_filtered_auto_transfer_move_line_values(self, lines, start_date, end_date):
|
|
"""
|
|
Get all values to create move lines corresponding to the transfers needed by all lines without analytic
|
|
account or partner for a given period. It contains the move lines concerning destination accounts and
|
|
the ones concerning the origin accounts. This process all the origin accounts one after one.
|
|
:param lines: the move model lines to handle
|
|
:param start_date: the start date of the period
|
|
:param end_date: the end date of the period
|
|
:return: a list of dict representing the values to use to create the needed move lines
|
|
:rtype: list
|
|
"""
|
|
self.ensure_one()
|
|
domain = self._get_move_lines_base_domain(start_date, end_date)
|
|
domain = expression.AND([domain, [('partner_id', 'not in', self.line_ids.partner_ids.ids), ]])
|
|
query = self.env['account.move.line']._search(domain)
|
|
if self.line_ids.analytic_account_ids.ids:
|
|
query.add_where(
|
|
SQL(
|
|
"(NOT %s && %s OR analytic_distribution IS NULL)",
|
|
[str(account_id) for account_id in self.line_ids.analytic_account_ids.ids],
|
|
self.env['account.move.line']._query_analytic_accounts(),
|
|
)
|
|
)
|
|
query_string, query_param = query.select('SUM(balance) AS balance', 'account_id')
|
|
query_string = f"{query_string} GROUP BY account_id ORDER BY account_id"
|
|
self._cr.execute(query_string, query_param)
|
|
# balance = debit - credit
|
|
# --> balance > 0 means a debit so it should be credited on the source account
|
|
# --> balance < 0 means a credit so it should be debited on the source account
|
|
values_list = []
|
|
for total_balance_account in self._cr.dictfetchall():
|
|
initial_amount = abs(total_balance_account['balance'])
|
|
source_account_is_debit = total_balance_account['balance'] >= 0
|
|
account_id = total_balance_account['account_id']
|
|
account = self.env['account.account'].browse(account_id)
|
|
if not float_is_zero(initial_amount, precision_digits=9):
|
|
move_lines_values, amount_left = self._get_non_analytic_transfer_values(account, lines, end_date,
|
|
initial_amount,
|
|
source_account_is_debit)
|
|
|
|
# the line which credit/debit the source account
|
|
substracted_amount = initial_amount - amount_left
|
|
source_move_line = {
|
|
'name': _('Automatic Transfer (-%s%%)', self.total_percent),
|
|
'account_id': account_id,
|
|
'date_maturity': end_date,
|
|
'credit' if source_account_is_debit else 'debit': substracted_amount
|
|
}
|
|
values_list += move_lines_values
|
|
values_list.append(source_move_line)
|
|
return values_list
|
|
|
|
def _get_non_analytic_transfer_values(self, account, lines, write_date, amount, is_debit):
|
|
"""
|
|
Get all values to create destination account move lines corresponding to the transfers needed by all lines
|
|
without analytic account for a given account.
|
|
:param account: the origin account to handle
|
|
:param write_date: the write date of the move lines
|
|
:param amount: the total amount to take care on the origin account
|
|
:type amount: float
|
|
:param is_debit: True if origin account has a debit balance, False if it's a credit
|
|
:type is_debit: bool
|
|
:return: a tuple containing the move lines values in a list and the amount left on the origin account after
|
|
processing as a float
|
|
:rtype: tuple
|
|
"""
|
|
# if total ventilated is 100%
|
|
# then the last line should not compute in % but take the rest
|
|
# else
|
|
# it should compute in % (as the rest will stay on the source account)
|
|
self.ensure_one()
|
|
amount_left = amount
|
|
|
|
take_the_rest = self.total_percent == 100.0
|
|
amount_of_lines = len(lines)
|
|
values_list = []
|
|
|
|
for i, line in enumerate(lines):
|
|
if take_the_rest and i == amount_of_lines - 1:
|
|
line_amount = amount_left
|
|
amount_left = 0
|
|
else:
|
|
currency = self.journal_id.currency_id or self.company_id.currency_id
|
|
line_amount = currency.round((line.percent / 100.0) * amount)
|
|
amount_left -= line_amount
|
|
|
|
move_line = line._get_destination_account_transfer_move_line_values(account, line_amount, is_debit,
|
|
write_date)
|
|
values_list.append(move_line)
|
|
|
|
return values_list, amount_left
|
|
|
|
|
|
class TransferModelLine(models.Model):
|
|
_name = "account.transfer.model.line"
|
|
_description = "Account Transfer Model Line"
|
|
_order = "sequence, id"
|
|
|
|
transfer_model_id = fields.Many2one('account.transfer.model', string="Transfer Model", required=True, ondelete='cascade')
|
|
account_id = fields.Many2one('account.account', string="Destination Account", required=True,
|
|
domain="[('account_type', '!=', 'off_balance')]")
|
|
percent = fields.Float(string="Percent", required=True, default=100, help="Percentage of the sum of lines from the origin accounts will be transferred to the destination account")
|
|
analytic_account_ids = fields.Many2many('account.analytic.account', string='Analytic Filter', help="Adds a condition to only transfer the sum of the lines from the origin accounts that match these analytic accounts to the destination account")
|
|
partner_ids = fields.Many2many('res.partner', string='Partner Filter', help="Adds a condition to only transfer the sum of the lines from the origin accounts that match these partners to the destination account")
|
|
percent_is_readonly = fields.Boolean(compute="_compute_percent_is_readonly")
|
|
sequence = fields.Integer("Sequence")
|
|
|
|
_sql_constraints = [
|
|
(
|
|
'unique_account_by_transfer_model', 'UNIQUE(transfer_model_id, account_id)',
|
|
'Only one account occurrence by transfer model')
|
|
]
|
|
|
|
@api.onchange('analytic_account_ids', 'partner_ids')
|
|
def set_percent_if_analytic_account_ids(self):
|
|
"""
|
|
Set percent to 100 if at least analytic account id is set.
|
|
"""
|
|
for record in self:
|
|
if record.analytic_account_ids or record.partner_ids:
|
|
record.percent = 100
|
|
|
|
def _get_transfer_move_lines_values(self, start_date, end_date):
|
|
"""
|
|
Get values to create the move lines to perform all needed transfers between accounts linked to current recordset
|
|
for a given period
|
|
:param start_date: the start date of the targeted period
|
|
:param end_date: the end date of the targeted period
|
|
:return: a list containing all the values needed to create the needed transfers
|
|
:rtype: list
|
|
"""
|
|
transfer_values = []
|
|
# Avoid to transfer two times the same entry
|
|
already_handled_move_line_ids = []
|
|
for transfer_model_line in self:
|
|
domain = transfer_model_line._get_move_lines_domain(start_date, end_date, already_handled_move_line_ids)
|
|
|
|
query = self.env['account.move.line']._search(domain)
|
|
if transfer_model_line.analytic_account_ids:
|
|
query.add_where(
|
|
SQL(
|
|
"%s && %s",
|
|
[str(account_id) for account_id in transfer_model_line.analytic_account_ids.ids],
|
|
self.env['account.move.line']._query_analytic_accounts(),
|
|
)
|
|
)
|
|
query_string, query_param = query.select('array_agg("account_move_line".id) AS ids', 'SUM(balance) AS balance', 'account_id')
|
|
query_string = f"{query_string} GROUP BY account_id ORDER BY account_id"
|
|
self._cr.execute(query_string, query_param)
|
|
total_balances_by_account = [expense for expense in self._cr.dictfetchall()]
|
|
|
|
for total_balance_account in total_balances_by_account:
|
|
already_handled_move_line_ids += total_balance_account['ids']
|
|
balance = total_balance_account['balance']
|
|
if not float_is_zero(balance, precision_digits=9):
|
|
amount = abs(balance)
|
|
source_account_is_debit = balance > 0
|
|
account_id = total_balance_account['account_id']
|
|
account = self.env['account.account'].browse(account_id)
|
|
transfer_values += transfer_model_line._get_transfer_values(account, amount, source_account_is_debit,
|
|
end_date)
|
|
return transfer_values
|
|
|
|
def _get_move_lines_domain(self, start_date, end_date, avoid_move_line_ids=None):
|
|
"""
|
|
Determine the domain to get all account move lines posted in a given period corresponding to self move model
|
|
line.
|
|
:param start_date: the start date of the targeted period
|
|
:param end_date: the end date of the targeted period
|
|
:param avoid_move_line_ids: the account.move.line ids that should be excluded from the domain
|
|
:return: the computed domain
|
|
:rtype: list
|
|
"""
|
|
self.ensure_one()
|
|
move_lines_domain = self.transfer_model_id._get_move_lines_base_domain(start_date, end_date)
|
|
if avoid_move_line_ids:
|
|
move_lines_domain.append(('id', 'not in', avoid_move_line_ids))
|
|
if self.partner_ids:
|
|
move_lines_domain.append(('partner_id', 'in', self.partner_ids.ids))
|
|
return move_lines_domain
|
|
|
|
def _get_transfer_values(self, account, amount, is_debit, write_date):
|
|
"""
|
|
Get values to create the move lines to perform a transfer between self account and given account
|
|
:param account: the account
|
|
:param amount: the amount that is being transferred
|
|
:type amount: float
|
|
:param is_debit: True if the transferred amount is a debit, False if credit
|
|
:type is_debit: bool
|
|
:param write_date: the date to use for the move line writing
|
|
:return: a list containing the values to create the needed move lines
|
|
:rtype: list
|
|
"""
|
|
self.ensure_one()
|
|
return [
|
|
self._get_destination_account_transfer_move_line_values(account, amount, is_debit, write_date),
|
|
self._get_origin_account_transfer_move_line_values(account, amount, is_debit, write_date)
|
|
]
|
|
|
|
def _get_origin_account_transfer_move_line_values(self, origin_account, amount, is_debit,
|
|
write_date):
|
|
"""
|
|
Get values to create the move line in the origin account side for a given transfer of a given amount from origin
|
|
account to a given destination account.
|
|
:param origin_account: the origin account
|
|
:param amount: the amount that is being transferred
|
|
:type amount: float
|
|
:param is_debit: True if the transferred amount is a debit, False if credit
|
|
:type is_debit: bool
|
|
:param write_date: the date to use for the move line writing
|
|
:return: a dict containing the values to create the move line
|
|
:rtype: dict
|
|
"""
|
|
anal_accounts = self.analytic_account_ids and ', '.join(self.analytic_account_ids.mapped('name'))
|
|
partners = self.partner_ids and ', '.join(self.partner_ids.mapped('name'))
|
|
if anal_accounts and partners:
|
|
name = _("Automatic Transfer (entries with analytic account(s): %s and partner(s): %s)", anal_accounts, partners)
|
|
elif anal_accounts:
|
|
name = _("Automatic Transfer (entries with analytic account(s): %s)", anal_accounts)
|
|
elif partners:
|
|
name = _("Automatic Transfer (entries with partner(s): %s)", partners)
|
|
else:
|
|
name = _("Automatic Transfer (to account %s)", self.account_id.code)
|
|
return {
|
|
'name': name,
|
|
'account_id': origin_account.id,
|
|
'date_maturity': write_date,
|
|
'credit' if is_debit else 'debit': amount
|
|
}
|
|
|
|
def _get_destination_account_transfer_move_line_values(self, origin_account, amount, is_debit,
|
|
write_date):
|
|
"""
|
|
Get values to create the move line in the destination account side for a given transfer of a given amount from
|
|
given origin account to destination account.
|
|
:param origin_account: the origin account
|
|
:param amount: the amount that is being transferred
|
|
:type amount: float
|
|
:param is_debit: True if the transferred amount is a debit, False if credit
|
|
:type is_debit: bool
|
|
:param write_date: the date to use for the move line writing
|
|
:return: a dict containing the values to create the move line
|
|
:rtype dict:
|
|
"""
|
|
anal_accounts = self.analytic_account_ids and ', '.join(self.analytic_account_ids.mapped('name'))
|
|
partners = self.partner_ids and ', '.join(self.partner_ids.mapped('name'))
|
|
if anal_accounts and partners:
|
|
name = _("Automatic Transfer (from account %s with analytic account(s): %s and partner(s): %s)", origin_account.code, anal_accounts, partners)
|
|
elif anal_accounts:
|
|
name = _("Automatic Transfer (from account %s with analytic account(s): %s)", origin_account.code, anal_accounts)
|
|
elif partners:
|
|
name = _("Automatic Transfer (from account %s with partner(s): %s)", origin_account.code, partners)
|
|
else:
|
|
name = _("Automatic Transfer (%s%% from account %s)", self.percent, origin_account.code)
|
|
return {
|
|
'name': name,
|
|
'account_id': self.account_id.id,
|
|
'date_maturity': write_date,
|
|
'debit' if is_debit else 'credit': amount
|
|
}
|
|
|
|
@api.depends('analytic_account_ids', 'partner_ids')
|
|
def _compute_percent_is_readonly(self):
|
|
for record in self:
|
|
record.percent_is_readonly = record.analytic_account_ids or record.partner_ids
|