186 lines
8.1 KiB
Python
186 lines
8.1 KiB
Python
|
||
from odoo import api, fields, models, _
|
||
from odoo.exceptions import UserError
|
||
from odoo.tools import float_compare
|
||
|
||
|
||
class SaleOrder(models.Model):
|
||
_inherit = 'sale.order'
|
||
|
||
amount_commission = fields.Monetary(
|
||
string='ИТОГО КОМИССИЙ', compute='_compute_amount_commission',
|
||
store=True, currency_field='currency_id',
|
||
help='Суммарные агентские комиссии по заказу. '
|
||
'Не входит в итог заказа — это забалансовый агентский расход.',
|
||
)
|
||
commission_bill_ids = fields.One2many(
|
||
'account.move', 'commission_sale_order_id',
|
||
string='Счета комиссий',
|
||
)
|
||
commission_bill_count = fields.Integer(
|
||
string='Кол-во счетов комиссий',
|
||
compute='_compute_commission_bill_count',
|
||
)
|
||
|
||
@api.depends('order_line.commission_amount')
|
||
def _compute_amount_commission(self):
|
||
for order in self:
|
||
order.amount_commission = sum(
|
||
order.order_line.mapped('commission_amount')
|
||
)
|
||
|
||
@api.depends('commission_bill_ids')
|
||
def _compute_commission_bill_count(self):
|
||
for order in self:
|
||
order.commission_bill_count = len(order.commission_bill_ids)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Начисление комиссии → счета поставщика (Vendor Bill)
|
||
# ------------------------------------------------------------------
|
||
def action_accrue_commission(self):
|
||
"""Сформировать и провести вендор-биллы по комиссиям заказа.
|
||
|
||
Обходим строки → привязанные комиссии, группируем строки будущих
|
||
биллов по (партнёр-получатель, журнал), создаём один или несколько
|
||
account.move (in_invoice) и проводим их.
|
||
"""
|
||
AccountMove = self.env['account.move']
|
||
created = AccountMove
|
||
for order in self:
|
||
if order.state != 'sale':
|
||
raise UserError(_(
|
||
"Начислять комиссию можно только по подтверждённому заказу."
|
||
))
|
||
# Дедупликация: не начисляем повторно при наличии активных биллов.
|
||
existing = order.commission_bill_ids.filtered(
|
||
lambda m: m.state != 'cancel'
|
||
)
|
||
if existing:
|
||
raise UserError(_(
|
||
"По заказу %(name)s комиссии уже начислены. "
|
||
"Отмените или удалите существующие счета комиссий.",
|
||
name=order.name,
|
||
))
|
||
|
||
buckets = order._prepare_commission_bill_buckets()
|
||
for (partner, journal), line_vals_list in buckets.items():
|
||
move = AccountMove.with_company(order.company_id).create({
|
||
'move_type': 'in_invoice',
|
||
'partner_id': partner.id,
|
||
'journal_id': journal.id,
|
||
'invoice_date': fields.Date.context_today(order),
|
||
'invoice_origin': order.name,
|
||
'commission_sale_order_id': order.id,
|
||
'invoice_line_ids': [
|
||
(0, 0, vals) for vals in line_vals_list
|
||
],
|
||
})
|
||
move.action_post()
|
||
created |= move
|
||
|
||
if not created:
|
||
raise UserError(_(
|
||
"По заказу нет комиссий с положительной суммой для начисления."
|
||
))
|
||
return self._action_open_commission_bills(created)
|
||
|
||
def _prepare_commission_bill_buckets(self):
|
||
"""Справочник строк биллов, сгруппированный по (получатель, журнал).
|
||
|
||
:return: dict {(partner, journal): [invoice_line_vals, ...]}
|
||
"""
|
||
self.ensure_one()
|
||
buckets = {}
|
||
precision = self.currency_id.rounding or 0.01
|
||
for line in self.order_line:
|
||
for commission in line.commission_ids:
|
||
amount = commission._compute_amount(line)
|
||
if float_compare(amount, 0.0, precision_rounding=precision) <= 0:
|
||
continue
|
||
key = (commission.partner_id, commission.journal_id)
|
||
buckets.setdefault(key, []).append(
|
||
commission._prepare_bill_line_vals(line, amount)
|
||
)
|
||
return buckets
|
||
|
||
def _action_open_commission_bills(self, moves):
|
||
"""Действие: открыть сформированные счета комиссий."""
|
||
self.ensure_one()
|
||
action = {
|
||
'name': _('Счета комиссий'),
|
||
'type': 'ir.actions.act_window',
|
||
'res_model': 'account.move',
|
||
'context': {'create': False},
|
||
}
|
||
if len(moves) == 1:
|
||
action.update({
|
||
'view_mode': 'form',
|
||
'res_id': moves.id,
|
||
})
|
||
else:
|
||
action.update({
|
||
'view_mode': 'list,form',
|
||
'domain': [('id', 'in', moves.ids)],
|
||
})
|
||
return action
|
||
|
||
def action_view_commission_bills(self):
|
||
"""Смарт-кнопка «Комиссии»: связанные вендор-биллы заказа."""
|
||
self.ensure_one()
|
||
return {
|
||
'name': _('Комиссии'),
|
||
'type': 'ir.actions.act_window',
|
||
'res_model': 'account.move',
|
||
'view_mode': 'list,form',
|
||
'domain': [('commission_sale_order_id', '=', self.id)],
|
||
'context': {
|
||
'create': False,
|
||
'default_move_type': 'in_invoice',
|
||
},
|
||
}
|
||
|
||
# ------------------------------------------------------------------
|
||
# Отмена заказа → каскад на счета комиссий
|
||
# ------------------------------------------------------------------
|
||
def action_cancel(self):
|
||
res = super().action_cancel()
|
||
for order in self:
|
||
bills = order.commission_bill_ids.filtered(
|
||
lambda m: m.state != 'cancel'
|
||
)
|
||
if bills:
|
||
order._cancel_commission_bills(bills)
|
||
return res
|
||
|
||
def _cancel_commission_bills(self, bills):
|
||
"""Отменить счета комиссий: отвязать платежи → черновик → удалить."""
|
||
self.ensure_one()
|
||
for bill in bills:
|
||
self._detach_bill_payments(bill)
|
||
if bill.state == 'posted':
|
||
bill.button_draft()
|
||
# Из черновика документ можно удалить штатно.
|
||
bill.unlink()
|
||
|
||
def _detach_bill_payments(self, bill):
|
||
"""Отвязать платежи от счёта комиссии штатными средствами.
|
||
|
||
Если ядро не позволяет отвязать платёж — сообщаем понятной ошибкой,
|
||
деструктивных обходов ядра не выполняем.
|
||
"""
|
||
self.ensure_one()
|
||
# Строки счёта, участвующие в сопоставлении с платежами.
|
||
reconciled_lines = bill.line_ids.filtered(
|
||
lambda ln: ln.matched_debit_ids or ln.matched_credit_ids
|
||
)
|
||
if not reconciled_lines:
|
||
return
|
||
try:
|
||
reconciled_lines.remove_move_reconcile()
|
||
except Exception as ex:
|
||
raise UserError(_(
|
||
"Не удалось отвязать платежи от счёта комиссии %(name)s. "
|
||
"Сначала отмените сопоставление платежей вручную.\n%(err)s",
|
||
name=bill.name or bill.display_name, err=ex,
|
||
)) from ex
|