Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ Use `prompts_common.SCHED_TASK_SORT_10M` and `prompts_common.SCHED_TODO_5M` as d
| `@rcx.on_tool_call("name")` | `async def(toolcall, args) -> str` |
| `@rcx.on_erp_change("table")` | `async def(action, new_record, old_record)` |

**ERP action types**: `"INSERT"`, `"UPDATE"`, `"DELETE"`, `"ARCHIVE"`
- `ARCHIVE`: soft delete (archived_ts: 0 → >0)
- `DELETE`: hard delete (removed from db)

---

## Setup Schema
Expand Down
2 changes: 1 addition & 1 deletion flexus_client_kit/ckit_bot_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ async def subscribe_and_produce_callbacks(

elif upd.news_about.startswith("erp."):
table_name = upd.news_about[4:]
if upd.news_action in ["INSERT", "UPDATE", "DELETE"]:
if upd.news_action in ["INSERT", "UPDATE", "DELETE", "ARCHIVE"]:
handled = True
new_record = upd.news_payload_erp_record_new
old_record = upd.news_payload_erp_record_old
Expand Down
39 changes: 29 additions & 10 deletions flexus_client_kit/ckit_erp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
import gql

from flexus_client_kit import ckit_client, gql_utils
from flexus_client_kit import ckit_client, gql_utils, erp_schema

T = TypeVar('T')

Expand Down Expand Up @@ -141,6 +141,33 @@ async def delete_erp_record(
return r["erp_table_delete"]


async def batch_upsert_erp_records(
client: ckit_client.FlexusClient,
table_name: str,
ws_id: str,
upsert_key: str,
records: List[Any],
) -> dict:
http = await client.use_http()
async with http as h:
r = await h.execute(gql.gql("""
mutation ErpTableBatchUpsert($schema_name: String!, $table_name: String!, $ws_id: String!, $upsert_key: String!, $records_json: String!) {
erp_table_batch_upsert(schema_name: $schema_name, table_name: $table_name, ws_id: $ws_id, upsert_key: $upsert_key, records_json: $records_json)
}"""),
variable_values={
"schema_name": "erp",
"table_name": table_name,
"ws_id": ws_id,
"upsert_key": upsert_key,
"records_json": json.dumps([dataclass_or_dict_to_dict(r) for r in records]),
},
)
result = r["erp_table_batch_upsert"]
if isinstance(result, str):
return json.loads(result)
return result


def check_record_matches_filters(record: dict, filters: List[Union[str, dict]], col_names: set = None) -> bool:
"""
Check if a record (dict) matches all filters.
Expand Down Expand Up @@ -296,17 +323,9 @@ def check_record_matches_filter(record: dict, f: str, col_names: set = None) ->


async def test():
from flexus_client_kit.erp_schema import ProductTemplate, ProductProduct
client = ckit_client.FlexusClient("ckit_erp_test")
ws_id = "solarsystem"
products = await query_erp_table(
client,
"product_product",
ws_id,
ProductProduct,
limit=10,
include=["prodt"],
)
products = await query_erp_table(client, "product_product", ws_id, erp_schema.ProductProduct, limit=10, include=["prodt"])
print(f"Found {len(products)} products:")
for p in products:
print(p)
Expand Down
269 changes: 130 additions & 139 deletions flexus_client_kit/erp_schema.py
Original file line number Diff line number Diff line change
@@ -1,128 +1,164 @@
import time
from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List


def get_pkey_field(cls: Type) -> str:
for name, f in cls.__dataclass_fields__.items():
if f.metadata.get("pkey"):
return name
raise ValueError(f"No pkey field in {cls.__name__}")


def get_important_fields(cls: Type) -> List[str]:
return [name for name, f in cls.__dataclass_fields__.items() if f.metadata.get("importance", 0) > 0]


def get_extra_search_fields(cls: Type) -> List[str]:
return [name for name, f in cls.__dataclass_fields__.items() if f.metadata.get("extra_search")]


def get_field_display(cls: Type, field_name: str) -> Optional[str]:
f = cls.__dataclass_fields__.get(field_name)
return f.metadata.get("display") if f else None


def get_field_enum(cls: Type, field_name: str) -> Optional[List[str]]:
f = cls.__dataclass_fields__.get(field_name)
return f.metadata.get("enum") if f else None


def get_field_display_name(cls: Type, field_name: str) -> Optional[str]:
f = cls.__dataclass_fields__.get(field_name)
return f.metadata.get("display_name") if f else None


def get_field_description(cls: Type, field_name: str) -> Optional[str]:
f = cls.__dataclass_fields__.get(field_name)
return f.metadata.get("description") if f else None


@dataclass
class CrmContact:
ws_id: str
contact_first_name: str
contact_last_name: str
contact_email: str
contact_id: str = ""
contact_notes: str = ""
contact_details: dict = field(default_factory=dict)
contact_tags: List[str] = field(default_factory=list)
contact_address_line1: str = ""
contact_address_line2: str = ""
contact_address_city: str = ""
contact_address_state: str = ""
contact_address_zip: str = ""
contact_address_country: str = ""
contact_utm_first_source: str = ""
contact_utm_first_medium: str = ""
contact_utm_first_campaign: str = ""
contact_utm_first_term: str = ""
contact_utm_first_content: str = ""
contact_utm_last_source: str = ""
contact_utm_last_medium: str = ""
contact_utm_last_campaign: str = ""
contact_utm_last_term: str = ""
contact_utm_last_content: str = ""
contact_bant_score: int = -1
contact_created_ts: float = 0.0
contact_modified_ts: float = 0.0
contact_archived_ts: float = 0.0
contact_first_name: str = field(metadata={"importance": 1, "display_name": "First Name"})
contact_last_name: str = field(metadata={"importance": 1, "display_name": "Last Name"})
contact_email: str = field(metadata={"importance": 1, "extra_search": True, "display_name": "Email"})
contact_phone: str = field(default="", metadata={"display_name": "Phone"})
contact_id: str = field(default="", metadata={"pkey": True, "display_name": "Contact ID"})
contact_notes: str = field(default="", metadata={"importance": 1, "display": "string_multiline", "display_name": "Notes"})
contact_details: dict = field(default_factory=dict, metadata={"display_name": "Details", "description": "Custom JSON data: BANT qualification reasons, social profiles, preferences, custom attributes"})
contact_tags: List[str] = field(default_factory=list, metadata={"importance": 1, "display_name": "Tags"})
contact_address_line1: str = field(default="", metadata={"display_name": "Address Line 1"})
contact_address_line2: str = field(default="", metadata={"display_name": "Address Line 2"})
contact_address_city: str = field(default="", metadata={"display_name": "City"})
contact_address_state: str = field(default="", metadata={"display_name": "State"})
contact_address_zip: str = field(default="", metadata={"display_name": "ZIP Code"})
contact_address_country: str = field(default="", metadata={"importance": 1, "display_name": "Country"})
contact_utm_first_source: str = field(default="", metadata={"importance": 1, "display_name": "UTM Source (first touch)", "description": "First marketing interaction that brought this contact"})
contact_utm_first_medium: str = field(default="", metadata={"display_name": "UTM Medium (first touch)"})
contact_utm_first_campaign: str = field(default="", metadata={"display_name": "UTM Campaign (first touch)"})
contact_utm_first_term: str = field(default="", metadata={"display_name": "UTM Term (first touch)"})
contact_utm_first_content: str = field(default="", metadata={"display_name": "UTM Content (first touch)"})
contact_utm_last_source: str = field(default="", metadata={"display_name": "UTM Source (last touch)", "description": "Most recent marketing interaction"})
contact_utm_last_medium: str = field(default="", metadata={"display_name": "UTM Medium (last touch)"})
contact_utm_last_campaign: str = field(default="", metadata={"display_name": "UTM Campaign (last touch)"})
contact_utm_last_term: str = field(default="", metadata={"display_name": "UTM Term (last touch)"})
contact_utm_last_content: str = field(default="", metadata={"display_name": "UTM Content (last touch)"})
contact_bant_score: int = field(default=-1, metadata={"display_name": "BANT Qualification Score", "description": "Budget, Authority, Need, Timeline. -1 means not qualified, 0-4 scale"})
contact_created_ts: float = field(default=0.0, metadata={"importance": 1, "display_name": "Created at"})
contact_modified_ts: float = field(default=0.0, metadata={"display_name": "Modified at"})
contact_archived_ts: float = field(default=0.0, metadata={"display_name": "Archived at"})


@dataclass
class CrmTask:
class CrmActivity:
ws_id: str
contact_id: str
task_type: str
task_title: str
task_notes: str = ""
task_details: dict = field(default_factory=dict)
task_id: str = ""
task_due_ts: float = 0.0
task_completed_ts: float = 0.0
task_created_ts: float = field(default_factory=time.time)
task_modified_ts: float = field(default_factory=time.time)
contact: Optional['CrmContact'] = None
activity_title: str = field(metadata={"importance": 1, "display_name": "Title"})
activity_type: str = field(metadata={"importance": 1, "display_name": "Type", "enum": ["WEB_CHAT", "MESSENGER_CHAT", "EMAIL", "CALL", "MEETING"]})
activity_direction: str = field(metadata={"importance": 1, "display_name": "Direction", "enum": ["INBOUND", "OUTBOUND"]})
activity_contact_id: str = field(metadata={"importance": 1, "display_name": "Contact"})
activity_id: str = field(default="", metadata={"pkey": True, "display_name": "Activity ID"})
activity_channel: str = field(default="", metadata={"importance": 1, "display_name": "Channel"})
activity_ft_id: Optional[str] = field(default=None, metadata={"importance": 1, "display_name": "Thread"})
activity_summary: str = field(default="", metadata={"importance": 1, "display": "string_multiline", "display_name": "Summary"})
activity_details: dict = field(default_factory=dict, metadata={"display_name": "Details"})
activity_occurred_ts: float = field(default=0.0, metadata={"importance": 1, "display_name": "Occurred at"})
activity_created_ts: float = field(default=0.0, metadata={"display_name": "Created at"})
activity_modified_ts: float = field(default=0.0, metadata={"display_name": "Modified at"})


@dataclass
class ProductTemplate:
prodt_id: str
prodt_name: str
prodt_description: str
prodt_target_customers: str
prodt_type: str
prodt_pcat_id: str
prodt_list_price: int # stored in cents
prodt_standard_price: int # stored in cents
prodt_uom_id: str
prodt_active: bool
ws_id: str
prodt_chips: List[str]
pcat: Optional['ProductCategory'] = None
uom: Optional['ProductUom'] = None
prodt_id: str = field(metadata={"pkey": True, "display_name": "Product Template ID"})
prodt_name: str = field(metadata={"importance": 1, "display_name": "Name"})
prodt_description: str = field(metadata={"importance": 1, "display": "string_multiline", "display_name": "Description"})
prodt_target_customers: str = field(metadata={"importance": 1, "display": "string_multiline", "display_name": "Target Customers"})
prodt_type: str = field(metadata={"importance": 1, "display_name": "Type"})
prodt_pcat_id: str = field(metadata={"display_name": "Category"})
prodt_list_price: int = field(metadata={"importance": 1, "display_name": "List Price"})
prodt_standard_price: int = field(metadata={"importance": 1, "display_name": "Standard Price"})
prodt_uom_id: str = field(metadata={"display_name": "Unit of Measure"})
prodt_active: bool = field(metadata={"importance": 1, "display_name": "Active"})
ws_id: str = field(metadata={"display_name": "Workspace ID"})
prodt_chips: List[str] = field(metadata={"importance": 1, "display_name": "Chips"})
pcat: Optional['ProductCategory'] = field(default=None, metadata={"display_name": "Category"})
uom: Optional['ProductUom'] = field(default=None, metadata={"display_name": "Unit of Measure"})


@dataclass
class ProductProduct:
prod_id: str
prodt_id: str
prod_default_code: Optional[str]
prod_barcode: Optional[str]
prod_active: bool
ws_id: str
prodt: Optional[ProductTemplate] = None # Optional not because it's not nullable, it's optional because you have an option to include or not include it when querying
prod_id: str = field(metadata={"pkey": True, "display_name": "Product ID"})
prodt_id: str = field(metadata={"importance": 1, "display_name": "Product Template"})
prod_default_code: Optional[str] = field(metadata={"importance": 1, "display_name": "Internal Reference"})
prod_barcode: Optional[str] = field(metadata={"importance": 1, "display_name": "Barcode"})
prod_active: bool = field(metadata={"importance": 1, "display_name": "Active"})
ws_id: str = field(metadata={"display_name": "Workspace ID"})
prodt: Optional[ProductTemplate] = field(default=None, metadata={"display_name": "Product Template"})


@dataclass
class ProductCategory:
pcat_id: str
pcat_name: str
pcat_parent_id: Optional[str]
pcat_active: bool
ws_id: str
parent: Optional['ProductCategory'] = None
pcat_id: str = field(metadata={"pkey": True, "display_name": "Category ID"})
pcat_name: str = field(metadata={"importance": 1, "display_name": "Name"})
pcat_parent_id: Optional[str] = field(metadata={"importance": 1, "display_name": "Parent Category"})
pcat_active: bool = field(metadata={"importance": 1, "display_name": "Active"})
ws_id: str = field(metadata={"display_name": "Workspace ID"})
parent: Optional['ProductCategory'] = field(default=None, metadata={"display_name": "Parent Category"})


@dataclass
class ProductTag:
tag_id: str
tag_name: str
tag_sequence: int
tag_color: str
tag_visible_to_customers: bool
ws_id: str
tag_id: str = field(metadata={"pkey": True, "display_name": "Tag ID"})
tag_name: str = field(metadata={"importance": 1, "display_name": "Name"})
tag_sequence: int = field(metadata={"importance": 1, "display_name": "Sequence"})
tag_color: str = field(metadata={"importance": 1, "display_name": "Color"})
tag_visible_to_customers: bool = field(metadata={"importance": 1, "display_name": "Visible to Customers"})
ws_id: str = field(metadata={"display_name": "Workspace ID"})


@dataclass
class ProductUom:
uom_id: str
uom_name: str
uom_category_id: Optional[str]
uom_active: bool
ws_id: str
category: Optional[ProductCategory] = None
uom_id: str = field(metadata={"pkey": True, "display_name": "UoM ID"})
uom_name: str = field(metadata={"importance": 1, "display_name": "Name"})
uom_category_id: Optional[str] = field(metadata={"importance": 1, "display_name": "Category"})
uom_active: bool = field(metadata={"importance": 1, "display_name": "Active"})
ws_id: str = field(metadata={"display_name": "Workspace ID"})
category: Optional[ProductCategory] = field(default=None, metadata={"display_name": "Category"})


@dataclass
class ProductM2mTemplateTag:
id: str
tag_id: str
prodt_id: str
ws_id: str
tag: Optional[ProductTag] = None
prodt: Optional['ProductTemplate'] = None
id: str = field(metadata={"pkey": True, "display_name": "ID"})
tag_id: str = field(metadata={"display_name": "Tag"})
prodt_id: str = field(metadata={"display_name": "Product Template"})
ws_id: str = field(metadata={"display_name": "Workspace ID"})
tag: Optional[ProductTag] = field(default=None, metadata={"display_name": "Tag"})
prodt: Optional['ProductTemplate'] = field(default=None, metadata={"display_name": "Product Template"})


ERP_TABLE_TO_SCHEMA: Dict[str, Type] = {
"crm_contact": CrmContact,
"crm_task": CrmTask,
"crm_activity": CrmActivity,
"product_template": ProductTemplate,
"product_product": ProductProduct,
"product_category": ProductCategory,
Expand All @@ -131,57 +167,12 @@ class ProductM2mTemplateTag:
"product_m2m_template_tag": ProductM2mTemplateTag,
}


ERP_DEFAULT_VISIBLE_FIELDS: Dict[str, List[str]] = {
"crm_contact": [
"contact_first_name",
"contact_last_name",
"contact_email",
"contact_notes",
"contact_tags",
"contact_utm_first_source",
"contact_address_country",
"contact_created_ts",
],
"crm_task": [
"task_title",
"task_type",
"task_notes",
"task_due_ts",
"task_completed_ts",
"contact_id",
],
"product_template": [
"prodt_name",
"prodt_type",
"prodt_list_price",
"prodt_standard_price",
"prodt_active",
"prodt_chips",
"prodt_description",
"prodt_target_customers",
],
"product_product": [
"prodt_id",
"prod_default_code",
"prod_barcode",
"prod_active",
],
"product_category": [
"pcat_name",
"pcat_parent_id",
"pcat_active",
],
"product_tag": [
"tag_name",
"tag_sequence",
"tag_color",
"tag_visible_to_customers",
],
"product_uom": [
"uom_name",
"uom_category_id",
"uom_active",
],
ERP_DISPLAY_NAME_CONFIGS: Dict[str, str] = {
"crm_contact": "{contact_first_name} {contact_last_name}",
"crm_activity": "{activity_title}",
"product_template": "{prodt_name}",
"product_product": "{prod_default_code} {prod_barcode}",
"product_category": "{pcat_name}",
"product_tag": "{tag_name}",
"product_uom": "{uom_name}",
}

Loading