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):
"""
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 case when no POS config matches the provided company_id.

If no config matches the company_id, config_id remains None, which will cause downstream errors when creating the POS session. Add validation after the loop.

🐛 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 AppException(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 AppException(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 may leave config_id as None if no config matches company_id; after
the loop (after the for config in pos_configs block) add a validation that
checks if config_id is None and handle it explicitly (e.g., log a clear error
including the company_id and either raise a specific exception or return an
error response/None to abort POS session creation) so downstream code that
creates the POS session does not operate with a None config_id; reference the
variables config_id, pos_configs, company_id and the post-loop validation point
for the change.


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]
Comment on lines +653 to +665
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Type mismatch: search_read returns list of dicts, not list of ints.

When fetching payment methods via search_read (lines 654-660), the result is a list of dictionaries. However, line 665 assigns payment_method_ids[0] directly to payment_method_id, expecting an integer. This will pass a dict to statement_ids, causing the order creation to fail.

🐛 Proposed fix
         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}
+                kwargs={"fields": ["id"], "limit": 1}
             )
+            if payment_method_ids:
+                payment_method_ids = [pm["id"] for pm in payment_method_ids]

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

         payment_method_id = payment_method_ids[0]
🤖 Prompt for AI Agents
In `@kairon/shared/pos/processor.py` around lines 653 - 665, search_read returns a
list of dicts, but the code assigns payment_method_ids[0] (a dict) to
payment_method_id and later passes it to statement_ids; change the assignment to
extract the actual integer id from the first dict returned by jsonrpc_call
(e.g., pull the 'id' field from payment_method_ids[0]) and keep the existing
empty-check; ensure payment_method_id is an int before using it in creating the
order/statement_ids.


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,
"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