Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 20 additions & 3 deletions kairon/api/app/routers/pos.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from kairon.pos.definitions.factory import POSFactory
from kairon.shared.pos.constants import POSType
from kairon.shared.pos.models import (
LoginRequest, ClientRequest, POSOrderRequest
LoginRequest, ClientRequest, POSOrderRequest, BranchRequest
)
from kairon.api.models import Response
from kairon.shared.pos.processor import POSProcessor
Expand All @@ -24,7 +24,7 @@ def pos_login(req: LoginRequest,
Returns session_id + cookies using /web/session/authenticate
"""
pos_instance = POSFactory.get_instance(pos_type)
response = pos_instance().authenticate(client_name=req.client_name, page_type=req.page_type, bot=current_user.get_bot())
response = pos_instance().authenticate(client_name=req.client_name, page_type=req.page_type, bot=current_user.get_bot(), company_id=req.company_id)

return response

Expand All @@ -44,6 +44,22 @@ def register(

return Response(data=data)

@router.post("/create/branch", response_model=Response)
def create_branch(
req: BranchRequest,
session_id: str = Query(..., description="Odoo session_id"),
current_user: User = Security(Authentication.get_current_user_and_bot, scopes=ADMIN_ACCESS)
):
result = pos_processor.create_branch(
session_id=session_id,
branch_name=req.branch_name,
street=req.street,
city=req.city,
state=req.state,
bot = current_user.get_bot(),
user = current_user.get_user()
)
return Response(data=result, message="Branch created")

@router.delete("/client/delete", response_model=Response)
def delete_client(
Expand Down Expand Up @@ -94,7 +110,8 @@ def create_order(req: POSOrderRequest, session_id: str = Query(...),
result = pos_processor.create_pos_order(
session_id=session_id,
products=[p.dict() for p in req.products],
partner_id=req.partner_id
partner_id=req.partner_id,
company_id=req.company_id
)
return Response(data=result, message="POS order created")

Expand Down
11 changes: 7 additions & 4 deletions kairon/pos/odoo/odoo_pos.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ def authenticate(self, **kwargs):
client_name = kwargs.get("client_name")
bot = kwargs.get('bot')
page_type = kwargs.get('page_type', PageType.pos_products.value)
company_id = kwargs.get("company_id", 1)
data = pos_processor.pos_login(client_name, bot)
page_url_json = None
if page_type == PageType.pos_products.value:
page_url_json = self.products_list()
page_url_json = self.products_list(company_id=company_id)
elif page_type == PageType.pos_orders.value:
page_url_json = self.orders_list()
page_url_json = self.orders_list(company_id=company_id)
data.update(page_url_json)

response = pos_processor.set_odoo_session_cookie(data)
Expand All @@ -40,16 +41,18 @@ def authenticate(self, **kwargs):
def products_list(self, **kwargs):
action = OdooPOSActions.ACTION_POS_PRODUCT_LIST.value
menu = OdooPOSMenus.MENU_POS_PRODUCTS.value
company_id = kwargs.get("company_id", 1)
product_list_json = {
"url": f"{self.__base_url}/web#action={action}&model=product.template&view_type=kanban&cids=1&menu_id={menu}"
"url": f"{self.__base_url}/web#action={action}&model=product.template&view_type=kanban&cids={company_id}&menu_id={menu}"
}
return product_list_json

def orders_list(self, **kwargs):
action = OdooPOSActions.ACTION_POS_ORDER_LIST.value
menu = OdooPOSMenus.MENU_POS_ORDERS.value
company_id = kwargs.get("company_id",1)
order_list_json = {
"url": f"{self.__base_url}/web#action={action}&model=pos.order&view_type=list&cids=1&menu_id={menu}"
"url": f"{self.__base_url}/web#action={action}&model=pos.order&view_type=list&cids={company_id}&menu_id={menu}"
}
return order_list_json

3 changes: 2 additions & 1 deletion kairon/shared/pos/data_objects.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime

from mongoengine import StringField, DateTimeField, DictField
from mongoengine import StringField, DateTimeField, DictField, ListField
from mongoengine.errors import ValidationError

from kairon import Utility
Expand All @@ -19,6 +19,7 @@ class POSClientDetails(Auditlog):
bot = StringField(required=True)
user = StringField(required=True)
config = DictField(required=True)
branches = ListField(DictField())
timestamp = DateTimeField(default=datetime.utcnow)


Expand Down
7 changes: 7 additions & 0 deletions kairon/shared/pos/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ class ClientRequest(BaseModel):
class LoginRequest(BaseModel):
client_name: str
page_type: PageType = PageType.pos_products.value
company_id: int = 1

class BranchRequest(BaseModel):
branch_name: str
street: str
city: str
state: str

class ProductItem(BaseModel):
product_id: int
Expand All @@ -22,6 +28,7 @@ class ProductItem(BaseModel):
class POSOrderRequest(BaseModel):
products: List[ProductItem]
partner_id: Optional[int] = None
company_id: int = 1


class ResponseMessage(BaseModel):
Expand Down
122 changes: 105 additions & 17 deletions kairon/shared/pos/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,27 @@ def save_client_details(
)
return record

@staticmethod
def save_branch_details(bot: str, branch_name: str, company_id: int, user: str, pos_type: POSType = POSType.odoo.value):
record = POSClientDetails.objects(bot=bot, pos_type=POSType.odoo.value).first()
if not record:
raise AppException("No POS client configuration found for this bot.")
data = record.to_mongo().to_dict()
client_name = data["client_name"]
branch_details = {
"branch_name": branch_name.strip(),
"company_id": company_id
}

record = POSClientDetails.objects(
bot=bot,
client_name=client_name
).update_one(
push__branches=branch_details
)

return record

@staticmethod
def get_client_details(bot: str):
"""
Expand Down Expand Up @@ -494,7 +515,7 @@ def jsonrpc_get_uid(self, session_id: str) -> int:
kwargs={"limit": 1}
)[0]

