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

222 lines
8.6 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from urllib.parse import urlsplit
import requests
from odoo import _, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import split_every
from odoo.addons.sale_tiktok import const, utils
_logger = logging.getLogger(__name__)
class StockPicking(models.Model):
_inherit = "stock.picking"
tiktok_order_ref = fields.Char(related="sale_id.tiktok_order_ref")
tiktok_packing_ref = fields.Char(related="sale_id.tiktok_packing_ref")
tiktok_delivery_status = fields.Selection(related="sale_id.tiktok_delivery_status")
tiktok_label_attachment_id = fields.Many2one(
comodel_name="ir.attachment", help="The attachment related to the TikTok shipping label."
)
# === ACTION METHODS === #
def action_tiktok_sync_pickings(self):
return self._sync_tiktok_pickings()
def action_tiktok_ship_order(self):
self.ensure_one()
if not (
self.tiktok_order_ref
and self.tiktok_packing_ref
and self.tiktok_delivery_status == "draft"
):
raise UserError(_("The collection of this order was already scheduled."))
wizard = self.env["tiktok.picking.ship.wizard"].create({"picking_id": self.id})
return wizard._get_records_action(name=_("Ship TikTok Orders"), target="new")
# === BUSINESS METHODS === #
def _sync_tiktok_pickings(self):
"""Fetch tracking number and shipping label from TikTok Shop.
Delivery information must be filled on TikTok Shop. Once TikTok assigns a tracking
number to the pickings, this method fetches the number and the shipping label.
:return: None
"""
pickings_by_shop = self._get_tiktok_pickings_by_shop()
for shop, shop_pickings in pickings_by_shop.items():
try:
if not shop.active:
continue
shop_pickings._fetch_tiktok_shipping_label(shop)
except utils.TikTokRateLimitError as error:
_logger.info(
"Rate limit reached while synchronizing pickings for TikTok Shop with"
" id %(shop)s. Operation: %(error_operation)s",
{"shop": shop.id, "error_operation": error.operation},
)
continue
def _get_tiktok_pickings_by_shop(self):
"""Group the pickings by their respective shops.
:return: The pickings grouped by TikTok shop.
:rtype: recordset of stock.picking
"""
domain = [
("sale_id.tiktok_shop_id", "!=", False),
("tiktok_order_ref", "!=", False),
("state", "!=", "cancel"),
]
pickings = self.filtered_domain(domain) if self else self.search(domain)
return pickings.grouped(lambda p: p.sale_id.tiktok_shop_id)
def _fetch_tiktok_shipping_label(self, shop):
"""Fetch tracking number and shipping label from TikTok Shop.
Delivery information is first recorded on TikTok Shop, which assigns the tracking
number. This helper fetches the number and creates the associated shipping label.
:param record shop: The shop whose shipments are synchronized.
:return: None
"""
shop.ensure_one()
error_messages = []
picking_sorted = self.filtered(
lambda p: (
not p.tiktok_label_attachment_id
and p.tiktok_delivery_status in ["draft", "confirmed"]
)
)
batch_size = min(const.ORDER_DETAIL_SIZE_LIMIT, const.SHIPPING_DOCUMENT_SIZE_LIMIT)
for picking_batch in split_every(batch_size, picking_sorted.ids, self.browse):
# Update delivery status to keep the order status up-to-date
try:
orders = picking_batch.sale_id.mapped("tiktok_order_ref")
picking_batch._update_tiktok_delivery_status(shop, orders)
except utils.TikTokApiError as error:
error_messages.append({
"picking_refs": ", ".join(picking_batch.mapped("name")),
"message": str(error),
})
continue
picking_to_label = picking_batch.filtered(
lambda p: (
p.tiktok_delivery_status == "confirmed" and not p.tiktok_label_attachment_id
)
)
for picking in picking_to_label:
try:
picking._download_tiktok_shipping_label(shop)
except utils.TikTokApiError as error:
error_messages.append({"picking_refs": picking.name, "message": str(error)})
continue
if error_messages:
_logger.info(
"Failed to fetch label for TikTok Shop with id %(shop)s. Error: %(message)s",
{"shop": shop.id, "message": error_messages},
)
raise ValidationError(
_("Failed to fetch label. Please wait and try again later. %s", error_messages)
)
def _download_tiktok_shipping_label(self, shop):
"""Fetch the shipping labels and tracking number.
:param record shop: The TikTok shop of the pickings as a `tiktok.shop` record.
:return: None
"""
self.ensure_one()
package_id = self.tiktok_packing_ref
response = utils.make_tiktok_api_request(
shop,
"get_package_label",
params={"document_type": "SHIPPING_LABEL", "shop_cipher": shop.tiktok_cipher},
path_params={"package_id": package_id},
)
# Packages with no shipping label will return 'None'
if not response:
return
self.carrier_tracking_ref = response.get("tracking_number")
label_url = response.get("doc_url")
if not (self.carrier_tracking_ref and label_url):
return
parsed_label_url = urlsplit(label_url)
if (
parsed_label_url.scheme != "https"
or not parsed_label_url.hostname
or not any(
parsed_label_url.hostname.endswith(domain)
for domain in const.ALLOWED_TIKTOK_DOC_HOSTS
)
):
raise UserError(self.env._("Invalid TikTok document URL"))
try:
label_response = requests.get(label_url, timeout=10, allow_redirects=False)
label_response.raise_for_status()
attachment_name = f"TikTok_Shop_Label_{self.carrier_tracking_ref}.pdf"
message = self.message_post(
subject=_("TikTok Shop Label"),
body=_("TikTok Shop Label"),
attachments=[(attachment_name, label_response.content)],
)
self.tiktok_label_attachment_id = message.attachment_ids[:1]
except requests.exceptions.RequestException:
self.message_post(
subject=_("TikTok Shop Label"),
body=_(
"Failed to download the shipping label. Use the provided link:\n%(link)s",
link=label_url,
),
)
def _ship_tiktok_order(self, time_period):
self.ensure_one()
if not self.tiktok_order_ref:
raise UserError(_("This picking is not linked to a TikTok order."))
shop = self.sale_id.tiktok_shop_id
if not shop:
raise UserError(_("No TikTok shop found for this order."))
try:
utils.make_tiktok_api_request(
shop,
"ship_package",
params={"shop_cipher": shop.tiktok_cipher},
path_params={"package_id": self.tiktok_packing_ref},
body=time_period or {"pickup_slot": {}},
)
self.sale_id.tiktok_delivery_status = "confirmed"
except utils.TikTokApiError as error:
raise UserError(_("Failed to ship order: %s", error)) from error
def _update_tiktok_delivery_status(self, shop, order_refs):
"""Update tiktok_delivery_status on associated orders based on TikTok order detail."""
orders_detail = shop._fetch_orders_detail(order_refs)
order_by_tiktok_ref = self.sale_id.grouped("tiktok_order_ref")
for order_detail in orders_detail:
new_delivery_status = const.DELIVERY_STATUS_MAPPING.get(
order_detail["status"], "manual"
)
order = order_by_tiktok_ref.get(order_detail["id"])
if order and order.tiktok_delivery_status != new_delivery_status:
order.tiktok_delivery_status = new_delivery_status