trytonpsk-sale_web_channel/shopify.py

791 lines
32 KiB
Python

# -*- coding: UTF-8 -*-
# This file is part electronic_mail_template module for Tryton.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from decimal import Decimal
from trytond.pool import Pool
from itertools import chain
from trytond.transaction import Transaction
import requests
from urllib.parse import urlencode
from datetime import datetime, date
from .web_channel import SaleWebChannel
import json
import base64
import hmac
import hashlib
HEADERS = {
'Accept': 'application/json',
'Content-type': 'application/json'
}
# class Shopify(SaleWebChannel):
# 'Shopify'
# __name__ = 'sale.web_channel.shopify'
# @classmethod
# def __setup__(cls):
# super(Shopify, cls).__setup__()
# def _get_context(self):
# print(self)
# return {
# 'company': self.company.id,
# 'user': self.user.id,
# 'shops': [self.shop.id],
# 'shop': self.shop.id,
# 'language': 'es'
# }
# # def _get_user(self):
# # User = Pool().get('res.user')
# # user, = User.search(
# # ('login', '=', 'shopify')
# # )
# # return user
# def _create_product(self, codes, line={}):
# pool = Pool()
# Product = pool.get('product.product')
# Template = pool.get('product.template')
# description = line['variant_title']
# list_price = Decimal(str(round(float(line['price'])/1.19, 2)))
# sale_price_w_tax = Decimal(str(round(Decimal(line['price']), 2)))
# create_template = {
# 'name': line['title'],
# 'list_price': list_price,
# 'sale_price_w_tax': sale_price_w_tax,
# 'type': 'goods',
# 'salable': True,
# 'purchasable': True,
# 'purchase_uom': 1,
# 'sale_uom': 1,
# 'default_uom': 1,
# 'account_category': 7,
# 'code': codes[0],
# }
# template, = Template.create([create_template])
# create_product = []
# for code in codes:
# create_product.append({
# 'code': code,
# 'description': description,
# 'template': template.id
# })
# return Product.create(create_product)
# def _create_party(self, customer):
# _pool = Pool()
# City = _pool.get('party.city_code')
# Party = _pool.get('party.party')
# PartyObligationTax = _pool.get('party.obligation_tax')
# email = customer['email']
# shipment_address, phone, city = '', '', None
# if customer.get('default_address'):
# default_address = customer['default_address']
# shipment_address = default_address['address1']
# phone = default_address['phone'] or '000'
# city_name = default_address['city']
# if city_name:
# cities = City.search([
# ('name', '=', city_name)
# ])
# if cities:
# city = cities[0]
# customer_name = customer['first_name'] + ' ' + customer['last_name']
# create_customer = {
# 'id_reference': str(customer['id']),
# 'name': customer_name.upper(),
# 'type_document': '13',
# 'id_number': default_address['company'],
# 'addresses': [('create', [{
# 'street': shipment_address,
# }])],
# 'contact_mechanisms': [
# ('create', [
# {'type': 'phone', 'value': phone.replace(" ","")},
# {'type': 'email', 'value': email},
# ])
# ]
# }
# if not city:
# city = City(149)
# create_customer['addresses'][0][1][0]['department_code'] = city.department.id
# create_customer['addresses'][0][1][0]['city_code'] = city.id
# party, = Party.create([create_customer])
# PartyObligationTax.create([{
# 'party': party.id,
# 'obligation_fiscal': 6,
# }])
# return party
# # def _return_sale(self, sale):
# # pool = Pool()
# # Sale = pool.get('sale.sale')
# # Date = pool.get('ir.date')
# # ctx = self._get_context()
# # dev_sales = []
# # sales = [sale]
# # if sale.pack_id:
# # sales = Sale.search([
# # ('pack_id', '=', sale.pack_id)
# # ])
# # with Transaction().set_context(ctx):
# # return_sales = Sale.copy(sales)
# # for return_sale, sale in zip(return_sales, sales):
# # return_sale.origin = sale
# # return_sale.reference = sale.reference
# # return_sale.state = 'draft'
# # return_sale.sale_date = Date.today()
# # if sale.invoice_type == '1':
# # return_sale.invoice_type = '91'
# # for line in return_sale.lines:
# # if line.type == 'line':
# # line.quantity *= -1
# # line.save()
# # if return_sale.untaxed_amount_cache:
# # return_sale.untaxed_amount_cache *= -1
# # if return_sale.tax_amount_cache:
# # return_sale.tax_amount_cache *= -1
# # if return_sale.total_amount_cache:
# # return_sale.total_amount_cache *= -1
# # return_sale.save()
# # Sale.quote([return_sale])
# # Sale.write([return_sale], {'state': 'confirmed'})
# # dev_sales.append(return_sale)
# # return dev_sales
# def _create_sale(self, sale_):
# print('ingresa a crear venta')
# _pool = Pool()
# Sale = _pool.get('sale.sale')
# sales = Sale.search([
# ('reference', '=', str(sale_['id']))
# ])
# if sales:
# return False
# SaleLine = _pool.get('sale.line')
# Party = _pool.get('party.party')
# User = _pool.get('res.user')
# if sale_.get('customer'):
# customer = sale_['customer']
# dom_party = [('id_number', '=', str(customer['default_address']['company']))]
# parties = Party.search(dom_party)
# if parties:
# party = parties[0]
# else:
# party = self._create_party(customer)
# create_lines = []
# ctx = self._get_context()
# user_ = User(ctx['user'])
# date_created = sale_['created_at'].split('T')
# year, month, day = date_created[0].split('-')
# sale_date = date(int(year), int(month), int(day))
# with Transaction().set_user(ctx['user']):
# comment = ''
# try:
# comment = 'GUIA DE ENVIO NO. ' + sale_['fulfillments'][0].get('tracking_number', '')\
# if sale_['fulfillment_status'] == 'fulfilled' and sale_['fulfillments'] else ''
# except:
# pass
# sale, = Sale.create([{
# 'payment_term': 1,
# 'party': party.id,
# 'sale_date': sale_date,
# 'comment': comment,
# 'state': 'draft',
# 'company': ctx['company'],
# 'currency': user_.company.currency.id,
# 'shop': user_.shop.id,
# 'reference': str(sale_['id']),
# 'invoice_address': Party.address_get(party, type='invoice'),
# 'shipment_address': Party.address_get(party, type='delivery'),
# 'description': 'VENTA SHOPIFY ' + sale_['name'],
# 'channel': self.id,
# 'invoice_type': self.invoice_type,
# 'pack_id': ''
# }])
# with Transaction().set_context(ctx):
# create_lines = self.get_sale_lines(sale_, sale)
# SaleLine.create(create_lines)
# sale.untaxed_amount_cache = sale.untaxed_amount
# if sale_['cancel_reason'] is not None:
# sale.state = 'cancelled'
# else:
# Sale.quote([sale])
# sale.save()
# if sale_['fulfillment_status'] == 'fulfilled':
# self.order_fulfilled(sale_, [sale])
# print('*******************', sale.state)
# return sale
# def get_sale_lines(self, sale_, sale, products_refund=[]):
# pool = Pool()
# Product = pool.get('product.product')
# sale_items = sale_['line_items']
# products_refund = {}
# if sale_.get('refunds'):
# for refunds in sale_['refunds']:
# for line_items in refunds['refund_line_items']:
# sku_code = line_items['line_item']['sku']
# quantity = line_items['quantity']
# if sku_code.count('+') > 0:
# codes = sku_code.split('+')
# for code in codes:
# if code not in products_refund.keys():
# products_refund[code] = quantity
# else:
# products_refund[code] += quantity
# else:
# if sku_code not in products_refund.keys():
# products_refund[sku_code] = quantity
# else:
# products_refund[sku_code] += quantity
# create_lines = []
# for line in sale_items:
# if line['title'] == 'Tip':
# if self.tip_product:
# product = self.tip_product
# total_tip = line['price']
# create_lines.append({
# 'sale': sale.id,
# 'type': 'line',
# 'unit': product.default_uom.id,
# 'quantity': 1,
# 'unit_price': Decimal(total_tip),
# 'base_price': Decimal(total_tip),
# 'unit_price_full': Decimal(total_tip),
# 'product': product.id,
# 'description': 'BONIFICACION',
# })
# else:
# sku_code = line['sku']
# if sku_code.count('+') > 0:
# codes = sku_code.split('+')
# line['price'] = round(Decimal(line['price']) / 2, 2)
# discount = round(sum([Decimal(n['amount']) for n in line['discount_allocations']]) / 2, 2)
# else:
# codes = [sku_code]
# discount = round(sum([Decimal(n['amount']) for n in line['discount_allocations']]))
# products = Product.search([
# ('code', 'in', codes),
# ('active', '=', True)
# ])
# description = ''
# if not products:
# products = self._create_product(codes, line)
# for product in products:
# quantity = line['quantity']
# if products_refund and product.code in products_refund.keys():
# if products_refund[product.code] < quantity:
# quantity -= products_refund[product.code]
# else:
# continue
# Tax = pool.get('account.tax')
# un_price = Tax.reverse_compute((Decimal(line['price']) - Decimal(discount)),
# product.customer_taxes_used)
# create_lines.append({
# 'sale': sale.id,
# 'type': 'line',
# 'unit': product.default_uom.id,
# 'quantity': quantity,
# 'base_price': round(Decimal(un_price), 3),
# 'unit_price': round(Decimal(un_price), 3),
# 'unit_price_full': round(Decimal(line['price']),2),
# 'product': product.id,
# 'taxes': [('add', product.customer_taxes_used)],
# 'description': description,
# })
# if self.freight_product and len(sale_['shipping_lines']) > 0:
# product = self.freight_product
# shipping_amount = sale_['shipping_lines'][0]['price']
# create_lines.append({
# 'sale': sale.id,
# 'type': 'line',
# 'unit': product.default_uom.id,
# 'quantity': 1,
# 'base_price': Decimal(shipping_amount),
# 'unit_price': Decimal(shipping_amount),
# 'unit_price_full': Decimal(shipping_amount),
# 'product': product.id,
# 'description': 'FLETE',
# })
# return create_lines
# def order_updated(self, data, sales):
# sale = sales[0]
# pool = Pool()
# SaleLine = pool.get('sale.line')
# Sale = pool.get('sale.sale')
# lines_to_remove = [line for line in sale.lines]
# Sale.draft([sale])
# SaleLine.delete(lines_to_remove)
# create_lines = self.get_sale_lines(data, sale)
# SaleLine.create(create_lines)
# Sale.quote([sale])
# if data['fulfillment_status'] == 'fulfilled':
# self.order_fulfilled(data, [sale])
# return sale
# def order_cancelled(self, data, sales):
# pool = Pool()
# Sale = pool.get('sale.sale')
# sale = sales[0]
# Sale.draft([sale])
# Sale.cancel([sale])
# return sale
# def order_fulfilled(self, data, sales):
# pool = Pool()
# Sale = pool.get('sale.sale')
# sale = sales[0]
# response = False
# if sale.invoices:
# return response
# elif sale.state == 'quotation':
# Sale.confirm([sale])
# if len(sales) > 1:
# # channel.upload_note(sale, 'Error, al generar factura orden duplicada')
# return response
# if data.get('fulfillment_status') == 'fulfilled':
# track_number = ''
# try:
# track_number = str(data['fulfillments'][0]['tracking_number'])
# except:
# pass
# Sale.write([sale], {
# 'description': sale.description + ' - ' + data['fulfillment_status'],
# 'comment': 'GUIA DE ENVIO NO. ' + track_number,
# 'tracking_number': track_number
# })
# return sale
# @classmethod
# def get_response(cls, URI, params={}):
# response = requests.get(URI, headers=HEADERS, params=urlencode(params))
# return response
# @classmethod
# def verify_webhook_shopify(cls, data, hmac_header, secret):
# digest = hmac.new(secret.encode('utf-8'), data, hashlib.sha256).digest()
# genHmac = base64.b64encode(digest)
# return hmac.compare_digest(genHmac, hmac_header.encode('utf-8'))
# @classmethod
# def request_api(cls, request):
# response = {'status': 'error', 'msg': 'Fail in process !!!'}
# data = request.get_data()
# action_topic = request.headers.get('X-Shopify-Topic')
# domain_name = request.headers.get('X-Shopify-Shop-Domain')
# channels = cls.search([('host_name', '=', domain_name), ])
# channel = channels[0]
# hmac_header = request.headers.get('X-Shopify-Hmac-SHA256')
# verified = cls.verify_webhook_shopify(data, hmac_header, channel.secret_key)
# if verified:
# req = data.decode("utf-8")
# data = json.loads(req)
# if action_topic.count('create'):
# res = channel._create_sale(data)
# if res:
# response = {'status': 'ok', 'msg': 'Successfull create sale !!!'}
# else:
# order_id = data.get('id')
# Sale = Pool().get('sale.sale')
# sales = Sale.search([
# ('reference', '=', str(order_id))
# ])
# if not sales:
# return response
# if action_topic.count('fulfilled'):
# res = channel.order_fulfilled(data, sales)
# if res:
# response = {'status': 'ok', 'msg': 'Successfull process sale' + res.number+' in state' + res.state + '!!!' }
# elif action_topic.count('paid'):
# pass
# elif action_topic.count('updated'):
# res = channel.order_updated(data, sales)
# if res:
# response = {'status': 'ok', 'msg': 'Successfull update sale ' + res.number+' in state' + res.state + '!!!' }
# elif action_topic.count('cancelled'):
# res = channel.order_cancelled(data, sales)
# if res:
# response = {'status': 'ok', 'msg': 'Successfull cancel sale ' + res.number+' in state' + res.state + '!!!' }
# return response
class Shopify:
'Shopify'
def __init__(self, web_shop):
self.access_token = web_shop.access_token if web_shop.access_token else ''
self.generic_product = web_shop.generic_product
self.refresh_token = web_shop.refresh_token
self.id = web_shop.id
self.app_id = web_shop.app_id
self.secret_key = web_shop.secret_key
self.freight_product = web_shop.freight_product
self.invoice_type = web_shop.invoice_type
def _get_context(self):
user_ = self._get_user()
return {
'company': user_.company.id,
'user': user_.id,
'shops': [user_.shop.id],
'shop': user_.shop.id,
'language': 'es'
}
def _get_user(self):
User = Pool().get('res.user')
user, = User.search(
('login', '=', 'shopify')
)
return user
def _create_product(self, codes, line={}):
pool = Pool()
Product = pool.get('product.product')
Template = pool.get('product.template')
description = line['variant_title']
list_price = Decimal(str(round(float(line['price'])/1.19, 2)))
sale_price_w_tax = Decimal(str(round(Decimal(line['price']), 2)))
create_template = {
'name': line['title'],
'list_price': list_price,
'sale_price_w_tax': sale_price_w_tax,
'type': 'goods',
'salable': True,
'purchasable': True,
'purchase_uom': 1,
'sale_uom': 1,
'default_uom': 1,
'account_category': 7,
'code': codes[0],
}
template, = Template.create([create_template])
create_product = []
for code in codes:
create_product.append({
'code': code,
'description': description,
'template': template.id
})
return Product.create(create_product)
def _create_party(self, customer):
_pool = Pool()
City = _pool.get('party.city_code')
Party = _pool.get('party.party')
PartyObligationTax = _pool.get('party.obligation_tax')
email = customer['email']
shipment_address, phone, city = '', '', None
if customer.get('default_address'):
default_address = customer['default_address']
shipment_address = default_address['address1']
phone = default_address['phone'] or '000'
city_name = default_address['city']
if city_name:
cities = City.search([
('name', '=', city_name)
])
if cities:
city = cities[0]
customer_name = customer['first_name'] + ' ' + customer['last_name']
create_customer = {
'id_reference': str(customer['id']),
'name': customer_name.upper(),
'type_document': '13',
'id_number': default_address['company'],
'addresses': [('create', [{
'street': shipment_address,
}])],
'contact_mechanisms': [
('create', [
{'type': 'phone', 'value': phone.replace(" ","")},
{'type': 'email', 'value': email},
])
]
}
if not city:
city = City(149)
create_customer['addresses'][0][1][0]['department_code'] = city.department.id
create_customer['addresses'][0][1][0]['city_code'] = city.id
party, = Party.create([create_customer])
PartyObligationTax.create([{
'party': party.id,
'obligation_fiscal': 6,
}])
return party
def _create_sale(self, sale_):
_pool = Pool()
Sale = _pool.get('sale.sale')
sales = Sale.search([
('reference', '=', str(sale_['id']))
])
if sales:
return False
SaleLine = _pool.get('sale.line')
Party = _pool.get('party.party')
User = _pool.get('res.user')
if sale_.get('customer'):
customer = sale_['customer']
dom_party = [('id_number', '=', str(customer['default_address']['company']))]
parties = Party.search(dom_party)
if parties:
party = parties[0]
else:
party = self._create_party(customer)
create_lines = []
ctx = self._get_context()
user_ = User(ctx['user'])
date_created = sale_['created_at'].split('T')
year, month, day = date_created[0].split('-')
sale_date = date(int(year), int(month), int(day))
with Transaction().set_user(ctx['user']):
comment = ''
try:
comment = 'GUIA DE ENVIO NO. ' + sale_['fulfillments'][0].get('tracking_number', '')\
if sale_['fulfillment_status'] == 'fulfilled' and sale_['fulfillments'] else ''
except:
pass
sale, = Sale.create([{
'payment_term': 1,
'party': party.id,
'sale_date': sale_date,
'comment': comment,
'state': 'draft',
'company': ctx['company'],
'currency': user_.company.currency.id,
'shop': user_.shop.id,
'reference': str(sale_['id']),
'invoice_address': Party.address_get(party, type='invoice'),
'shipment_address': Party.address_get(party, type='delivery'),
'description': 'VENTA SHOPIFY ' + sale_['name'],
'web_shop': self.id,
'web_id': str(sale_['id']),
'invoice_type': self.invoice_type,
'pack_id': ''
}])
with Transaction().set_context(ctx):
create_lines = self.get_sale_lines(sale_, sale)
SaleLine.create(create_lines)
sale.untaxed_amount_cache = sale.untaxed_amount
if sale_['cancel_reason'] is not None:
sale.state = 'cancelled'
else:
Sale.quote([sale])
sale.save()
if sale_['fulfillment_status'] == 'fulfilled':
self.order_fulfilled(sale_, [sale])
print('*******************', sale.state)
return sale
def get_sale_lines(self, sale_, sale, products_refund=[]):
pool = Pool()
Product = pool.get('product.product')
sale_items = sale_['line_items']
products_refund = {}
if sale_.get('refunds'):
for refunds in sale_['refunds']:
for line_items in refunds['refund_line_items']:
sku_code = line_items['line_item']['sku']
quantity = line_items['quantity']
if sku_code.count('+') > 0:
codes = sku_code.split('+')
for code in codes:
if code not in products_refund.keys():
products_refund[code] = quantity
else:
products_refund[code] += quantity
else:
if sku_code not in products_refund.keys():
products_refund[sku_code] = quantity
else:
products_refund[sku_code] += quantity
create_lines = []
for line in sale_items:
if line['title'] == 'Tip':
if self.tip_product:
product = self.tip_product
total_tip = line['price']
create_lines.append({
'sale': sale.id,
'type': 'line',
'unit': product.default_uom.id,
'quantity': 1,
'unit_price': Decimal(total_tip),
'base_price': Decimal(total_tip),
'unit_price_full': Decimal(total_tip),
'product': product.id,
'description': 'BONIFICACION',
})
else:
sku_code = line['sku']
if sku_code.count('+') > 0:
codes = sku_code.split('+')
line['price'] = round(Decimal(line['price']) / 2, 2)
discount = round(sum([Decimal(n['amount']) for n in line['discount_allocations']]) / 2, 2)
else:
codes = [sku_code]
discount = round(sum([Decimal(n['amount']) for n in line['discount_allocations']]))
products = Product.search([
('code', 'in', codes),
('active', '=', True)
])
description = ''
if not products:
products = self._create_product(codes, line)
for product in products:
quantity = line['quantity']
if products_refund and product.code in products_refund.keys():
if products_refund[product.code] < quantity:
quantity -= products_refund[product.code]
else:
continue
Tax = pool.get('account.tax')
un_price = Tax.reverse_compute((Decimal(line['price']) - Decimal(discount)),
product.customer_taxes_used)
create_lines.append({
'sale': sale.id,
'type': 'line',
'unit': product.default_uom.id,
'quantity': quantity,
'base_price': round(Decimal(un_price), 3),
'unit_price': round(Decimal(un_price), 3),
'unit_price_full': round(Decimal(line['price']),2),
'product': product.id,
'taxes': [('add', product.customer_taxes_used)],
'description': description,
})
if self.freight_product and len(sale_['shipping_lines']) > 0:
product = self.freight_product
shipping_amount = sale_['shipping_lines'][0]['price']
create_lines.append({
'sale': sale.id,
'type': 'line',
'unit': product.default_uom.id,
'quantity': 1,
'base_price': Decimal(shipping_amount),
'unit_price': Decimal(shipping_amount),
'unit_price_full': Decimal(shipping_amount),
'product': product.id,
'description': 'FLETE',
})
return create_lines
def order_updated(self, data, sales):
sale = sales[0]
pool = Pool()
SaleLine = pool.get('sale.line')
Sale = pool.get('sale.sale')
lines_to_remove = [line for line in sale.lines]
Sale.draft([sale])
SaleLine.delete(lines_to_remove)
create_lines = self.get_sale_lines(data, sale)
SaleLine.create(create_lines)
Sale.quote([sale])
if data['fulfillment_status'] == 'fulfilled':
self.order_fulfilled(data, [sale])
return sale
def order_cancelled(self, data, sales):
pool = Pool()
Sale = pool.get('sale.sale')
sale = sales[0]
Sale.draft([sale])
Sale.cancel([sale])
return sale
def order_fulfilled(self, data, sales):
pool = Pool()
Sale = pool.get('sale.sale')
sale = sales[0]
response = False
if sale.invoices:
return response
elif sale.state == 'quotation':
Sale.confirm([sale])
if len(sales) > 1:
# channel.upload_note(sale, 'Error, al generar factura orden duplicada')
return response
if data.get('fulfillment_status') == 'fulfilled':
track_number = ''
try:
track_number = str(data['fulfillments'][0]['tracking_number'])
except:
pass
Sale.write([sale], {
'description': sale.description + ' - ' + data['fulfillment_status'],
'comment': 'GUIA DE ENVIO NO. ' + track_number,
'tracking_number': track_number
})
return sale
@classmethod
def get_response(cls, URI, params={}):
response = requests.get(URI, headers=HEADERS, params=urlencode(params))
return response
@classmethod
def verify_webhook_shopify(cls, data, hmac_header, secret):
digest = hmac.new(secret.encode('utf-8'), data, hashlib.sha256).digest()
genHmac = base64.b64encode(digest)
return hmac.compare_digest(genHmac, hmac_header.encode('utf-8'))
@classmethod
def request_api(cls, request):
response = {'status': 'error', 'msg': 'Fail in process !!!'}
data = request.get_data()
action_topic = request.headers.get('X-Shopify-Topic')
domain_name = request.headers.get('X-Shopify-Shop-Domain')
channels = cls.search([('host_name', '=', domain_name), ])
channel = channels[0]
hmac_header = request.headers.get('X-Shopify-Hmac-SHA256')
verified = cls.verify_webhook_shopify(data, hmac_header, channel.secret_key)
if verified:
req = data.decode("utf-8")
data = json.loads(req)
if action_topic.count('create'):
res = channel._create_sale(data)
if res:
response = {'status': 'ok', 'msg': 'Successfull create sale !!!'}
else:
order_id = data.get('id')
Sale = Pool().get('sale.sale')
sales = Sale.search([
('reference', '=', str(order_id))
])
if not sales:
return response
if action_topic.count('fulfilled'):
res = channel.order_fulfilled(data, sales)
if res:
response = {'status': 'ok', 'msg': 'Successfull process sale' + res.number+' in state' + res.state + '!!!' }
elif action_topic.count('paid'):
pass
elif action_topic.count('updated'):
res = channel.order_updated(data, sales)
if res:
response = {'status': 'ok', 'msg': 'Successfull update sale ' + res.number+' in state' + res.state + '!!!' }
elif action_topic.count('cancelled'):
res = channel.order_cancelled(data, sales)
if res:
response = {'status': 'ok', 'msg': 'Successfull cancel sale ' + res.number+' in state' + res.state + '!!!' }
return response