Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
lib-py-b2b / order_line.py
Size: Mime:
from datetime import datetime
from decimal import Decimal

from dateutil.parser import isoparse

from lib_b2b.address import Address
from lib_b2b.base import BaseClass
from lib_b2b.discount import Discount
from lib_b2b.persistent import Persistable
from lib_b2b.tax_line import TaxLine
import logging
from os import environ
from aws_xray_sdk.core import xray_recorder

log_level = logging.getLevelName(environ['LOG_LEVEL']) if 'LOG_LEVEL' in environ else logging.INFO
logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
                    datefmt='%d-%m-%Y:%H:%M:%S',
                    level=log_level)
logger = logging.getLogger('lib-b2b-order-line')


class LineAmount(BaseClass):

    def __init__(self, amount: Decimal, unit_price) -> None:
        super().__init__()
        self.unit_price = unit_price
        self.amount = amount


class OrderLineCancellation(Persistable):
    def __init__(self, cancellation_id, cancellation_reason: str, cancelled_at: datetime,
                 cancel_qty: int, order_id: str, purchase_order_line: str = None, channel_line_id: str = None):
        self.cancel_qty = cancel_qty
        self.order_id = order_id
        self.cancellation_id = cancellation_id
        self.channel_line_id = channel_line_id
        self.purchase_order_line = purchase_order_line
        self.cancelled_at = cancelled_at
        self.cancellation_reason = cancellation_reason
        if not purchase_order_line and not channel_line_id:
            raise ValueError("Either the purchase_order_line or the channel_line_id is required.")

    @staticmethod
    def from_dict(data: dict):
        return OrderLineCancellation(
            cancellation_id=data.get('cancellation_id'),
            cancellation_reason=data.get('cancellation_reason'),
            cancelled_at=isoparse(data.get('cancelled_at')),
            cancel_qty=data.get('cancel_qty'),
            purchase_order_line=data.get('purchase_order_line'),
            channel_line_id=data.get('channel_line_id'),
            order_id=data.get('order_id')
        )

    def as_dict(self) -> dict:
        data = {
            'cancellation_id': self.cancellation_id,
            'cancel_qty': self.cancel_qty,
            'cancelled_at': self.cancelled_at.isoformat(),
            'cancellation_reason': self.cancellation_reason,
            'order_id': self.order_id
        }
        if self.channel_line_id:
            data['channel_line_id'] = self.channel_line_id
        if self.purchase_order_line:
            data['purchase_order_line'] = self.purchase_order_line
        return data


