[FIX] website_sale_aplicoop: Remove redundant string= attributes and fix OCA linting warnings
- Remove redundant string= from 17 field definitions where name matches string value (W8113) - Convert @staticmethod to instance methods in selection methods for proper self.env._() access - Fix W8161 (prefer-env-translation) by using self.env._() instead of standalone _() - Fix W8301/W8115 (translation-not-lazy) by proper placement of % interpolation outside self.env._() - Remove unused imports of odoo._ from group_order.py and sale_order_extension.py - All OCA linting warnings in website_sale_aplicoop main models are now resolved Changes: - website_sale_aplicoop/models/group_order.py: 21 field definitions cleaned - website_sale_aplicoop/models/sale_order_extension.py: 5 field definitions cleaned + @staticmethod conversion - Consistent with OCA standards for addon submission
This commit is contained in:
parent
5c89795e30
commit
6fbc7b9456
73 changed files with 5386 additions and 4354 deletions
|
|
@ -1,7 +1,7 @@
|
|||
# BEFORE & AFTER - Error Fixes
|
||||
|
||||
**Document**: Visual comparison of all changes made to fix installation errors
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Document**: Visual comparison of all changes made to fix installation errors
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Status**: ✅ All fixed and working
|
||||
|
||||
---
|
||||
|
|
@ -146,7 +146,7 @@ class ResPartner(models.Model):
|
|||
product_count = self.env['product.template'].search_count([
|
||||
('default_supplier_id', '=', self.id)
|
||||
])
|
||||
|
||||
|
||||
# ... rest of method
|
||||
```
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ class ResPartner(models.Model):
|
|||
product_count = self.env['product.template'].search_count([
|
||||
('default_supplier_id', '=', self.id)
|
||||
])
|
||||
|
||||
|
||||
# ... rest of method
|
||||
```
|
||||
|
||||
|
|
@ -315,8 +315,8 @@ class WizardUpdateProductCategory(models.TransientModel):
|
|||
| `models/res_partner.py` | `_()` in field def | 2 `_()` calls | Removed | ✅ Fixed |
|
||||
| `models/wizard_update_product_category.py` | `_()` in field defs | 4 `_()` calls | Removed | ✅ Fixed |
|
||||
|
||||
**Total Changes**: 8 modifications across 3 files
|
||||
**Total Errors Fixed**: 2 categories (XPath + Translation)
|
||||
**Total Changes**: 8 modifications across 3 files
|
||||
**Total Errors Fixed**: 2 categories (XPath + Translation)
|
||||
**Result**: ✅ **All fixed, addon working**
|
||||
|
||||
---
|
||||
|
|
@ -325,11 +325,11 @@ class WizardUpdateProductCategory(models.TransientModel):
|
|||
|
||||
### Before (with errors):
|
||||
```
|
||||
2026-02-10 16:17:56,252 47 INFO odoo odoo.modules.registry: module product_price_category_supplier: creating or updating database tables
|
||||
2026-02-10 16:17:56,344 47 INFO odoo odoo.modules.loading: loading product_price_category_supplier/security/ir.model.access.csv
|
||||
2026-02-10 16:17:56,351 47 INFO odoo odoo.modules.loading: loading product_price_category_supplier/views/res_partner_views.xml
|
||||
2026-02-10 16:17:56,362 47 WARNING odoo odoo.modules.loading: Transient module states were reset
|
||||
2026-02-10 16:17:56,362 47 ERROR odoo odoo.modules.registry: Failed to load registry
|
||||
2026-02-10 16:17:56,252 47 INFO odoo odoo.modules.registry: module product_price_category_supplier: creating or updating database tables
|
||||
2026-02-10 16:17:56,344 47 INFO odoo odoo.modules.loading: loading product_price_category_supplier/security/ir.model.access.csv
|
||||
2026-02-10 16:17:56,351 47 INFO odoo odoo.modules.loading: loading product_price_category_supplier/views/res_partner_views.xml
|
||||
2026-02-10 16:17:56,362 47 WARNING odoo odoo.modules.loading: Transient module states were reset
|
||||
2026-02-10 16:17:56,362 47 ERROR odoo odoo.modules.registry: Failed to load registry
|
||||
2026-02-10 16:17:56,362 47 CRITICAL odoo odoo.service.server: Failed to initialize database `odoo`.
|
||||
❌ ParseError: while parsing /mnt/extra-addons/product_price_category_supplier/views/res_partner_views.xml:4
|
||||
```
|
||||
|
|
@ -365,6 +365,6 @@ In Odoo 18.0 partner form:
|
|||
|
||||
---
|
||||
|
||||
**Document Status**: ✅ Complete
|
||||
**Last Updated**: 10 de febrero de 2026
|
||||
**Document Status**: ✅ Complete
|
||||
**Last Updated**: 10 de febrero de 2026
|
||||
**License**: AGPL-3.0
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# ERROR FIX REPORT - product_price_category_supplier
|
||||
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Status**: ✅ FIXED & VERIFIED
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Status**: ✅ FIXED & VERIFIED
|
||||
**Author**: GitHub Copilot
|
||||
|
||||
---
|
||||
|
|
@ -57,7 +57,7 @@ Element '<xpath expr="//notebook/page[@name='purchase']">' cannot be located in
|
|||
|
||||
**Warning Message**:
|
||||
```
|
||||
2026-02-10 16:17:56,165 47 WARNING odoo odoo.tools.translate: no translation language detected,
|
||||
2026-02-10 16:17:56,165 47 WARNING odoo odoo.tools.translate: no translation language detected,
|
||||
skipping translation <frame at ..., file '...wizard_update_product_category.py', line 21, code WizardUpdateProductCategory>
|
||||
```
|
||||
|
||||
|
|
@ -262,9 +262,9 @@ docker-compose exec -T odoo odoo -d odoo -i product_price_category_supplier --st
|
|||
|
||||
## Summary of Changes
|
||||
|
||||
**Total Files Modified**: 3
|
||||
**Total Changes**: 8
|
||||
**Status**: ✅ All Fixed & Tested
|
||||
**Total Files Modified**: 3
|
||||
**Total Changes**: 8
|
||||
**Status**: ✅ All Fixed & Tested
|
||||
|
||||
The addon is now **ready for production use** with proper:
|
||||
- ✅ View inheritance (correct XPath paths)
|
||||
|
|
@ -275,5 +275,5 @@ The addon is now **ready for production use** with proper:
|
|||
|
||||
---
|
||||
|
||||
**Maintained by**: Criptomart | **License**: AGPL-3.0
|
||||
**Maintained by**: Criptomart | **License**: AGPL-3.0
|
||||
**Last Updated**: 10 de febrero de 2026
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# INSTALLATION COMPLETE - product_price_category_supplier
|
||||
|
||||
**Status**: ✅ **ADDON SUCCESSFULLY INSTALLED**
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Version**: 18.0.1.0.0
|
||||
**Status**: ✅ **ADDON SUCCESSFULLY INSTALLED**
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Version**: 18.0.1.0.0
|
||||
**License**: AGPL-3.0
|
||||
|
||||
---
|
||||
|
|
@ -11,11 +11,11 @@
|
|||
|
||||
El addon `product_price_category_supplier` ha sido creado, corregido y **instalado exitosamente** en tu instancia Odoo 18.0.
|
||||
|
||||
✅ **21 files created**
|
||||
✅ **3 files fixed** (XPath errors & translation issues)
|
||||
✅ **0 remaining errors**
|
||||
✅ **Database tables created**
|
||||
✅ **Translations loaded** (Spanish + Euskera)
|
||||
✅ **21 files created**
|
||||
✅ **3 files fixed** (XPath errors & translation issues)
|
||||
✅ **0 remaining errors**
|
||||
✅ **Database tables created**
|
||||
✅ **Translations loaded** (Spanish + Euskera)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ El addon `product_price_category_supplier` ha sido creado, corregido y **instala
|
|||
### Problem 2: Translation Warnings
|
||||
- **Issue**: Uso de `_()` en definiciones de campos causaba warnings al importar módulo
|
||||
- **Solution**: Removidos `_()` de field definitions (se extraen automáticamente)
|
||||
- **Files**:
|
||||
- **Files**:
|
||||
- `models/res_partner.py` (1 cambio)
|
||||
- `models/wizard_update_product_category.py` (4 cambios)
|
||||
|
||||
|
|
@ -177,22 +177,22 @@ python3 -m py_compile product_price_category_supplier/models/*.py
|
|||
## Installation Output
|
||||
|
||||
```
|
||||
2026-02-10 16:21:04,843 69 INFO odoo odoo.modules.loading:
|
||||
2026-02-10 16:21:04,843 69 INFO odoo odoo.modules.loading:
|
||||
loading product_price_category_supplier/security/ir.model.access.csv
|
||||
|
||||
2026-02-10 16:21:04,868 69 INFO odoo odoo.modules.loading:
|
||||
2026-02-10 16:21:04,868 69 INFO odoo odoo.modules.loading:
|
||||
loading product_price_category_supplier/views/res_partner_views.xml
|
||||
|
||||
2026-02-10 16:21:04,875 69 INFO odoo odoo.modules.loading:
|
||||
2026-02-10 16:21:04,875 69 INFO odoo odoo.modules.loading:
|
||||
loading product_price_category_supplier/views/wizard_update_product_category.xml
|
||||
|
||||
2026-02-10 16:21:04,876 69 INFO odoo odoo.addons.base.models.ir_module:
|
||||
2026-02-10 16:21:04,876 69 INFO odoo odoo.addons.base.models.ir_module:
|
||||
module product_price_category_supplier: loading translation file ...eu.po
|
||||
|
||||
2026-02-10 16:21:04,876 69 INFO odoo odoo.addons.base.models.ir_module:
|
||||
2026-02-10 16:21:04,876 69 INFO odoo odoo.addons.base.models.ir_module:
|
||||
module product_price_category_supplier: loading translation file ...es.po
|
||||
|
||||
2026-02-10 16:21:04,912 69 INFO odoo odoo.modules.loading:
|
||||
2026-02-10 16:21:04,912 69 INFO odoo odoo.modules.loading:
|
||||
Module product_price_category_supplier loaded in 0.68s, 179 queries
|
||||
|
||||
✅ No errors
|
||||
|
|
@ -294,7 +294,7 @@ If you need to:
|
|||
|
||||
---
|
||||
|
||||
**Status**: ✅ Production Ready
|
||||
**Created**: 10 de febrero de 2026
|
||||
**License**: AGPL-3.0
|
||||
**Status**: ✅ Production Ready
|
||||
**Created**: 10 de febrero de 2026
|
||||
**License**: AGPL-3.0
|
||||
**Author**: Criptomart
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# ✅ ADDON INSTALLATION STATUS REPORT
|
||||
|
||||
**Addon**: `product_price_category_supplier`
|
||||
**Status**: ✅ **INSTALLED & WORKING**
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Addon**: `product_price_category_supplier`
|
||||
**Status**: ✅ **INSTALLED & WORKING**
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Installation Time**: 2 cycles (fixed errors on 2nd attempt)
|
||||
|
||||
---
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
El addon `product_price_category_supplier` fue creado exitosamente para extender Odoo 18.0 con funcionalidad de categorías de precio por proveedor.
|
||||
|
||||
**Ciclo 1**: Error ParseError en XPath (vista XML)
|
||||
**Ciclo 1**: Error ParseError en XPath (vista XML)
|
||||
**Ciclo 2**: ✅ Errores corregidos, addon instalado correctamente
|
||||
|
||||
---
|
||||
|
|
@ -90,7 +90,7 @@ Error: Element '<xpath expr="//notebook/page[@name='purchase']">' cannot be loca
|
|||
- <xpath expr="//notebook/page[@name='purchase']" position="inside">
|
||||
+ <xpath expr="//page[@name='sales_purchases']" position="inside">
|
||||
```
|
||||
**File**: `views/res_partner_views.xml` line 11
|
||||
**File**: `views/res_partner_views.xml` line 11
|
||||
**Reason**: Odoo 18 partner form uses `sales_purchases` page name
|
||||
|
||||
### Fix 2: Field Name in Tree View
|
||||
|
|
@ -98,7 +98,7 @@ Error: Element '<xpath expr="//notebook/page[@name='purchase']">' cannot be loca
|
|||
- <field name="name" position="after">
|
||||
+ <field name="complete_name" position="after">
|
||||
```
|
||||
**File**: `views/res_partner_views.xml` line 27
|
||||
**File**: `views/res_partner_views.xml` line 27
|
||||
**Reason**: Tree view uses `complete_name` as first field
|
||||
|
||||
### Fix 3: Remove _() from Partner Field
|
||||
|
|
@ -108,7 +108,7 @@ Error: Element '<xpath expr="//notebook/page[@name='purchase']">' cannot be loca
|
|||
+ string='Default Price Category',
|
||||
+ help='Default price category for products from this supplier',
|
||||
```
|
||||
**File**: `models/res_partner.py` lines 13-15
|
||||
**File**: `models/res_partner.py` lines 13-15
|
||||
**Reason**: Automatic extraction, `_()` causes translation warnings
|
||||
|
||||
### Fix 4: Remove _() from Wizard Fields
|
||||
|
|
@ -122,7 +122,7 @@ Error: Element '<xpath expr="//notebook/page[@name='purchase']">' cannot be loca
|
|||
+ string='Price Category',
|
||||
+ string='Number of Products',
|
||||
```
|
||||
**File**: `models/wizard_update_product_category.py` lines 15, 21, 27, 34
|
||||
**File**: `models/wizard_update_product_category.py` lines 15, 21, 27, 34
|
||||
**Reason**: Same as Fix 3 - automatic extraction by Odoo
|
||||
|
||||
---
|
||||
|
|
@ -307,7 +307,7 @@ The addon is **production-ready** and fully functional.
|
|||
|
||||
---
|
||||
|
||||
**Created**: 10 de febrero de 2026
|
||||
**Status**: ✅ Installation Complete
|
||||
**License**: AGPL-3.0
|
||||
**Created**: 10 de febrero de 2026
|
||||
**Status**: ✅ Installation Complete
|
||||
**License**: AGPL-3.0
|
||||
**Maintainer**: Criptomart
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# QUICK REFERENCE - Fixes Applied
|
||||
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Addon**: product_price_category_supplier
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Addon**: product_price_category_supplier
|
||||
**Status**: ✅ All fixed
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
# TEST REPORT - product_price_category_supplier
|
||||
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Status**: ✅ ALL TESTS PASSING
|
||||
**Test Framework**: Odoo TransactionCase
|
||||
**Date**: 10 de febrero de 2026
|
||||
**Status**: ✅ ALL TESTS PASSING
|
||||
**Test Framework**: Odoo TransactionCase
|
||||
**Test Count**: 10 comprehensive tests
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
✅ **10/10 tests passing** (0 failures, 0 errors)
|
||||
⏱️ **Execution time**: 0.35 seconds
|
||||
📊 **Database queries**: 379 queries
|
||||
✅ **10/10 tests passing** (0 failures, 0 errors)
|
||||
⏱️ **Execution time**: 0.35 seconds
|
||||
📊 **Database queries**: 379 queries
|
||||
🎯 **Coverage**: All critical features tested
|
||||
|
||||
---
|
||||
|
|
@ -33,8 +33,8 @@
|
|||
## Test Cases
|
||||
|
||||
### ✅ Test 01: Supplier Has Default Price Category Field
|
||||
**Purpose**: Verify field existence and assignment
|
||||
**Status**: PASSED
|
||||
**Purpose**: Verify field existence and assignment
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- `default_price_category_id` field exists on res.partner
|
||||
- Supplier can have category assigned
|
||||
|
|
@ -43,8 +43,8 @@
|
|||
---
|
||||
|
||||
### ✅ Test 02: Action Opens Wizard
|
||||
**Purpose**: Test wizard opening action
|
||||
**Status**: PASSED
|
||||
**Purpose**: Test wizard opening action
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- Action type is `ir.actions.act_window`
|
||||
- Opens `wizard.update.product.category` model
|
||||
|
|
@ -54,8 +54,8 @@
|
|||
---
|
||||
|
||||
### ✅ Test 03: Wizard Counts Products Correctly
|
||||
**Purpose**: Verify product counting logic
|
||||
**Status**: PASSED
|
||||
**Purpose**: Verify product counting logic
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- Wizard shows correct product count (3 for Supplier A)
|
||||
- Partner name displays correctly
|
||||
|
|
@ -64,8 +64,8 @@
|
|||
---
|
||||
|
||||
### ✅ Test 04: Wizard Updates All Products
|
||||
**Purpose**: Test bulk update functionality
|
||||
**Status**: PASSED
|
||||
**Purpose**: Test bulk update functionality
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- All products from supplier get updated
|
||||
- Products from other suppliers remain unchanged
|
||||
|
|
@ -83,8 +83,8 @@ Result: Products 1,2,3 now have Premium category
|
|||
---
|
||||
|
||||
### ✅ Test 05: Wizard Handles No Products
|
||||
**Purpose**: Test edge case - supplier with no products
|
||||
**Status**: PASSED
|
||||
**Purpose**: Test edge case - supplier with no products
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- Warning notification displayed
|
||||
- No database errors
|
||||
|
|
@ -93,8 +93,8 @@ Result: Products 1,2,3 now have Premium category
|
|||
---
|
||||
|
||||
### ✅ Test 06: Customer Field Visibility
|
||||
**Purpose**: Verify customers don't see price category
|
||||
**Status**: PASSED
|
||||
**Purpose**: Verify customers don't see price category
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- Customer has `supplier_rank = 0`
|
||||
- No price category assigned to customer
|
||||
|
|
@ -103,8 +103,8 @@ Result: Products 1,2,3 now have Premium category
|
|||
---
|
||||
|
||||
### ✅ Test 07: Wizard Overwrites Existing Categories
|
||||
**Purpose**: Test update behavior on pre-existing categories
|
||||
**Status**: PASSED
|
||||
**Purpose**: Test update behavior on pre-existing categories
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- Existing categories get overwritten
|
||||
- No data loss or corruption
|
||||
|
|
@ -120,8 +120,8 @@ Result: All products now Premium (overwritten)
|
|||
---
|
||||
|
||||
### ✅ Test 08: Multiple Suppliers Independent Updates
|
||||
**Purpose**: Test isolation between suppliers
|
||||
**Status**: PASSED
|
||||
**Purpose**: Test isolation between suppliers
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- Updating Supplier A doesn't affect Supplier B products
|
||||
- Each supplier maintains independent category
|
||||
|
|
@ -137,8 +137,8 @@ Both remain independent after updates
|
|||
---
|
||||
|
||||
### ✅ Test 09: Wizard Readonly Fields
|
||||
**Purpose**: Verify display field computations
|
||||
**Status**: PASSED
|
||||
**Purpose**: Verify display field computations
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- `partner_name` computed from `partner_id.name`
|
||||
- Related fields work correctly
|
||||
|
|
@ -147,8 +147,8 @@ Both remain independent after updates
|
|||
---
|
||||
|
||||
### ✅ Test 10: Action Counts Products Correctly
|
||||
**Purpose**: Verify product count accuracy
|
||||
**Status**: PASSED
|
||||
**Purpose**: Verify product count accuracy
|
||||
**Status**: PASSED
|
||||
**Verifies**:
|
||||
- Manual count matches wizard count
|
||||
- Search logic is correct
|
||||
|
|
@ -159,10 +159,10 @@ Both remain independent after updates
|
|||
## Test Execution Results
|
||||
|
||||
```
|
||||
2026-02-10 16:40:38,977 1 INFO odoo odoo.tests.stats:
|
||||
2026-02-10 16:40:38,977 1 INFO odoo odoo.tests.stats:
|
||||
product_price_category_supplier: 12 tests 0.35s 379 queries
|
||||
|
||||
2026-02-10 16:40:38,977 1 INFO odoo odoo.tests.result:
|
||||
2026-02-10 16:40:38,977 1 INFO odoo odoo.tests.result:
|
||||
0 failed, 0 error(s) of 10 tests when loading database 'odoo'
|
||||
|
||||
✅ Result: ALL TESTS PASSED
|
||||
|
|
@ -267,11 +267,11 @@ All tests use `TransactionCase` which ensures:
|
|||
|
||||
## Code Quality Indicators
|
||||
|
||||
✅ **No test flakiness** - All tests pass consistently
|
||||
✅ **Fast execution** - 0.35s for full suite
|
||||
✅ **Good coverage** - All major features tested
|
||||
✅ **Edge cases handled** - Empty suppliers, overwrites, isolation
|
||||
✅ **Clear assertions** - Descriptive error messages
|
||||
✅ **No test flakiness** - All tests pass consistently
|
||||
✅ **Fast execution** - 0.35s for full suite
|
||||
✅ **Good coverage** - All major features tested
|
||||
✅ **Edge cases handled** - Empty suppliers, overwrites, isolation
|
||||
✅ **Clear assertions** - Descriptive error messages
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -307,6 +307,6 @@ All 10 tests passing with 0 failures and 0 errors confirms the addon is stable a
|
|||
|
||||
---
|
||||
|
||||
**Maintained by**: Criptomart
|
||||
**License**: AGPL-3.0
|
||||
**Maintained by**: Criptomart
|
||||
**License**: AGPL-3.0
|
||||
**Last Updated**: 10 de febrero de 2026
|
||||
|
|
|
|||
|
|
@ -304,6 +304,6 @@ docker-compose exec -T odoo odoo -d odoo \
|
|||
|
||||
---
|
||||
|
||||
**Status**: ✅ **IMPLEMENTACIÓN COMPLETA**
|
||||
**Fecha**: 10 de febrero de 2026
|
||||
**Status**: ✅ **IMPLEMENTACIÓN COMPLETA**
|
||||
**Fecha**: 10 de febrero de 2026
|
||||
**Licencia**: AGPL-3.0
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
# Copyright 2026 Your Company
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import _
|
||||
from odoo import api
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
"""Extend res.partner with default price category for suppliers."""
|
||||
|
||||
_inherit = 'res.partner'
|
||||
_inherit = "res.partner"
|
||||
|
||||
default_price_category_id = fields.Many2one(
|
||||
comodel_name='product.price.category',
|
||||
string='Default Price Category',
|
||||
help='Default price category for products from this supplier',
|
||||
comodel_name="product.price.category",
|
||||
string="Default Price Category",
|
||||
help="Default price category for products from this supplier",
|
||||
domain=[],
|
||||
)
|
||||
|
||||
|
|
@ -21,24 +24,26 @@ class ResPartner(models.Model):
|
|||
self.ensure_one()
|
||||
|
||||
# Count products where this partner is the default supplier
|
||||
product_count = self.env['product.template'].search_count([
|
||||
('main_seller_id', '=', self.id)
|
||||
])
|
||||
product_count = self.env["product.template"].search_count(
|
||||
[("main_seller_id", "=", self.id)]
|
||||
)
|
||||
|
||||
# Create wizard record with context data
|
||||
wizard = self.env['wizard.update.product.category'].create({
|
||||
'partner_id': self.id,
|
||||
'partner_name': self.name,
|
||||
'price_category_id': self.default_price_category_id.id,
|
||||
'product_count': product_count,
|
||||
})
|
||||
wizard = self.env["wizard.update.product.category"].create(
|
||||
{
|
||||
"partner_id": self.id,
|
||||
"partner_name": self.name,
|
||||
"price_category_id": self.default_price_category_id.id,
|
||||
"product_count": product_count,
|
||||
}
|
||||
)
|
||||
|
||||
# Return action to open wizard modal
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Update Product Price Category'),
|
||||
'res_model': 'wizard.update.product.category',
|
||||
'res_id': wizard.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Update Product Price Category"),
|
||||
"res_model": "wizard.update.product.category",
|
||||
"res_id": wizard.id,
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,40 @@
|
|||
# Copyright 2026 Your Company
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import _
|
||||
from odoo import api
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class WizardUpdateProductCategory(models.TransientModel):
|
||||
"""Wizard to confirm and bulk update product price categories."""
|
||||
|
||||
_name = 'wizard.update.product.category'
|
||||
_description = 'Update Product Price Category'
|
||||
_name = "wizard.update.product.category"
|
||||
_description = "Update Product Price Category"
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name='res.partner',
|
||||
string='Supplier',
|
||||
comodel_name="res.partner",
|
||||
string="Supplier",
|
||||
readonly=True,
|
||||
required=True,
|
||||
)
|
||||
|
||||
partner_name = fields.Char(
|
||||
string='Supplier Name',
|
||||
string="Supplier Name",
|
||||
readonly=True,
|
||||
related='partner_id.name',
|
||||
related="partner_id.name",
|
||||
)
|
||||
|
||||
price_category_id = fields.Many2one(
|
||||
comodel_name='product.price.category',
|
||||
string='Price Category',
|
||||
comodel_name="product.price.category",
|
||||
string="Price Category",
|
||||
readonly=True,
|
||||
required=True,
|
||||
)
|
||||
|
||||
product_count = fields.Integer(
|
||||
string='Number of Products',
|
||||
string="Number of Products",
|
||||
readonly=True,
|
||||
required=True,
|
||||
)
|
||||
|
|
@ -41,36 +44,33 @@ class WizardUpdateProductCategory(models.TransientModel):
|
|||
self.ensure_one()
|
||||
|
||||
# Search all products where this partner is the default supplier
|
||||
products = self.env['product.template'].search([
|
||||
('main_seller_id', '=', self.partner_id.id)
|
||||
])
|
||||
products = self.env["product.template"].search(
|
||||
[("main_seller_id", "=", self.partner_id.id)]
|
||||
)
|
||||
|
||||
if not products:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('No Products'),
|
||||
'message': _('No products found with this supplier.'),
|
||||
'type': 'warning',
|
||||
'sticky': False,
|
||||
}
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": _("No Products"),
|
||||
"message": _("No products found with this supplier."),
|
||||
"type": "warning",
|
||||
"sticky": False,
|
||||
},
|
||||
}
|
||||
|
||||
# Bulk update all products
|
||||
products.write({
|
||||
'price_category_id': self.price_category_id.id
|
||||
})
|
||||
products.write({"price_category_id": self.price_category_id.id})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Success'),
|
||||
'message': _(
|
||||
'%d products updated with category "%s".'
|
||||
) % (len(products), self.price_category_id.display_name),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": _("Success"),
|
||||
"message": _('%d products updated with category "%s".')
|
||||
% (len(products), self.price_category_id.display_name),
|
||||
"type": "success",
|
||||
"sticky": False,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright 2026 Your Company
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestProductPriceCategorySupplier(TransactionCase):
|
||||
|
|
@ -14,68 +14,88 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
|||
super().setUpClass()
|
||||
|
||||
# Create price categories
|
||||
cls.category_premium = cls.env['product.price.category'].create({
|
||||
'name': 'Premium',
|
||||
})
|
||||
cls.category_standard = cls.env['product.price.category'].create({
|
||||
'name': 'Standard',
|
||||
})
|
||||
cls.category_premium = cls.env["product.price.category"].create(
|
||||
{
|
||||
"name": "Premium",
|
||||
}
|
||||
)
|
||||
cls.category_standard = cls.env["product.price.category"].create(
|
||||
{
|
||||
"name": "Standard",
|
||||
}
|
||||
)
|
||||
|
||||
# Create suppliers
|
||||
cls.supplier_a = cls.env['res.partner'].create({
|
||||
'name': 'Supplier A',
|
||||
'supplier_rank': 1,
|
||||
'default_price_category_id': cls.category_premium.id,
|
||||
})
|
||||
cls.supplier_b = cls.env['res.partner'].create({
|
||||
'name': 'Supplier B',
|
||||
'supplier_rank': 1,
|
||||
'default_price_category_id': cls.category_standard.id,
|
||||
})
|
||||
cls.supplier_a = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Supplier A",
|
||||
"supplier_rank": 1,
|
||||
"default_price_category_id": cls.category_premium.id,
|
||||
}
|
||||
)
|
||||
cls.supplier_b = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Supplier B",
|
||||
"supplier_rank": 1,
|
||||
"default_price_category_id": cls.category_standard.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Create a non-supplier partner
|
||||
cls.customer = cls.env['res.partner'].create({
|
||||
'name': 'Customer A',
|
||||
'customer_rank': 1,
|
||||
'supplier_rank': 0,
|
||||
})
|
||||
cls.customer = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Customer A",
|
||||
"customer_rank": 1,
|
||||
"supplier_rank": 0,
|
||||
}
|
||||
)
|
||||
|
||||
# Create products with supplier A as default
|
||||
cls.product_1 = cls.env['product.template'].create({
|
||||
'name': 'Product 1',
|
||||
'default_supplier_id': cls.supplier_a.id,
|
||||
})
|
||||
cls.product_2 = cls.env['product.template'].create({
|
||||
'name': 'Product 2',
|
||||
'default_supplier_id': cls.supplier_a.id,
|
||||
})
|
||||
cls.product_3 = cls.env['product.template'].create({
|
||||
'name': 'Product 3',
|
||||
'default_supplier_id': cls.supplier_a.id,
|
||||
})
|
||||
cls.product_1 = cls.env["product.template"].create(
|
||||
{
|
||||
"name": "Product 1",
|
||||
"default_supplier_id": cls.supplier_a.id,
|
||||
}
|
||||
)
|
||||
cls.product_2 = cls.env["product.template"].create(
|
||||
{
|
||||
"name": "Product 2",
|
||||
"default_supplier_id": cls.supplier_a.id,
|
||||
}
|
||||
)
|
||||
cls.product_3 = cls.env["product.template"].create(
|
||||
{
|
||||
"name": "Product 3",
|
||||
"default_supplier_id": cls.supplier_a.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Create product with supplier B
|
||||
cls.product_4 = cls.env['product.template'].create({
|
||||
'name': 'Product 4',
|
||||
'default_supplier_id': cls.supplier_b.id,
|
||||
})
|
||||
cls.product_4 = cls.env["product.template"].create(
|
||||
{
|
||||
"name": "Product 4",
|
||||
"default_supplier_id": cls.supplier_b.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Create product without supplier
|
||||
cls.product_5 = cls.env['product.template'].create({
|
||||
'name': 'Product 5',
|
||||
'default_supplier_id': False,
|
||||
})
|
||||
cls.product_5 = cls.env["product.template"].create(
|
||||
{
|
||||
"name": "Product 5",
|
||||
"default_supplier_id": False,
|
||||
}
|
||||
)
|
||||
|
||||
def test_01_supplier_has_default_price_category_field(self):
|
||||
"""Test that supplier has default_price_category_id field."""
|
||||
self.assertTrue(
|
||||
hasattr(self.supplier_a, 'default_price_category_id'),
|
||||
'Supplier should have default_price_category_id field'
|
||||
hasattr(self.supplier_a, "default_price_category_id"),
|
||||
"Supplier should have default_price_category_id field",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.supplier_a.default_price_category_id.id,
|
||||
self.category_premium.id,
|
||||
'Supplier should have Premium category assigned'
|
||||
"Supplier should have Premium category assigned",
|
||||
)
|
||||
|
||||
def test_02_action_update_products_opens_wizard(self):
|
||||
|
|
@ -83,41 +103,38 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
|||
action = self.supplier_a.action_update_products_price_category()
|
||||
|
||||
self.assertEqual(
|
||||
action['type'], 'ir.actions.act_window',
|
||||
'Action should be a window action'
|
||||
action["type"], "ir.actions.act_window", "Action should be a window action"
|
||||
)
|
||||
self.assertEqual(
|
||||
action['res_model'], 'wizard.update.product.category',
|
||||
'Action should open wizard model'
|
||||
action["res_model"],
|
||||
"wizard.update.product.category",
|
||||
"Action should open wizard model",
|
||||
)
|
||||
self.assertEqual(
|
||||
action['target'], 'new',
|
||||
'Action should open in modal (target=new)'
|
||||
action["target"], "new", "Action should open in modal (target=new)"
|
||||
)
|
||||
self.assertIn('res_id', action, 'Action should have res_id')
|
||||
self.assertIn("res_id", action, "Action should have res_id")
|
||||
self.assertTrue(
|
||||
action['res_id'] > 0,
|
||||
'res_id should be a valid wizard record ID'
|
||||
action["res_id"] > 0, "res_id should be a valid wizard record ID"
|
||||
)
|
||||
|
||||
def test_03_wizard_counts_products_correctly(self):
|
||||
"""Test that wizard counts products from supplier correctly."""
|
||||
action = self.supplier_a.action_update_products_price_category()
|
||||
|
||||
|
||||
# Get the wizard record that was created
|
||||
wizard = self.env['wizard.update.product.category'].browse(action['res_id'])
|
||||
wizard = self.env["wizard.update.product.category"].browse(action["res_id"])
|
||||
|
||||
self.assertEqual(
|
||||
wizard.product_count, 3,
|
||||
'Wizard should count 3 products from Supplier A'
|
||||
wizard.product_count, 3, "Wizard should count 3 products from Supplier A"
|
||||
)
|
||||
self.assertEqual(
|
||||
wizard.partner_name, 'Supplier A',
|
||||
'Wizard should display supplier name'
|
||||
wizard.partner_name, "Supplier A", "Wizard should display supplier name"
|
||||
)
|
||||
self.assertEqual(
|
||||
wizard.price_category_id.id, self.category_premium.id,
|
||||
'Wizard should have Premium category from supplier'
|
||||
wizard.price_category_id.id,
|
||||
self.category_premium.id,
|
||||
"Wizard should have Premium category from supplier",
|
||||
)
|
||||
|
||||
def test_04_wizard_updates_all_products_from_supplier(self):
|
||||
|
|
@ -125,75 +142,82 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
|||
# Verify initial state - no categories assigned
|
||||
self.assertFalse(
|
||||
self.product_1.price_category_id,
|
||||
'Product 1 should not have category initially'
|
||||
"Product 1 should not have category initially",
|
||||
)
|
||||
self.assertFalse(
|
||||
self.product_2.price_category_id,
|
||||
'Product 2 should not have category initially'
|
||||
"Product 2 should not have category initially",
|
||||
)
|
||||
|
||||
# Create and execute wizard
|
||||
wizard = self.env['wizard.update.product.category'].create({
|
||||
'partner_id': self.supplier_a.id,
|
||||
'price_category_id': self.category_premium.id,
|
||||
'product_count': 3,
|
||||
})
|
||||
wizard = self.env["wizard.update.product.category"].create(
|
||||
{
|
||||
"partner_id": self.supplier_a.id,
|
||||
"price_category_id": self.category_premium.id,
|
||||
"product_count": 3,
|
||||
}
|
||||
)
|
||||
result = wizard.action_confirm()
|
||||
|
||||
# Verify products were updated
|
||||
self.assertEqual(
|
||||
self.product_1.price_category_id.id, self.category_premium.id,
|
||||
'Product 1 should have Premium category'
|
||||
self.product_1.price_category_id.id,
|
||||
self.category_premium.id,
|
||||
"Product 1 should have Premium category",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.product_2.price_category_id.id, self.category_premium.id,
|
||||
'Product 2 should have Premium category'
|
||||
self.product_2.price_category_id.id,
|
||||
self.category_premium.id,
|
||||
"Product 2 should have Premium category",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.product_3.price_category_id.id, self.category_premium.id,
|
||||
'Product 3 should have Premium category'
|
||||
self.product_3.price_category_id.id,
|
||||
self.category_premium.id,
|
||||
"Product 3 should have Premium category",
|
||||
)
|
||||
|
||||
# Verify product from other supplier was NOT updated
|
||||
self.assertFalse(
|
||||
self.product_4.price_category_id,
|
||||
'Product 4 (from Supplier B) should not be updated'
|
||||
"Product 4 (from Supplier B) should not be updated",
|
||||
)
|
||||
|
||||
# Verify success notification
|
||||
self.assertEqual(
|
||||
result['type'], 'ir.actions.client',
|
||||
'Result should be a client action'
|
||||
result["type"], "ir.actions.client", "Result should be a client action"
|
||||
)
|
||||
self.assertEqual(
|
||||
result['tag'], 'display_notification',
|
||||
'Result should display a notification'
|
||||
result["tag"],
|
||||
"display_notification",
|
||||
"Result should display a notification",
|
||||
)
|
||||
|
||||
def test_05_wizard_handles_supplier_with_no_products(self):
|
||||
"""Test wizard behavior when supplier has no products."""
|
||||
# Create supplier without products
|
||||
supplier_no_products = self.env['res.partner'].create({
|
||||
'name': 'Supplier No Products',
|
||||
'supplier_rank': 1,
|
||||
'default_price_category_id': self.category_standard.id,
|
||||
})
|
||||
supplier_no_products = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Supplier No Products",
|
||||
"supplier_rank": 1,
|
||||
"default_price_category_id": self.category_standard.id,
|
||||
}
|
||||
)
|
||||
|
||||
wizard = self.env['wizard.update.product.category'].create({
|
||||
'partner_id': supplier_no_products.id,
|
||||
'price_category_id': self.category_standard.id,
|
||||
'product_count': 0,
|
||||
})
|
||||
wizard = self.env["wizard.update.product.category"].create(
|
||||
{
|
||||
"partner_id": supplier_no_products.id,
|
||||
"price_category_id": self.category_standard.id,
|
||||
"product_count": 0,
|
||||
}
|
||||
)
|
||||
result = wizard.action_confirm()
|
||||
|
||||
# Verify warning notification
|
||||
self.assertEqual(
|
||||
result['type'], 'ir.actions.client',
|
||||
'Result should be a client action'
|
||||
result["type"], "ir.actions.client", "Result should be a client action"
|
||||
)
|
||||
self.assertEqual(
|
||||
result['params']['type'], 'warning',
|
||||
'Should display warning notification'
|
||||
result["params"]["type"], "warning", "Should display warning notification"
|
||||
)
|
||||
|
||||
def test_06_customer_does_not_show_price_category_field(self):
|
||||
|
|
@ -201,11 +225,10 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
|||
# This is a view-level test - we verify the field exists but logic is correct
|
||||
self.assertFalse(
|
||||
self.customer.default_price_category_id,
|
||||
'Customer should not have price category set'
|
||||
"Customer should not have price category set",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.customer.supplier_rank, 0,
|
||||
'Customer should have supplier_rank = 0'
|
||||
self.customer.supplier_rank, 0, "Customer should have supplier_rank = 0"
|
||||
)
|
||||
|
||||
def test_07_wizard_overwrites_existing_categories(self):
|
||||
|
|
@ -215,87 +238,99 @@ class TestProductPriceCategorySupplier(TransactionCase):
|
|||
self.product_2.price_category_id = self.category_standard.id
|
||||
|
||||
self.assertEqual(
|
||||
self.product_1.price_category_id.id, self.category_standard.id,
|
||||
'Product 1 should have Standard category initially'
|
||||
self.product_1.price_category_id.id,
|
||||
self.category_standard.id,
|
||||
"Product 1 should have Standard category initially",
|
||||
)
|
||||
|
||||
# Execute wizard to change to Premium
|
||||
wizard = self.env['wizard.update.product.category'].create({
|
||||
'partner_id': self.supplier_a.id,
|
||||
'price_category_id': self.category_premium.id,
|
||||
'product_count': 3,
|
||||
})
|
||||
wizard = self.env["wizard.update.product.category"].create(
|
||||
{
|
||||
"partner_id": self.supplier_a.id,
|
||||
"price_category_id": self.category_premium.id,
|
||||
"product_count": 3,
|
||||
}
|
||||
)
|
||||
wizard.action_confirm()
|
||||
|
||||
# Verify categories were overwritten
|
||||
self.assertEqual(
|
||||
self.product_1.price_category_id.id, self.category_premium.id,
|
||||
'Product 1 category should be overwritten to Premium'
|
||||
self.product_1.price_category_id.id,
|
||||
self.category_premium.id,
|
||||
"Product 1 category should be overwritten to Premium",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.product_2.price_category_id.id, self.category_premium.id,
|
||||
'Product 2 category should be overwritten to Premium'
|
||||
self.product_2.price_category_id.id,
|
||||
self.category_premium.id,
|
||||
"Product 2 category should be overwritten to Premium",
|
||||
)
|
||||
|
||||
def test_08_multiple_suppliers_independent_updates(self):
|
||||
"""Test that updating one supplier doesn't affect other suppliers' products."""
|
||||
# Update Supplier A products
|
||||
wizard_a = self.env['wizard.update.product.category'].create({
|
||||
'partner_id': self.supplier_a.id,
|
||||
'price_category_id': self.category_premium.id,
|
||||
'product_count': 3,
|
||||
})
|
||||
wizard_a = self.env["wizard.update.product.category"].create(
|
||||
{
|
||||
"partner_id": self.supplier_a.id,
|
||||
"price_category_id": self.category_premium.id,
|
||||
"product_count": 3,
|
||||
}
|
||||
)
|
||||
wizard_a.action_confirm()
|
||||
|
||||
# Update Supplier B products
|
||||
wizard_b = self.env['wizard.update.product.category'].create({
|
||||
'partner_id': self.supplier_b.id,
|
||||
'price_category_id': self.category_standard.id,
|
||||
'product_count': 1,
|
||||
})
|
||||
wizard_b = self.env["wizard.update.product.category"].create(
|
||||
{
|
||||
"partner_id": self.supplier_b.id,
|
||||
"price_category_id": self.category_standard.id,
|
||||
"product_count": 1,
|
||||
}
|
||||
)
|
||||
wizard_b.action_confirm()
|
||||
|
||||
# Verify each supplier's products have correct category
|
||||
self.assertEqual(
|
||||
self.product_1.price_category_id.id, self.category_premium.id,
|
||||
'Supplier A products should have Premium'
|
||||
self.product_1.price_category_id.id,
|
||||
self.category_premium.id,
|
||||
"Supplier A products should have Premium",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.product_4.price_category_id.id, self.category_standard.id,
|
||||
'Supplier B products should have Standard'
|
||||
self.product_4.price_category_id.id,
|
||||
self.category_standard.id,
|
||||
"Supplier B products should have Standard",
|
||||
)
|
||||
|
||||
def test_09_wizard_readonly_fields(self):
|
||||
"""Test that wizard display fields are readonly."""
|
||||
wizard = self.env['wizard.update.product.category'].create({
|
||||
'partner_id': self.supplier_a.id,
|
||||
'price_category_id': self.category_premium.id,
|
||||
'product_count': 3,
|
||||
})
|
||||
wizard = self.env["wizard.update.product.category"].create(
|
||||
{
|
||||
"partner_id": self.supplier_a.id,
|
||||
"price_category_id": self.category_premium.id,
|
||||
"product_count": 3,
|
||||
}
|
||||
)
|
||||
|
||||
# Verify partner_name is computed from partner_id
|
||||
self.assertEqual(
|
||||
wizard.partner_name, 'Supplier A',
|
||||
'partner_name should be related to partner_id.name'
|
||||
wizard.partner_name,
|
||||
"Supplier A",
|
||||
"partner_name should be related to partner_id.name",
|
||||
)
|
||||
|
||||
def test_10_action_counts_products_correctly(self):
|
||||
"""Test that action_update_products_price_category counts products correctly."""
|
||||
action = self.supplier_a.action_update_products_price_category()
|
||||
|
||||
|
||||
# Get the wizard that was created
|
||||
wizard = self.env['wizard.update.product.category'].browse(action['res_id'])
|
||||
wizard = self.env["wizard.update.product.category"].browse(action["res_id"])
|
||||
|
||||
# Count products manually
|
||||
actual_count = self.env['product.template'].search_count([
|
||||
('default_supplier_id', '=', self.supplier_a.id)
|
||||
])
|
||||
actual_count = self.env["product.template"].search_count(
|
||||
[("default_supplier_id", "=", self.supplier_a.id)]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
wizard.product_count, actual_count,
|
||||
f'Wizard should count {actual_count} products'
|
||||
)
|
||||
self.assertEqual(
|
||||
wizard.product_count, 3,
|
||||
'Supplier A should have 3 products'
|
||||
wizard.product_count,
|
||||
actual_count,
|
||||
f"Wizard should count {actual_count} products",
|
||||
)
|
||||
self.assertEqual(wizard.product_count, 3, "Supplier A should have 3 products")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue