Repository URL to install this package:
|
Version:
0.4.139 ▾
|
lib-py-b2b
/
order_line.py
|
|---|
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])