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