class OrderLine(Persistable):

    @staticmethod
    def generate_id(customer_edi_id: str, purchase_order: str, purchase_order_line: str):
        return f"{customer_edi_id}-{purchase_order}-{purchase_order_line}"

    def __init__(self,
                 purchase_order_line:str,
                 product_external_id: str,
                 product_id: str,
                 quantity: int,
                 note: str = None,
                 channel_line_id=None,
                 channel_line_variant_id=None,
                 amount: Decimal = None,
                 unit_price: Decimal = None,
                 tax_lines: [TaxLine] = None,
                 discounts: [Discount] = None,
                 order_id: str = None,
                 customer_edi_id: str = None,
                 ship_to: Address = None,
                 cancelled: bool = False,
                 cancelled_qty: int = 0,
                 shipped_qty: int = 0,
                 cancellations: [OrderLineCancellation] = None):

        if cancellations is None:
            cancellations = []
        self.cancellations = cancellations
        self.shipped_qty = shipped_qty
        self.cancelled_qty = cancelled_qty
        self.cancelled = cancelled
        self.order_id = order_id
        self.discounts = discounts
        self.tax_lines = tax_lines
        self._unit_price = unit_price
        self._amount = amount
        self.channel_line_variant_id = channel_line_variant_id
        self.channel_line_id = channel_line_id
        self.note = note
        self.quantity = quantity
        self.product_id = product_id
        self.product_external_id = product_external_id
        self.purchase_order_line = purchase_order_line
        self.customer_edi_id = customer_edi_id
        if not tax_lines:
            tax_lines = TaxLine.create_for(self, customer_edi_id, ship_to)
            self.tax_lines = tax_lines

    @property
    def unit_price(self):
        if self._unit_price is None:
            self.__add_amount(self.customer_edi_id)
        return self._unit_price

    @property
    def amount(self):
        if self._amount is None:
            self.__add_amount(self.customer_edi_id)
        return self._amount

    @staticmethod
    @xray_recorder.capture()
    def calculate_line_amount(customer, product_id: str, quantity: int = 1) -> LineAmount:
        """
        Calculate the line amount based on customer pricing in the ERP.
        :param customer: the customer account
        :type customer: Customer or str
        :param product_id: the product id configured for the customer
        :type product_id: str
        :param quantity: the quantity
        :type quantity: int
        :return: the unit price and amount in a LineAmount object
        :rtype: LineAmount
        """
        if customer and product_id:
            _customer_edi_id = customer if isinstance(customer, str) else customer.get('customer_edi_id')
            from .util import as_decimal
            from .profile import Profile
            profile = Profile.profile_for(_customer_edi_id)
            from . import config as cfg
            if cfg.IntegrationType(profile.integration_type) is cfg.IntegrationType.STANDARD:
                from lib_b2b.erp import ERP
                erp = ERP.for_(_customer_edi_id)
                unit_price = erp.get_customer_price(_customer_edi_id, product_id)
                amount = as_decimal(unit_price * as_decimal(quantity, 'quantity'), 'line_amount')
                return LineAmount(amount=amount, unit_price=unit_price)

    def add_missing(self, customer_edi_id: str = None):
        if self._amount is None:
            _customer_edi_id = customer_edi_id or self.customer_edi_id
            self.__add_amount(_customer_edi_id)
        return self

    def __add_amount(self, customer_edi_id: str):
        """
        If the line doesn't have an amount and the integration type is standard, add an amount field.
        :param customer_edi_id: the customer account
        :type customer_edi_id: str
        :return: None
        :rtype: None
        """
        # Don't override an existing amount
        if self._amount is None:
            if customer_edi_id:
                try:
                    line_amount = OrderLine.calculate_line_amount(customer_edi_id, self.product_id, self.quantity)
                    self._unit_price = line_amount.unit_price
                    self._amount = line_amount.amount
                except Exception as e:
                    logger.warning(f"Unable to calculate amount for order line [{self.order_line_id}]. This will be caught "
                                   f"in the validation step as well. So, producing a warning for now.", e)

    def requires_freight(self, customer) -> bool:
        from . import profile as pf
        _customer_edi_id = customer if isinstance(customer, str) else customer.get('customer_edi_id')
        profile = pf.Profile.profile_for(_customer_edi_id)
        if profile.fulfillment_config.free_shipping:
            return False
        elif self.amount == 0:
            return False
        else:
            return True

    @staticmethod
    def from_dict(data: dict):
        return OrderLine(
            order_id=data.get('order_id'),
            purchase_order_line=data.get('purchase_order_line'),
            product_external_id=data.get('product_external_id'),
            product_id=data.get('product_id'),
            quantity=int(data.get('quantity')),
            note=data.get('note'),
            channel_line_id=data.get('channel_line_id'),
            channel_line_variant_id =data.get('channel_line_variant_id'),
            amount=Decimal(str(data.get('amount'))) if data.get('amount') else None,
            unit_price=Decimal(str(data.get('unit_price'))) if data.get('unit_price') else None,
            tax_lines=list(map(lambda x: TaxLine.from_dict(x), data.get('tax_lines', []))),
            discounts=list(map(lambda x: Discount.from_dict(x), data.get('discounts', []))),
            cancelled=bool(data.get('cancelled')),
            cancelled_qty=data.get('cancelled_qty', 0),
            shipped_qty=data.get('shipped_qty', 0),
            cancellations=list(map(lambda x: OrderLineCancellation.from_dict(x), data.get('cancellations', []))),
            customer_edi_id=data.get('customer_edi_id')
        )

    def as_dict(self) -> dict:
        line = {
            'order_id': self.order_id,
            'purchase_order_line': self.purchase_order_line,
            'product_external_id': self.product_external_id,
            'product_id': self.product_id,
            'quantity': int(self.quantity),
            'cancelled_qty': int(self.cancelled_qty),
            'shipped_qty': int(self.shipped_qty)
        }
        if self.note:
            line['note'] = self.note
        if self.customer_edi_id:
            line['customer_edi_id'] = self.customer_edi_id
        if self.channel_line_id:
            line['channel_line_id'] = self.channel_line_id
        if self.channel_line_variant_id:
            line['channel_line_variant_id'] = self.channel_line_variant_id
        if self.amount or self.amount == 0:
            line['amount'] = self.amount
        if self.unit_price or self.unit_price == 0:
            line['unit_price'] = self.unit_price
        if self.tax_lines:
            line['tax_lines'] = list(map(lambda x: x.as_dict(), self.tax_lines))
        if self.discounts:
            line['discounts'] = list(map(lambda x: x.as_dict(), self.discounts))
        if self.cancelled:
            line['cancelled'] = str(self.cancelled)
        if self.cancellations:
            line['cancellations'] = list(map(lambda x: x.as_dict(), self.cancellations))
        return line

    @property
    def order_line_id(self) -> str:
        return f"{self.order_id}-{self.purchase_order_line}"

    @property
    def fulfilled(self) -> bool:
        return self.remaining_qty <= 0

    @property
    def remaining_qty(self) -> int:
        return self.quantity - self.cancelled_qty - self.shipped_qty

    def cancellation_exists(self, cancellation: OrderLineCancellation):
        return any(c.cancellation_id == cancellation.cancellation_id for c in self.cancellations)

    def cancel(self, cancellation: OrderLineCancellation, on_behalf_of: str = 'System'):
        from lib_b2b.order_change.cancel_line import OrderCancelLineChangeDataProvider, OrderCancelLineChangeRequest
        from lib_b2b.orders import Orders

        class __OCDP(OrderCancelLineChangeDataProvider):
            @property
            def cancellations(self) -> [OrderLineCancellation]:
                return [cancellation]

            def data(self) -> dict:
                return {}

        change_request = OrderCancelLineChangeRequest(on_behalf_of=on_behalf_of, change_data_provider=__OCDP())
        order = Orders.for_(self.order_id)
        order.change([change_request])