# -*- 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