Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
==========================================
РуОду — Комиссии агентам: сценарные тесты
==========================================
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
|badge1| |badge2|
Сквозной функциональный тест модуля ``ruodoo_commission`` на штатном
механизме туров Odoo (``web_tour`` + ``HttpCase.start_tour``, headless
Chrome). Тест имитирует работу пользователя через интерфейс и после
каждого этапа проверяет результат по данным в БД (ORM), а не только по
экрану. Предназначен для запуска перед релизом: падает, если бизнес-
процесс комиссий сломан.
Только для тестирования — модуль не несёт бизнес-логики.
**Содержание**
.. contents::
:local:
Что проверяется
===============
Один тестовый метод (``test_commission_full_flow``) последовательно
запускает три тура под пользователем «продавец + начисляет комиссии»:
#. **Заказ и начисление** (``ruodoo_commission_order_tour``):
создать заказ, выбрать клиента, добавить строку (товар, количество),
выбрать две комиссии в колонке «Комиссии», сохранить, подтвердить,
нажать «Начислить комиссию».
#. **Смарт-кнопка** (``ruodoo_commission_smart_button_tour``):
открыть заказ, нажать смарт-кнопку «Комиссии», увидеть список
сформированных счетов поставщика.
#. **Отмена** (``ruodoo_commission_cancel_tour``): отменить заказ через
интерфейс.
Проверки по БД между турами:
* заказ подтверждён; в строке один товар, количество 10, две комиссии;
* сумма комиссий строки и «ИТОГО КОМИССИЙ» заказа совпадают (150);
* комиссии не входят в итог заказа (``amount_total`` = 1000);
* сформированы два счёта поставщика (по числу получателей), оба
проведены; суммы (100 и 50) совпадают с суммами комиссий;
* журнал и счёт расходов в счетах — из настроек комиссии;
* счётчик смарт-кнопки равен 2;
* после отмены заказ в состоянии «Отменён», счета комиссий удалены.
Зависимости
===========
* ``ruodoo_commission`` — тестируемый модуль
* ``sale_management`` — заказы на продажу (UI-сценарий)
* ``account`` — счета поставщика
Требования окружения
====================
Туры выполняются в headless-браузере, поэтому в окружении нужны:
* **Chrome/Chromium** (Odoo ищет ``google-chrome`` / ``chromium`` в
``PATH``; путь можно задать переменной ``ODOO_BROWSER_BIN``);
* Python-пакет **websocket-client** (иначе тур-тест будет пропущен).
.. note::
Если браузер или ``websocket-client`` недоступны, Odoo помечает
тест как *skipped*. Пропуск не является успехом — для достоверного
прогона перед релизом обеспечьте оба компонента (например, в
``Dockerfile`` образа Odoo).
Запуск
======
::
odoo -d <test_db> -i ruodoo_commission_test_tours \
--test-enable --test-tags=/ruodoo_commission_test_tours \
--stop-after-init
Полезные опции: ``--screenshots=/tmp/commission_tours`` для скриншотов
при падении; ``ODOO_BROWSER_BIN=/path/to/chrome`` — явный путь к браузеру.
Как это устроено
================
* Туры (``static/tests/tours/*.js``) подключены как ассеты в
``web.assets_tests`` и зарегистрированы в
``registry.category("web_tour.tours")``.
* Селекторы — стабильные: по ``name`` полей/кнопок
(``button[name=action_accrue_commission]``,
``.o_field_many2many_tags[name=commission_ids]``), семантическим
классам Odoo и ``:contains`` только для данных, созданных тестом
(«Tour Customer», «Tour Commission …»).
* Данные (клиент, товар без налогов, два вида комиссий с журналом,
счётом расходов и партнёрами-получателями, пользователь с группами)
создаются в ``setUpClass`` через ORM. ORM используется только для
подготовки и проверок; действия сценария выполняются в UI.
Авторы
======
* MK.Lab, RuOdoo
Модуль сопровождается MK.Lab, RuOdoo.
.. image:: https://ruodoo.ru/logo.png
:alt: MK.Lab, RuOdoo
:target: https://ruodoo.ru
Модуль является частью решения РуОду.

View File

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
{
'name': 'РуОду — Комиссии агентам: сценарные тесты (туры)',
'version': '19.0.2026.09.27',
'summary': 'Сквозной UI-тест модуля комиссий через web_tour',
'description': """
РуОду — Комиссии агентам: сценарные тесты
=========================================
Сквозной функциональный тест модуля ``ruodoo_commission`` на штатном
механизме туров Odoo (``web_tour`` + ``HttpCase.start_tour``, headless
Chrome).
Сценарий имитирует работу пользователя через интерфейс:
* создание заказа продаж, выбор клиента, заполнение строки (товар,
количество), выбор нескольких комиссий в колонке «Комиссии»;
* сохранение и подтверждение заказа;
* начисление комиссии (кнопка «Начислить комиссию») — формирование
счетов поставщика;
* переход по смарт-кнопке «Комиссии» к сформированным счетам;
* отмена заказа.
После каждого этапа результат проверяется по данным в БД (ORM):
совпадение сумм комиссий в строке/заказе и в счетах, корректность
подставленных журнала/счёта расходов/партнёра, а также штатная отмена
с удалением счетов комиссий.
Только для тестирования: модуль не несёт бизнес-логики.
""",
'author': 'MK.Lab, RuOdoo',
'website': 'https://ruodoo.ru',
'category': 'Accounting',
'depends': [
'ruodoo_commission',
'sale_management',
'account',
],
'assets': {
'web.assets_tests': [
'ruodoo_commission_test_tours/static/tests/tours/*.js',
],
},
'installable': True,
'application': False,
'license': 'LGPL-3',
'maintainers': ['mklab', 'ruodoo'],
}

View File

@@ -0,0 +1,46 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
/**
* Этап 3: отменить заказ через интерфейс и убедиться, что отмена
* отрабатывает штатно (заказ переходит в состояние «Cancelled»).
* Каскадное удаление счетов комиссий проверяется в Python после тура.
*
* Кнопка «Отмена» в Odoo 19 может находиться в выпадающем меню
* действий строки состояния — используем statusbarButtonsSteps.
*/
registry.category("web_tour.tours").add("ruodoo_commission_cancel_tour", {
url: "/odoo",
steps: () => [
...stepUtils.goToAppSteps("sale.sale_menu_root", "Открыть приложение «Продажи»"),
{
content: "Открыть заказ Tour Customer из списка",
trigger: '.o_list_view td.o_data_cell:contains("Tour Customer")',
run: "click",
},
{
content: "Форма заказа открыта",
trigger: ".o_form_view .o_statusbar_status",
},
// Нажать «Отмена» (может быть в дропдауне действий).
...stepUtils.statusbarButtonsSteps("Cancel", "Нажать «Отмена»"),
// Базовый sale.action_cancel не открывает визард — состояние сразу
// меняется. На случай подтверждающего модального окна закроем его.
{
content: "Подтвердить отмену, если появилось модальное окно",
trigger: "body",
run: function () {
const btn = document.querySelector(
".modal-dialog footer button.btn-primary"
);
if (btn) {
btn.click();
}
},
},
{
content: "Заказ отменён",
trigger: ".o_statusbar_status .o_arrow_button_current:contains(Cancelled)",
},
],
});

View File

@@ -0,0 +1,98 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import saleUtils from "@sale/js/tours/tour_utils";
/**
* Этап 1: создание заказа, выбор клиента, заполнение строки,
* выбор двух комиссий в колонке «Комиссии», сохранение, подтверждение,
* затем начисление комиссии (кнопка «Начислить комиссию»).
*
* Данные (клиент «Tour Customer», товар «Tour Product», комиссии
* «Tour Commission 10» и «Tour Commission 5») создаёт Python-тест.
* Тур ничего не делает через ORM — только UI.
*/
registry.category("web_tour.tours").add("ruodoo_commission_order_tour", {
url: "/odoo",
steps: () => [
...stepUtils.goToAppSteps("sale.sale_menu_root", "Открыть приложение «Продажи»"),
...saleUtils.createNewSalesOrder(),
...saleUtils.selectCustomer("Tour Customer"),
// --- Строка заказа: товар ---
...saleUtils.addProduct("Tour Product"),
// Количество = 10
{
content: "Указать количество",
trigger: ".o_selected_row div[name=product_uom_qty] input",
run: "edit 10",
},
// --- Комиссии строки: выбрать две ---
{
content: "Открыть поле «Комиссии» в строке",
trigger: ".o_selected_row .o_field_many2many_tags[name=commission_ids] input",
run: "click",
},
{
content: "Ввести первую комиссию",
trigger: ".o_selected_row .o_field_many2many_tags[name=commission_ids] input",
run: "edit Tour Commission 10",
},
{
content: "Выбрать комиссию 10%",
trigger: 'ul.ui-autocomplete a:contains("Tour Commission 10")',
run: "click",
},
{
content: "Убедиться, что тег комиссии 10% добавлен",
trigger: '.o_selected_row .o_field_many2many_tags[name=commission_ids] .o_tag:contains("Tour Commission 10")',
},
{
content: "Ввести вторую комиссию",
trigger: ".o_selected_row .o_field_many2many_tags[name=commission_ids] input",
run: "edit Tour Commission 5",
},
{
content: "Выбрать комиссию 5%",
trigger: 'ul.ui-autocomplete a:contains("Tour Commission 5")',
run: "click",
},
{
content: "Убедиться, что тег комиссии 5% добавлен",
trigger: '.o_selected_row .o_field_many2many_tags[name=commission_ids] .o_tag:contains("Tour Commission 5")',
},
// --- Сохранить заказ ---
...stepUtils.saveForm(),
// Проверить, что «ИТОГО КОМИССИЙ» отобразилось (поле видно группе)
{
content: "Итог комиссий присутствует на форме",
trigger: ".o_field_widget[name=amount_commission]",
},
// --- Подтвердить заказ ---
{
content: "Подтвердить заказ",
trigger: "button[name=action_confirm]",
run: "click",
},
{
content: "Заказ перешёл в состояние «Заказ продаж»",
trigger: ".o_statusbar_status .o_arrow_button_current:contains(Sales Order)",
},
// --- Начислить комиссию ---
{
content: "Нажать «Начислить комиссию»",
trigger: "button[name=action_accrue_commission]",
run: "click",
},
// После начисления открывается список/форма счетов комиссий.
{
content: "Открылись счета комиссий (вендор-биллы)",
trigger: ".o_list_view .o_data_row, .o_form_view .o_breadcrumb",
},
],
});

View File

@@ -0,0 +1,43 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
/**
* Этап 2: открыть заказ и проверить смарт-кнопку «Комиссии»:
* она нажимается и выводит связанные счета поставщика.
*
* Заказ уже подтверждён и по нему начислены комиссии (этап 1).
*/
registry.category("web_tour.tours").add("ruodoo_commission_smart_button_tour", {
url: "/odoo",
steps: () => [
...stepUtils.goToAppSteps("sale.sale_menu_root", "Открыть приложение «Продажи»"),
{
content: "Открыть заказ Tour Customer из списка",
trigger: '.o_list_view td.o_data_cell:contains("Tour Customer")',
run: "click",
},
{
content: "Форма заказа открыта",
trigger: ".o_form_view .o_statusbar_status",
},
// Смарт-кнопка «Комиссии» видна и содержит счётчик > 0
{
content: "Смарт-кнопка «Комиссии» присутствует",
trigger: "button[name=action_view_commission_bills]",
},
{
content: "Нажать смарт-кнопку «Комиссии»",
trigger: "button[name=action_view_commission_bills]",
run: "click",
},
// Открывается список счетов поставщика по этому заказу.
{
content: "Открылся список счетов комиссий",
trigger: ".o_list_view .o_data_row",
},
{
content: "Это счета поставщика (тип «Bill»)",
trigger: ".o_breadcrumb",
},
],
});

View File

@@ -0,0 +1 @@
from . import test_commission_tours

View File

@@ -0,0 +1,193 @@
from odoo import Command
from odoo.tests import tagged, HttpCase
from odoo.addons.sale.tests.common import TestSaleCommon
@tagged('post_install', '-at_install')
class TestCommissionTours(TestSaleCommon, HttpCase):
"""Сквозной UI-тест модуля комиссий через web_tour.
Данные готовятся через ORM (R1), действия пользователя выполняются
в интерфейсе турами, результат каждого этапа проверяется по БД.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
company = cls.company_data['company']
cls.purchase_journal = cls.company_data['default_journal_purchase']
cls.expense_account = cls.company_data['default_account_expense']
# --- Клиент ---
cls.customer = cls.env['res.partner'].create({
'name': 'Tour Customer',
'company_id': False,
})
# --- Товар: складируемый/потребляемый, цена 100, без налогов ---
cls.tour_product = cls.env['product.product'].create({
'name': 'Tour Product',
'type': 'consu',
'list_price': 100.0,
'taxes_id': [Command.clear()],
})
# --- Партнёры-получатели комиссий ---
cls.agent_10 = cls.env['res.partner'].create({'name': 'Tour Agent Ten'})
cls.agent_5 = cls.env['res.partner'].create({'name': 'Tour Agent Five'})
Commission = cls.env['commission.commission']
cls.commission_10 = Commission.create({
'name': 'Tour Commission 10',
'calc_type': 'percentage',
'percentage': 10.0,
'partner_id': cls.agent_10.id,
'journal_id': cls.purchase_journal.id,
'account_id': cls.expense_account.id,
'company_id': company.id,
})
cls.commission_5 = Commission.create({
'name': 'Tour Commission 5',
'calc_type': 'percentage',
'percentage': 5.0,
'partner_id': cls.agent_5.id,
'journal_id': cls.purchase_journal.id,
'account_id': cls.expense_account.id,
'company_id': company.id,
})
# --- Пользователь: продавец + менеджер комиссий ---
# group_commission_manager включает group_commission_viewer.
cls.tour_user = cls.env['res.users'].create({
'name': 'Tour Commission Manager',
'login': 'tour_commission',
'password': 'tour_commission',
'company_id': company.id,
'company_ids': [Command.set([company.id])],
'group_ids': [Command.set([
cls.env.ref('sales_team.group_sale_salesman_all_leads').id,
cls.env.ref('ruodoo_commission.group_commission_manager').id,
# Право просматривать счета поставщика (открытие по смарт-кнопке).
cls.env.ref('account.group_account_invoice').id,
])],
})
# ------------------------------------------------------------------
def _get_order(self):
return self.env['sale.order'].search(
[('partner_id', '=', self.customer.id)], limit=1,
)
def test_commission_full_flow(self):
# =============== Этап 1: заказ + комиссии + начисление ===============
self.start_tour(
'/odoo', 'ruodoo_commission_order_tour', login='tour_commission',
)
order = self._get_order()
self.assertTrue(order, msg="Этап 1: заказ должен быть создан")
self.assertEqual(
order.state, 'sale',
msg="Этап 1: заказ должен быть подтверждён (state='sale')",
)
self.assertEqual(
len(order.order_line), 1,
msg="Этап 1: в заказе должна быть одна строка",
)
line = order.order_line
self.assertEqual(
line.product_id, self.tour_product,
msg="Этап 1: товар строки — Tour Product",
)
self.assertEqual(
line.product_uom_qty, 10.0,
msg="Этап 1: количество должно быть 10",
)
self.assertEqual(
line.commission_ids, self.commission_10 | self.commission_5,
msg="Этап 1: к строке должны быть привязаны обе комиссии",
)
# Суммы комиссий: 10 шт × 100 = 1000; 10% = 100; 5% = 50; итог 150.
self.assertAlmostEqual(
line.commission_amount, 150.0,
msg="Этап 1: сумма комиссий строки должна быть 150",
)
self.assertAlmostEqual(
order.amount_commission, 150.0,
msg="Этап 1: «ИТОГО КОМИССИЙ» заказа должно быть 150",
)
# Комиссии не входят в итог заказа.
self.assertAlmostEqual(
order.amount_total, 1000.0,
msg="Этап 1: итог заказа не должен включать комиссии (1000)",
)
# Начислены счета комиссий: два партнёра-получателя → два билла.
bills = order.commission_bill_ids
self.assertEqual(
len(bills), 2,
msg="Этап 1: должно быть два счёта комиссий (по получателям)",
)
self.assertEqual(
set(bills.mapped('move_type')), {'in_invoice'},
msg="Этап 1: счета комиссий — счета поставщика (in_invoice)",
)
self.assertEqual(
set(bills.mapped('state')), {'posted'},
msg="Этап 1: счета комиссий должны быть проведены",
)
# Суммы в биллах совпадают с суммами комиссий заказа.
bill_10 = bills.filtered(lambda m: m.partner_id == self.agent_10)
bill_5 = bills.filtered(lambda m: m.partner_id == self.agent_5)
self.assertAlmostEqual(
bill_10.amount_total, 100.0,
msg="Этап 1: счёт агенту 10% должен быть на 100",
)
self.assertAlmostEqual(
bill_5.amount_total, 50.0,
msg="Этап 1: счёт агенту 5% должен быть на 50",
)
self.assertAlmostEqual(
sum(bills.mapped('amount_total')), order.amount_commission,
msg="Этап 1: сумма счетов должна совпадать с итогом комиссий заказа",
)
# Счета подставляются правильно: журнал и счёт расходов из карточки.
self.assertEqual(
set(bills.mapped('journal_id')), {self.purchase_journal},
msg="Этап 1: журнал счетов — из настроек комиссии",
)
self.assertEqual(
set(bills.invoice_line_ids.mapped('account_id')),
{self.expense_account},
msg="Этап 1: счёт расходов строк — из настроек комиссии",
)
# =============== Этап 2: смарт-кнопка «Комиссии» ===============
self.assertEqual(
order.commission_bill_count, 2,
msg="Этап 2: счётчик смарт-кнопки должен быть 2",
)
self.start_tour(
'/odoo', 'ruodoo_commission_smart_button_tour',
login='tour_commission',
)
# =============== Этап 3: отмена заказа ===============
bill_ids = bills.ids
self.start_tour(
'/odoo', 'ruodoo_commission_cancel_tour', login='tour_commission',
)
order.invalidate_recordset()
self.assertEqual(
order.state, 'cancel',
msg="Этап 3: заказ должен быть отменён",
)
remaining = self.env['account.move'].search([('id', 'in', bill_ids)])
self.assertFalse(
remaining,
msg="Этап 3: счета комиссий должны быть удалены при отмене заказа",
)