Dynamic Models
Type-safe Python dataclasses generated from your Acumatica schema
Overview
When you initialize the AcumaticaClient, it automatically generates Python dataclass models for every entity in your Acumatica instance. These models provide type hints, IDE autocomplete, and validation for working with Acumatica data.
from easy_acumatica import AcumaticaClient
# Initialize client
client = AcumaticaClient()
# Models are automatically generated and available
customer = client.models.Customer(
CustomerID="CUST001",
CustomerName="Acme Corporation"
)
# Create in Acumatica
created = client.customers.put_entity(customer)Accessing Models
Models are accessed via client.models using the entity name:
# Access models via client.models
Customer = client.models.Customer
Invoice = client.models.Invoice
SalesOrder = client.models.SalesOrder
# Check if a model exists
if hasattr(client.models, 'Customer'):
print("Customer model available")Creating Entities
Instantiate models with field values as keyword arguments:
# Simple entity
customer = client.models.Customer(
CustomerID="CUST001",
CustomerName="Acme Corporation",
Email="contact@acme.com",
Phone="555-0123"
)
# Only required fields need values
# Optional fields can be omitted
minimal_customer = client.models.Customer(
CustomerID="CUST002",
CustomerName="Minimal Corp"
)Field Types
Models use Python type hints that correspond to Acumatica field types:
from datetime import datetime
# String fields
customer = client.models.Customer(
CustomerID="CUST001", # str
CustomerName="Acme Corp", # str
Email="contact@acme.com" # Optional[str]
)
# Numeric fields
invoice = client.models.Invoice(
RefNbr="INV001",
Amount=1500.50, # float
TaxTotal=150.05 # Optional[float]
)
# Date/DateTime fields
order = client.models.SalesOrder(
OrderNbr="SO001",
Date=datetime.now(), # datetime
RequestedOn=datetime(2024, 3, 15) # Optional[datetime]
)
# Boolean fields
contact = client.models.Contact(
DisplayName="John Doe",
Active=True # bool
) Most fields are Optional to support partial updates and missing data. Only explicitly required fields in the schema are non-optional.
Complex Fields
Entities with detail lines or related objects use nested models:
Detail Lines
# Create invoice with detail lines
invoice = client.models.Invoice(
CustomerID="CUST001",
Date=datetime.now(),
Description="March Services",
Details=[
client.models.InvoiceDetail(
InventoryID="SERVICE01",
Description="Consulting",
Quantity=10,
UnitPrice=150.00
),
client.models.InvoiceDetail(
InventoryID="SERVICE02",
Description="Support",
Quantity=5,
UnitPrice=100.00
)
]
)
created = client.invoices.put_entity(invoice)Related Entities
# Some entities include related objects
sales_order = client.models.SalesOrder(
CustomerID="CUST001",
Date=datetime.now(),
# Shipping address as nested object
ShipToAddress=client.models.Address(
AddressLine1="123 Main St",
City="New York",
State="NY",
PostalCode="10001"
),
Details=[...]
)Custom Fields
Custom fields defined in your Acumatica instance are included in the generated models. They appear alongside standard fields with appropriate type hints:
# Custom fields appear alongside standard fields
# Assuming "UsrCustomerTier" is a custom field in your instance
customer = client.models.Customer(
CustomerID="CUST001",
CustomerName="Acme Corp",
# Standard field
CreditLimit=50000.00,
# Custom field (if exists in your instance)
UsrCustomerTier="Gold",
UsrAccountManager="John Smith"
)Partial Updates
Models support partial updates - you only need to provide the key fields and the fields you want to change:
# Partial update - only provide fields to change
update = client.models.Customer(
CustomerID="CUST001", # Required: identifies the record
Email="newemail@acme.com", # Only field we want to update
Phone="555-9999" # And this one
)
# CreditLimit, CustomerName, etc. are not affected
updated = client.customers.put_entity(update)Using with Services
Models work seamlessly with dynamic services. Services accept both model instances and plain dictionaries:
# Using a model instance
customer = client.models.Customer(
CustomerID="CUST001",
CustomerName="Acme Corp"
)
created = client.customers.put_entity(customer)
# Using a plain dictionary (also works)
customer_dict = {
"CustomerID": {"value": "CUST002"},
"CustomerName": {"value": "Other Corp"}
}
created = client.customers.put_entity(customer_dict)
# Models are preferred for type safetyType Hints and IDE Support
Models provide full type hints for IDE autocomplete and type checking. Generate stub files for enhanced IDE support:
from easy_acumatica import AcumaticaClient
from easy_acumatica.generate_stubs import generate_stubs_from_client
# Initialize client
client = AcumaticaClient()
# Generate stub files for IDE support
generate_stubs_from_client(client)
# Now your IDE will provide autocomplete for all models
# Example: typing "client.models." will show all available models
# Example: typing "client.models.Customer(" will show all fields Stub generation creates .pyi files that provide type information to IDEs like VSCode and PyCharm without requiring runtime model generation.
Data Validation
Models perform basic type validation when instantiated:
# Type validation happens at instantiation
try:
customer = client.models.Customer(
CustomerID="CUST001",
CustomerName=12345 # Wrong type - should be string
)
except TypeError as e:
print(f"Type error: {e}")
# Field name validation
try:
customer = client.models.Customer(
CustomerID="CUST001",
NonExistentField="value" # Unknown field
)
except TypeError as e:
print(f"Unknown field: {e}")Converting to Dictionaries
Use the build() method to convert models to API-ready dictionaries:
# Create a model
customer = client.models.Customer(
CustomerID="CUST001",
CustomerName="Acme Corp",
Email="contact@acme.com"
)
# Convert to API-ready dictionary
payload = customer.build()
print(payload)
# {
# "CustomerID": {"value": "CUST001"},
# "CustomerName": {"value": "Acme Corp"},
# "Email": {"value": "contact@acme.com"}
# }
# Useful for debugging what will be sent to the APIModel Discovery
List all available models and inspect their structure:
# List all available models using built-in method
models = client.list_models()
print(f"Found {len(models)} models")
print("Sample models:", models[:10])
# Get detailed model information
model_info = client.get_model_info('Customer')
print(f"Customer fields: {model_info['fields']}")
print(f"Field count: {model_info['field_count']}")
# Get model class directly
Customer = client.models.Customer
# Inspect model fields with dataclasses
import dataclasses
fields = dataclasses.fields(Customer)
for field in fields:
print(f"{field.name}: {field.type}")Schema Inspection
Models provide a get_schema() classmethod that returns a simplified schema showing Python types for all fields. This is useful for understanding model structure and for programmatically exploring field types:
# Get the simplified schema for a model
schema = client.models.Customer.get_schema()
print(schema)
# Example output (each field has 'type' and 'fields' keys):
# {
# 'CustomerID': {'type': 'str', 'fields': {}},
# 'CustomerName': {'type': 'str', 'fields': {}},
# 'Email': {'type': 'str', 'fields': {}},
# 'CreditLimit': {'type': 'float', 'fields': {}},
# 'Status': {'type': 'str', 'fields': {}},
# 'MainContact': {
# 'type': 'Contact',
# 'fields': {
# 'Email': {'type': 'str', 'fields': {}},
# 'DisplayName': {'type': 'str', 'fields': {}},
# 'Phone': {'type': 'str', 'fields': {}}
# }
# },
# 'Addresses': {
# 'type': 'List[Address]',
# 'fields': {
# 'AddressLine1': {'type': 'str', 'fields': {}},
# 'City': {'type': 'str', 'fields': {}},
# 'State': {'type': 'str', 'fields': {}},
# 'PostalCode': {'type': 'str', 'fields': {}}
# }
# }
# }
# Check field types programmatically
customer_id_info = schema['CustomerID']
print(f"CustomerID is a {customer_id_info['type']} field")
# Check for nested models
main_contact_info = schema.get('MainContact', {})
if main_contact_info.get('type') and main_contact_info['type'] != 'str':
print(f"MainContact is a nested {main_contact_info['type']} model:")
for field_name, field_info in main_contact_info['fields'].items():
print(f" {field_name}: {field_info['type']}")
# Check for array fields
addresses_info = schema.get('Addresses', {})
if 'List[' in addresses_info.get('type', ''):
print(f"Addresses is an array: {addresses_info['type']}")
print(f"With fields: {list(addresses_info['fields'].keys())}")
# Useful for generating documentation or validation
def print_schema(model_class, indent=0):
"""Recursively print a model's schema"""
schema = model_class.get_schema()
for field_name, field_info in schema.items():
field_type = field_info.get('type', 'unknown')
fields = field_info.get('fields', {})
if 'List[' in field_type:
# Array field
print(" " * indent + f"{field_name}: {field_type}")
if fields:
print_nested_fields(fields, indent + 1)
elif fields:
# Nested model
print(" " * indent + f"{field_name}: {field_type}")
print_nested_fields(fields, indent + 1)
else:
# Primitive field
print(" " * indent + f"{field_name}: {field_type}")
def print_nested_fields(fields, indent):
for name, field_info in fields.items():
field_type = field_info.get('type', 'unknown')
nested_fields = field_info.get('fields', {})
if nested_fields:
print(" " * indent + f"{name}: {field_type}")
print_nested_fields(nested_fields, indent + 1)
else:
print(" " * indent + f"{name}: {field_type}")
# Print the full schema
print_schema(client.models.SalesOrder)Schema Structure
The get_schema() method returns a nested dictionary where each field has:
- Primitive fields:
{'type': 'str', 'fields': {}}(types: 'str', 'int', 'bool', 'float', 'datetime') - Nested models:
{'type': 'ModelName', 'fields': {...}}with recursively expanded field schemas - Array fields:
{'type': 'List[TypeName]', 'fields': {...}}with the item schema in 'fields' - Circular references:
{'type': '(circular: ClassName)', 'fields': {}}to prevent infinite recursion - Any type:
{'type': 'Any', 'fields': {}}for untyped or dynamic fields
This is different from the service's get_schema() method, which retrieves Acumatica's native $adHocSchema. The model's get_schema() returns simplified Python type information for the generated dataclass.