Sales Category Documentation¶
Overview¶
The Sales Category module (sale.categ) provides a classification system for organizing sales orders into distinct business categories. This master data model enables companies to segment their sales operations, apply category-specific numbering sequences, and generate category-based reports and analytics.
Model Information¶
Model Name: sale.categ
Display Name: Sales Category
Key Fields: name (unique)
Features¶
- Unique key constraint per category name
- Custom sales order numbering sequences per category
- Search-enabled fields for quick lookups
- Simple master data structure for easy maintenance
Understanding Key Fields¶
What are Key Fields?¶
In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.
For the sale.categ model, the key field is:
This means the category name must be unique:
- name - The unique category name (required field)
Why Key Fields Matter¶
Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique names:
# Examples of valid combinations:
{"name": "Retail Sales"} # Valid
{"name": "Wholesale"} # Valid
{"name": "B2B Sales"} # Valid
# This would fail - duplicate key:
{"name": "Retail Sales"} # ERROR: Category name already exists!
Database Implementation¶
The key fields are enforced at the database level using a unique constraint:
_sql_constraints = [
("sale_categ_name_unique",
"unique (name)",
"Sales category name already exists!")
]
This translates to:
Common Sales Category Types¶
| Category Type | Example Code | Typical Use Case |
|---|---|---|
| Retail | RETAIL |
Direct-to-consumer sales through stores |
| Wholesale | WHOLESALE |
Bulk sales to resellers |
| B2B | B2B |
Business-to-business transactions |
| E-commerce | ECOM |
Online sales channel |
| Export | EXPORT |
International sales |
| Government | GOV |
Government contracts and tenders |
Field Reference¶
Basic Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
name |
Char | Yes | Unique category name (e.g., "Retail Sales") |
code |
Char | No | Short code for reports and identification (e.g., "RET") |
description |
Text | No | Detailed description of category purpose and usage |
Configuration Fields¶
| Field | Type | Description |
|---|---|---|
sale_sequence_id |
Many2One | Link to custom sales order number sequence for this category |
The sale_sequence_id field allows each category to have its own numbering format:
- Retail orders: SO-R-0001, SO-R-0002
- Wholesale orders: SO-W-0001, SO-W-0002
- Export orders: SO-E-0001, SO-E-0002
API Methods¶
1. Create Sales Category¶
Method: create(vals, context)
Creates a new sales category record.
Parameters:
vals = {
"name": "Retail Sales", # Required: Category name
"code": "RETAIL", # Optional: Short code
"description": "Direct sales to consumers", # Optional: Description
"sale_sequence_id": 15 # Optional: Sequence ID
}
Returns: int - New category ID
Example:
# Create a retail sales category
category_id = get_model("sale.categ").create({
"name": "Retail Sales",
"code": "RETAIL",
"description": "Point of sale and direct customer sales",
"sale_sequence_id": get_model("sequence").search([["type","=","sale_order"],["code","=","RETAIL"]])[0]
})
2. Search Categories¶
Method: search(condition, context)
Find sales categories by various criteria.
Examples:
# Find category by name
category_ids = get_model("sale.categ").search([["name", "=", "Retail Sales"]])
# Find categories with codes starting with "W"
category_ids = get_model("sale.categ").search([["code", "ilike", "W%"]])
# Search in name or code
category_ids = get_model("sale.categ").search([["name", "ilike", "retail"]])
3. Update Category¶
Method: write(ids, vals, context)
Update existing category records.
Example:
# Update category description
get_model("sale.categ").write([category_id], {
"description": "Updated description for retail category",
"sale_sequence_id": new_sequence_id
})
Search Functions¶
Search by Name¶
# Exact match
condition = [["name", "=", "Retail Sales"]]
# Partial match (case-insensitive)
condition = [["name", "ilike", "retail"]]
Search by Code¶
# Find by code
condition = [["code", "=", "RETAIL"]]
# Find all codes starting with "W"
condition = [["code", "ilike", "W%"]]
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.order |
Referenced by | Sales orders reference category via categ_id |
sequence |
Many2One | Custom numbering sequences for each category |
sale.quot |
Referenced by | Sales quotations may reference category |
Common Use Cases¶
Use Case 1: Initial Category Setup¶
# 1. Create standard business categories
categories = [
{"name": "Retail Sales", "code": "RETAIL"},
{"name": "Wholesale", "code": "WHOLESALE"},
{"name": "B2B Sales", "code": "B2B"},
{"name": "E-commerce", "code": "ECOM"},
{"name": "Export Sales", "code": "EXPORT"}
]
for categ in categories:
get_model("sale.categ").create(categ)
# 2. Verify created categories
all_categs = get_model("sale.categ").search([])
print(f"Created {len(all_categs)} sales categories")
Use Case 2: Category with Custom Numbering¶
# 1. Create a sequence for retail sales
seq_id = get_model("sequence").create({
"name": "Retail Sales Orders",
"code": "SO-RETAIL",
"type": "sale_order",
"prefix": "SO-R-",
"padding": 4,
"number_next": 1
})
# 2. Create category with sequence
categ_id = get_model("sale.categ").create({
"name": "Retail Sales",
"code": "RETAIL",
"description": "Direct-to-consumer sales",
"sale_sequence_id": seq_id
})
# 3. When creating sales orders with this category,
# they will automatically use SO-R-0001, SO-R-0002, etc.
Use Case 3: Category-Based Reporting¶
# Generate sales report by category
def get_sales_by_category(date_from, date_to):
results = []
# Get all categories
category_ids = get_model("sale.categ").search([])
categories = get_model("sale.categ").browse(category_ids)
for categ in categories:
# Find sales orders in this category
order_ids = get_model("sale.order").search([
["categ_id", "=", categ.id],
["date", ">=", date_from],
["date", "<=", date_to],
["state", "in", ["confirmed", "done"]]
])
orders = get_model("sale.order").browse(order_ids)
total_amount = sum(o.amount_total for o in orders)
results.append({
"category": categ.name,
"code": categ.code,
"order_count": len(orders),
"total_amount": total_amount
})
return results
Use Case 4: Filtering Orders by Category¶
# Find all retail sales orders this month
import datetime
start_of_month = datetime.date.today().replace(day=1)
# Get retail category
retail_categ = get_model("sale.categ").search([["code", "=", "RETAIL"]])
# Find orders in this category
order_ids = get_model("sale.order").search([
["categ_id", "in", retail_categ],
["date", ">=", start_of_month.strftime("%Y-%m-%d")]
])
print(f"Found {len(order_ids)} retail orders this month")
Use Case 5: Category Migration¶
# Migrate old category to new category
def migrate_category(old_categ_id, new_categ_id):
# Find all orders with old category
order_ids = get_model("sale.order").search([
["categ_id", "=", old_categ_id]
])
# Update to new category
if order_ids:
get_model("sale.order").write(order_ids, {
"categ_id": new_categ_id
})
print(f"Migrated {len(order_ids)} orders to new category")
# Optionally delete old category
# get_model("sale.categ").delete([old_categ_id])
Best Practices¶
1. Naming Conventions¶
# Good: Clear, descriptive names
{"name": "Retail Sales", "code": "RETAIL"}
{"name": "Wholesale Distribution", "code": "WHOLESALE"}
{"name": "B2B Corporate Sales", "code": "B2B"}
# Bad: Vague or inconsistent names
{"name": "Cat1", "code": "C1"} # Not descriptive
{"name": "retail", "code": "retail"} # Inconsistent casing
{"name": "Sales Type A", "code": "A"} # Unclear meaning
Guidelines: - Use title case for category names - Use uppercase for codes - Keep codes short but meaningful (3-8 characters) - Be consistent across all categories
2. Category Organization¶
Start with core categories:
# Basic set for most businesses
core_categories = [
"Retail Sales", # Direct consumer sales
"Wholesale", # Bulk/reseller sales
"Online Sales" # E-commerce channel
]
Expand as needed:
# Add specialized categories only when necessary
specialized = [
"Government Contracts",
"Export Sales",
"Tender Projects",
"Consignment Sales"
]
Avoid over-categorization: - Don't create too many categories (aim for 5-10 maximum) - Each category should have a clear business purpose - Categories should be mutually exclusive
3. Sequence Assignment¶
# Good: Each major category has its own sequence
retail_seq = create_sequence("SO-R-", "Retail Sales")
wholesale_seq = create_sequence("SO-W-", "Wholesale")
online_seq = create_sequence("SO-O-", "Online Sales")
# This makes it easy to identify order type from number:
# SO-R-0001 = Retail order
# SO-W-0001 = Wholesale order
# SO-O-0001 = Online order
# Bad: No sequences or shared sequences
# All orders use same numbering (harder to track by category)
4. Data Governance¶
Establish clear ownership:
# Define who can create/modify categories
# Typically: Sales Manager, System Administrator
# Document category purposes
category_docs = {
"RETAIL": {
"purpose": "Direct customer sales through physical stores",
"owner": "Retail Sales Manager",
"sequence_format": "SO-R-XXXX"
},
"WHOLESALE": {
"purpose": "Bulk sales to authorized resellers",
"owner": "Wholesale Manager",
"sequence_format": "SO-W-XXXX"
}
}
Review periodically: - Quarterly review of category usage - Remove unused categories - Merge similar categories - Update descriptions as business evolves
Database Constraints¶
Unique Name Constraint¶
This ensures no two categories can have the same name, preventing confusion and data integrity issues.
Performance Tips¶
1. Use Codes for Queries¶
# Good: Search by code (faster, indexed)
categ_ids = get_model("sale.categ").search([["code", "=", "RETAIL"]])
# Less optimal: Search by description
categ_ids = get_model("sale.categ").search([["description", "ilike", "%retail%"]])
2. Cache Category Lookups¶
# Cache frequently used categories
_category_cache = {}
def get_category_by_code(code):
if code not in _category_cache:
categ_ids = get_model("sale.categ").search([["code", "=", code]])
if categ_ids:
_category_cache[code] = categ_ids[0]
return _category_cache.get(code)
3. Limit Category Count¶
- Keep total categories under 20 for optimal performance
- More categories = more complex reporting and slower queries
- Use other fields (tags, customer groups) for finer segmentation
Troubleshooting¶
"Sales category name already exists!"¶
Cause: Attempting to create a category with a name that already exists in the system.
Solution:
- Check existing categories: get_model("sale.categ").search([["name", "ilike", "retail"]])
- Use a different name or update the existing category
- Ensure no leading/trailing spaces in category name
"Cannot delete category - referenced by sales orders"¶
Cause: Category is being used by existing sales orders and cannot be deleted. Solution: - Migrate orders to a different category first - Or mark the category as inactive (add custom field) instead of deleting - Use category migration script (see Use Case 5)
"Missing sequence - orders not numbered correctly"¶
Cause: Category's sale_sequence_id is not set or references deleted sequence.
Solution:
# Create new sequence for category
seq_id = get_model("sequence").create({
"name": "Sales Orders - Retail",
"type": "sale_order",
"prefix": "SO-R-",
"padding": 4
})
# Update category
get_model("sale.categ").write([categ_id], {
"sale_sequence_id": seq_id
})
Testing Examples¶
Unit Test: Create Category¶
def test_create_sales_category():
# Create category
categ_id = get_model("sale.categ").create({
"name": "Test Retail",
"code": "TEST_RET"
})
# Verification
assert categ_id > 0
# Read back
categ = get_model("sale.categ").browse(categ_id)
assert categ.name == "Test Retail"
assert categ.code == "TEST_RET"
# Cleanup
get_model("sale.categ").delete([categ_id])
Unit Test: Unique Name Constraint¶
def test_unique_category_name():
# Create first category
categ1_id = get_model("sale.categ").create({
"name": "Unique Category Test"
})
# Try to create duplicate
try:
categ2_id = get_model("sale.categ").create({
"name": "Unique Category Test"
})
assert False, "Should have raised error for duplicate name"
except Exception as e:
assert "already exists" in str(e).lower()
# Cleanup
get_model("sale.categ").delete([categ1_id])
Security Considerations¶
Permission Model¶
sale_categ_create- Create new sales categoriessale_categ_write- Modify existing categoriessale_categ_delete- Delete categories (requires no order references)sale_categ_read- View category information
Data Access¶
- Categories are typically company-wide master data
- No row-level security needed (all users see all categories)
- Restrict create/modify/delete to sales managers and administrators
- All sales users should have read access for order creation
Integration Points¶
Internal Modules¶
- sale.order: References categories for order classification
- sale.quot: May reference categories for quotation tracking
- sequence: Provides custom numbering sequences per category
- report: Categories used for sales analytics and reports
Reporting Integration¶
# Categories commonly used in:
# - Sales by Category Report
# - Category Performance Dashboard
# - Order Volume by Category
# - Revenue Analysis by Sales Channel
Version History¶
Last Updated: 2026-01-05 Model Version: sale_categ.py Framework: Netforce
Additional Resources¶
- Sales Order Documentation:
sale.order - Sequence Configuration:
sequence - Sales Quotation:
sale.quot - Sales Reporting Guide
Support & Feedback¶
For issues or questions about this module: 1. Check existing categories before creating new ones 2. Review sequence configuration for numbering issues 3. Verify category codes are unique and meaningful 4. Test category changes in development environment first
This documentation is generated for developer onboarding and reference purposes.