celerp 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- celerp/__init__.py +6 -0
- celerp/ai/__init__.py +2 -0
- celerp/ai/batch.py +228 -0
- celerp/ai/cleanup.py +84 -0
- celerp/ai/commands.py +184 -0
- celerp/ai/conversations.py +234 -0
- celerp/ai/files.py +49 -0
- celerp/ai/intent.py +92 -0
- celerp/ai/llm.py +126 -0
- celerp/ai/memory.py +102 -0
- celerp/ai/models.py +43 -0
- celerp/ai/page_count.py +67 -0
- celerp/ai/quota.py +153 -0
- celerp/ai/service.py +265 -0
- celerp/ai/tools.py +286 -0
- celerp/alembic.ini +39 -0
- celerp/cli.py +763 -0
- celerp/compute/__init__.py +3 -0
- celerp/compute/aggregations.py +25 -0
- celerp/compute/valuation.py +38 -0
- celerp/config.py +293 -0
- celerp/connectors/__init__.py +26 -0
- celerp/connectors/base.py +149 -0
- celerp/connectors/daily_scheduler.py +120 -0
- celerp/connectors/http.py +65 -0
- celerp/connectors/outbound_queue.py +207 -0
- celerp/connectors/quickbooks.py +296 -0
- celerp/connectors/registry.py +32 -0
- celerp/connectors/shopify.py +356 -0
- celerp/connectors/sync_runner.py +126 -0
- celerp/connectors/upsert.py +64 -0
- celerp/connectors/webhooks.py +76 -0
- celerp/connectors/woocommerce.py +319 -0
- celerp/connectors/xero.py +266 -0
- celerp/db.py +23 -0
- celerp/events/__init__.py +3 -0
- celerp/events/engine.py +83 -0
- celerp/events/schemas.py +825 -0
- celerp/events/types.py +113 -0
- celerp/gateway/__init__.py +3 -0
- celerp/gateway/client.py +337 -0
- celerp/gateway/state.py +48 -0
- celerp/importers/__init__.py +2 -0
- celerp/importers/importer.py +384 -0
- celerp/importers/schema.py +199 -0
- celerp/main.py +255 -0
- celerp/middleware.py +131 -0
- celerp/migrations/README.md +47 -0
- celerp/migrations/env.py +83 -0
- celerp/migrations/script.py.mako +31 -0
- celerp/migrations/versions/a1b2c3d4e5f6_add_doc_share_tokens.py +45 -0
- celerp/migrations/versions/b2c3d4e5f6a7_add_import_batches.py +51 -0
- celerp/migrations/versions/c1d2e3f4a5b6_add_marketplace.py +52 -0
- celerp/migrations/versions/d2e3f4a5b6c7_add_password_reset_token.py +35 -0
- celerp/migrations/versions/e3f4a5b6c7d8_add_company_is_active.py +37 -0
- celerp/migrations/versions/f1a2b3c4d5e6_move_accounts_marketplace_to_modules.py +31 -0
- celerp/migrations/versions/fd5de461e14e_initial_schema.py +125 -0
- celerp/migrations/versions/g1h2i3j4k5l6_add_consignment_flag.py +35 -0
- celerp/migrations/versions/h2i3j4k5l6m7_add_bank_accounts_and_reconciliation.py +56 -0
- celerp/migrations/versions/j4k5l6m7n8o9_labels_db_persistence.py +42 -0
- celerp/migrations/versions/k5l6m7n8o9p0_add_reconciliation_v2.py +82 -0
- celerp/migrations/versions/l6m7n8o9p0q1_add_sync_runs_table.py +43 -0
- celerp/migrations/versions/m7n8o9p0q1r2_add_connector_configs_and_outbound_queue.py +60 -0
- celerp/models/__init__.py +9 -0
- celerp/models/accounting.py +25 -0
- celerp/models/ai.py +114 -0
- celerp/models/base.py +8 -0
- celerp/models/company.py +55 -0
- celerp/models/connector_config.py +60 -0
- celerp/models/import_batch.py +38 -0
- celerp/models/ledger.py +36 -0
- celerp/models/marketplace.py +8 -0
- celerp/models/notification.py +46 -0
- celerp/models/projections.py +32 -0
- celerp/models/share.py +32 -0
- celerp/models/sync_run.py +36 -0
- celerp/modules/__init__.py +25 -0
- celerp/modules/api.py +116 -0
- celerp/modules/license.py +139 -0
- celerp/modules/loader.py +440 -0
- celerp/modules/references.py +35 -0
- celerp/modules/registry.py +57 -0
- celerp/modules/slots.py +83 -0
- celerp/notifications/__init__.py +2 -0
- celerp/notifications/service.py +160 -0
- celerp/notifications/sse.py +139 -0
- celerp/projections/__init__.py +3 -0
- celerp/projections/engine.py +120 -0
- celerp/projections/handlers/__init__.py +3 -0
- celerp/projections/handlers/marketplace.py +42 -0
- celerp/projections/handlers/scanning.py +32 -0
- celerp/projections/handlers/system.py +30 -0
- celerp/routers/__init__.py +3 -0
- celerp/routers/auth.py +395 -0
- celerp/routers/companies.py +1657 -0
- celerp/routers/doctor.py +528 -0
- celerp/routers/health.py +81 -0
- celerp/routers/ledger.py +103 -0
- celerp/routers/notifications.py +121 -0
- celerp/routers/system.py +331 -0
- celerp/schemas/__init__.py +2 -0
- celerp/services/__init__.py +3 -0
- celerp/services/attachments.py +244 -0
- celerp/services/auth.py +156 -0
- celerp/services/auto_je.py +380 -0
- celerp/services/backup.py +215 -0
- celerp/services/backup_export.py +117 -0
- celerp/services/backup_files.py +173 -0
- celerp/services/backup_import.py +132 -0
- celerp/services/backup_scheduler.py +177 -0
- celerp/services/demo.py +1283 -0
- celerp/services/email.py +83 -0
- celerp/services/field_schema.py +105 -0
- celerp/services/fulfill.py +293 -0
- celerp/services/je_keys.py +18 -0
- celerp/services/pick.py +149 -0
- celerp/services/session_tracker.py +37 -0
- celerp/services/system_health.py +79 -0
- celerp/session_gate.py +83 -0
- celerp/tax_regimes.py +218 -0
- celerp-1.0.3.dist-info/METADATA +256 -0
- celerp-1.0.3.dist-info/RECORD +392 -0
- celerp-1.0.3.dist-info/WHEEL +5 -0
- celerp-1.0.3.dist-info/entry_points.txt +2 -0
- celerp-1.0.3.dist-info/licenses/LICENSE +77 -0
- celerp-1.0.3.dist-info/licenses/NOTICE +25 -0
- celerp-1.0.3.dist-info/top_level.txt +3 -0
- default_modules/__init__.py +8 -0
- default_modules/celerp-accounting/__init__.py +26 -0
- default_modules/celerp-accounting/celerp_accounting/__init__.py +2 -0
- default_modules/celerp-accounting/celerp_accounting/api_setup.py +9 -0
- default_modules/celerp-accounting/celerp_accounting/csv_parser.py +145 -0
- default_modules/celerp-accounting/celerp_accounting/matcher.py +196 -0
- default_modules/celerp-accounting/celerp_accounting/models.py +126 -0
- default_modules/celerp-accounting/celerp_accounting/projections.py +31 -0
- default_modules/celerp-accounting/celerp_accounting/routes.py +2048 -0
- default_modules/celerp-accounting/celerp_accounting/ui_routes.py +288 -0
- default_modules/celerp-admin/__init__.py +17 -0
- default_modules/celerp-admin/celerp_admin/__init__.py +2 -0
- default_modules/celerp-admin/celerp_admin/routes.py +528 -0
- default_modules/celerp-admin/celerp_admin/setup.py +7 -0
- default_modules/celerp-ai/__init__.py +22 -0
- default_modules/celerp-ai/celerp_ai/__init__.py +2 -0
- default_modules/celerp-ai/celerp_ai/routes.py +753 -0
- default_modules/celerp-ai/celerp_ai/setup.py +8 -0
- default_modules/celerp-ai/celerp_ai/ui_routes.py +1076 -0
- default_modules/celerp-backup/__init__.py +20 -0
- default_modules/celerp-backup/celerp_backup/__init__.py +2 -0
- default_modules/celerp-backup/celerp_backup/routes.py +261 -0
- default_modules/celerp-backup/celerp_backup/setup.py +7 -0
- default_modules/celerp-connectors/__init__.py +42 -0
- default_modules/celerp-connectors/celerp_connectors/__init__.py +2 -0
- default_modules/celerp-connectors/celerp_connectors/models.py +26 -0
- default_modules/celerp-connectors/celerp_connectors/routes.py +146 -0
- default_modules/celerp-contacts/__init__.py +36 -0
- default_modules/celerp-contacts/celerp_contacts/__init__.py +3 -0
- default_modules/celerp-contacts/celerp_contacts/projections.py +159 -0
- default_modules/celerp-contacts/celerp_contacts/routes.py +1082 -0
- default_modules/celerp-contacts/celerp_contacts/services.py +80 -0
- default_modules/celerp-contacts/celerp_contacts/ui_routes.py +15 -0
- default_modules/celerp-dashboard/__init__.py +19 -0
- default_modules/celerp-dashboard/celerp_dashboard/__init__.py +2 -0
- default_modules/celerp-dashboard/celerp_dashboard/routes.py +112 -0
- default_modules/celerp-dashboard/celerp_dashboard/setup.py +7 -0
- default_modules/celerp-docs/__init__.py +37 -0
- default_modules/celerp-docs/celerp_docs/__init__.py +3 -0
- default_modules/celerp-docs/celerp_docs/_ui_documents.py +1250 -0
- default_modules/celerp-docs/celerp_docs/api_setup.py +14 -0
- default_modules/celerp-docs/celerp_docs/doc_constants.py +6 -0
- default_modules/celerp-docs/celerp_docs/doc_projections.py +257 -0
- default_modules/celerp-docs/celerp_docs/doc_service.py +79 -0
- default_modules/celerp-docs/celerp_docs/models_share.py +4 -0
- default_modules/celerp-docs/celerp_docs/pdf.py +442 -0
- default_modules/celerp-docs/celerp_docs/routes.py +2020 -0
- default_modules/celerp-docs/celerp_docs/routes_share.py +619 -0
- default_modules/celerp-docs/celerp_docs/sequences.py +164 -0
- default_modules/celerp-docs/celerp_docs/taxes.py +40 -0
- default_modules/celerp-docs/celerp_docs/ui_routes.py +9 -0
- default_modules/celerp-inventory/__init__.py +35 -0
- default_modules/celerp-inventory/celerp_inventory/__init__.py +22 -0
- default_modules/celerp-inventory/celerp_inventory/models_import_batch.py +4 -0
- default_modules/celerp-inventory/celerp_inventory/projections.py +122 -0
- default_modules/celerp-inventory/celerp_inventory/routes.py +1683 -0
- default_modules/celerp-inventory/celerp_inventory/routes_attachments.py +264 -0
- default_modules/celerp-inventory/celerp_inventory/routes_scanning.py +107 -0
- default_modules/celerp-inventory/celerp_inventory/services.py +80 -0
- default_modules/celerp-inventory/celerp_inventory/ui_routes.py +20 -0
- default_modules/celerp-labels/LICENSE +21 -0
- default_modules/celerp-labels/__init__.py +64 -0
- default_modules/celerp-labels/celerp_labels/__init__.py +2 -0
- default_modules/celerp-labels/celerp_labels/migrations/__init__.py +7 -0
- default_modules/celerp-labels/celerp_labels/migrations/labels_001_create_label_templates.py +38 -0
- default_modules/celerp-labels/celerp_labels/models.py +71 -0
- default_modules/celerp-labels/celerp_labels/routes.py +264 -0
- default_modules/celerp-labels/celerp_labels/service.py +263 -0
- default_modules/celerp-labels/celerp_labels/ui_routes.py +1415 -0
- default_modules/celerp-labels/requirements.txt +4 -0
- default_modules/celerp-manufacturing/LICENSE +21 -0
- default_modules/celerp-manufacturing/__init__.py +53 -0
- default_modules/celerp-manufacturing/celerp_manufacturing/__init__.py +2 -0
- default_modules/celerp-manufacturing/celerp_manufacturing/projection_handler.py +49 -0
- default_modules/celerp-manufacturing/celerp_manufacturing/routes.py +621 -0
- default_modules/celerp-manufacturing/celerp_manufacturing/ui_routes.py +22 -0
- default_modules/celerp-manufacturing/requirements.txt +3 -0
- default_modules/celerp-reports/__init__.py +17 -0
- default_modules/celerp-reports/celerp_reports/__init__.py +2 -0
- default_modules/celerp-reports/celerp_reports/api_setup.py +9 -0
- default_modules/celerp-reports/celerp_reports/routes.py +554 -0
- default_modules/celerp-reports/celerp_reports/ui_routes.py +561 -0
- default_modules/celerp-subscriptions/__init__.py +26 -0
- default_modules/celerp-subscriptions/celerp_subscriptions/__init__.py +22 -0
- default_modules/celerp-subscriptions/celerp_subscriptions/projection_handler.py +34 -0
- default_modules/celerp-subscriptions/celerp_subscriptions/routes.py +412 -0
- default_modules/celerp-subscriptions/celerp_subscriptions/ui_routes.py +482 -0
- default_modules/celerp-subscriptions/celerp_subscriptions/ui_routes_import.py +189 -0
- default_modules/celerp-verticals/__init__.py +23 -0
- default_modules/celerp-verticals/celerp_verticals/__init__.py +19 -0
- default_modules/celerp-verticals/celerp_verticals/categories/accessory_fashion.json +66 -0
- default_modules/celerp-verticals/celerp_verticals/categories/activewear.json +72 -0
- default_modules/celerp-verticals/celerp_verticals/categories/art_photography.json +121 -0
- default_modules/celerp-verticals/celerp_verticals/categories/audio_equipment.json +89 -0
- default_modules/celerp-verticals/celerp_verticals/categories/bag_handbag.json +88 -0
- default_modules/celerp-verticals/celerp_verticals/categories/banknote.json +78 -0
- default_modules/celerp-verticals/celerp_verticals/categories/beauty_tool.json +53 -0
- default_modules/celerp-verticals/celerp_verticals/categories/bed_bedroom.json +97 -0
- default_modules/celerp-verticals/celerp_verticals/categories/beer.json +85 -0
- default_modules/celerp-verticals/celerp_verticals/categories/beverage_nonalc.json +81 -0
- default_modules/celerp-verticals/celerp_verticals/categories/body_part_auto.json +97 -0
- default_modules/celerp-verticals/celerp_verticals/categories/book.json +79 -0
- default_modules/celerp-verticals/celerp_verticals/categories/bottoms.json +86 -0
- default_modules/celerp-verticals/celerp_verticals/categories/brake_suspension.json +99 -0
- default_modules/celerp-verticals/celerp_verticals/categories/bullion_coin.json +85 -0
- default_modules/celerp-verticals/celerp_verticals/categories/camera.json +96 -0
- default_modules/celerp-verticals/celerp_verticals/categories/colored_stone.json +211 -0
- default_modules/celerp-verticals/celerp_verticals/categories/comic.json +71 -0
- default_modules/celerp-verticals/celerp_verticals/categories/commercial_space.json +131 -0
- default_modules/celerp-verticals/celerp_verticals/categories/component_part.json +63 -0
- default_modules/celerp-verticals/celerp_verticals/categories/confectionery.json +68 -0
- default_modules/celerp-verticals/celerp_verticals/categories/consulting_service.json +88 -0
- default_modules/celerp-verticals/celerp_verticals/categories/decorative_art.json +115 -0
- default_modules/celerp-verticals/celerp_verticals/categories/diamond.json +175 -0
- default_modules/celerp-verticals/celerp_verticals/categories/drawing.json +114 -0
- default_modules/celerp-verticals/celerp_verticals/categories/dress_jumpsuit.json +81 -0
- default_modules/celerp-verticals/celerp_verticals/categories/electrical_auto.json +76 -0
- default_modules/celerp-verticals/celerp_verticals/categories/electrical_component.json +63 -0
- default_modules/celerp-verticals/celerp_verticals/categories/emerald.json +141 -0
- default_modules/celerp-verticals/celerp_verticals/categories/engine_part.json +80 -0
- default_modules/celerp-verticals/celerp_verticals/categories/fastener.json +79 -0
- default_modules/celerp-verticals/celerp_verticals/categories/fertilizer_chemical.json +83 -0
- default_modules/celerp-verticals/celerp_verticals/categories/film_video.json +91 -0
- default_modules/celerp-verticals/celerp_verticals/categories/fine_writing_instrument.json +117 -0
- default_modules/celerp-verticals/celerp_verticals/categories/fluid_lubricant.json +56 -0
- default_modules/celerp-verticals/celerp_verticals/categories/footwear.json +75 -0
- default_modules/celerp-verticals/celerp_verticals/categories/fragrance.json +63 -0
- default_modules/celerp-verticals/celerp_verticals/categories/fresh_food.json +67 -0
- default_modules/celerp-verticals/celerp_verticals/categories/fresh_produce.json +82 -0
- default_modules/celerp-verticals/celerp_verticals/categories/frozen_food.json +78 -0
- default_modules/celerp-verticals/celerp_verticals/categories/gaming_console.json +65 -0
- default_modules/celerp-verticals/celerp_verticals/categories/gold_bullion.json +86 -0
- default_modules/celerp-verticals/celerp_verticals/categories/grain_cereal.json +84 -0
- default_modules/celerp-verticals/celerp_verticals/categories/haircare.json +74 -0
- default_modules/celerp-verticals/celerp_verticals/categories/hand_tool.json +61 -0
- default_modules/celerp-verticals/celerp_verticals/categories/ingredient_bulk.json +63 -0
- default_modules/celerp-verticals/celerp_verticals/categories/interior_part.json +83 -0
- default_modules/celerp-verticals/celerp_verticals/categories/jewelry.json +91 -0
- default_modules/celerp-verticals/celerp_verticals/categories/kitchen_dining.json +60 -0
- default_modules/celerp-verticals/celerp_verticals/categories/laptop.json +105 -0
- default_modules/celerp-verticals/celerp_verticals/categories/lighting.json +89 -0
- default_modules/celerp-verticals/celerp_verticals/categories/livestock_feed.json +81 -0
- default_modules/celerp-verticals/celerp_verticals/categories/makeup.json +75 -0
- default_modules/celerp-verticals/celerp_verticals/categories/measuring_instrument.json +68 -0
- default_modules/celerp-verticals/celerp_verticals/categories/medal_token.json +80 -0
- default_modules/celerp-verticals/celerp_verticals/categories/mineral_specimen.json +118 -0
- default_modules/celerp-verticals/celerp_verticals/categories/mobile_phone.json +79 -0
- default_modules/celerp-verticals/celerp_verticals/categories/music_cd.json +74 -0
- default_modules/celerp-verticals/celerp_verticals/categories/music_vinyl.json +115 -0
- default_modules/celerp-verticals/celerp_verticals/categories/nail.json +63 -0
- default_modules/celerp-verticals/celerp_verticals/categories/numismatic_coin.json +102 -0
- default_modules/celerp-verticals/celerp_verticals/categories/outdoor_furniture.json +86 -0
- default_modules/celerp-verticals/celerp_verticals/categories/outerwear.json +86 -0
- default_modules/celerp-verticals/celerp_verticals/categories/packaged_food.json +82 -0
- default_modules/celerp-verticals/celerp_verticals/categories/painting.json +138 -0
- default_modules/celerp-verticals/celerp_verticals/categories/parking_bay.json +79 -0
- default_modules/celerp-verticals/celerp_verticals/categories/pearl.json +122 -0
- default_modules/celerp-verticals/celerp_verticals/categories/personal_care.json +56 -0
- default_modules/celerp-verticals/celerp_verticals/categories/pipe_plumbing.json +65 -0
- default_modules/celerp-verticals/celerp_verticals/categories/platinum_bullion.json +92 -0
- default_modules/celerp-verticals/celerp_verticals/categories/power_tool.json +83 -0
- default_modules/celerp-verticals/celerp_verticals/categories/print_edition.json +142 -0
- default_modules/celerp-verticals/celerp_verticals/categories/residential_unit.json +110 -0
- default_modules/celerp-verticals/celerp_verticals/categories/rough_gemstone.json +124 -0
- default_modules/celerp-verticals/celerp_verticals/categories/ruby.json +142 -0
- default_modules/celerp-verticals/celerp_verticals/categories/saas_plan.json +86 -0
- default_modules/celerp-verticals/celerp_verticals/categories/safety_ppe.json +59 -0
- default_modules/celerp-verticals/celerp_verticals/categories/sapphire.json +148 -0
- default_modules/celerp-verticals/celerp_verticals/categories/sculpture.json +140 -0
- default_modules/celerp-verticals/celerp_verticals/categories/seating.json +98 -0
- default_modules/celerp-verticals/celerp_verticals/categories/seeds.json +72 -0
- default_modules/celerp-verticals/celerp_verticals/categories/silver_bullion.json +85 -0
- default_modules/celerp-verticals/celerp_verticals/categories/skincare.json +72 -0
- default_modules/celerp-verticals/celerp_verticals/categories/soft_furnishing.json +60 -0
- default_modules/celerp-verticals/celerp_verticals/categories/software_addon.json +56 -0
- default_modules/celerp-verticals/celerp_verticals/categories/software_license.json +85 -0
- default_modules/celerp-verticals/celerp_verticals/categories/spirit.json +91 -0
- default_modules/celerp-verticals/celerp_verticals/categories/storage_furniture.json +77 -0
- default_modules/celerp-verticals/celerp_verticals/categories/swimwear.json +75 -0
- default_modules/celerp-verticals/celerp_verticals/categories/table_desk.json +90 -0
- default_modules/celerp-verticals/celerp_verticals/categories/tablet.json +81 -0
- default_modules/celerp-verticals/celerp_verticals/categories/tire_wheel.json +83 -0
- default_modules/celerp-verticals/celerp_verticals/categories/tops.json +71 -0
- default_modules/celerp-verticals/celerp_verticals/categories/trading_card.json +80 -0
- default_modules/celerp-verticals/celerp_verticals/categories/tv_display.json +86 -0
- default_modules/celerp-verticals/celerp_verticals/categories/video_game.json +84 -0
- default_modules/celerp-verticals/celerp_verticals/categories/watch.json +131 -0
- default_modules/celerp-verticals/celerp_verticals/categories/watch_strap.json +92 -0
- default_modules/celerp-verticals/celerp_verticals/categories/wine.json +111 -0
- default_modules/celerp-verticals/celerp_verticals/presets/agricultural.json +27 -0
- default_modules/celerp-verticals/celerp_verticals/presets/artwork.json +25 -0
- default_modules/celerp-verticals/celerp_verticals/presets/automotive.json +26 -0
- default_modules/celerp-verticals/celerp_verticals/presets/blank.json +12 -0
- default_modules/celerp-verticals/celerp_verticals/presets/books_media.json +26 -0
- default_modules/celerp-verticals/celerp_verticals/presets/coins_precious_metals.json +26 -0
- default_modules/celerp-verticals/celerp_verticals/presets/consulting.json +22 -0
- default_modules/celerp-verticals/celerp_verticals/presets/cosmetics.json +26 -0
- default_modules/celerp-verticals/celerp_verticals/presets/electronics.json +27 -0
- default_modules/celerp-verticals/celerp_verticals/presets/fashion.json +28 -0
- default_modules/celerp-verticals/celerp_verticals/presets/food_beverage.json +28 -0
- default_modules/celerp-verticals/celerp_verticals/presets/furniture.json +27 -0
- default_modules/celerp-verticals/celerp_verticals/presets/gemstones.json +28 -0
- default_modules/celerp-verticals/celerp_verticals/presets/hardware.json +26 -0
- default_modules/celerp-verticals/celerp_verticals/presets/property_rental.json +24 -0
- default_modules/celerp-verticals/celerp_verticals/presets/saas.json +24 -0
- default_modules/celerp-verticals/celerp_verticals/presets/watches.json +23 -0
- default_modules/celerp-verticals/celerp_verticals/presets/wine_spirits.json +25 -0
- default_modules/celerp-verticals/celerp_verticals/routes.py +237 -0
- default_modules/celerp-verticals/celerp_verticals/ui_routes.py +8 -0
- ui/__init__.py +4 -0
- ui/api_client.py +1627 -0
- ui/app.py +264 -0
- ui/components/__init__.py +2 -0
- ui/components/activity.py +356 -0
- ui/components/backup.py +87 -0
- ui/components/cloud_gate.py +90 -0
- ui/components/shell.py +647 -0
- ui/components/table.py +1099 -0
- ui/config.py +54 -0
- ui/i18n.py +71 -0
- ui/locales/ar.json +1146 -0
- ui/locales/de.json +1146 -0
- ui/locales/en.json +1146 -0
- ui/locales/es.json +1146 -0
- ui/locales/fr.json +1146 -0
- ui/locales/id.json +1146 -0
- ui/locales/it.json +1146 -0
- ui/locales/ja.json +1146 -0
- ui/locales/pt.json +1146 -0
- ui/locales/th.json +1146 -0
- ui/locales/vi.json +1146 -0
- ui/routes/__init__.py +2 -0
- ui/routes/accounting.py +248 -0
- ui/routes/accounting_import.py +272 -0
- ui/routes/auth.py +650 -0
- ui/routes/contacts.py +2289 -0
- ui/routes/csv_import.py +1342 -0
- ui/routes/dashboard.py +813 -0
- ui/routes/docs_import.py +356 -0
- ui/routes/documents.py +4620 -0
- ui/routes/inventory.py +2959 -0
- ui/routes/lists_import.py +296 -0
- ui/routes/manufacturing.py +814 -0
- ui/routes/manufacturing_import.py +568 -0
- ui/routes/notifications.py +100 -0
- ui/routes/reconciliation.py +885 -0
- ui/routes/reports.py +561 -0
- ui/routes/search.py +72 -0
- ui/routes/settings.py +4071 -0
- ui/routes/settings_accounting.py +643 -0
- ui/routes/settings_cloud.py +710 -0
- ui/routes/settings_connectors.py +711 -0
- ui/routes/settings_contacts.py +298 -0
- ui/routes/settings_general.py +150 -0
- ui/routes/settings_import.py +644 -0
- ui/routes/settings_inventory.py +513 -0
- ui/routes/settings_purchasing.py +86 -0
- ui/routes/settings_sales.py +291 -0
- ui/routes/setup.py +647 -0
- ui/routes/subscriptions.py +483 -0
- ui/routes/subscriptions_import.py +319 -0
- ui/static/app.css +1901 -0
- ui/static/htmx.min.js +1 -0
- ui/static/icon.png +0 -0
- ui/static/logo.png +0 -0
celerp/__init__.py
ADDED
celerp/ai/__init__.py
ADDED
celerp/ai/batch.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Copyright (c) 2026 Noah Severs
|
|
2
|
+
# SPDX-License-Identifier: BSL-1.1
|
|
3
|
+
|
|
4
|
+
"""AI batch processing - parallel multi-file processing via OpenRouter.
|
|
5
|
+
|
|
6
|
+
Processes 2-100 files in parallel (up to BATCH_CONCURRENCY concurrent LLM calls).
|
|
7
|
+
Each file gets its own API call with the bulk extraction model.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
job = await start_batch(session, company_id, user_id, query, file_ids, credits)
|
|
11
|
+
# Background task runs the batch
|
|
12
|
+
# SSE events emitted per-file completion
|
|
13
|
+
# Notification created on finish
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import base64
|
|
20
|
+
import logging
|
|
21
|
+
import uuid
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
|
|
24
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
25
|
+
|
|
26
|
+
from celerp.ai.files import load_file, load_file_for_llm
|
|
27
|
+
from celerp.ai.llm import call_llm
|
|
28
|
+
from celerp.ai.models import BULK_EXTRACTION
|
|
29
|
+
from celerp.models.ai import AIBatchJob
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
BATCH_CONCURRENCY = 10
|
|
34
|
+
MAX_BATCH_FILES = 100
|
|
35
|
+
|
|
36
|
+
_BATCH_SYSTEM_PROMPT = """\
|
|
37
|
+
You are analyzing a business document (receipt, invoice, or contract).
|
|
38
|
+
Extract structured data: vendor name, date, total amount, line items with description/quantity/unit price.
|
|
39
|
+
|
|
40
|
+
Output a JSON block:
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"vendor_name": "string",
|
|
44
|
+
"date": "YYYY-MM-DD",
|
|
45
|
+
"total": 0.00,
|
|
46
|
+
"line_items": [
|
|
47
|
+
{"description": "string", "quantity": 1, "unit_price": 0.00}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If you cannot extract structured data, return a brief text summary instead (no JSON block).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _process_single_file(
|
|
57
|
+
file_id: str,
|
|
58
|
+
query: str,
|
|
59
|
+
company_id: uuid.UUID,
|
|
60
|
+
semaphore: asyncio.Semaphore,
|
|
61
|
+
) -> dict:
|
|
62
|
+
"""Process a single file through the LLM. Returns result dict."""
|
|
63
|
+
try:
|
|
64
|
+
data_bytes, meta = load_file(file_id, company_id)
|
|
65
|
+
except (FileNotFoundError, PermissionError):
|
|
66
|
+
return {"file_id": file_id, "status": "error", "error": f"File {file_id} not found"}
|
|
67
|
+
|
|
68
|
+
b64 = base64.b64encode(data_bytes).decode("utf-8")
|
|
69
|
+
files = [{"media_type": meta.get("content_type", "image/jpeg"), "data": b64}]
|
|
70
|
+
|
|
71
|
+
prompt = f"{query}\n\nAnalyze the attached document." if query else "Analyze the attached document."
|
|
72
|
+
|
|
73
|
+
async with semaphore:
|
|
74
|
+
try:
|
|
75
|
+
answer = await call_llm(BULK_EXTRACTION, _BATCH_SYSTEM_PROMPT, prompt, files=files)
|
|
76
|
+
return {
|
|
77
|
+
"file_id": file_id,
|
|
78
|
+
"filename": meta.get("filename", file_id),
|
|
79
|
+
"status": "success",
|
|
80
|
+
"answer": answer,
|
|
81
|
+
}
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
log.warning("Batch file %s failed: %s", file_id, exc)
|
|
84
|
+
return {
|
|
85
|
+
"file_id": file_id,
|
|
86
|
+
"filename": meta.get("filename", file_id),
|
|
87
|
+
"status": "error",
|
|
88
|
+
"error": "Processing failed for this file",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def create_batch_job(
|
|
93
|
+
session: AsyncSession,
|
|
94
|
+
company_id: uuid.UUID,
|
|
95
|
+
user_id: uuid.UUID,
|
|
96
|
+
query: str,
|
|
97
|
+
file_ids: list[str],
|
|
98
|
+
credits: int,
|
|
99
|
+
conversation_id: uuid.UUID | None = None,
|
|
100
|
+
) -> AIBatchJob:
|
|
101
|
+
"""Create a pending batch job record."""
|
|
102
|
+
if len(file_ids) > MAX_BATCH_FILES:
|
|
103
|
+
raise ValueError(f"Maximum {MAX_BATCH_FILES} files per batch")
|
|
104
|
+
if len(file_ids) < 2:
|
|
105
|
+
raise ValueError("Batch requires at least 2 files")
|
|
106
|
+
|
|
107
|
+
job = AIBatchJob(
|
|
108
|
+
company_id=company_id,
|
|
109
|
+
user_id=user_id,
|
|
110
|
+
conversation_id=conversation_id,
|
|
111
|
+
query=query,
|
|
112
|
+
file_ids=file_ids,
|
|
113
|
+
total_files=len(file_ids),
|
|
114
|
+
credits_consumed=credits,
|
|
115
|
+
status="pending",
|
|
116
|
+
)
|
|
117
|
+
session.add(job)
|
|
118
|
+
await session.flush()
|
|
119
|
+
return job
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def run_batch(
|
|
123
|
+
job_id: uuid.UUID,
|
|
124
|
+
company_id: uuid.UUID,
|
|
125
|
+
user_id: uuid.UUID,
|
|
126
|
+
query: str,
|
|
127
|
+
file_ids: list[str],
|
|
128
|
+
db_factory,
|
|
129
|
+
on_progress=None,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Execute a batch job: parallel file processing.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
db_factory: Callable that returns an AsyncSession context manager.
|
|
135
|
+
on_progress: Optional async callback(job_id, completed, failed, total, result).
|
|
136
|
+
"""
|
|
137
|
+
semaphore = asyncio.Semaphore(BATCH_CONCURRENCY)
|
|
138
|
+
results: list[dict] = []
|
|
139
|
+
completed = 0
|
|
140
|
+
failed = 0
|
|
141
|
+
|
|
142
|
+
# Update status to running
|
|
143
|
+
async with db_factory() as session:
|
|
144
|
+
job = await session.get(AIBatchJob, job_id)
|
|
145
|
+
if job:
|
|
146
|
+
job.status = "running"
|
|
147
|
+
session.add(job)
|
|
148
|
+
await session.commit()
|
|
149
|
+
|
|
150
|
+
# Fan out: process all files concurrently (bounded by semaphore)
|
|
151
|
+
tasks = [
|
|
152
|
+
_process_single_file(fid, query, company_id, semaphore)
|
|
153
|
+
for fid in file_ids
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
for coro in asyncio.as_completed(tasks):
|
|
157
|
+
result = await coro
|
|
158
|
+
results.append(result)
|
|
159
|
+
|
|
160
|
+
if result["status"] == "success":
|
|
161
|
+
completed += 1
|
|
162
|
+
else:
|
|
163
|
+
failed += 1
|
|
164
|
+
|
|
165
|
+
# Update DB progress
|
|
166
|
+
async with db_factory() as session:
|
|
167
|
+
job = await session.get(AIBatchJob, job_id)
|
|
168
|
+
if job:
|
|
169
|
+
job.completed_files = completed
|
|
170
|
+
job.failed_files = failed
|
|
171
|
+
session.add(job)
|
|
172
|
+
await session.commit()
|
|
173
|
+
|
|
174
|
+
# Notify progress
|
|
175
|
+
if on_progress:
|
|
176
|
+
try:
|
|
177
|
+
await on_progress(job_id, completed, failed, len(file_ids), result)
|
|
178
|
+
except Exception:
|
|
179
|
+
log.debug("on_progress callback failed", exc_info=True)
|
|
180
|
+
|
|
181
|
+
# Finalize
|
|
182
|
+
final_status = "failed" if failed == len(file_ids) else "completed"
|
|
183
|
+
async with db_factory() as session:
|
|
184
|
+
job = await session.get(AIBatchJob, job_id)
|
|
185
|
+
if job:
|
|
186
|
+
job.status = final_status
|
|
187
|
+
job.results = {"files": results}
|
|
188
|
+
job.completed_at = datetime.now(timezone.utc)
|
|
189
|
+
session.add(job)
|
|
190
|
+
await session.commit()
|
|
191
|
+
|
|
192
|
+
# Create notification
|
|
193
|
+
try:
|
|
194
|
+
from celerp.notifications.service import create as create_notification
|
|
195
|
+
async with db_factory() as session:
|
|
196
|
+
if final_status == "completed":
|
|
197
|
+
await create_notification(
|
|
198
|
+
session, company_id, "ai",
|
|
199
|
+
f"Batch complete: {completed}/{len(file_ids)} files processed",
|
|
200
|
+
f"{completed} files processed successfully, {failed} failed.",
|
|
201
|
+
user_id=user_id,
|
|
202
|
+
action_url="/ai",
|
|
203
|
+
priority="high",
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
await create_notification(
|
|
207
|
+
session, company_id, "ai",
|
|
208
|
+
f"Batch failed: {failed}/{len(file_ids)} files",
|
|
209
|
+
"All files failed to process. Please try again.",
|
|
210
|
+
user_id=user_id,
|
|
211
|
+
action_url="/ai",
|
|
212
|
+
priority="high",
|
|
213
|
+
)
|
|
214
|
+
await session.commit()
|
|
215
|
+
except Exception:
|
|
216
|
+
log.warning("Failed to create batch notification", exc_info=True)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def get_batch_job(
|
|
220
|
+
session: AsyncSession,
|
|
221
|
+
job_id: uuid.UUID,
|
|
222
|
+
company_id: uuid.UUID,
|
|
223
|
+
) -> AIBatchJob | None:
|
|
224
|
+
"""Get a batch job by ID, scoped to company."""
|
|
225
|
+
job = await session.get(AIBatchJob, job_id)
|
|
226
|
+
if job is None or job.company_id != company_id:
|
|
227
|
+
return None
|
|
228
|
+
return job
|
celerp/ai/cleanup.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copyright (c) 2026 Noah Severs
|
|
2
|
+
# SPDX-License-Identifier: BSL-1.1
|
|
3
|
+
|
|
4
|
+
"""AI file cleanup - periodic removal of old upload files.
|
|
5
|
+
|
|
6
|
+
All files older than 30 days are deleted unconditionally.
|
|
7
|
+
Runs every 6 hours via background task.
|
|
8
|
+
|
|
9
|
+
Does NOT require a database session - operates purely on filesystem.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from celerp.ai.files import upload_dir
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
CLEANUP_INTERVAL_SECONDS = 6 * 3600 # 6 hours
|
|
24
|
+
MAX_AGE_DAYS = 30
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _delete_file_pair(meta_path: Path) -> None:
|
|
28
|
+
"""Delete both .meta and .bin files."""
|
|
29
|
+
bin_path = meta_path.with_suffix(".bin")
|
|
30
|
+
try:
|
|
31
|
+
if bin_path.exists():
|
|
32
|
+
bin_path.unlink()
|
|
33
|
+
if meta_path.exists():
|
|
34
|
+
meta_path.unlink()
|
|
35
|
+
except OSError:
|
|
36
|
+
log.warning("Failed to delete %s", meta_path.stem, exc_info=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cleanup_uploads() -> int:
|
|
40
|
+
"""Remove AI upload files older than 30 days. Returns count deleted."""
|
|
41
|
+
try:
|
|
42
|
+
ud = upload_dir()
|
|
43
|
+
except (PermissionError, OSError) as exc:
|
|
44
|
+
log.debug("AI upload dir unavailable, skipping cleanup: %s", exc)
|
|
45
|
+
return 0
|
|
46
|
+
if not ud.exists():
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
now = time.time()
|
|
50
|
+
deleted = 0
|
|
51
|
+
|
|
52
|
+
for meta_path in list(ud.glob("*.meta")):
|
|
53
|
+
try:
|
|
54
|
+
age_days = (now - meta_path.stat().st_mtime) / 86400
|
|
55
|
+
except OSError:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
if age_days > MAX_AGE_DAYS:
|
|
59
|
+
_delete_file_pair(meta_path)
|
|
60
|
+
deleted += 1
|
|
61
|
+
log.debug("Deleted old file %s (%.0f days)", meta_path.stem, age_days)
|
|
62
|
+
|
|
63
|
+
# Clean up orphaned .bin files (no matching .meta)
|
|
64
|
+
for bin_path in list(ud.glob("*.bin")):
|
|
65
|
+
if not bin_path.with_suffix(".meta").exists():
|
|
66
|
+
try:
|
|
67
|
+
bin_path.unlink()
|
|
68
|
+
deleted += 1
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
if deleted:
|
|
73
|
+
log.info("AI file cleanup: removed %d files", deleted)
|
|
74
|
+
return deleted
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def run_cleanup_loop() -> None:
|
|
78
|
+
"""Background task: run cleanup every CLEANUP_INTERVAL_SECONDS."""
|
|
79
|
+
while True:
|
|
80
|
+
await asyncio.sleep(CLEANUP_INTERVAL_SECONDS)
|
|
81
|
+
try:
|
|
82
|
+
cleanup_uploads()
|
|
83
|
+
except Exception:
|
|
84
|
+
log.warning("AI file cleanup failed", exc_info=True)
|
celerp/ai/commands.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Copyright (c) 2026 Noah Severs
|
|
2
|
+
# SPDX-License-Identifier: BSL-1.1
|
|
3
|
+
|
|
4
|
+
"""AI command parsing and execution - structured bill creation from LLM output.
|
|
5
|
+
|
|
6
|
+
Two-phase flow:
|
|
7
|
+
1. parse_bill_commands() - validate LLM JSON, return structured data
|
|
8
|
+
2. create_bills() - execute against event store (only after user confirms)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, field_validator
|
|
19
|
+
from sqlalchemy import cast, select, String
|
|
20
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# -- Pydantic models -------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
class LineItem(BaseModel):
|
|
28
|
+
description: str
|
|
29
|
+
quantity: float
|
|
30
|
+
unit_price: float
|
|
31
|
+
|
|
32
|
+
@field_validator("quantity")
|
|
33
|
+
@classmethod
|
|
34
|
+
def qty_positive(cls, v: float) -> float:
|
|
35
|
+
if v <= 0:
|
|
36
|
+
raise ValueError("quantity must be > 0")
|
|
37
|
+
return v
|
|
38
|
+
|
|
39
|
+
@field_validator("unit_price")
|
|
40
|
+
@classmethod
|
|
41
|
+
def price_non_negative(cls, v: float) -> float:
|
|
42
|
+
if v < 0:
|
|
43
|
+
raise ValueError("unit_price must be >= 0")
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DraftBill(BaseModel):
|
|
48
|
+
vendor_name: str
|
|
49
|
+
date: str
|
|
50
|
+
total: float
|
|
51
|
+
source_file_id: str | None = None
|
|
52
|
+
line_items: list[LineItem]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# -- Phase 1: Parse + validate ---------------------------------------------
|
|
56
|
+
|
|
57
|
+
def parse_bill_commands(raw_json: dict) -> list[DraftBill]:
|
|
58
|
+
"""Validate LLM JSON output into structured bill data.
|
|
59
|
+
|
|
60
|
+
Raises ValueError with details on validation failure.
|
|
61
|
+
"""
|
|
62
|
+
bills_data = raw_json.get("create_draft_bills", [])
|
|
63
|
+
if not bills_data:
|
|
64
|
+
return []
|
|
65
|
+
bills: list[DraftBill] = []
|
|
66
|
+
for i, entry in enumerate(bills_data):
|
|
67
|
+
try:
|
|
68
|
+
bills.append(DraftBill.model_validate(entry))
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
raise ValueError(f"Bill #{i + 1} validation failed: {exc}") from exc
|
|
71
|
+
return bills
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# -- Phase 2: Execute (after user confirmation) ----------------------------
|
|
75
|
+
|
|
76
|
+
async def create_bills(
|
|
77
|
+
session: AsyncSession,
|
|
78
|
+
company_id: uuid.UUID,
|
|
79
|
+
user_id: uuid.UUID,
|
|
80
|
+
bills: list[DraftBill],
|
|
81
|
+
) -> str:
|
|
82
|
+
"""Create draft bills and contacts in the event store. Returns feedback text."""
|
|
83
|
+
from celerp.events.engine import emit_event
|
|
84
|
+
from celerp.models.company import Company
|
|
85
|
+
from celerp.models.projections import Projection
|
|
86
|
+
|
|
87
|
+
company = await session.get(Company, company_id)
|
|
88
|
+
if not company:
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
feedback_lines: list[str] = []
|
|
92
|
+
currency = company.settings.get("currency", "USD")
|
|
93
|
+
|
|
94
|
+
for bill in bills:
|
|
95
|
+
# Find vendor in projection
|
|
96
|
+
vendor_row = (await session.execute(
|
|
97
|
+
select(Projection).where(
|
|
98
|
+
Projection.company_id == company_id,
|
|
99
|
+
Projection.entity_type == "contact",
|
|
100
|
+
cast(Projection.state["name"], String).ilike(f"%{bill.vendor_name}%"),
|
|
101
|
+
)
|
|
102
|
+
)).scalars().first()
|
|
103
|
+
|
|
104
|
+
if vendor_row:
|
|
105
|
+
contact_id = str(vendor_row.entity_id)
|
|
106
|
+
else:
|
|
107
|
+
slug = re.sub(r"[^a-zA-Z0-9]+", "-", bill.vendor_name.lower()) or "vendor"
|
|
108
|
+
ref_id = f"{slug}-{str(uuid.uuid4())[:4]}"
|
|
109
|
+
contact_id = f"contact:{ref_id}"
|
|
110
|
+
idem_key = _idempotency_key(company_id, "contact", bill.vendor_name, bill.date)
|
|
111
|
+
await emit_event(
|
|
112
|
+
session,
|
|
113
|
+
company_id=company_id,
|
|
114
|
+
entity_id=contact_id,
|
|
115
|
+
entity_type="contact",
|
|
116
|
+
event_type="contact.created",
|
|
117
|
+
data={
|
|
118
|
+
"name": bill.vendor_name,
|
|
119
|
+
"contact_type": "vendor",
|
|
120
|
+
"status": "draft",
|
|
121
|
+
"ref_id": ref_id,
|
|
122
|
+
"currency": currency,
|
|
123
|
+
},
|
|
124
|
+
actor_id=user_id,
|
|
125
|
+
location_id=None,
|
|
126
|
+
source="ai",
|
|
127
|
+
idempotency_key=idem_key,
|
|
128
|
+
metadata_={},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
from celerp_docs.sequences import next_doc_ref
|
|
133
|
+
bill_ref = next_doc_ref(company, "bill")
|
|
134
|
+
except ImportError:
|
|
135
|
+
bill_ref = f"BIL-{str(uuid.uuid4())[:6].upper()}"
|
|
136
|
+
|
|
137
|
+
entity_id = f"doc:{bill_ref}"
|
|
138
|
+
line_items = [
|
|
139
|
+
{
|
|
140
|
+
"description": li.description,
|
|
141
|
+
"quantity": li.quantity,
|
|
142
|
+
"unit_price": li.unit_price,
|
|
143
|
+
"line_total": li.quantity * li.unit_price,
|
|
144
|
+
}
|
|
145
|
+
for li in bill.line_items
|
|
146
|
+
]
|
|
147
|
+
idem_key = _idempotency_key(company_id, "bill", bill.vendor_name, bill.date, str(bill.total))
|
|
148
|
+
|
|
149
|
+
await emit_event(
|
|
150
|
+
session,
|
|
151
|
+
company_id=company_id,
|
|
152
|
+
entity_id=entity_id,
|
|
153
|
+
entity_type="doc",
|
|
154
|
+
event_type="doc.created",
|
|
155
|
+
data={
|
|
156
|
+
"doc_type": "bill",
|
|
157
|
+
"status": "draft",
|
|
158
|
+
"ref_id": bill_ref,
|
|
159
|
+
"date": bill.date,
|
|
160
|
+
"contact_id": contact_id,
|
|
161
|
+
"location_id": "loc:default",
|
|
162
|
+
"total": bill.total,
|
|
163
|
+
"subtotal": bill.total,
|
|
164
|
+
"currency": currency,
|
|
165
|
+
"amount_outstanding": bill.total,
|
|
166
|
+
"line_items": line_items,
|
|
167
|
+
},
|
|
168
|
+
actor_id=user_id,
|
|
169
|
+
location_id=None,
|
|
170
|
+
source="ai",
|
|
171
|
+
idempotency_key=idem_key,
|
|
172
|
+
metadata_={"ai_source_file_id": bill.source_file_id} if bill.source_file_id else {},
|
|
173
|
+
)
|
|
174
|
+
feedback_lines.append(
|
|
175
|
+
f"Created Draft Bill {bill_ref} for {bill.vendor_name} ({currency} {bill.total:.2f})"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return "\n".join(feedback_lines)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _idempotency_key(company_id: uuid.UUID, entity: str, *parts: str) -> str:
|
|
182
|
+
"""Deterministic idempotency key from company + entity + parts."""
|
|
183
|
+
raw = f"{company_id}:{entity}:" + ":".join(parts)
|
|
184
|
+
return f"ai_{entity}_{hashlib.sha256(raw.encode()).hexdigest()[:12]}"
|