Easy-Acumatica
For Python

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.

Python
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:

Python
# 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:

Python
# 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:

Python
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

Python
# 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

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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 safety

Type Hints and IDE Support

Models provide full type hints for IDE autocomplete and type checking. Generate stub files for enhanced IDE support:

Python
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:

Python
# 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:

Python
# 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 API

Model Discovery

List all available models and inspect their structure:

Python
# 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:

Python
# 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.