577 lines
23 KiB
Python
577 lines
23 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 trytond.transaction import Transaction
|
|
import requests
|
|
from urllib.parse import urlencode
|
|
from datetime import datetime, date
|
|
from .web_channel import SaleWebChannel
|
|
import json
|
|
from .exceptions import NotProductFoundError, NotProductFound
|
|
from trytond.i18n import gettext
|
|
from trytond.exceptions import UserError
|
|
|
|
HEADERS = {
|
|
'Accept': 'application/json',
|
|
'Content-type': 'application/json'
|
|
}
|
|
|
|
|
|
class MercadoLibre(SaleWebChannel):
|
|
'MercadoLibre'
|
|
__name__ = 'sale.web_channel.mercado_libre'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(MercadoLibre, cls).__setup__()
|
|
|
|
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', '=', 'mercado.libre')
|
|
])
|
|
return user
|
|
|
|
def _create_product(self, codes, line={}):
|
|
item = line['item']
|
|
pool = Pool()
|
|
Product = pool.get('product.product')
|
|
Template = pool.get('product.template')
|
|
description = ''
|
|
if item.get('variation_attributes'):
|
|
var = item['variation_attributes'][0]
|
|
if var['id'] and var['name']:
|
|
description = var['id'] + ' ' + var['name']
|
|
create_template = {
|
|
'name': item['title'],
|
|
'list_price': line['unit_price'],
|
|
'sale_price_w_tax': line['full_unit_price'],
|
|
'type': 'goods',
|
|
'salable': True,
|
|
'purchasable': True,
|
|
'purchase_uom': 1,
|
|
'sale_uom': 1,
|
|
'default_uom': 1,
|
|
'account_category': 7,
|
|
}
|
|
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.get('email', '@')
|
|
shipment_address, phone, city = '', '', None
|
|
if customer.get('receiver_address'):
|
|
receiver_address = customer['receiver_address']
|
|
shipment_address = receiver_address['address_line']
|
|
phone = receiver_address['receiver_phone'] or '000'
|
|
city_name = receiver_address['city']['name']
|
|
if city_name:
|
|
cities = City.search([
|
|
('name', '=', city_name)
|
|
])
|
|
if cities:
|
|
city = cities[0]
|
|
if customer['billing_info']['doc_type'] == 'NIT':
|
|
type_document = '31'
|
|
customer_name = customer['billing_info']['business_name']
|
|
type_person = 'persona_juridica'
|
|
else:
|
|
customer_name = customer['billing_info']['first_name'] + ' ' + customer['billing_info']['last_name']
|
|
type_document = '13'
|
|
type_person = 'persona_natural'
|
|
|
|
create_customer = {
|
|
'id_reference': str(customer['id']),
|
|
'name': customer_name.upper(),
|
|
'type_document': type_document,
|
|
'type_person': type_person,
|
|
'id_number': customer['billing_info']['doc_number'],
|
|
'addresses': [('create', [{
|
|
'street': shipment_address,
|
|
}])],
|
|
'contact_mechanisms': [
|
|
('create', [
|
|
{'type': 'phone', 'value': phone},
|
|
{'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 get_shipment_api(self, shipment_id):
|
|
if not shipment_id:
|
|
return None
|
|
URI = 'https://api.mercadolibre.com/shipments/%s?access_token=%s' % (
|
|
shipment_id, self.access_token)
|
|
res = self.get_response(URI)
|
|
return res.json()
|
|
|
|
def get_billing_info_api(self, order_id):
|
|
if not order_id:
|
|
return None
|
|
URI = 'https://api.mercadolibre.com/orders/%s/billing_info?access_token=%s' % (
|
|
order_id, self.access_token)
|
|
res = self.get_response(URI)
|
|
return res.json()
|
|
|
|
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_lines_sale(self, sale, sale_, freight=False):
|
|
_pool = Pool()
|
|
SaleLine = _pool.get('sale.line')
|
|
Product = _pool.get('product.product')
|
|
create_lines = []
|
|
sale_items = sale_['order_items']
|
|
ctx = self._get_context()
|
|
sfm_id = sale_['shipping']['id']
|
|
# reference = sale.reference.split(',')
|
|
# reference_order = str(sale_['id'])
|
|
# if reference_order not in reference:
|
|
# sale.referece = sale.reference + ',' + reference_order
|
|
# sale.save()
|
|
with Transaction().set_context(ctx):
|
|
for line in sale_items:
|
|
item = line['item']
|
|
sku_code = item['seller_sku']
|
|
|
|
if sku_code:
|
|
generic = False
|
|
if sku_code.count('+') > 0:
|
|
codes = sku_code.split('+')
|
|
line['unit_price'] = Decimal(
|
|
round((line['unit_price'] / 2), 2))
|
|
else:
|
|
codes = [sku_code]
|
|
|
|
products = Product.search([
|
|
('code', 'in', codes),
|
|
('active', '=', True)
|
|
])
|
|
description = ''
|
|
if not products:
|
|
products = self._create_product(codes, line)
|
|
else:
|
|
if not self.generic_product:
|
|
raise NotProductFoundError(
|
|
gettext('sale_web_channel.msg_product_generic_not_found'))
|
|
products = [self.generic_product]
|
|
generic = True
|
|
description = ''
|
|
raise NotProductFound(
|
|
gettext('sale_web_channel.msg_product_not_found', s=self.generic_product.rec_name))
|
|
for product in products:
|
|
Tax = _pool.get('account.tax')
|
|
un_price = Tax.reverse_compute(line['unit_price'],
|
|
product.customer_taxes_used)
|
|
create_lines.append({
|
|
'sale': sale.id,
|
|
'type': 'line',
|
|
'unit': product.default_uom.id,
|
|
'quantity': line['quantity'],
|
|
'unit_price': round(Decimal(un_price), 3),
|
|
'unit_price_full': Decimal(line['unit_price']),
|
|
'product': product.id,
|
|
'taxes': [('add', product.customer_taxes_used)],
|
|
'description': description,
|
|
})
|
|
if freight and self.freight_product:
|
|
product = self.freight_product
|
|
URI = 'https://api.mercadolibre.com/shipments/%s/costs?access_token=%s' % (
|
|
sfm_id, self.access_token)
|
|
result = self.get_response(URI).json()
|
|
shipping_amount = result['receiver']['cost']
|
|
create_lines.append({
|
|
'sale': sale.id,
|
|
'type': 'line',
|
|
'unit': product.default_uom.id,
|
|
'quantity': 1,
|
|
'unit_price': Decimal(shipping_amount),
|
|
'unit_price_full': Decimal(shipping_amount),
|
|
'product': product.id,
|
|
'description': 'FLETE',
|
|
})
|
|
SaleLine.create(create_lines)
|
|
return generic
|
|
|
|
def validate_sale(self, sale_):
|
|
if sale_.get('pack_id'):
|
|
# if not sale_.get('shipping'):
|
|
# return
|
|
# shipment_id = sale_['shipping']['id']
|
|
# URI = 'https://api.mercadolibre.com/shipments/%s/items?access_token=%s' % (
|
|
# shipment_id, self.access_token)
|
|
# res = self.get_response(URI)
|
|
# shipment_items = res.json()
|
|
# if len(shipment_items) > 1:
|
|
URI = 'https://api.mercadolibre.com/packs/%s?access_token=%s' % (
|
|
sale_.get('pack_id'), self.access_token)
|
|
response = self.get_response(URI)
|
|
response = response.json()
|
|
if response.get('orders'):
|
|
ids = ''
|
|
for item in response['orders']:
|
|
order_id = str(item['id'])
|
|
if order_id != str(sale_['id']):
|
|
URI2 = 'https://api.mercadolibre.com/orders/%s?access_token=%s' % (
|
|
order_id, self.access_token)
|
|
order = self.get_response(URI2)
|
|
sale_order = order.json()
|
|
ids += ',' + order_id
|
|
sale_['order_items'].extend(sale_order['order_items'])
|
|
sale_['id'] = str(sale_['id']) + ids
|
|
return self._create_sale(sale_)
|
|
else:
|
|
return self._create_sale(sale_)
|
|
else:
|
|
return self._create_sale(sale_)
|
|
|
|
def _create_sale(self, sale_):
|
|
_pool = Pool()
|
|
Sale = _pool.get('sale.sale')
|
|
reference = '%' + str(sale_['id']).split(',')[0] + '%'
|
|
dom = [
|
|
('reference', 'like', reference)
|
|
]
|
|
sales = Sale.search(dom)
|
|
if sales:
|
|
if len(sales) == 1 and sale_['status'] == 'cancelled':
|
|
sale = sales[0]
|
|
if not sale.invoices:
|
|
self.cancel_sales([sale], sale.pack_id)
|
|
else:
|
|
dev_sales = self._return_sale(sale)
|
|
self._finish_sale(dev_sales[0], type='return')
|
|
return True
|
|
return False
|
|
Party = _pool.get('party.party')
|
|
User = _pool.get('res.user')
|
|
sfm_id = sale_['shipping']['id']
|
|
shipment_ = self.get_shipment_api(sfm_id)
|
|
if sale_.get('buyer'):
|
|
sale_id = str(sale_['id']).split(',')[0]
|
|
billing_info = self.get_billing_info_api(sale_id)
|
|
customer = sale_['buyer']
|
|
dom_party = [('id_reference', '=', str(customer['id']))]
|
|
if billing_info and billing_info['billing_info'].get('doc_number'):
|
|
dom_party = [('id_number', '=', str(
|
|
billing_info['billing_info']['doc_number']))]
|
|
parties = Party.search(dom_party)
|
|
if parties:
|
|
party = parties[0]
|
|
else:
|
|
customer['receiver_address'] = shipment_['receiver_address']
|
|
customer['billing_info'] = billing_info['billing_info']
|
|
for dic in customer['billing_info']['additional_info']:
|
|
customer['billing_info'][list(dic.values())[0].lower()
|
|
] = list(dic.values())[1]
|
|
try:
|
|
customer['billing_info']['additional_info'].pop()
|
|
except Exception as e:
|
|
msg = 'sin informacion de tercero' + e
|
|
raise UserError(
|
|
gettext('sale_web_channel.msg_billing_info', msg=msg))
|
|
party = self._create_party(customer)
|
|
ctx = self._get_context()
|
|
user_ = User(ctx['user'])
|
|
date_created = sale_['date_created'].split('T')
|
|
year, month, day = date_created[0].split('-')
|
|
sale_date = date(int(year), int(month), int(day))
|
|
with Transaction().set_user(ctx['user']):
|
|
sale, = Sale.create([{
|
|
'payment_term': 1,
|
|
'party': party.id,
|
|
'sale_date': sale_date,
|
|
'comment': shipment_['status'],
|
|
'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 WEB ',
|
|
'channel': self.id,
|
|
'invoice_type': self.invoice_type,
|
|
'pack_id': str(sale_['pack_id']) if sale_['pack_id'] else '',
|
|
}])
|
|
generic = self.create_lines_sale(
|
|
sale, sale_, freight=True)
|
|
sale.untaxed_amount_cache = sale.untaxed_amount
|
|
if sale_['status'] == 'cancelled':
|
|
sale.state = 'cancelled'
|
|
else:
|
|
Sale.quote([sale])
|
|
if not generic:
|
|
sale.state = 'confirmed'
|
|
sale.save()
|
|
# if shipment_['status'] in ['shipped', 'delivered']:
|
|
# self._finish_sale(sale)
|
|
return sale
|
|
|
|
def cancel_sales(self, sales, pack_id=None):
|
|
Sale = Pool().get('sale.sale')
|
|
if pack_id:
|
|
sales = Sale.search([('pack_id', '=', pack_id)])
|
|
for sale in sales:
|
|
sale.state = 'cancelled'
|
|
sale.save()
|
|
|
|
def _get_shipment_amount(self, order_id, shipment={}):
|
|
URI = 'https://api.mercadolibre.com/orders/%s?access_token=%s' % (
|
|
order_id, self.access_token)
|
|
res = self.get_response(URI)
|
|
sale_ = res.json()
|
|
shipping_amount = 0
|
|
if sale_.get('payments'):
|
|
shipping_amount = sale_['payments'][0]['shipping_cost']
|
|
return shipping_amount
|
|
|
|
def get_access_token(self, refresh_token, client_id, client_secret):
|
|
params = {'grant_type': 'refresh_token',
|
|
'client_id': client_id,
|
|
'client_secret': client_secret,
|
|
'refresh_token': refresh_token
|
|
}
|
|
|
|
uri = 'https://api.mercadolibre.com/oauth/token'
|
|
|
|
response = requests.post(uri, params=urlencode(params),
|
|
headers=HEADERS, data=params)
|
|
res = response.json()
|
|
return res['access_token'], res['refresh_token']
|
|
|
|
@classmethod
|
|
def get_response(cls, URI, params={}):
|
|
response = requests.get(URI, headers=HEADERS, params=urlencode(params))
|
|
return response
|
|
|
|
def _validate_token(self):
|
|
access_token = self.access_token
|
|
refresh_token = self.refresh_token
|
|
client_id = self.app_id
|
|
client_secret = self.secret_key
|
|
if self.status_token != 'active':
|
|
access_token, refresh_token = self.get_access_token(
|
|
refresh_token, client_id, client_secret
|
|
)
|
|
self.write([self], {'access_token': access_token,
|
|
'refresh_token': refresh_token,
|
|
'creation_time': datetime.now()
|
|
})
|
|
return access_token, refresh_token, client_id, client_secret
|
|
|
|
@classmethod
|
|
def _get_channel(cls):
|
|
channels = cls.search([
|
|
('state', '=', 'active'),
|
|
('channel_name', '=', 'mercadolibre'),
|
|
])
|
|
if channels:
|
|
return channels[0]
|
|
else:
|
|
return None
|
|
|
|
@classmethod
|
|
def request_api(cls, data):
|
|
channel = cls._get_channel()
|
|
response = {'status': 'error', 'msg_response': 'Fail in process !!!'}
|
|
if not channel:
|
|
return response
|
|
access_token, refresh_token, client_id, client_secret = channel._validate_token()
|
|
if data.get('resource') and data['resource'].count('orders'):
|
|
order_id = str(data['resource'].replace('/orders/', ''))
|
|
URI = 'https://api.mercadolibre.com/orders/%s?access_token=%s' % (
|
|
order_id, access_token)
|
|
result = cls.get_response(URI).json()
|
|
res = channel.validate_sale(result)
|
|
if res:
|
|
response = {
|
|
'status': 'ok',
|
|
'msg_response': 'Successfull process !!!',
|
|
'order': order_id
|
|
}
|
|
else:
|
|
response = {
|
|
'status': 'ok',
|
|
'msg_response': 'Sale processed before !!!',
|
|
'order': order_id
|
|
}
|
|
elif data.get('resource') and data['resource'].count('shipments'):
|
|
shipment_id = str(data['resource'].replace('/shipments/', ''))
|
|
shipment_ = channel.get_shipment_api(shipment_id)
|
|
order_id = shipment_['order_id']
|
|
|
|
Sale = Pool().get('sale.sale')
|
|
sales = Sale.search([
|
|
('reference', 'ilike', '%' + str(order_id) + '%')
|
|
])
|
|
response.update({'order': order_id})
|
|
if not sales:
|
|
return response
|
|
|
|
sale = sales[0]
|
|
if sale.invoices:
|
|
return response
|
|
|
|
if len(sales) > 1:
|
|
channel.upload_note(
|
|
sale, 'Error, al generar factura orden duplicada')
|
|
return response
|
|
|
|
if shipment_.get('tracking_number'):
|
|
Sale.write([sale], {
|
|
'description': 'GUIA DE ENVIO NO. ' + shipment_['tracking_number'],
|
|
'tracking_number': shipment_['tracking_number']
|
|
})
|
|
if shipment_['status'] in ['shipped', 'delivered']:
|
|
try:
|
|
channel._finish_sale(sale)
|
|
except:
|
|
channel.upload_note(sale, 'Error al finalizar factura')
|
|
response = {'status': 'ok',
|
|
'msg_response': 'Successfull process !!!',
|
|
'order': order_id}
|
|
else:
|
|
return {'status': 'none', 'msg_response': 'Don`t process this topic'}
|
|
return response
|
|
|
|
def _finish_sale(self, sale, type='invoice'):
|
|
ctx = self._get_context()
|
|
pool = Pool()
|
|
Sale = pool.get('sale.sale')
|
|
Invoice = pool.get('account.invoice')
|
|
Date = pool.get('ir.date')
|
|
with Transaction().set_context(ctx):
|
|
Sale.process([sale])
|
|
if not sale.invoices:
|
|
return
|
|
invoice = sale.invoices[0]
|
|
invoice.invoice_date = Date.today()
|
|
if type == 'return':
|
|
sale_origin = sale.origin
|
|
if sale_origin.invoices:
|
|
inv_origin = sale_origin.invoices[0]
|
|
invoice.credit_note_concept = '2'
|
|
invoice.original_invoice = inv_origin.id
|
|
invoice.save()
|
|
Invoice.validate_invoice([invoice])
|
|
if invoice.invoice_type not in ('C', 'P', 'M'):
|
|
try:
|
|
invoice.submit([invoice])
|
|
if not invoice.cufe:
|
|
return
|
|
except:
|
|
self.upload_note(sale, 'Error de envio DIAN')
|
|
try:
|
|
Invoice.post([invoice])
|
|
self.upload_note(sale, 'Factura generada')
|
|
self.upload_invoice(sale)
|
|
except:
|
|
pass
|
|
|
|
def upload_invoice(self, sale):
|
|
if not sale.reference or not sale.invoices:
|
|
return
|
|
|
|
invoice = sale.invoices[0]
|
|
pack_id = sale.reference
|
|
if sale.pack_id:
|
|
pack_id = sale.pack_id
|
|
URI = 'https://api.mercadolibre.com/packs/%s/fiscal_documents?access_token=%s' % (
|
|
pack_id, self.access_token)
|
|
if sale.uploaded_invoice:
|
|
response = requests.delete(URI)
|
|
|
|
report = self.render_report(invoice)
|
|
file = {"fiscal_document": report}
|
|
response = requests.post(URI, files=file)
|
|
print(response.status_code)
|
|
message = 'Error al subir factura'
|
|
if response.status_code in [200, 201, 202]:
|
|
res = response.json()
|
|
upload_ids = 'Upload ids: ' + ', '.join(list(res['ids']))
|
|
sale.write([sale], {
|
|
'uploaded_invoice': True,
|
|
'document_invoice': upload_ids,
|
|
})
|
|
message = 'Factura Cargada exitosamente'
|
|
self.upload_note(sale, message)
|
|
|
|
def upload_note(self, sale, message):
|
|
URI = 'https://api.mercadolibre.com/orders/%s/notes?access_token=%s' % (
|
|
sale.reference, self.access_token)
|
|
|
|
params = {"note": message}
|
|
request = json.dumps(params)
|
|
response = requests.post(URI, headers=HEADERS, data=request)
|
|
if not response.status_code in [200, 201, 202]:
|
|
self.send_mail_notification(
|
|
'error al crear nota en orden ' + sale.reference)
|