From e974f3c227ba56ba2b2d8ac522b2b00897a04a90 Mon Sep 17 00:00:00 2001 From: theerayut Date: Fri, 15 Aug 2025 10:42:04 +0700 Subject: [PATCH 01/33] [18.0][ADD] zort_connector --- zort_connector/__init__.py | 5 + zort_connector/__manifest__.py | 24 +++ zort_connector/controllers/__init__.py | 3 + zort_connector/controllers/controllers.py | 22 +++ zort_connector/data/ir_cron_data.xml | 14 ++ zort_connector/data/partner_data.xml | 8 + zort_connector/data/product_data.xml | 23 +++ zort_connector/models/__init__.py | 5 + zort_connector/models/product_template.py | 50 ++++++ zort_connector/models/res_config_settings.py | 48 ++++++ zort_connector/models/sale_order.py | 143 ++++++++++++++++ zort_connector/security/ir.model.access.csv | 2 + .../views/product_template_view.xml | 22 +++ .../views/res_config_settings_view.xml | 26 +++ zort_connector/views/sale_order_view.xml | 22 +++ zort_connector/zort_api/__init__.py | 1 + zort_connector/zort_api/zort_api.py | 162 ++++++++++++++++++ 17 files changed, 580 insertions(+) create mode 100644 zort_connector/__init__.py create mode 100644 zort_connector/__manifest__.py create mode 100644 zort_connector/controllers/__init__.py create mode 100644 zort_connector/controllers/controllers.py create mode 100644 zort_connector/data/ir_cron_data.xml create mode 100644 zort_connector/data/partner_data.xml create mode 100644 zort_connector/data/product_data.xml create mode 100644 zort_connector/models/__init__.py create mode 100644 zort_connector/models/product_template.py create mode 100644 zort_connector/models/res_config_settings.py create mode 100644 zort_connector/models/sale_order.py create mode 100644 zort_connector/security/ir.model.access.csv create mode 100644 zort_connector/views/product_template_view.xml create mode 100644 zort_connector/views/res_config_settings_view.xml create mode 100644 zort_connector/views/sale_order_view.xml create mode 100644 zort_connector/zort_api/__init__.py create mode 100644 zort_connector/zort_api/zort_api.py diff --git a/zort_connector/__init__.py b/zort_connector/__init__.py new file mode 100644 index 00000000..93eb5155 --- /dev/null +++ b/zort_connector/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import zort_api +from . import controllers +from . import models diff --git a/zort_connector/__manifest__.py b/zort_connector/__manifest__.py new file mode 100644 index 00000000..9453c6c9 --- /dev/null +++ b/zort_connector/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +{ + 'name': "zort_connector", + 'summary': "Connects Odoo with Zort", + 'description': """ +Long description of module's purpose + """, + 'version': '18.0.1.0.0', + 'license': 'LGPL-3', + 'author': "Ecosoft., Ltd.", + 'maintainers': ['theerayuta@ecosoft.co.th'], + 'website': "https://www.yourcompany.com", + 'depends': ['sale_management'], + 'data': [ + # 'security/ir.model.access.csv', + 'data/ir_cron_data.xml', + 'data/partner_data.xml', + 'data/product_data.xml', + 'views/res_config_settings_view.xml', + 'views/sale_order_view.xml', + 'views/product_template_view.xml', + ], +} + diff --git a/zort_connector/controllers/__init__.py b/zort_connector/controllers/__init__.py new file mode 100644 index 00000000..b0f26a9a --- /dev/null +++ b/zort_connector/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers diff --git a/zort_connector/controllers/controllers.py b/zort_connector/controllers/controllers.py new file mode 100644 index 00000000..952c0cb4 --- /dev/null +++ b/zort_connector/controllers/controllers.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from odoo import http + + +class ZortConnector(http.Controller): + @http.route('/zort_connector/zort_connector', auth='public') + def index(self, **kw): + return "Hello, world" + +# @http.route('/zort_connector/zort_connector/objects', auth='public') +# def list(self, **kw): +# return http.request.render('zort_connector.listing', { +# 'root': '/zort_connector/zort_connector', +# 'objects': http.request.env['zort_connector.zort_connector'].search([]), +# }) + +# @http.route('/zort_connector/zort_connector/objects/', auth='public') +# def object(self, obj, **kw): +# return http.request.render('zort_connector.object', { +# 'object': obj +# }) + diff --git a/zort_connector/data/ir_cron_data.xml b/zort_connector/data/ir_cron_data.xml new file mode 100644 index 00000000..52f17f82 --- /dev/null +++ b/zort_connector/data/ir_cron_data.xml @@ -0,0 +1,14 @@ + + + + + + Auto Zort Order Import + + code + model.create_sales_order_from_zort() + 10 + minutes + + + diff --git a/zort_connector/data/partner_data.xml b/zort_connector/data/partner_data.xml new file mode 100644 index 00000000..b9791fde --- /dev/null +++ b/zort_connector/data/partner_data.xml @@ -0,0 +1,8 @@ + + + + + Marketplace Customer + + + diff --git a/zort_connector/data/product_data.xml b/zort_connector/data/product_data.xml new file mode 100644 index 00000000..ccd0f77d --- /dev/null +++ b/zort_connector/data/product_data.xml @@ -0,0 +1,23 @@ + + + + + Shipping Fee + service + + + 0.0 + 0.0 + shipping_fee + + + Discount + service + + + 0.0 + 0.0 + zort_discount + + + diff --git a/zort_connector/models/__init__.py b/zort_connector/models/__init__.py new file mode 100644 index 00000000..e30c42c1 --- /dev/null +++ b/zort_connector/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import res_config_settings +from . import sale_order +from . import product_template diff --git a/zort_connector/models/product_template.py b/zort_connector/models/product_template.py new file mode 100644 index 00000000..7dd1f9c4 --- /dev/null +++ b/zort_connector/models/product_template.py @@ -0,0 +1,50 @@ +import logging +from odoo import _, api, fields, models, exceptions + +_logger = logging.getLogger(__name__) + +class ProductTemplate(models.Model): + _name = 'product.template' + _inherit = ['product.template', 'zort.api'] + + sync_with_zort = fields.Boolean( + string='Sync with Zort', + default=False, + help='Enable synchronization of this product with Zort API' + ) + + def action_update_create_product_on_zort(self): + """Update or create product in Zort based on the current product template.""" + self.ensure_one() + # print(f"Updating/Creating product in Zort for {self.name}") + + if not self.sync_with_zort: + return + + try: + data = { + "sku": self.default_code, + "name": self.name, + "sellprice": self.list_price, + "purchaseprice": self.standard_price, + "unittext": self.uom_name, + # "weight": self.weight, // we can uncomment later + # "sell_vat_status": 0, // we can uncomment later + # "purchase_vat_status": 0 // we can uncomment later + } + response = self._add_product(data) + self._log_api_response(response_json={ + 'product_id': response.get('id'), + 'status': response.get('status'), + 'message': 'Product created successfully' + }, func='action_update_create_zort', path='zort_connector/models/product_template.py', line=16) + except Exception as e: + _logger.error("Error updating/creating product in Zort: %s", e) + self._log_api_response(response_json={ + 'product_id': self.default_code, + 'status': 'error', + 'error': str(e) + }, func='action_update_create_zort', level='error', path='zort_connector/models/product_template.py', line=16) + + def action_update_qty_to_zort(self): + pass diff --git a/zort_connector/models/res_config_settings.py b/zort_connector/models/res_config_settings.py new file mode 100644 index 00000000..1a0b157b --- /dev/null +++ b/zort_connector/models/res_config_settings.py @@ -0,0 +1,48 @@ +# Copyright 2025 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + + zort_connector_enabled = fields.Boolean( + string='Enable Zort Connector', + help='Enable the Zort Connector to connect Odoo with Zort.', + default=False, + config_parameter='zort_connector.enabled', + ) + zort_endpoint_url = fields.Char( + string='Zort Endpoint URL', + help='The URL of the Zort endpoint to connect with.', + default='https://open-api.zortout.com/v4', + config_parameter='zort_connector.endpoint_url', + ) + zort_api_key = fields.Char( + string='Zort API Key', + help='The API key to authenticate with the Zort endpoint.', + default='', + config_parameter='zort_connector.api_key', + ) + zort_api_secret = fields.Char( + string='Zort API Secret', + help='The API secret to authenticate with the Zort endpoint.', + default='', + config_parameter='zort_connector.api_secret', + ) + zort_store_name = fields.Char( + string='Zort Store Name', + help='The name of the store in Zort.', + default='', + config_parameter='zort_connector.store_name', + ) + + @api.onchange('zort_connector_enabled') + def _onchange_zort_connector_enabled(self): + if not self.zort_connector_enabled: + self.env['ir.config_parameter'].sudo().set_param('zort_connector.endpoint_url', '') + self.env['ir.config_parameter'].sudo().set_param('zort_connector.api_key', '') + self.env['ir.config_parameter'].sudo().set_param('zort_connector.api_secret', '') + self.env['ir.config_parameter'].sudo().set_param('zort_connector.store_name', '') diff --git a/zort_connector/models/sale_order.py b/zort_connector/models/sale_order.py new file mode 100644 index 00000000..ac1743b7 --- /dev/null +++ b/zort_connector/models/sale_order.py @@ -0,0 +1,143 @@ +import logging +from odoo import _, api, fields, models, exceptions + +_logger = logging.getLogger(__name__) + +class SaleOrder(models.Model): + _name = 'sale.order' + _inherit = ['sale.order', 'zort.api'] + + # This field can be used to store the Zort order ID + is_zort_order = fields.Boolean( + string='Is Zort Order', + help='Indicates if this sale order is created from Zort.', + default=False, + # readonly=True, + ) + zort_order_id = fields.Char( + string='Zort Order ID', + help='The ID of the order in Zort.', + copy=False, + # readonly=True, + ) + zort_order_number = fields.Char( + string='Zort Order Number', + help='The order number in Zort.', + copy=False, + # readonly=True, + ) + zort_order_status = fields.Char( + string='Zort Order Status', + help='The status of the order in Zort.', + copy=False, + # readonly=True, + ) + + @api.model + def create_sales_order_from_zort(self, status="0", orderidlist="", numberlist=""): + """ + Fetch orders from Zort based on the provided status and optional filters. + :param status: str - Status of the orders to fetch. + :param orderidlist: str - Comma-separated list of order IDs to filter (optional). + :param numberlist: str - Comma-separated list of order numbers to filter (optional). + :return: dict - JSON response containing the list of orders. + """ + try: + response = self._get_list_order(status, orderidlist, numberlist) + except Exception as e: + _logger.error("Error fetching Zort orders: %s", e) + + if response.get('count') == 0: + _logger.warning("No orders fetched from Zort for the given criteria.") + return + + orders = response.get("list", []) + for order in orders: + try: + self._create_or_update_sale_order(order) + self._log_api_response(response_json={ + 'order_id': order.get('id'), + 'status': order.get('status'), + 'message': 'Sale Order created successfully' + }, func='create_sales_order_from_zort', path='zort_connector/models/sale_order.py', line=37) + except Exception as e: + _logger.error("Error creating/updating sale order: %s", e) + self._log_api_response(response_json={ + 'order_id': order.get('id'), + 'status': order.get('status'), + 'error': str(e) + }, func='create_sales_order_from_zort', level='error', path='zort_connector/models/sale_order.py', line=37) + + def _create_or_update_sale_order(self, order): + """ + Create or update a sale order based on the Zort order data. + :param order: dict - The Zort order data. + """ + # Check if the order already exists in Odoo + existing_order = self.search([('zort_order_id', '=', order.get('id'))], limit=1) + if existing_order: + # Update existing order + existing_order.write({ + 'zort_order_number': order.get('number'), + 'zort_order_status': order.get('status'), + }) + _logger.info("Updated Sale Order: %s", existing_order.name) + return + + else: + # Create a new sale order + _logger.info("Creating Sale Order for Zort Order ID: %s", order.get('id')) + + # Prepare the data for the sale order + order_data = { + 'partner_id': self.env.ref('zort_connector.marketplace_customer').id, # a default customer + 'is_zort_order': True, + 'zort_order_id': order.get('id'), + 'zort_order_number': order.get('number'), + 'zort_order_status': order.get('status'), + } + + # Create the sale order + sale_order = self.create(order_data) + + # Add order lines + order_lines = [] + for line in order.get('list', []): + product = self.env['product.product'].search([('default_code', '=', line.get('sku'))], limit=1) + if not product: + _logger.warning("Product with SKU %s not found. Skipping line.", line.get('sku')) + continue + order_lines.append((0, 0, { + 'product_id': product.id, + 'product_uom_qty': line.get('number', 1), + 'price_unit': line.get('totalprice', 0.0), + 'name': line.get('name', product.name), + })) + + # check if there is shipping fee + if order.get('shippingamount', 0.0) > 0: + shipping_fee_product = self.env['product.product'].search([('default_code', '=', 'shipping_fee')], limit=1) + order_lines.append((0, 0, { + 'product_id': shipping_fee_product.id, + 'product_uom_qty': 1, + 'price_unit': order.get('shippingamount', 0.0), + 'name': "Shipping Fee", + })) + + # check if there is discount + if order.get('discountamount', 0.0) > 0: + discount_product = self.env['product.product'].search([('default_code', '=', 'zort_discount')], limit=1) + order_lines.append((0, 0, { + 'product_id': discount_product.id, + 'product_uom_qty': 1, + 'price_unit': -order.get('discountamount', 0.0), + 'name': "Discount", + })) + + if order_lines: + sale_order.order_line = order_lines + + _logger.info("Created Sale Order: %s", sale_order.name) + + return sale_order + diff --git a/zort_connector/security/ir.model.access.csv b/zort_connector/security/ir.model.access.csv new file mode 100644 index 00000000..ff608852 --- /dev/null +++ b/zort_connector/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_zort_connector_zort_connector,zort_connector.zort_connector,model_zort_connector_zort_connector,base.group_user,1,1,1,1 diff --git a/zort_connector/views/product_template_view.xml b/zort_connector/views/product_template_view.xml new file mode 100644 index 00000000..d0f68bcf --- /dev/null +++ b/zort_connector/views/product_template_view.xml @@ -0,0 +1,22 @@ + + + + product.template.only.form.view.inherit.sale + product.template + + + + +