def create_pos_order(self, session_id: str, products: list, partner_id: int = None):
def create_pos_order(self, session_id: str, products: list, partner_id: int = None, company_id: int = 1):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix type hint for optional parameter.

The partner_id parameter should be explicitly typed as optional per PEP 484.

🔧 Suggested fix
-    def create_pos_order(self, session_id: str, products: list, partner_id: int = None, company_id: int = 1):
+    def create_pos_order(self, session_id: str, products: list, partner_id: int | None = None, company_id: int = 1):
🧰 Tools
🪛 Ruff (0.14.10)

497-497: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

🤖 Prompt for AI Agents
In @kairon/shared/pos/processor.py at line 497, Update the create_pos_order
function signature to mark partner_id as optional per PEP 484 (e.g., partner_id:
Optional[int] or partner_id: int | None) and add the corresponding import (from
typing import Optional) if using Optional; keep the default = None unchanged and
ensure any internal uses handle None.

"""
Create POS order using JSON-RPC (create_from_ui)
with check for available_in_pos for every product.
Expand Down Expand Up @@ -579,15 +600,20 @@ def create_pos_order(self, session_id: str, products: list, partner_id: int = No
model="pos.config",
method="search_read",
args=[[["active", "=", True]]],
kwargs={"limit": 1}
kwargs={
"fields": ["id", "company_id"]
}
)

if not pos_configs:
raise AppException("No POS Config found")

config = pos_configs[0]
config_id = config["id"]
company_id = config["company_id"][0] if config.get("company_id") else False
config_id = None
for config in pos_configs:
comp_id = config["company_id"][0] if config.get("company_id") else False
if company_id == comp_id:
config_id = config["id"]
break
Comment on lines +611 to +616
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle the case when no matching POS config is found for the company_id.

If no config matches the provided company_id, config_id remains None, which will cause issues in subsequent operations (e.g., line 624 where it's used in a domain filter). This should raise an explicit error.

Proposed fix
         config_id = None
         for config in pos_configs:
             comp_id = config["company_id"][0] if config.get("company_id") else False
             if company_id == comp_id:
                 config_id = config["id"]
                 break
+
+        if config_id is None:
+            raise HTTPException(
+                status_code=404,
+                detail=f"No POS config found for company_id {company_id}"
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
config_id = None
for config in pos_configs:
comp_id = config["company_id"][0] if config.get("company_id") else False
if company_id == comp_id:
config_id = config["id"]
break
config_id = None
for config in pos_configs:
comp_id = config["company_id"][0] if config.get("company_id") else False
if company_id == comp_id:
config_id = config["id"]
break
if config_id is None:
raise HTTPException(
status_code=404,
detail=f"No POS config found for company_id {company_id}"
)
🤖 Prompt for AI Agents
In @kairon/shared/pos/processor.py around lines 611 - 616, The loop over
pos_configs can leave config_id as None when no entry matches company_id; after
the loop (before any use like the subsequent domain filter), add an explicit
check that raises a clear exception (e.g., ValueError or a domain-specific
exception) if config_id is None, including the company_id in the message so
callers know which company had no POS config; ensure this check is placed right
after the for-loop that sets config_id and before any code that uses config_id.


open_session = self.jsonrpc_call(
session_id=session_id,
Expand All @@ -602,9 +628,11 @@ def create_pos_order(self, session_id: str, products: list, partner_id: int = No
kwargs={"limit": 1}
)

payment_method_ids=[]
if open_session:
session_id_odoo = open_session[0]["id"]
sequence_number = open_session[0].get("sequence_number", 1)
payment_method_ids = open_session[0].get("payment_method_ids", [])
else:
session_id_odoo = self.jsonrpc_call(
session_id=session_id,
Expand All @@ -622,27 +650,19 @@ def create_pos_order(self, session_id: str, products: list, partner_id: int = No

sequence_number = 1

pay_method = self.jsonrpc_call(
session_id=session_id,
model="pos.payment.method",
method="search_read",
args=[[["is_cash_count", "=", True]]],
kwargs={"limit": 1}
)

if not pay_method:
pay_method = self.jsonrpc_call(
if not payment_method_ids:
payment_method_ids = self.jsonrpc_call(
session_id=session_id,
model="pos.payment.method",
method="search_read",
args=[[[]]],
kwargs={"limit": 1}
)

if not pay_method:
if not payment_method_ids:
raise HTTPException(status_code=400, detail="No POS payment methods found")

payment_method_id = pay_method[0]["id"]
payment_method_id = payment_method_ids[0]

order_data = {
"name": f"POS/{int(time.time())}",
Expand Down Expand Up @@ -680,6 +700,74 @@ def create_pos_order(self, session_id: str, products: list, partner_id: int = No

return {"order_id": order_id, "status": "created"}

def create_branch(self, session_id: str, branch_name: str, street: str, city: str, state: str, bot: str, user: str):
INDIA_STATE_MAP = {
"Andaman and Nicobar": 577,
"Andhra Pradesh": 578,
"Arunachal Pradesh": 579,
"Assam": 580,
"Bihar": 581,
"Chattisgarh": 583,
"Chandigarh": 582,
"Daman and Diu": 585,
"Delhi": 586,
"Dadra and Nagar Haveli": 584,
"Goa": 587,
"Gujarat": 588,
"Himachal Pradesh": 590,
"Haryana": 589,
"Jharkhand": 592,
"Jammu and Kashmir": 591,
"Karnataka": 593,
"Kerala": 594,
"Lakshadweep": 595,
"Maharashtra": 597,
"Meghalaya": 599,
"Manipur": 598,
"Madhya Pradesh": 596,
"Mizoram": 600,
"Nagaland": 601,
"Odisha": 602,
"Punjab": 604,
"Puducherry": 603,
"Rajasthan": 605,
"Sikkim": 606,
"Tamil Nadu": 607,
"Tripura": 609,
"Telangana": 608,
"Uttarakhand": 611,
"Uttar Pradesh": 610,
"West Bengal": 612
}
try:
state_id = INDIA_STATE_MAP[state]
except KeyError:
raise HTTPException(status_code=400, detail=f"Invalid state: {state}")

branch_data = self.jsonrpc_call(
session_id=session_id,
model="res.company",
method="create",
args= [
{
"name": branch_name,
"parent_id": 1,
"currency_id": 1,
"country_id": 104,
Comment on lines +754 to +756
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the specific file and lines in question
cd /repo && cat -n kairon/shared/pos/processor.py | sed -n '750,760p'

Repository: digiteinfotech/kairon

Length of output: 120


🏁 Script executed:

# Search for usage patterns of these IDs across the codebase
rg -n "parent_id|currency_id|country_id" --type py -B2 -A2

Repository: digiteinfotech/kairon

Length of output: 2602


🏁 Script executed:

# Check if there's any configuration file or module for Odoo settings
fd -e py -e json -e yaml -e toml -e conf | xargs rg -l "parent_id|currency_id|country_id|odoo.*config" 2>/dev/null | head -20

Repository: digiteinfotech/kairon

Length of output: 489


🏁 Script executed:

cat -n kairon/shared/pos/processor.py | sed -n '740,770p'

Repository: digiteinfotech/kairon

Length of output: 1290


🏁 Script executed:

# Search for the function containing these lines and understand the context
rg -n "def.*\(" kairon/shared/pos/processor.py | head -20

Repository: digiteinfotech/kairon

Length of output: 1651


🏁 Script executed:

# Check if there's any Odoo configuration or settings file
rg -n "odoo|currency_id|country_id" kairon/shared/pos/processor.py -B5 -A5

Repository: digiteinfotech/kairon

Length of output: 4795


🏁 Script executed:

# Look for configuration files that might contain these ID mappings
fd -type f \( -name "*.py" -o -name "*.json" -o -name "*.yaml" -o -name "*.toml" -o -name "*.conf" \) | xargs rg -l "config|setting" | grep -E "(config|setting|constant|env)" | head -10

Repository: digiteinfotech/kairon

Length of output: 852


🏁 Script executed:

# Look for configuration files and environment setup
rg -n "parent_id|currency_id|country_id|odoo.*config" kairon/ --type py | grep -v test | grep -v ".pyc"

Repository: digiteinfotech/kairon

Length of output: 478


🏁 Script executed:

# Check the Utility.environment setup to understand config structure
rg -n "environment\[.*pos" kairon/ --type py | head -20

Repository: digiteinfotech/kairon

Length of output: 480


🏁 Script executed:

# Look for any dynamic ID resolution or query patterns for these entities
rg -n "search.*parent|search.*currency|search.*country" kairon/shared/pos/ --type py -B2 -A2

Repository: digiteinfotech/kairon

Length of output: 47


🏁 Script executed:

# Check if there are similar hardcoded patterns elsewhere in the codebase
rg -n "jsonrpc_call.*create" kairon/shared/pos/processor.py --type py -B3 -A3

Repository: digiteinfotech/kairon

Length of output: 458


🏁 Script executed:

# Check if there's any documentation or comments about India-specificity
rg -n "india|104|country_id" kairon/shared/pos/processor.py -B5 -A5 | head -50

Repository: digiteinfotech/kairon

Length of output: 467


🏁 Script executed:

# Check the create_branch function signature and docstring
sed -n '703,720p' kairon/shared/pos/processor.py

Repository: digiteinfotech/kairon

Length of output: 729


🏁 Script executed:

# Search for any test files that might show intended usage
rg -n "create_branch|country_id.*104" tests/ --type py

Repository: digiteinfotech/kairon

Length of output: 1195


🏁 Script executed:

# Check Utility.environment structure to see how config is loaded
rg -n "class Utility" kairon/shared/utils.py -A 30 | head -50

Repository: digiteinfotech/kairon

Length of output: 1210


🏁 Script executed:

# Look for environment configuration files
fd -type f -name "*.yaml" -o -name "*.yml" -o -name "*.json" | xargs rg -l "pos.*odoo\|country\|currency" 2>/dev/null | head -10

Repository: digiteinfotech/kairon

Length of output: 235


🏁 Script executed:

# Check if the IDs are mentioned in documentation or readme
find . -name "README*" -o -name "*.md" | xargs rg -l "104\|country_id\|create_branch" 2>/dev/null

Repository: digiteinfotech/kairon

Length of output: 49


🏁 Script executed:

# Look at the full create_branch function to understand the context better
sed -n '703,770p' kairon/shared/pos/processor.py

Repository: digiteinfotech/kairon

Length of output: 2306


🏁 Script executed:

# Check if there's any configuration that might override these values
rg -n "parent_id.*=|currency_id.*=|country_id.*=" kairon/ --type py | grep -v test | grep -v ".pyc"

Repository: digiteinfotech/kairon

Length of output: 183


🏁 Script executed:

# Look for Odoo documentation or comments about these standard IDs
rg -n "INR|india|odoo" kairon/shared/pos/processor.py -i

Repository: digiteinfotech/kairon

Length of output: 1795


Make country_id, currency_id, and parent_id configurable instead of hardcoded.

The hardcoded values parent_id: 1, currency_id: 1, and country_id: 104 are specific to a particular Odoo instance setup. Different deployments may have different ID assignments for the parent company, default currency, and country. Since the codebase already uses Utility.environment["pos"]["odoo"] for configuration (odoo_url, odoo_domain, odoo_master_password), these IDs should be moved to the same configuration mechanism rather than hardcoded. Without this, the code will fail or create incorrect associations when deployed to Odoo instances with different configurations.

🤖 Prompt for AI Agents
In @kairon/shared/pos/processor.py around lines 754 - 756, Replace the hardcoded
parent_id: 1, currency_id: 1, and country_id: 104 with values read from the
existing POS Odoo config object (Utility.environment["pos"]["odoo"]); update the
payload construction in the pos processor where these keys are set to use
Utility.environment["pos"]["odoo"]["parent_id"], ["currency_id"], and
["country_id"] (or sensible defaults if keys are missing) so different
deployments can override them via config rather than using fixed IDs.

"state_id": state_id,
"street": street,
"city": city,
"active": True
}
],
kwargs= {}
)
if not branch_data:
raise HTTPException(status_code=404, detail="Error in creating branch")

POSProcessor.save_branch_details(bot, branch_name, branch_data, user)
return {"branch_id": branch_data, "status": "created"}

def accept_pos_order(self, session_id: str, order_id: int) -> Dict[str, Any]:
"""
Accept a POS order: tries to create payment and invoice it.
Expand Down
Loading
Loading