🎉 Initial commit - Hazard Odoo 16 project
- Custom modules: hazard_shopify, hazard_inventory_report, hazard_website - Third-party modules: bi_sql_editor, query_deluxe, sql_request_abstract, l10n_mx_sat_sync_itadmin_ee - Docker setup: Odoo 16 + PostgreSQL 17 - Utility scripts: backup_hazard.sh, update_module.py - Agent configuration: .agents/AGENTS.md - Security: Enterprise modules, credentials, backups and production data excluded via .gitignore
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import requests
|
||||
import xmlrpc.client
|
||||
from collections import defaultdict
|
||||
|
||||
ODOO_URL = 'http://localhost:16001'
|
||||
ODOO_DB = 'hazard_new'
|
||||
ODOO_USER = 'admin'
|
||||
ODOO_PASS = 'admin'
|
||||
|
||||
def get_shopify_credentials():
|
||||
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
|
||||
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASS, {})
|
||||
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
|
||||
|
||||
config_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'shopify.config', 'search', [[]])
|
||||
config = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'shopify.config', 'read', [config_ids[0]], {'fields': ['shop_url', 'access_token']})[0]
|
||||
return config.get('shop_url'), config.get('access_token')
|
||||
|
||||
def get_collection_products(shop_url, access_token, collection_id):
|
||||
query = """
|
||||
query getProducts($id: ID!, $cursor: String) {
|
||||
collection(id: $id) {
|
||||
products(first: 250, after: $cursor) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
legacyResourceId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
url = f"https://{shop_url}/admin/api/2023-10/graphql.json"
|
||||
headers = {
|
||||
'X-Shopify-Access-Token': access_token,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
product_ids = set()
|
||||
cursor = None
|
||||
has_next_page = True
|
||||
|
||||
while has_next_page:
|
||||
variables = {"id": f"gid://shopify/Collection/{collection_id}"}
|
||||
if cursor:
|
||||
variables["cursor"] = cursor
|
||||
|
||||
resp = requests.post(url, headers=headers, json={'query': query, 'variables': variables})
|
||||
data = resp.json().get('data', {}).get('collection', {}).get('products', {})
|
||||
if not data:
|
||||
break
|
||||
|
||||
for edge in data.get('edges', []):
|
||||
product_ids.add(str(edge['node']['legacyResourceId']))
|
||||
|
||||
page_info = data.get('pageInfo', {})
|
||||
has_next_page = page_info.get('hasNextPage', False)
|
||||
cursor = page_info.get('endCursor')
|
||||
|
||||
return product_ids
|
||||
|
||||
def get_may_orders(shop_url, access_token):
|
||||
url = f"https://{shop_url}/admin/api/2023-10/orders.json"
|
||||
headers = {
|
||||
'X-Shopify-Access-Token': access_token,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
params = {
|
||||
'status': 'any',
|
||||
'created_at_min': '2026-05-01T00:00:00-06:00',
|
||||
'created_at_max': '2026-05-31T23:59:59-06:00',
|
||||
'limit': 250
|
||||
}
|
||||
|
||||
orders = []
|
||||
while url:
|
||||
resp = requests.get(url, headers=headers, params=params)
|
||||
if resp.status_code != 200:
|
||||
break
|
||||
data = resp.json()
|
||||
orders.extend(data.get('orders', []))
|
||||
|
||||
link_header = resp.headers.get('Link')
|
||||
url = None
|
||||
params = None
|
||||
if link_header:
|
||||
links = link_header.split(',')
|
||||
for link in links:
|
||||
if 'rel="next"' in link:
|
||||
url = link[link.find('<')+1 : link.find('>')]
|
||||
break
|
||||
|
||||
return orders
|
||||
|
||||
def main():
|
||||
shop_url, access_token = get_shopify_credentials()
|
||||
padel_products = get_collection_products(shop_url, access_token, '487990952129')
|
||||
orders = get_may_orders(shop_url, access_token)
|
||||
|
||||
sales_by_channel = defaultdict(float)
|
||||
items_by_channel = defaultdict(int)
|
||||
total_sales = 0.0
|
||||
total_items = 0
|
||||
|
||||
for order in orders:
|
||||
if order.get('cancelled_at'):
|
||||
continue
|
||||
|
||||
source = order.get('source_name', 'Unknown')
|
||||
|
||||
# calculate refunds for padel products
|
||||
refunds_for_product = defaultdict(float)
|
||||
refunded_qty_for_product = defaultdict(int)
|
||||
|
||||
for refund in order.get('refunds', []):
|
||||
for refund_line_item in refund.get('refund_line_items', []):
|
||||
line_item = refund_line_item.get('line_item', {})
|
||||
product_id = str(line_item.get('product_id'))
|
||||
if product_id in padel_products:
|
||||
refunds_for_product[line_item.get('id')] += float(refund_line_item.get('subtotal', 0))
|
||||
refunded_qty_for_product[line_item.get('id')] += int(refund_line_item.get('quantity', 0))
|
||||
|
||||
for item in order.get('line_items', []):
|
||||
product_id = str(item.get('product_id'))
|
||||
if product_id in padel_products:
|
||||
price = float(item.get('price', 0))
|
||||
quantity = int(item.get('quantity', 0))
|
||||
|
||||
discount = 0.0
|
||||
for d in item.get('discount_allocations', []):
|
||||
discount += float(d.get('amount', 0))
|
||||
|
||||
net_sale = (price * quantity) - discount
|
||||
|
||||
# subtract refund for this line item
|
||||
line_item_id = item.get('id')
|
||||
if line_item_id in refunds_for_product:
|
||||
net_sale -= refunds_for_product[line_item_id]
|
||||
quantity -= refunded_qty_for_product[line_item_id]
|
||||
|
||||
sales_by_channel[source] += net_sale
|
||||
items_by_channel[source] += quantity
|
||||
total_sales += net_sale
|
||||
total_items += quantity
|
||||
|
||||
for channel in sales_by_channel:
|
||||
print(f"{channel}: {sales_by_channel[channel]}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user