lambda-erp 0.8.31__tar.gz → 0.8.32__tar.gz

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.
Files changed (96) hide show
  1. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/PKG-INFO +1 -1
  2. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/chat.py +197 -61
  3. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/pyproject.toml +1 -1
  4. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/.gitignore +0 -0
  5. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/LICENSE +0 -0
  6. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/README.md +0 -0
  7. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/__init__.py +0 -0
  8. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/attachments.py +0 -0
  9. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/auth.py +0 -0
  10. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/bootstrap.py +0 -0
  11. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/demo_limits.py +0 -0
  12. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/deps.py +0 -0
  13. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/errors.py +0 -0
  14. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/list_values.py +0 -0
  15. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/main.py +0 -0
  16. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/oauth.py +0 -0
  17. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/pdf.py +0 -0
  18. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/providers.py +0 -0
  19. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/remarks_md.py +0 -0
  20. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/__init__.py +0 -0
  21. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/accounting.py +0 -0
  22. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/actions.py +0 -0
  23. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/admin.py +0 -0
  24. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/analytics.py +0 -0
  25. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/availability.py +0 -0
  26. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/bank_reconciliation.py +0 -0
  27. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/bank_statements.py +0 -0
  28. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/chat_api.py +0 -0
  29. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/documents.py +0 -0
  30. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/masters.py +0 -0
  31. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/mcp.py +0 -0
  32. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/proposals.py +0 -0
  33. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/reports.py +0 -0
  34. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/routers/setup.py +0 -0
  35. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/services.py +0 -0
  36. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/templates/document.html +0 -0
  37. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/templates/proposal.html +0 -0
  38. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/api/tool_permissions.py +0 -0
  39. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/docs/agents/README.md +0 -0
  40. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/frontend/LICENSE +0 -0
  41. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/frontend/README.md +0 -0
  42. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/frontend/src/api/client.ts +0 -0
  43. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/__init__.py +0 -0
  44. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/__init__.py +0 -0
  45. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/bank_account.py +0 -0
  46. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/bank_reconciliation.py +0 -0
  47. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/bank_statement_import.py +0 -0
  48. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/bank_transaction.py +0 -0
  49. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/budget.py +0 -0
  50. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/camt.py +0 -0
  51. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  52. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/general_ledger.py +0 -0
  53. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/journal_entry.py +0 -0
  54. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/payment_entry.py +0 -0
  55. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/pos_invoice.py +0 -0
  56. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/purchase_invoice.py +0 -0
  57. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/revaluation.py +0 -0
  58. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/sales_invoice.py +0 -0
  59. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/__init__.py +0 -0
  60. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/engine.py +0 -0
  61. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/pack.py +0 -0
  62. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  63. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  64. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  65. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  66. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  67. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  68. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/profiles.py +0 -0
  69. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/setup/spine.py +0 -0
  70. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/accounting/subscription.py +0 -0
  71. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/assets/__init__.py +0 -0
  72. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/assets/asset.py +0 -0
  73. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/assets/reservation.py +0 -0
  74. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/buying/__init__.py +0 -0
  75. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/buying/purchase_order.py +0 -0
  76. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/controllers/__init__.py +0 -0
  77. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/controllers/currency.py +0 -0
  78. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/controllers/defaults.py +0 -0
  79. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/controllers/pricing_rule.py +0 -0
  80. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  81. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/database.py +0 -0
  82. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/exceptions.py +0 -0
  83. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/hooks.py +0 -0
  84. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/model.py +0 -0
  85. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/selling/__init__.py +0 -0
  86. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/selling/proposal.py +0 -0
  87. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/selling/quotation.py +0 -0
  88. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/selling/sales_order.py +0 -0
  89. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/simulation.py +0 -0
  90. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/stock/__init__.py +0 -0
  91. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/stock/delivery_note.py +0 -0
  92. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/stock/purchase_receipt.py +0 -0
  93. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/stock/stock_entry.py +0 -0
  94. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/stock/stock_ledger.py +0 -0
  95. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/lambda_erp/utils.py +0 -0
  96. {lambda_erp-0.8.31 → lambda_erp-0.8.32}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 0.8.31
