Files
enterprise-addons/sale_shopee/models/stock_picking.py
T

299 lines
13 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from datetime import timedelta
from odoo import _, fields, models
from odoo.addons.sale_shopee import const, utils
_logger = logging.getLogger(__name__)
class StockPicking(models.Model):
_inherit = 'stock.picking'
shopee_order_ref = fields.Char(related='sale_id.shopee_order_ref')
shopee_delivery_status = fields.Selection(related='sale_id.shopee_delivery_status')
shopee_label_status = fields.Selection(
string="Label Status",
help="The status of the shipping label:\n"
"- Not Available: The shipping label is not available yet.\n"
"- Processing: The shipping label is being processed.\n"
"- Ready: The shipping label is ready to be downloaded.\n"
"- Stored: The shipping label has been downloaded and stored.\n"
"- Failed: The shipping label creation failed.",
selection=[
('not available', "Not Available"),
('processing', "Processing"),
('ready', "Ready"),
('stored', "Stored"),
('failed', "Failed"),
],
readonly=True,
default='not available',
)
last_picking_sync_date = fields.Datetime(
help="The last time the picking was synchronized with Shopee.",
readonly=True,
default=fields.Datetime.now,
)
# === ACTION METHODS === #
def action_shopee_sync_pickings(self):
return self._sync_shopee_pickings()
# === BUSINESS METHODS === #
def _sync_shopee_pickings(self, shop_ids=(), auto_commit=True):
""" Fetch tracking number and shipping label from Shopee.
Delivery information needs to be filled directly on Shopee, once done, Shopee will assign a
tracking number to the pickings. _sync_picking will fetch this number, and create the
associated shipping label.
:param tuple shop_ids: The shop whose deliveries should be synchronized, as a tuple of
`shopee.shop` record ids.
:param auto_commit: If True, the transaction will be committed after each successful sync
:return: None
"""
pickings_by_shop = self._get_pickings_by_shop(shop_ids)
for shop, shop_pickings in pickings_by_shop.items():
try:
shop._update_shop_information()
if shop.status != 'active':
continue
shop_pickings._fetch_shipment_label(auto_commit)
except utils.ShopeeRateLimitError as error:
_logger.info(
"Rate limit reached while synchronizing pickings for Shopee account with"
" id %(account)d. Operation: %(error_operation)s",
{'account': shop.account_id.id, 'error_operation': error.operation},
)
continue
# If there are pickings in processing status, schedule a cron to retry in 1 minute
all_pickings = self.env['stock.picking'].concat(*pickings_by_shop.values())
processing_pickings = all_pickings.filtered(lambda p: p.shopee_label_status == 'processing')
if processing_pickings:
cron = self.env.ref('sale_shopee.ir_cron_sync_shopee_pickings_retry')
cron._trigger(at=fields.Datetime.now() + timedelta(minutes=1))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _("Shipping Label Not Ready"),
'message': _(
"Shopee is processing the shipping label. Odoo will try fetching the"
" shipping label again later."
),
'type': 'warning',
},
}
def _get_pickings_by_shop(self, shop_ids=()):
""" Group the pickings by their respective shops.
:param tuple shop_ids: The shops whose pickings should be considered, as a tuple of
`shopee.shop` records ids.
:return: The pickings grouped by Shopee shop.
:rtype: dict
"""
pickings_by_shop = {}
domain = [
('sale_id.shopee_shop_id', '!=', False),
('shopee_order_ref', '!=', False),
('shopee_delivery_status', 'in', ['draft', 'confirmed']),
('shopee_label_status', '!=', 'stored'),
('state', 'not in', ['done', 'cancel']),
]
if self:
domain.append(['id', 'in', self.ids])
if shop_ids:
domain.append(('sale_id.shopee_shop_id', 'in', shop_ids))
for picking in self.search(domain):
shop = picking.sale_id.shopee_shop_id
if not shop:
continue
pickings_by_shop.setdefault(shop, self.env['stock.picking'])
pickings_by_shop[shop] += picking
return pickings_by_shop
def _fetch_shipment_label(self, auto_commit=True):
""" Fetch tracking number and shipping label from Shopee.
Delivery information needs to be filled directly on Shopee, once done, Shopee will assign a
tracking number to the pickings. _sync_picking will fetch this number, and create the
associated shipping label.
:param auto_commit: If True, the transaction will be committed after each successful sync
:return: None
"""
shop = self[0].sale_id.shopee_shop_id
shop.ensure_one()
# fetch tracking numbers for pickings without tracking number
# fetch pickings 50 per 50 and sort by the last_shopee_sync_date to avoid cron timeout
error_messages = []
current_datetime = fields.Datetime.now()
picking_sorted = self.filtered(
lambda p: p.state not in ['cancel', 'done'] \
and p.shopee_label_status != 'stored' \
and p.shopee_delivery_status in ['draft', 'confirmed']
).sorted(lambda p: (p.last_picking_sync_date, p.id))
batch_size = min(const.ORDER_DETAIL_SIZE_LIMIT, const.SHIPPING_DOCUMENT_SIZE_LIMIT) # same
picking_batches = [
picking_sorted[i:i + batch_size] for i in range(0, len(picking_sorted), batch_size)
]
for picking_batch in picking_batches:
try:
# Update the order delivery status in case it changed.
orders_detail = shop._fetch_orders_detail(picking_batch.sale_id.mapped('shopee_order_ref'))
for order_detail in orders_detail:
new_delivery_status = const.DELIVERY_STATUS_MAPPING.get(
order_detail['order_status'], 'error'
)
order = picking_batch.sale_id.filtered(
lambda o: o.shopee_order_ref == order_detail['order_sn']
)
if order and order.shopee_delivery_status != new_delivery_status:
order.shopee_delivery_status = new_delivery_status
picking_batch = picking_batch.filtered(
lambda p: p.shopee_delivery_status in {'draft', 'confirmed'}
)
# Set the tracking number as soon as it's available
picking_batch._set_tracking_number(shop)
pickings = picking_batch.filtered(lambda p: p.carrier_tracking_ref)
# Request shipping label
pickings_no_label_requested = pickings.filtered(
lambda p: p.shopee_label_status in ['not available', 'failed']
)
pickings_no_label_requested._generate_shipping_label(shop)
# Fetch the shipping label status to flag the pickings ready to be downloaded.
pickings_label_requested = pickings.filtered(
lambda p: p.shopee_label_status == 'processing'
)
pickings_label_requested._fetch_shipping_label_status(shop)
# Download the shipping label
pickings_label_ready_to_print = pickings.filtered(
lambda p: p.shopee_label_status == 'ready'
)
pickings_label_ready_to_print._download_shipping_label(shop)
except utils.ShopeeRateLimitError:
raise
except Exception as error:
error_messages.append({
'batch_picking_refs': ", ".join(picking_batch.mapped('name')),
'message': str(error),
})
continue
finally:
picking_batch.write({'last_picking_sync_date': current_datetime})
if auto_commit:
self.env.cr.commit()
if error_messages:
shop._handle_sync_failure(flow='picking_sync', error_messages=error_messages)
def _set_tracking_number(self, shop):
""" Fetch the tracking number from Shopee API and update the carrier_tracking_ref field.
If the tracking number is not available, the field will be left empty.
:param record shop: The Shopee shop of the pickings as a `shopee.shop` record.
:return: None
"""
for picking in self.filtered(lambda p: not p.carrier_tracking_ref):
tracking_number = utils.make_shopee_api_request(
shop, 'get_tracking_number', {'order_sn': picking.shopee_order_ref}
).get('tracking_number')
if tracking_number:
picking.carrier_tracking_ref = tracking_number
def _generate_shipping_label(self, shop):
""" Initiate the shipping label generation from Shopee for the pickings.
:param record shop: The Shopee shop of the pickings as a `shopee.shop` record.
:return: None
"""
for i in range(0, len(self), const.SHIPPING_DOCUMENT_SIZE_LIMIT):
pickings = self[i:i + const.SHIPPING_DOCUMENT_SIZE_LIMIT]
order_list = [{
'order_sn': pick.shopee_order_ref, 'tracking_number': pick.carrier_tracking_ref,
} for pick in pickings]
response = utils.make_shopee_api_request(
shop, 'create_shipping_document', body={'order_list': order_list}, method='POST'
)
failed_orders = {
result['order_sn'] for result in response['result_list'] if result.get('fail_error')
}
failed_pickings = pickings.filtered(lambda p: p.shopee_order_ref in failed_orders)
failed_pickings.shopee_label_status = 'failed'
successful_pickings = pickings - failed_pickings
successful_pickings.shopee_label_status = 'processing'
def _fetch_shipping_label_status(self, shop):
""" Fetch the shipping label from Shopee API and update the label status for the pickings.
:param record shop: The Shopee shop of the pickings as a `shopee.shop` record.
:return: None
"""
for i in range(0, len(self), const.SHIPPING_DOCUMENT_SIZE_LIMIT):
pickings = self[i:i + const.SHIPPING_DOCUMENT_SIZE_LIMIT]
order_list = [{'order_sn': picking.shopee_order_ref} for picking in pickings]
response = utils.make_shopee_api_request(
shop, 'get_shipping_document_result', body={'order_list': order_list}, method='POST'
)
order_sn_status = {res['order_sn']: res['status'] for res in response['result_list']}
for picking in pickings: # update the label status for each picking
status = order_sn_status.get(picking.shopee_order_ref)
if status:
picking.shopee_label_status = const.LOWER_SHIPPING_LABEL_MAPPING.get(
status, 'failed'
)
def _download_shipping_label(self, shop):
""" Download the shipping labels and store them in the picking messages.
:param record shop: The Shopee shop of the pickings as a `shopee.shop` record.
:return: None
"""
for picking in self:
order_sn = picking.shopee_order_ref
content = utils.make_shopee_api_request(shop, 'download_shipping_document', body={
'order_list': [{'order_sn': order_sn}],
}, method='POST')
# If the label exists in the picking messages, do not store it again
attachment_name = f"Shopee_Label_{picking.name.replace(' ', '_')}.pdf"
existing_attachment = self.env['ir.attachment'].search([
('company_id', '=', self.company_id.id),
('name', '=', attachment_name),
('res_model', '=', 'stock.picking'),
('res_id', '=', picking.id),
], limit=1)
if not existing_attachment:
picking.message_post(
subject=_("Shopee Label"), attachments=[(attachment_name, content)]
)
picking.shopee_label_status = 'stored'
def _sync_processing_pickings(self):
""" Retry fetching shipping label from Shopee for pickings in processing status. """
processing_pickings = self.search([('shopee_label_status', '=', 'processing')])
if processing_pickings:
return processing_pickings._sync_shopee_pickings()