Add ERPNext invoice validation Discord command#290
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds ERPNext client invoice retrieval/search, a validation module enforcing project/cost-center and purchase due-date rules, a Discord Cog exposing /validate-invoice with autocomplete and embed formatting, and unit tests covering client, validation, and Cog behavior. ChangesERPNext Invoice Validation Integration
Sequence DiagramsequenceDiagram
participant User
participant Discord
participant ErpNextCog
participant ERPNextClient
participant Validator
User->>Discord: /validate-invoice (doctype, invoice_name)
Discord->>ErpNextCog: interaction
ErpNextCog->>ErpNextCog: defer(ephemeral)
ErpNextCog->>ERPNextClient: get_invoice(doctype, name)
ERPNextClient-->>ErpNextCog: invoice dict | None | error
alt invoice found
ErpNextCog->>Validator: validate_invoice(invoice, doctype)
Validator-->>ErpNextCog: ValidationResult
ErpNextCog->>Discord: followup(embed with fields/issues)
else not found
ErpNextCog->>Discord: followup("not found or you don't have access")
else error
ErpNextCog->>Discord: followup(error message)
end
Discord->>User: ephemeral followup visible to user
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a6b323aea9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
packages/shared/src/five08/clients/erpnext.py (1)
39-86: ⚡ Quick winRemove unused write operation support from read-only client.
The
_requestmethod supportsPOST,PATCH, andPUT(line 48), but the client is documented as "read-only" (line 15) and exposes no write methods. These unused code paths add cognitive load and could mislead future maintainers into thinking write operations are supported.♻️ Proposed simplification
Since all public methods (
get_invoice,search_invoices) use GET, simplify_requestto only support GET:def _request( self, method: str, endpoint: str, params: dict[str, Any] | None = None, ) -> Any: url = f"{self.base_url}/api/resource/{endpoint}" - method = method.upper() try: - if method in ("POST", "PATCH", "PUT"): - response = self._session.request( - method, - url, - json=params, - timeout=self.timeout, - verify=default_ca_bundle_path(), - ) - else: - response = self._session.request( - method, - url, - params=params, - timeout=self.timeout, - verify=default_ca_bundle_path(), - ) + response = self._session.get( + url, + params=params, + timeout=self.timeout, + verify=default_ca_bundle_path(), + )Alternatively, if future write support is planned, update the docstring to reflect this.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/five08/clients/erpnext.py` around lines 39 - 86, The _request method currently contains branching for POST/PATCH/PUT even though this client is intended to be read-only and public methods (get_invoice, search_invoices) only perform GETs; remove the unused write-branching in _request by simplifying the call to always send parameters as query params (i.e., call self._session.request(method, url, params=params, ...)) and drop the json=params branch and any write-specific logic, keeping error handling intact; if write support is planned later, instead update the class/docstring to not claim read-only or add a clear TODO indicating planned write methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py`:
- Around line 43-115: The validate_invoice_command handler must POST an audit
record to the worker audit ingest endpoint using AUDIT_API_BASE_URL and
API_SHARED_SECRET whenever a human triggers this validation; add an async HTTP
POST (e.g., via aiohttp) inside validate_invoice_command (referencing the
function name) after you determine the final outcome (after building/sending the
embed or immediately before interaction.followup.send) that sends a JSON payload
including actor (interaction.user.id/name), action ("validate_invoice"), doctype
(doctype.value), invoice_name, result (passed/failed and number of issues), and
timestamp, and authenticate the request with API_SHARED_SECRET (e.g., header
like Authorization or X-Api-Key as used by other audit calls); also ensure you
send an audit event in the ErpNextAPIError and generic Exception handlers so
failures are recorded as well.
- Around line 133-137: The autocomplete Choice.name built for
app_commands.Choice can exceed Discord's 100-char limit; modify the generator
that builds the label (currently using STATUS_LABEL.get(inv.get('docstatus'))
and inv['name']/inv.get('owner')/inv.get('posting_date')) to clamp the resulting
label to 100 characters before passing to app_commands.Choice. Concretely,
compose the label into a local variable, and if len(label) > 100 truncate it
(e.g. label = label[:97] + "..." to preserve length), then use that truncated
label as the Choice.name so Choice creation always meets Discord's 1-100 char
requirement.
In `@apps/discord_bot/src/five08/discord_bot/config.py`:
- Around line 68-70: Move the ERPNext config fields out of the service-specific
Settings and into the shared settings: remove erpnext_base_url and
erpnext_api_token from the Discord bot's Settings class and add those two fields
to the SharedSettings class so all services reuse them; update no logic
here—just relocate the declarations (erpnext_base_url, erpnext_api_token) into
SharedSettings and rely on the existing inheritance of Settings to pick them up.
---
Nitpick comments:
In `@packages/shared/src/five08/clients/erpnext.py`:
- Around line 39-86: The _request method currently contains branching for
POST/PATCH/PUT even though this client is intended to be read-only and public
methods (get_invoice, search_invoices) only perform GETs; remove the unused
write-branching in _request by simplifying the call to always send parameters as
query params (i.e., call self._session.request(method, url, params=params, ...))
and drop the json=params branch and any write-specific logic, keeping error
handling intact; if write support is planned later, instead update the
class/docstring to not claim read-only or add a clear TODO indicating planned
write methods.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f2481c43-9760-470a-aa80-0b815ca47ce8
📒 Files selected for processing (7)
.env.exampleapps/discord_bot/src/five08/discord_bot/cogs/erpnext.pyapps/discord_bot/src/five08/discord_bot/config.pypackages/shared/src/five08/clients/erpnext.pypackages/shared/src/five08/erpnext_validation.pytests/unit/test_erpnext_client.pytests/unit/test_erpnext_cog.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1148844547
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
a5e387e to
579f81c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 579f81c2eb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dbc3e320a4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
5e7052a to
8337d0c
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
packages/shared/src/five08/clients/erpnext.py (1)
230-245: ⚡ Quick winNormalize
queryand boundlimitin invoice search.
search_invoicesshould strip whitespace-only queries and clamplimitto avoid noisy filters and unbounded/invalid page sizes.Proposed refactor
def search_invoices( self, doctype: str, query: str = "", docstatus: int | None = None, limit: int = 10, ) -> list[dict[str, Any]]: """Search invoices for autocomplete, ordered newest first.""" + normalized_query = query.strip() + normalized_limit = max(1, limit) filters: list[Any] = [] - if query: - filters.append([doctype, "name", "like", f"%{query}%"]) + if normalized_query: + filters.append([doctype, "name", "like", f"%{normalized_query}%"]) if docstatus is not None: filters.append([doctype, "docstatus", "=", docstatus]) params: dict[str, Any] = { "fields": json.dumps(["name", "posting_date", "docstatus", "owner"]), "order_by": "posting_date desc", - "limit_page_length": limit, + "limit_page_length": normalized_limit, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/five08/clients/erpnext.py` around lines 230 - 245, In search_invoices, normalize the incoming query by trimming whitespace and treating empty/whitespace-only queries as no query (so only append the name filter if query.strip() is non-empty), and clamp the limit parameter to a safe range (e.g., coerce to int and bound it between 1 and a sensible max like 100) before building filters and params; update the logic around the filters list and the params["limit_page_length"] assignment in the search_invoices function so it uses the normalized query and clamped limit.tests/unit/test_erpnext_validation.py (1)
1-6: ⚡ Quick winAdd a contract test for unsupported doctypes.
validate_invoiceexplicitly raisesValueErrorfor unsupported doctypes, but this behavior is currently untested.Proposed test addition
from __future__ import annotations from typing import Any +import pytest from five08.erpnext_validation import validate_invoice @@ def test_multiple_issues_reported() -> None: @@ result = validate_invoice(invoice, "Purchase Invoice") assert len(result.issues) >= 2 + + +def test_unsupported_doctype_raises() -> None: + with pytest.raises(ValueError, match="Unsupported doctype"): + validate_invoice(VALID_SALES_INVOICE, "Delivery Note")Also applies to: 129-138
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_erpnext_validation.py` around lines 1 - 6, Add a contract test that exercises validate_invoice's error path for unsupported doctypes: call validate_invoice with a doctype value that is not handled by the function and assert that it raises a ValueError; place the test in tests/unit/test_erpnext_validation.py alongside the existing imports and name it clearly (e.g., test_validate_invoice_unsupported_doctype) so future changes to validate_invoice will be covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py`:
- Around line 148-161: The autocomplete is returning more choices than Discord
allows because search_invoices can return >25 items; before returning from the
function that builds choices (the loop that creates app_commands.Choice from
invoices), truncate the choices list to at most 25 items (e.g., choices =
choices[:25]) or enforce the limit when mapping invoices to choices so only the
first 25 invoices are converted; ensure the truncation happens right before
return to guarantee the response never exceeds Discord's max autocomplete
choices.
In `@packages/shared/src/five08/clients/erpnext.py`:
- Around line 224-225: The current code treats a non-dict payload as "not found"
by returning None from the block that does row = data.get("data"); instead, only
return None for an actual HTTP 404 — for other cases validate that row is a dict
and if it's malformed raise a descriptive error (e.g., ValueError or an
ApiSchemaError) so upstream callers can distinguish schema/bad-response errors
from missing resources; update the code around row = data.get("data") to check
the HTTP status (or response error field) and return None only on 404, otherwise
raise an exception with details about the unexpected payload.
In `@packages/shared/src/five08/erpnext_validation.py`:
- Around line 87-90: The loop over invoice.get("items", []) can still fail when
ERP returns "items": null; before iterating (where item_cost_center and
item_project are used) ensure the items value is a proper list by normalizing it
(e.g., items = invoice.get("items") or [] and/or assert isinstance(items, list)
fallback to []), then iterate over that normalized 'items' variable; update the
loop that creates item_cost_center and item_project to use this guarded 'items'
so a null payload does not raise a TypeError during validation.
- Around line 124-128: The date-parsing try/except around datetime.strptime for
posting_date_raw and due_date_raw only catches ValueError and will still crash
on non-string inputs (TypeError); update the exception handler in the block that
parses posting_date and due_date (the try surrounding datetime.strptime for
posting_date_raw and due_date_raw) to catch both ValueError and TypeError (e.g.,
except (ValueError, TypeError): return) so non-string or bad-format payloads are
handled gracefully.
---
Nitpick comments:
In `@packages/shared/src/five08/clients/erpnext.py`:
- Around line 230-245: In search_invoices, normalize the incoming query by
trimming whitespace and treating empty/whitespace-only queries as no query (so
only append the name filter if query.strip() is non-empty), and clamp the limit
parameter to a safe range (e.g., coerce to int and bound it between 1 and a
sensible max like 100) before building filters and params; update the logic
around the filters list and the params["limit_page_length"] assignment in the
search_invoices function so it uses the normalized query and clamped limit.
In `@tests/unit/test_erpnext_validation.py`:
- Around line 1-6: Add a contract test that exercises validate_invoice's error
path for unsupported doctypes: call validate_invoice with a doctype value that
is not handled by the function and assert that it raises a ValueError; place the
test in tests/unit/test_erpnext_validation.py alongside the existing imports and
name it clearly (e.g., test_validate_invoice_unsupported_doctype) so future
changes to validate_invoice will be covered.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f564b3df-4ceb-4e9d-a578-85929e56bd25
📒 Files selected for processing (6)
apps/discord_bot/src/five08/discord_bot/cogs/erpnext.pypackages/shared/src/five08/clients/erpnext.pypackages/shared/src/five08/erpnext_validation.pytests/unit/test_erpnext_client.pytests/unit/test_erpnext_cog.pytests/unit/test_erpnext_validation.py
fcb4b0b to
ddff72f
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/shared/src/five08/clients/erpnext.py (1)
669-675:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winValidate identifiers and clamp
limitin invoice helpers.Line 669 and Line 687 skip the required
strip()/emptychecks used by the rest of this client, and Line 716 sendslimitwithout a lower-bound clamp. That can produce malformed resource paths and invalid/zero-length list queries.Suggested fix
def get_invoice(self, doctype: str, name: str) -> dict[str, Any] | None: """Fetch a single Sales Invoice or Purchase Invoice by name. Returns None on 404.""" + normalized_doctype = doctype.strip() + normalized_name = name.strip() + if not normalized_doctype: + raise ERPNextAPIError("DocType is required") + if not normalized_name: + raise ERPNextAPIError(f"{normalized_doctype} name is required") try: data = self.request( "GET", - f"/api/resource/{quote(doctype, safe='')}/{quote(name, safe='')}", + f"/api/resource/{quote(normalized_doctype, safe='')}/{quote(normalized_name, safe='')}", ) @@ def search_invoices( self, doctype: str, @@ ) -> list[dict[str, Any]]: @@ + normalized_doctype = doctype.strip() + if not normalized_doctype: + raise ERPNextAPIError("DocType is required") + normalized_limit = max(1, limit) filters: list[Any] = [] if query: - filters.append([doctype, "name", "like", f"%{query}%"]) + filters.append([normalized_doctype, "name", "like", f"%{query}%"]) if docstatus is not None: - filters.append([doctype, "docstatus", "=", docstatus]) + filters.append([normalized_doctype, "docstatus", "=", docstatus]) @@ if owners: - or_filters.append([doctype, "owner", "in", owners]) + or_filters.append([normalized_doctype, "owner", "in", owners]) if projects: - or_filters.append([doctype, "project", "in", projects]) + or_filters.append([normalized_doctype, "project", "in", projects]) @@ params: dict[str, Any] = { "fields": json.dumps(["name", "posting_date", "docstatus", "owner"]), "order_by": "posting_date desc", - "limit_page_length": limit, + "limit_page_length": normalized_limit, } @@ "GET", - f"/api/resource/{quote(doctype, safe='')}", + f"/api/resource/{quote(normalized_doctype, safe='')}", params=params, )Also applies to: 713-717
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/five08/clients/erpnext.py` around lines 669 - 675, The get_invoice and the invoice-list helper that builds list queries (e.g., get_invoices) currently pass doctype/name and limit straight through; validate and normalize identifiers by calling .strip() and rejecting/raising or returning None for empty doctype or name before quoting to avoid malformed resource paths, and clamp the limit parameter to a sensible lower bound (e.g., max(1, int(limit))) before including it in query params so you never send zero/negative limits; update the request calls in get_invoice and the invoice-list helper to use the stripped values and the clamped limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py`:
- Around line 225-227: The autocomplete logic currently assumes invoice "name"
is a string and calls len(inv_name) which raises TypeError for non-string
values; update the checks around inv_name (the variable extracted from each inv
in the autocomplete function/method) to first ensure it's a string (e.g., if not
isinstance(inv_name, str): continue) before calling len or otherwise coerce
safely (inv_name = str(inv_name)) and apply the same safe-type check where the
code later trims or uses inv_name (the block that filters/limits results and
builds the autocomplete response) so non-string names are skipped or handled
without throwing.
---
Outside diff comments:
In `@packages/shared/src/five08/clients/erpnext.py`:
- Around line 669-675: The get_invoice and the invoice-list helper that builds
list queries (e.g., get_invoices) currently pass doctype/name and limit straight
through; validate and normalize identifiers by calling .strip() and
rejecting/raising or returning None for empty doctype or name before quoting to
avoid malformed resource paths, and clamp the limit parameter to a sensible
lower bound (e.g., max(1, int(limit))) before including it in query params so
you never send zero/negative limits; update the request calls in get_invoice and
the invoice-list helper to use the stripped values and the clamped limit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 79c9741b-20d6-49d6-ae97-cbae3b96ae8d
📒 Files selected for processing (4)
apps/discord_bot/src/five08/discord_bot/cogs/erpnext.pypackages/shared/src/five08/clients/erpnext.pytests/unit/test_erpnext_client.pytests/unit/test_erpnext_cog.py
ddff72f to
d95659d
Compare
Fixed a bug where truncating app_commands.Choice.value broke subsequent invoice lookups. Now overlong IDs are skipped entirely, and display names are gracefully truncated with ellipses.
d95659d to
3f8aa67
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f8aa67088
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| include_all, emails, project_ids = await asyncio.to_thread( | ||
| self._resolve_access, interaction | ||
| ) |
There was a problem hiding this comment.
Avoid full dashboard queries inside autocomplete access checks
invoice_name_autocomplete runs _resolve_access on every keystroke, and _resolve_access calls list_dashboard_projects(..., limit=500), which is the heavy dashboard path (it loads full project data/rosters, not just IDs). Since autocomplete interactions must return almost immediately and cannot be deferred, this repeated DB work is on the critical path before the ERPNext HTTP lookup, so users with larger project visibility can hit interaction timeouts and get empty suggestions. Use a lightweight project-id lookup or cache access scope per user to keep autocomplete latency bounded.
Useful? React with 👍 / 👎.
- Rename cogs/erpnext.py → invoices.py, ErpNextCog → InvoicesCog to reflect feature scope over system name - Add include_roster param to list_dashboard_projects (default True, backward compatible) and use include_roster=False in _resolve_access to skip the roster-members query on every autocomplete keystroke - Fix get_invoice input validation to raise ERPNextAPIError consistent with other ERPNextClient methods - Clamp search_invoices limit with max(1, limit)
| # 1. They have Steering Committee role or above (full access). | ||
| # 2. They created the invoice (invoice owner matches one of their ERP emails). | ||
| # 3. They are on the invoice's ERP project roster. | ||
| _PRIVILEGED_ROLES = ["Steering Committee"] |
There was a problem hiding this comment.
If you want we can add Workflows Engineer to this
There was a problem hiding this comment.
Thanks for the suggestion! Adding Workflows Engineer to _PRIVILEGED_ROLES in a follow-up PR#300.
michaelmwu
left a comment
There was a problem hiding this comment.
This is a Codex generated review. Please validate.
Re-reviewed the invoice permission changes at ff6c09e. The previous blocking feedback is addressed: /validate-invoice now resolves caller access before fetching, unauthorized users without ERP identity are denied before any invoice lookup, direct fetches re-check owner/project access after retrieval, and autocomplete is scoped via owner/project filters before returning choices.
Verification: uv run --all-groups python3 -m pytest tests/unit/test_erpnext_client.py tests/unit/test_invoices_cog.py tests/unit/test_erpnext_validation.py tests/unit/test_projects.py -q passed with 68 tests and 1 existing Discord audioop deprecation warning.
Description
Adds /validate-invoice slash command to the Discord bot, backed by a
read-only ERPNext REST API client and invoice validation rules defined
by the community admin. Responses show an invoice summary (owner,
project, cost centre, dates) followed by actionable validation issues.
Summary
ErpNextClient(read-only REST client) inpackages/sharedvalidate_invoice()with 3 rules: cost centre vs project, line-item cost centre/project consistency, purchase invoice due date (≥ 29 days from posting date)/validate-invoiceDiscord slash command with autocomplete, showing an invoice summary embed followed by flagged issuesERPNEXT_BASE_URL/ERPNEXT_API_TOKENto config and.env.exampleRelated Issue
From Discode #billing threads - Does the erp have an mcp or an api
How Has This Been Tested?
pytest tests/unit/test_erpnext_client.py tests/unit/test_erpnext_cog.pyERPNEXT_BASE_URLandERPNEXT_API_TOKENin.envand start the bot/validate-invoiceon a known good invoice → green embed with Invoice Info/validate-invoiceon an invoice with cost centre mismatch → red embed with issue details/validate-invoicewith a non-existent invoice number → "not found" messageSummary by CodeRabbit
New Features
New Validation
Tests