3
+ Version: 0.8.32
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -2880,6 +2880,118 @@ def _extract_json_object(text: str) -> dict:
2880
2880
  return json.loads(candidate)
2881
2881
 
2882
2882
 
2883
+ def _report_validation_summary(exc: Exception, *, max_errors: int = 4) -> str:
2884
+ """Return useful schema paths without logging report contents.
2885
+
2886
+ Pydantic's full error string embeds rejected input values. Report intents
2887
+ can contain customer/account labels, so provider diagnostics deliberately
2888
+ retain only the field path, error type, and message.
2889
+ """
2890
+ errors = getattr(exc, "errors", None)
2891
+ if not callable(errors):
2892
+ return f"{type(exc).__name__}: {exc}"
2893
+ error_items = errors()
2894
+ parts: list[str] = []
2895
+ for error in error_items[:max_errors]:
2896
+ location = ".".join(str(part) for part in error.get("loc", ())) or "report"
2897
+ error_type = error.get("type") or "validation_error"
2898
+ message = error.get("msg") or "Invalid value"
2899
+ parts.append(f"{location} [{error_type}]: {message}")
2900
+ remaining = len(error_items) - len(parts)
2901
+ if remaining > 0:
2902
+ parts.append(f"+{remaining} more")
2903
+ return "; ".join(parts) or type(exc).__name__
2904
+
2905
+
2906
+ def _report_validation_log_summary(exc: Exception, *, max_errors: int = 4) -> str:
2907
+ """Return paths/types only, keeping model-generated labels out of logs."""
2908
+ errors = getattr(exc, "errors", None)
2909
+ if not callable(errors):
2910
+ return type(exc).__name__
2911
+ error_items = errors()
2912
+ parts = []
2913
+ for error in error_items[:max_errors]:
2914
+ location = ".".join(str(part) for part in error.get("loc", ())) or "report"
2915
+ error_type = error.get("type") or "validation_error"
2916
+ parts.append(f"{location}[{error_type}]")
2917
+ remaining = len(error_items) - len(parts)
2918
+ if remaining > 0:
2919
+ parts.append(f"+{remaining}_more")
2920
+ return ",".join(parts) or type(exc).__name__
2921
+
2922
+
2923
+ def _strip_report_model_extras(value, annotation):
2924
+ """Copy a report value through its declared Pydantic model shape.
2925
+
2926
+ Dictionaries used as actual values (notably RuntimeDataRequest.filters)
2927
+ stay untouched. Only dictionaries representing report models are narrowed
2928
+ to declared fields, recursively through nested model lists/optionals. This
2929
+ avoids depending on the per-validation ``extra=`` override added by newer
2930
+ Pydantic releases; Lambda ERP still supports Pydantic 2.x generally.
2931
+ """
2932
+ from typing import get_args, get_origin
2933
+ from pydantic import BaseModel
2934
+
2935
+ def is_model(candidate) -> bool:
2936
+ return isinstance(candidate, type) and issubclass(candidate, BaseModel)
2937
+
2938
+ if is_model(annotation):
2939
+ if not isinstance(value, dict):
2940
+ return value
2941
+ return {
2942
+ name: _strip_report_model_extras(value[name], field.annotation)
2943
+ for name, field in annotation.model_fields.items()
2944
+ if name in value
2945
+ }
2946
+
2947
+ origin = get_origin(annotation)
2948
+ args = get_args(annotation)
2949
+ if origin is list and args and isinstance(value, list):
2950
+ return [_strip_report_model_extras(item, args[0]) for item in value]
2951
+
2952
+ # The report models only use unions for optional fields. Follow the model
2953
+ # or model-list branch when one exists; primitive unions need no traversal.
2954
+ for candidate in args:
2955
+ candidate_origin = get_origin(candidate)
2956
+ if is_model(candidate) or candidate_origin is list:
2957
+ return _strip_report_model_extras(value, candidate)
2958
+ return value
2959
+
2960
+
2961
+ def _validate_report_specialist_spec(spec: dict, *, response_id: str) -> dict:
2962
+ """Normalize harmless model-only fields, then enforce the canonical model.
2963
+
2964
+ ReportDraftPayload remains strict for every stored/browser-facing report.
2965
+ The lenient pass exists only at this LLM boundary: it removes invented
2966
+ presentation hints such as ``stacked`` or ``color``. Serializing that model
2967
+ produces a canonical object which is immediately validated strictly again,
2968
+ so ignored input can never reach persistence or the browser.
2969
+ """
2970
+ from pydantic import ValidationError
2971
+ from api.routers.analytics import ReportDraftPayload
2972
+
2973
+ try:
2974
+ return ReportDraftPayload.model_validate(spec).model_dump()
2975
+ except ValidationError as strict_error:
2976
+ extra_paths = [
2977
+ ".".join(str(part) for part in error.get("loc", ())) or "report"
2978
+ for error in strict_error.errors()
2979
+ if error.get("type") == "extra_forbidden"
2980
+ ]
2981
+ if not extra_paths:
2982
+ raise
2983
+
2984
+ stripped = _strip_report_model_extras(spec, ReportDraftPayload)
2985
+ normalized = ReportDraftPayload.model_validate(stripped).model_dump()
2986
+ canonical = ReportDraftPayload.model_validate(normalized).model_dump()
2987
+ print(
2988
+ "[report_specialist] "
2989
+ f"response_id={response_id} normalized_extra_fields={','.join(extra_paths)}",
2990
+ flush=True,
2991
+ )
2992
+ return canonical
2993
+
2994
+
2883
2995
  def _generate_report_spec_via_openai(
2884
2996
  intent: str,
2885
2997
  existing_spec: dict | None = None,
@@ -2920,72 +3032,96 @@ def _generate_report_spec_via_openai(
2920
3032
  )
2921
3033
  user_msg = "\n\n".join(user_parts)
2922
3034
 
2923
- print(
2924
- f"[chat_llm] provider=openai role=report_specialist model={model}",
2925
- flush=True,
2926
- )
2927
3035
  client = OpenAI(
2928
3036
  api_key=api_key,
2929
3037
  timeout=httpx.Timeout(120.0, connect=10.0),
2930
3038
  )
2931
- reservation_id = None
2932
- if is_demo_role(user_role):
2933
- _blocked, reservation_id = demo_limiter.reserve(
2934
- client_ip or "unknown",
2935
- estimated_usd=demo_call_reserve_usd(),
2936
- role=user_role,
2937
- )
2938
- if _blocked:
2939
- raise RuntimeError(_blocked)
2940
-
2941
- # try/finally + `settled` flag guarantees reservation release even if
2942
- # a CancelledError slips between the SDK call returning and settle()
2943
- # completing. Without this, a cancelled coroutine could leak the
2944
- # reservation for the process lifetime (or until TTL sweep).
2945
- settled = False
2946
- try:
2947
- response = client.responses.create(
2948
- model=model,
2949
- instructions=_REPORT_CODE_SYSTEM_PROMPT,
2950
- input=user_msg,
2951
- max_output_tokens=4096,
2952
- reasoning={"effort": "low"},
2953
- )
2954
- # Log every call for the admin dashboard. Only public_manager rows
2955
- # count against the demo cap — other roles are logged for
2956
- # visibility but exempt from rate limiting.
2957
- usage = getattr(response, "usage", None)
2958
- demo_limiter.settle(
2959
- reservation_id,
2960
- actual_cost_usd=cost_of_openai_call(model, usage),
2961
- ip=client_ip or "unknown",
2962
- role=user_role,
2963
- provider="openai",
2964
- model=model,
2965
- prompt_tokens=int(getattr(usage, "input_tokens", 0) or 0) if usage else 0,
2966
- completion_tokens=int(getattr(usage, "output_tokens", 0) or 0) if usage else 0,
2967
- session_id=session_id,
2968
- )
2969
- settled = True
2970
- finally:
2971
- if not settled:
2972
- demo_limiter.release(reservation_id)
2973
-
2974
- text = getattr(response, "output_text", "") or ""
2975
- if not text:
2976
- text = "".join(
2977
- getattr(block, "text", "") or ""
2978
- for item in (getattr(response, "output", None) or [])
2979
- for block in (getattr(item, "content", None) or [])
2980
- if getattr(block, "type", "") in ("output_text", "text")
3039
+ validation_feedback: str | None = None
3040
+ max_attempts = 2
3041
+
3042
+ for attempt in range(1, max_attempts + 1):
3043
+ attempt_input = user_msg
3044
+ if validation_feedback:
3045
+ attempt_input += (
3046
+ "\n\n## Validation errors from the previous attempt\n"
3047
+ + validation_feedback
3048
+ + "\nRegenerate the complete report spec and correct these errors."
3049
+ )
3050
+
3051
+ print(
3052
+ f"[chat_llm] provider=openai role=report_specialist model={model} attempt={attempt}",
3053
+ flush=True,
2981
3054
  )
2982
- spec = _extract_json_object(text)
2983
- if not spec.get("report") or not spec.get("data_requests"):
2984
- raise RuntimeError("Report specialist returned an incomplete spec.")
2985
- # Validate the complete declarative language before it can be persisted or
2986
- # sent to a browser. This also rejects legacy executable transform strings.
2987
- from api.routers.analytics import ReportDraftPayload
2988
- return ReportDraftPayload.model_validate(spec).model_dump()
3055
+ reservation_id = None
3056
+ if is_demo_role(user_role):
3057
+ _blocked, reservation_id = demo_limiter.reserve(
3058
+ client_ip or "unknown",
3059
+ estimated_usd=demo_call_reserve_usd(),
3060
+ role=user_role,
3061
+ )
3062
+ if _blocked:
3063
+ raise RuntimeError(_blocked)
3064
+
3065
+ # try/finally + `settled` flag guarantees reservation release even if
3066
+ # a CancelledError slips between the SDK call returning and settle()
3067
+ # completing. Without this, a cancelled coroutine could leak the
3068
+ # reservation for the process lifetime (or until TTL sweep).
3069
+ settled = False
3070
+ try:
3071
+ response = client.responses.create(
3072
+ model=model,
3073
+ instructions=_REPORT_CODE_SYSTEM_PROMPT,
3074
+ input=attempt_input,
3075
+ max_output_tokens=4096,
3076
+ reasoning={"effort": "low"},
3077
+ )
3078
+ # Log every call for the admin dashboard. Only public_manager rows
3079
+ # count against the demo cap — other roles are logged for
3080
+ # visibility but exempt from rate limiting.
3081
+ usage = getattr(response, "usage", None)
3082
+ demo_limiter.settle(
3083
+ reservation_id,
3084
+ actual_cost_usd=cost_of_openai_call(model, usage),
3085
+ ip=client_ip or "unknown",
3086
+ role=user_role,
3087
+ provider="openai",
3088
+ model=model,
3089
+ prompt_tokens=int(getattr(usage, "input_tokens", 0) or 0) if usage else 0,
3090
+ completion_tokens=int(getattr(usage, "output_tokens", 0) or 0) if usage else 0,
3091
+ session_id=session_id,
3092
+ )
3093
+ settled = True
3094
+ finally:
3095
+ if not settled:
3096
+ demo_limiter.release(reservation_id)
3097
+
3098
+ response_id = str(getattr(response, "id", None) or "unknown")
3099
+ try:
3100
+ text = getattr(response, "output_text", "") or ""
3101
+ if not text:
3102
+ text = "".join(
3103
+ getattr(block, "text", "") or ""
3104
+ for item in (getattr(response, "output", None) or [])
3105
+ for block in (getattr(item, "content", None) or [])
3106
+ if getattr(block, "type", "") in ("output_text", "text")
3107
+ )
3108
+ spec = _extract_json_object(text)
3109
+ return _validate_report_specialist_spec(spec, response_id=response_id)
3110
+ except Exception as exc:
3111
+ validation_feedback = _report_validation_summary(exc)
3112
+ print(
3113
+ "[report_specialist] "
3114
+ f"response_id={response_id} attempt={attempt} "
3115
+ f"validation_error={_report_validation_log_summary(exc)}",
3116
+ flush=True,
3117
+ )
3118
+ if attempt == max_attempts:
3119
+ raise RuntimeError(
3120
+ "Report specialist returned an invalid spec after "
3121
+ f"{max_attempts} attempts: {validation_feedback}"
3122
+ ) from exc
3123
+
3124
+ raise RuntimeError("Report specialist did not return a report spec.")
2989
3125
 
2990
3126
 
2991
3127
  # ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.8.31"
3
+ version = "0.8.32"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes