Add "most of" support for truck dump receiving
This commit is contained in:
parent
d3662fe55d
commit
225a0ba380
|
@ -310,6 +310,16 @@ class BatchHandler(object):
|
||||||
Update the batch status, as needed...
|
Update the batch status, as needed...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def why_not_execute(self, batch):
|
||||||
|
"""
|
||||||
|
This method should return a string indicating the reason why the given
|
||||||
|
batch should not be considered executable. By default it returns
|
||||||
|
``None`` which means the batch *is* to be considered executable.
|
||||||
|
|
||||||
|
Note that it is assumed the batch has not already been executed, since
|
||||||
|
execution is globally prevented for such batches.
|
||||||
|
"""
|
||||||
|
|
||||||
def executable(self, batch):
|
def executable(self, batch):
|
||||||
"""
|
"""
|
||||||
This method should return a boolean indicating whether or not execution
|
This method should return a boolean indicating whether or not execution
|
||||||
|
@ -321,7 +331,11 @@ class BatchHandler(object):
|
||||||
"""
|
"""
|
||||||
if batch is None:
|
if batch is None:
|
||||||
return True
|
return True
|
||||||
return not bool(batch.executed)
|
if batch.executed:
|
||||||
|
return False
|
||||||
|
if self.why_not_execute(batch):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def auto_executable(self, batch):
|
def auto_executable(self, batch):
|
||||||
"""
|
"""
|
||||||
|
@ -352,29 +366,57 @@ class BatchHandler(object):
|
||||||
batch.executed_by = kwargs['user']
|
batch.executed_by = kwargs['user']
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def clone(self, oldbatch, created_by):
|
def delete(self, batch, progress=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Delete all data for the batch, including any related (e.g. row)
|
||||||
|
records, as well as files on disk etc. This method should *not* delete
|
||||||
|
the batch itself however.
|
||||||
|
"""
|
||||||
|
if hasattr(batch, 'delete_data'):
|
||||||
|
batch.delete_data(self.config)
|
||||||
|
if hasattr(batch, 'data_rows'):
|
||||||
|
del batch.data_rows[:]
|
||||||
|
|
||||||
|
def clone(self, oldbatch, created_by, progress=None):
|
||||||
"""
|
"""
|
||||||
Clone the given batch as a new batch, and return the new batch.
|
Clone the given batch as a new batch, and return the new batch.
|
||||||
"""
|
"""
|
||||||
newbatch = self.batch_model_class()
|
batch_class = self.batch_model_class
|
||||||
|
batch_mapper = orm.class_mapper(batch_class)
|
||||||
|
|
||||||
|
newbatch = batch_class()
|
||||||
newbatch.created_by = created_by
|
newbatch.created_by = created_by
|
||||||
newbatch.rowcount = 0
|
newbatch.rowcount = 0
|
||||||
rowclass = newbatch.row_class
|
for name in batch_mapper.columns.keys():
|
||||||
mapper = orm.class_mapper(rowclass)
|
if name not in ('uuid', 'id', 'created', 'created_by_uuid', 'executed', 'executed_by_uuid'):
|
||||||
|
setattr(newbatch, name, getattr(oldbatch, name))
|
||||||
for oldrow in oldbatch.data_rows:
|
|
||||||
newrow = rowclass()
|
|
||||||
for name in mapper.columns.keys():
|
|
||||||
if name not in ('uuid', 'batch_uuid'):
|
|
||||||
setattr(newrow, name, getattr(oldrow, name))
|
|
||||||
newbatch.data_rows.append(newrow)
|
|
||||||
newbatch.rowcount += 1
|
|
||||||
|
|
||||||
session = orm.object_session(oldbatch)
|
session = orm.object_session(oldbatch)
|
||||||
session.add(newbatch)
|
session.add(newbatch)
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
|
row_class = newbatch.row_class
|
||||||
|
row_mapper = orm.class_mapper(row_class)
|
||||||
|
|
||||||
|
def clone_row(oldrow, i):
|
||||||
|
newrow = self.clone_row(oldrow)
|
||||||
|
self.add_row(newbatch, newrow)
|
||||||
|
|
||||||
|
self.progress_loop(clone_row, oldbatch.data_rows, progress,
|
||||||
|
message="Cloning data rows for new batch")
|
||||||
|
|
||||||
|
self.refresh_batch_status(newbatch)
|
||||||
return newbatch
|
return newbatch
|
||||||
|
|
||||||
|
def clone_row(self, oldrow):
|
||||||
|
row_class = self.batch_model_class.row_class
|
||||||
|
row_mapper = orm.class_mapper(row_class)
|
||||||
|
newrow = row_class()
|
||||||
|
for name in row_mapper.columns.keys():
|
||||||
|
if name not in ('uuid', 'batch_uuid'):
|
||||||
|
setattr(newrow, name, getattr(oldrow, name))
|
||||||
|
return newrow
|
||||||
|
|
||||||
def cache_model(self, session, model, **kwargs):
|
def cache_model(self, session, model, **kwargs):
|
||||||
return cache_model(session, model, **kwargs)
|
return cache_model(session, model, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -26,12 +26,18 @@ Handler for purchase order batches
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.db import model, api
|
from rattail.db import model, api
|
||||||
from rattail.batch import BatchHandler
|
from rattail.batch import BatchHandler
|
||||||
from rattail.time import make_utc
|
from rattail.time import make_utc
|
||||||
|
from rattail.vendors.invoices import require_invoice_parser
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseBatchHandler(BatchHandler):
|
class PurchaseBatchHandler(BatchHandler):
|
||||||
|
@ -75,6 +81,285 @@ class PurchaseBatchHandler(BatchHandler):
|
||||||
session.flush()
|
session.flush()
|
||||||
self.refresh_batch_status(batch)
|
self.refresh_batch_status(batch)
|
||||||
|
|
||||||
|
def populate_from_truck_dump_invoice(self, batch, progress=None):
|
||||||
|
parser = require_invoice_parser(batch.invoice_parser_key)
|
||||||
|
|
||||||
|
session = orm.object_session(batch)
|
||||||
|
parser.session = session
|
||||||
|
|
||||||
|
parser.vendor = api.get_vendor(session, parser.vendor_key)
|
||||||
|
if parser.vendor is not batch.vendor:
|
||||||
|
raise RuntimeError("Parser is for vendor '{}' but batch is for: {}".format(
|
||||||
|
parser.vendor_key, batch.vendor))
|
||||||
|
|
||||||
|
path = batch.filepath(self.config, batch.invoice_file)
|
||||||
|
batch.invoice_date = parser.parse_invoice_date(path)
|
||||||
|
|
||||||
|
def append(invoice_row, i):
|
||||||
|
row = model.PurchaseBatchRow()
|
||||||
|
row.upc = invoice_row.upc
|
||||||
|
row.vendor_code = invoice_row.vendor_code
|
||||||
|
row.brand_name = invoice_row.brand_name
|
||||||
|
row.description = invoice_row.description
|
||||||
|
row.size = invoice_row.size
|
||||||
|
row.case_quantity = invoice_row.case_quantity
|
||||||
|
row.cases_ordered = invoice_row.ordered_cases
|
||||||
|
row.units_ordered = invoice_row.ordered_units
|
||||||
|
row.cases_shipped = invoice_row.shipped_cases
|
||||||
|
row.units_shipped = invoice_row.shipped_units
|
||||||
|
row.invoice_unit_cost = invoice_row.unit_cost
|
||||||
|
row.invoice_total = invoice_row.total_cost
|
||||||
|
row.invoice_case_cost = invoice_row.case_cost
|
||||||
|
self.add_row(batch, row)
|
||||||
|
|
||||||
|
self.progress_loop(append, list(parser.parse_rows(path)), progress,
|
||||||
|
message="Adding initial rows to batch")
|
||||||
|
|
||||||
|
self.make_truck_dump_claims_for_child_batch(batch, progress=progress)
|
||||||
|
self.refresh_batch_status(batch.truck_dump_batch)
|
||||||
|
|
||||||
|
def make_truck_dump_claims_for_child_batch(self, batch, progress=None):
|
||||||
|
"""
|
||||||
|
Make all "claims" against a truck dump, for the given child batch.
|
||||||
|
This assumes no claims exist for the child batch at time of calling,
|
||||||
|
and that the truck dump batch is complete and not yet executed.
|
||||||
|
"""
|
||||||
|
session = orm.object_session(batch)
|
||||||
|
truck_dump_rows = batch.truck_dump_batch.active_rows()
|
||||||
|
child_rows = batch.active_rows()
|
||||||
|
|
||||||
|
# organize truck dump by product and UPC
|
||||||
|
truck_dump_by_product = {}
|
||||||
|
truck_dump_by_upc = {}
|
||||||
|
for row in truck_dump_rows:
|
||||||
|
if row.product:
|
||||||
|
truck_dump_by_product.setdefault(row.product.uuid, []).append(row)
|
||||||
|
if row.upc:
|
||||||
|
truck_dump_by_upc.setdefault(row.upc, []).append(row)
|
||||||
|
|
||||||
|
# organize child batch by product and UPC
|
||||||
|
child_by_product = {}
|
||||||
|
child_by_upc = {}
|
||||||
|
for row in child_rows:
|
||||||
|
if row.product:
|
||||||
|
child_by_product.setdefault(row.product.uuid, []).append(row)
|
||||||
|
if row.upc:
|
||||||
|
child_by_upc.setdefault(row.upc, []).append(row)
|
||||||
|
|
||||||
|
# first pass looks only for exact product and quantity match
|
||||||
|
for uuid, child_product_rows in six.iteritems(child_by_product):
|
||||||
|
if uuid not in truck_dump_by_product:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# inspect truck dump to find exact match on child 'ordered' count
|
||||||
|
index = 0
|
||||||
|
truck_dump_product_rows = truck_dump_by_product[uuid]
|
||||||
|
for truck_dump_row in list(truck_dump_product_rows):
|
||||||
|
matched_child_rows = None
|
||||||
|
|
||||||
|
# Note: A possibility we do not address here is one where the
|
||||||
|
# truck dump quantity would match against a certain aggregation
|
||||||
|
# of child rows but not *all* child rows. E.g. if the child
|
||||||
|
# contained 3 rows but only 2 of them should (combined) match
|
||||||
|
# the truck dump row. As of this writing this is a
|
||||||
|
# hypothetical edge case but it will probably happen at some
|
||||||
|
# point. We can maybe assess the situation then.
|
||||||
|
|
||||||
|
available = self.get_units_available(truck_dump_row)
|
||||||
|
|
||||||
|
# first look at each child row individually (first match wins)
|
||||||
|
for child_row in child_product_rows:
|
||||||
|
ordered = self.get_units_ordered(child_row)
|
||||||
|
if ordered == available:
|
||||||
|
matched_child_rows = [child_row]
|
||||||
|
break
|
||||||
|
|
||||||
|
# maybe also look at the aggregate child counts
|
||||||
|
if not matched_child_rows:
|
||||||
|
ordered = sum([self.get_units_ordered(row)
|
||||||
|
for row in child_product_rows])
|
||||||
|
if ordered == available:
|
||||||
|
matched_child_rows = child_product_rows
|
||||||
|
|
||||||
|
# did we find a match?
|
||||||
|
claims = False
|
||||||
|
if matched_child_rows:
|
||||||
|
|
||||||
|
# make some truck dump claim(s)
|
||||||
|
claims = self.make_truck_dump_claims(truck_dump_row, matched_child_rows)
|
||||||
|
|
||||||
|
if claims:
|
||||||
|
|
||||||
|
# remove truck dump row from working set
|
||||||
|
truck_dump_product_rows.pop(index)
|
||||||
|
if not truck_dump_product_rows:
|
||||||
|
del truck_dump_by_product[uuid]
|
||||||
|
|
||||||
|
# filter working set of child batch rows, removing any
|
||||||
|
# which contributed to the match
|
||||||
|
remaining = []
|
||||||
|
for child_row in child_product_rows:
|
||||||
|
matched = False
|
||||||
|
for match_row in matched_child_rows:
|
||||||
|
if match_row is child_row:
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
if not matched:
|
||||||
|
remaining.append(child_row)
|
||||||
|
child_product_rows = remaining
|
||||||
|
|
||||||
|
# if no match, shift index so future list pops work
|
||||||
|
else:
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
# TODO: second pass to look for inexact and UPC matches, yes?
|
||||||
|
|
||||||
|
def make_truck_dump_claims(self, truck_dump_row, child_rows):
|
||||||
|
|
||||||
|
avail_received = self.get_units_received(truck_dump_row) - self.get_units_claimed_received(truck_dump_row)
|
||||||
|
avail_damaged = self.get_units_damaged(truck_dump_row) - self.get_units_claimed_damaged(truck_dump_row)
|
||||||
|
avail_expired = self.get_units_expired(truck_dump_row) - self.get_units_claimed_expired(truck_dump_row)
|
||||||
|
|
||||||
|
claims = []
|
||||||
|
for child_row in child_rows:
|
||||||
|
|
||||||
|
ordered = self.get_units_ordered(child_row)
|
||||||
|
|
||||||
|
# TODO: must look into why this can happen...
|
||||||
|
# assert ordered, "child row has no ordered count: {}".format(child_row)
|
||||||
|
if not ordered:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TODO: should we ever use case quantity from truck dump instead?
|
||||||
|
case_quantity = child_row.case_quantity
|
||||||
|
|
||||||
|
claim = model.PurchaseBatchRowClaim()
|
||||||
|
claim.claiming_row = child_row
|
||||||
|
truck_dump_row.claims.append(claim)
|
||||||
|
|
||||||
|
# if "received" can account for all we ordered, use only that
|
||||||
|
if ordered <= avail_received:
|
||||||
|
child_row.cases_received = claim.cases_received = child_row.cases_ordered
|
||||||
|
child_row.units_received = claim.units_received = child_row.units_ordered
|
||||||
|
self.refresh_row(child_row)
|
||||||
|
|
||||||
|
# if "damaged" can account for all we ordered, use only that
|
||||||
|
elif ordered <= avail_damaged:
|
||||||
|
|
||||||
|
matched = False
|
||||||
|
for credit in truck_dump_row.credits:
|
||||||
|
if credit.credit_type == 'damaged':
|
||||||
|
shorted = self.get_units_shorted(credit)
|
||||||
|
if shorted == ordered:
|
||||||
|
child_row.cases_damaged = claim.cases_damaged = credit.cases_shorted
|
||||||
|
child_row.units_damaged = claim.units_damaged = credit.units_shorted
|
||||||
|
self.clone_truck_dump_credit(credit, child_row)
|
||||||
|
self.refresh_row(child_row)
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
assert matched, "could not find matching 'damaged' credit to clone for {}".format(truck_dump_row)
|
||||||
|
|
||||||
|
# if "expired" can account for all we ordered, use only that
|
||||||
|
elif ordered <= avail_expired:
|
||||||
|
|
||||||
|
matched = False
|
||||||
|
for credit in truck_dump_row.credits:
|
||||||
|
if credit.credit_type == 'expired':
|
||||||
|
shorted = self.get_units_shorted(credit)
|
||||||
|
if shorted == ordered:
|
||||||
|
child_row.cases_expired = claim.cases_expired = credit.cases_shorted
|
||||||
|
child_row.units_expired = claim.units_expired = credit.units_shorted
|
||||||
|
self.clone_truck_dump_credit(credit, child_row)
|
||||||
|
self.refresh_row(child_row)
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
assert matched, "could not find matching 'expired' credit to clone for {}".format(truck_dump_row)
|
||||||
|
|
||||||
|
else: # things are a bit trickier in this scenario
|
||||||
|
|
||||||
|
# TODO: should we ever use case quantity from truck dump instead?
|
||||||
|
|
||||||
|
if ordered and avail_received:
|
||||||
|
cases, units = self.calc_best_fit(avail_received, case_quantity)
|
||||||
|
total = self.get_units(cases, units, case_quantity)
|
||||||
|
assert total == avail_received, "total units doesn't match avail_received for {}".format(truck_dump_row)
|
||||||
|
child_row.cases_received = claim.cases_received = cases or None
|
||||||
|
child_row.units_received = claim.units_received = units or None
|
||||||
|
self.refresh_row(child_row)
|
||||||
|
avail_received -= total
|
||||||
|
ordered -= total
|
||||||
|
|
||||||
|
if ordered and avail_damaged:
|
||||||
|
assert ordered > avail_damaged
|
||||||
|
possible_credits = [credit for credit in truck_dump_row.credits
|
||||||
|
if credit.credit_type == 'damaged']
|
||||||
|
possible_shorted = sum([self.get_units_shorted(credit)
|
||||||
|
for credit in possible_credits])
|
||||||
|
if possible_shorted == avail_damaged:
|
||||||
|
cases = sum([credit.cases_shorted or 0
|
||||||
|
for credit in possible_credits])
|
||||||
|
units = sum([credit.units_shorted or 0
|
||||||
|
for credit in possible_credits])
|
||||||
|
child_row.cases_damaged = claim.cases_damaged = cases or None
|
||||||
|
child_row.units_damaged = claim.units_damaged = units or None
|
||||||
|
for credit in possible_credits:
|
||||||
|
self.clone_truck_dump_credit(credit, child_row)
|
||||||
|
self.refresh_row(child_row)
|
||||||
|
ordered -= avail_damaged
|
||||||
|
avail_damaged = 0
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# TODO: need to add support for credits to this one
|
||||||
|
if ordered and avail_expired:
|
||||||
|
cases, units = self.calc_best_fit(avail_expired, case_quantity)
|
||||||
|
total = self.get_units(cases, units, case_quantity)
|
||||||
|
assert total == avail_expired, "total units doesn't match avail_expired for {}".format(truck_dump_row)
|
||||||
|
child_row.cases_expired = claim.cases_expired = cases or None
|
||||||
|
child_row.cases_expired = claim.units_expired = units or None
|
||||||
|
self.refresh_row(child_row)
|
||||||
|
avail_expired -= total
|
||||||
|
ordered -= total
|
||||||
|
|
||||||
|
if ordered:
|
||||||
|
raise NotImplementedError("TODO: can't yet handle match with mixture of received/damaged/expired (?)")
|
||||||
|
|
||||||
|
self.refresh_row(truck_dump_row)
|
||||||
|
claims.append(claim)
|
||||||
|
|
||||||
|
return claims
|
||||||
|
|
||||||
|
def clone_truck_dump_credit(self, truck_dump_credit, child_row):
|
||||||
|
"""
|
||||||
|
Clone a credit record from a truck dump, onto the given child row.
|
||||||
|
"""
|
||||||
|
child_batch = child_row.batch
|
||||||
|
child_credit = model.PurchaseBatchCredit()
|
||||||
|
self.copy_credit_attributes(truck_dump_credit, child_credit)
|
||||||
|
child_credit.date_ordered = child_batch.date_ordered
|
||||||
|
child_credit.date_shipped = child_batch.date_shipped
|
||||||
|
child_credit.date_received = child_batch.date_received
|
||||||
|
child_credit.invoice_date = child_batch.invoice_date
|
||||||
|
child_credit.invoice_number = child_batch.invoice_number
|
||||||
|
child_credit.invoice_line_number = child_row.invoice_line_number
|
||||||
|
child_credit.invoice_case_cost = child_row.invoice_case_cost
|
||||||
|
child_credit.invoice_unit_cost = child_row.invoice_unit_cost
|
||||||
|
child_credit.invoice_total = child_row.invoice_total
|
||||||
|
child_row.credits.append(child_credit)
|
||||||
|
return child_credit
|
||||||
|
|
||||||
|
# TODO: surely this should live elsewhere
|
||||||
|
def calc_best_fit(self, units, case_quantity):
|
||||||
|
case_quantity = case_quantity or 1
|
||||||
|
if case_quantity == 1:
|
||||||
|
return 0, units
|
||||||
|
cases = units // case_quantity
|
||||||
|
if cases:
|
||||||
|
return cases, units % cases
|
||||||
|
return 0, units
|
||||||
|
|
||||||
def refresh(self, batch, progress=None):
|
def refresh(self, batch, progress=None):
|
||||||
if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
|
if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
|
||||||
batch.po_total = 0
|
batch.po_total = 0
|
||||||
|
@ -84,8 +369,14 @@ class PurchaseBatchHandler(BatchHandler):
|
||||||
return super(PurchaseBatchHandler, self).refresh(batch, progress=progress)
|
return super(PurchaseBatchHandler, self).refresh(batch, progress=progress)
|
||||||
|
|
||||||
def refresh_batch_status(self, batch):
|
def refresh_batch_status(self, batch):
|
||||||
if any([not row.product_uuid for row in batch.active_rows()]):
|
rows = batch.active_rows()
|
||||||
|
if any([not row.product_uuid for row in rows]):
|
||||||
batch.status_code = batch.STATUS_UNKNOWN_PRODUCT
|
batch.status_code = batch.STATUS_UNKNOWN_PRODUCT
|
||||||
|
elif batch.truck_dump:
|
||||||
|
if any([row.status_code == row.STATUS_TRUCKDUMP_UNCLAIMED for row in rows]):
|
||||||
|
batch.status_code = batch.STATUS_TRUCKDUMP_UNCLAIMED
|
||||||
|
else:
|
||||||
|
batch.status_code = batch.STATUS_TRUCKDUMP_CLAIMED
|
||||||
else:
|
else:
|
||||||
batch.status_code = batch.STATUS_OK
|
batch.status_code = batch.STATUS_OK
|
||||||
|
|
||||||
|
@ -138,7 +429,16 @@ class PurchaseBatchHandler(BatchHandler):
|
||||||
row.status_code = row.STATUS_INCOMPLETE
|
row.status_code = row.STATUS_INCOMPLETE
|
||||||
else:
|
else:
|
||||||
if batch.truck_dump:
|
if batch.truck_dump:
|
||||||
row.status_code = row.STATUS_OK
|
confirmed = self.get_units_confirmed(row)
|
||||||
|
claimed = self.get_units_claimed(row)
|
||||||
|
if claimed == confirmed:
|
||||||
|
row.status_code = row.STATUS_TRUCKDUMP_CLAIMED
|
||||||
|
elif claimed < confirmed:
|
||||||
|
row.status_code = row.STATUS_TRUCKDUMP_UNCLAIMED
|
||||||
|
elif claimed > confirmed:
|
||||||
|
row.status_code = row.STATUS_TRUCKDUMP_OVERCLAIMED
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
else: # not truck_dump
|
else: # not truck_dump
|
||||||
if self.get_units_ordered(row) != self.get_units_accounted_for(row):
|
if self.get_units_ordered(row) != self.get_units_accounted_for(row):
|
||||||
row.status_code = row.STATUS_ORDERED_RECEIVED_DIFFER
|
row.status_code = row.STATUS_ORDERED_RECEIVED_DIFFER
|
||||||
|
@ -192,20 +492,78 @@ class PurchaseBatchHandler(BatchHandler):
|
||||||
if cost:
|
if cost:
|
||||||
return cost.unit_cost
|
return cost.unit_cost
|
||||||
|
|
||||||
def get_units_ordered(self, row):
|
def get_units(self, cases, units, case_quantity):
|
||||||
return (row.units_ordered or 0) + (row.case_quantity or 1) * (row.cases_ordered or 0)
|
case_quantity = case_quantity or 1
|
||||||
|
return (units or 0) + case_quantity * (cases or 0)
|
||||||
|
|
||||||
def get_units_received(self, row):
|
def get_units_ordered(self, row, case_quantity=None):
|
||||||
return (row.units_received or 0) + (row.case_quantity or 1) * (row.cases_received or 0)
|
case_quantity = case_quantity or row.case_quantity or 1
|
||||||
|
return self.get_units(row.cases_ordered, row.units_ordered, case_quantity)
|
||||||
|
|
||||||
def get_units_shipped(self, row):
|
# TODO: we now have shipped quantities...should return sum of those instead?
|
||||||
units_damaged = (row.units_damaged or 0) + (row.case_quantity or 1) * (row.cases_damaged or 0)
|
def get_units_shipped(self, row, case_quantity=None):
|
||||||
units_expired = (row.units_expired or 0) + (row.case_quantity or 1) * (row.cases_expired or 0)
|
case_quantity = case_quantity or row.case_quantity or 1
|
||||||
|
units_damaged = (row.units_damaged or 0) + case_quantity * (row.cases_damaged or 0)
|
||||||
|
units_expired = (row.units_expired or 0) + case_quantity * (row.cases_expired or 0)
|
||||||
return self.get_units_received(row) + units_damaged + units_expired
|
return self.get_units_received(row) + units_damaged + units_expired
|
||||||
|
|
||||||
def get_units_accounted_for(self, row):
|
def get_units_received(self, row, case_quantity=None):
|
||||||
units_mispick = (row.units_mispick or 0) + (row.case_quantity or 1) * (row.cases_mispick or 0)
|
case_quantity = case_quantity or row.case_quantity or 1
|
||||||
return self.get_units_shipped(row) + units_mispick
|
return self.get_units(row.cases_received, row.units_received, case_quantity)
|
||||||
|
|
||||||
|
def get_units_damaged(self, row, case_quantity=None):
|
||||||
|
case_quantity = case_quantity or row.case_quantity or 1
|
||||||
|
return self.get_units(row.cases_damaged, row.units_damaged, case_quantity)
|
||||||
|
|
||||||
|
def get_units_expired(self, row, case_quantity=None):
|
||||||
|
case_quantity = case_quantity or row.case_quantity or 1
|
||||||
|
return self.get_units(row.cases_expired, row.units_expired, case_quantity)
|
||||||
|
|
||||||
|
def get_units_confirmed(self, row, case_quantity=None):
|
||||||
|
received = self.get_units_received(row, case_quantity=case_quantity)
|
||||||
|
damaged = self.get_units_damaged(row, case_quantity=case_quantity)
|
||||||
|
expired = self.get_units_expired(row, case_quantity=case_quantity)
|
||||||
|
return received + damaged + expired
|
||||||
|
|
||||||
|
def get_units_mispick(self, row, case_quantity=None):
|
||||||
|
case_quantity = case_quantity or row.case_quantity or 1
|
||||||
|
return self.get_units(row.cases_mispick, row.units_mispick, case_quantity)
|
||||||
|
|
||||||
|
def get_units_accounted_for(self, row, case_quantity=None):
|
||||||
|
confirmed = self.get_units_confirmed(row, case_quantity=case_quantity)
|
||||||
|
mispick = self.get_units_mispick(row, case_quantity=case_quantity)
|
||||||
|
return confirmed + mispick
|
||||||
|
|
||||||
|
def get_units_shorted(self, obj, case_quantity=None):
|
||||||
|
case_quantity = case_quantity or obj.case_quantity or 1
|
||||||
|
if hasattr(obj, 'cases_shorted'):
|
||||||
|
# obj is really a credit
|
||||||
|
return self.get_units(obj.cases_shorted, obj.units_shorted, case_quantity)
|
||||||
|
else:
|
||||||
|
# obj is a row, so sum the credits
|
||||||
|
return sum([self.get_units(credit.cases_shorted, credit.units_shorted, case_quantity)
|
||||||
|
for credit in obj.credits])
|
||||||
|
|
||||||
|
def get_units_claimed(self, row, case_quantity=None):
|
||||||
|
return sum([self.get_units_confirmed(claim, case_quantity=row.case_quantity)
|
||||||
|
for claim in row.claims])
|
||||||
|
|
||||||
|
def get_units_claimed_received(self, row, case_quantity=None):
|
||||||
|
return sum([self.get_units_received(claim, case_quantity=row.case_quantity)
|
||||||
|
for claim in row.claims])
|
||||||
|
|
||||||
|
def get_units_claimed_damaged(self, row, case_quantity=None):
|
||||||
|
return sum([self.get_units_damaged(claim, case_quantity=row.case_quantity)
|
||||||
|
for claim in row.claims])
|
||||||
|
|
||||||
|
def get_units_claimed_expired(self, row, case_quantity=None):
|
||||||
|
return sum([self.get_units_expired(claim, case_quantity=row.case_quantity)
|
||||||
|
for claim in row.claims])
|
||||||
|
|
||||||
|
def get_units_available(self, row, case_quantity=None):
|
||||||
|
confirmed = self.get_units_confirmed(row, case_quantity=case_quantity)
|
||||||
|
claimed = self.get_units_claimed(row, case_quantity=case_quantity)
|
||||||
|
return confirmed - claimed
|
||||||
|
|
||||||
def update_order_counts(self, purchase, progress=None):
|
def update_order_counts(self, purchase, progress=None):
|
||||||
|
|
||||||
|
@ -234,6 +592,22 @@ class PurchaseBatchHandler(BatchHandler):
|
||||||
self.progress_loop(update, purchase.items, progress,
|
self.progress_loop(update, purchase.items, progress,
|
||||||
message="Updating inventory counts")
|
message="Updating inventory counts")
|
||||||
|
|
||||||
|
def why_not_execute(self, batch):
|
||||||
|
"""
|
||||||
|
This method should return a string indicating the reason why the given
|
||||||
|
batch should not be considered executable. By default it returns
|
||||||
|
``None`` which means the batch *is* to be considered executable.
|
||||||
|
|
||||||
|
Note that it is assumed the batch has not already been executed, since
|
||||||
|
execution is globally prevented for such batches.
|
||||||
|
"""
|
||||||
|
# not all receiving batches are executable
|
||||||
|
if batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||||
|
|
||||||
|
if batch.truck_dump_batch:
|
||||||
|
return ("Can't directly execute batch which is child of a truck dump "
|
||||||
|
"(must execute truck dump instead)")
|
||||||
|
|
||||||
def execute(self, batch, user, progress=None):
|
def execute(self, batch, user, progress=None):
|
||||||
"""
|
"""
|
||||||
Default behavior for executing a purchase batch will create a new
|
Default behavior for executing a purchase batch will create a new
|
||||||
|
@ -247,6 +621,10 @@ class PurchaseBatchHandler(BatchHandler):
|
||||||
return purchase
|
return purchase
|
||||||
|
|
||||||
elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
|
elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||||
|
if self.allow_truck_dump and batch.truck_dump:
|
||||||
|
self.execute_truck_dump(batch, user, progress=progress)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
with session.no_autoflush:
|
with session.no_autoflush:
|
||||||
return self.receive_purchase(batch, progress=progress)
|
return self.receive_purchase(batch, progress=progress)
|
||||||
|
|
||||||
|
@ -261,6 +639,14 @@ class PurchaseBatchHandler(BatchHandler):
|
||||||
|
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
|
def execute_truck_dump(self, batch, user, progress=None):
|
||||||
|
now = make_utc()
|
||||||
|
for child in batch.truck_dump_children:
|
||||||
|
if not self.execute(child, user, progress=progress):
|
||||||
|
raise RuntimeError("Failed to execute child batch: {}".format(child))
|
||||||
|
child.executed = now
|
||||||
|
child.executed_by = user
|
||||||
|
|
||||||
def make_credits(self, batch, progress=None):
|
def make_credits(self, batch, progress=None):
|
||||||
session = orm.object_session(batch)
|
session = orm.object_session(batch)
|
||||||
mapper = orm.class_mapper(model.PurchaseBatchCredit)
|
mapper = orm.class_mapper(model.PurchaseBatchCredit)
|
||||||
|
@ -369,3 +755,20 @@ class PurchaseBatchHandler(BatchHandler):
|
||||||
|
|
||||||
purchase.status = self.enum.PURCHASE_STATUS_RECEIVED
|
purchase.status = self.enum.PURCHASE_STATUS_RECEIVED
|
||||||
return purchase
|
return purchase
|
||||||
|
|
||||||
|
def clone_row(self, oldrow):
|
||||||
|
newrow = super(PurchaseBatchHandler, self).clone_row(oldrow)
|
||||||
|
|
||||||
|
for oldcredit in oldrow.credits:
|
||||||
|
newcredit = model.PurchaseBatchCredit()
|
||||||
|
self.copy_credit_attributes(oldcredit, newcredit)
|
||||||
|
newrow.credits.append(newcredit)
|
||||||
|
|
||||||
|
return newrow
|
||||||
|
|
||||||
|
def copy_credit_attributes(self, source_credit, target_credit):
|
||||||
|
mapper = orm.class_mapper(model.PurchaseBatchCredit)
|
||||||
|
for prop in mapper.iterate_properties:
|
||||||
|
if prop.key not in ('uuid', 'row_uuid'):
|
||||||
|
if isinstance(prop, orm.ColumnProperty):
|
||||||
|
setattr(target_credit, prop.key, getattr(source_credit, prop.key))
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""add receiving.truck_dump_batch
|
||||||
|
|
||||||
|
Revision ID: 6551a4d9ff25
|
||||||
|
Revises: c37154504ae1
|
||||||
|
Create Date: 2018-05-16 10:40:14.855471
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6551a4d9ff25'
|
||||||
|
down_revision = u'c37154504ae1'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import rattail.db.types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
|
||||||
|
# purchase_batch
|
||||||
|
op.add_column('purchase_batch', sa.Column('truck_dump_batch_uuid', sa.String(length=32), nullable=True))
|
||||||
|
op.create_foreign_key(u'purchase_batch_fk_truck_dump_batch', 'purchase_batch', 'purchase_batch', ['truck_dump_batch_uuid'], ['uuid'], use_alter=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
|
||||||
|
# purchase_batch
|
||||||
|
op.drop_constraint(u'purchase_batch_fk_truck_dump_batch', 'purchase_batch', type_='foreignkey')
|
||||||
|
op.drop_column('purchase_batch', 'truck_dump_batch_uuid')
|
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""add purchase_batch.invoice_file
|
||||||
|
|
||||||
|
Revision ID: 92a49edc45c3
|
||||||
|
Revises: 6551a4d9ff25
|
||||||
|
Create Date: 2018-05-16 13:46:31.940108
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '92a49edc45c3'
|
||||||
|
down_revision = u'6551a4d9ff25'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import rattail.db.types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
|
||||||
|
# purchase_batch
|
||||||
|
op.add_column('purchase_batch', sa.Column('invoice_file', sa.String(length=255), nullable=True))
|
||||||
|
op.add_column('purchase_batch', sa.Column('invoice_parser_key', sa.String(length=100), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
|
||||||
|
# purchase_batch
|
||||||
|
op.drop_column('purchase_batch', 'invoice_parser_key')
|
||||||
|
op.drop_column('purchase_batch', 'invoice_file')
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""add purchase shipped counts
|
||||||
|
|
||||||
|
Revision ID: 977a76f259bd
|
||||||
|
Revises: 92a49edc45c3
|
||||||
|
Create Date: 2018-05-16 16:06:13.691091
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '977a76f259bd'
|
||||||
|
down_revision = u'92a49edc45c3'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import rattail.db.types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
|
||||||
|
# purchase_batch_row
|
||||||
|
op.add_column('purchase_batch_row', sa.Column('cases_shipped', sa.Numeric(precision=10, scale=4), nullable=True))
|
||||||
|
op.add_column('purchase_batch_row', sa.Column('units_shipped', sa.Numeric(precision=10, scale=4), nullable=True))
|
||||||
|
|
||||||
|
# purchase_item
|
||||||
|
op.add_column('purchase_item', sa.Column('cases_shipped', sa.Numeric(precision=10, scale=4), nullable=True))
|
||||||
|
op.add_column('purchase_item', sa.Column('units_shipped', sa.Numeric(precision=10, scale=4), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
|
||||||
|
# purchase_item
|
||||||
|
op.drop_column('purchase_item', 'units_shipped')
|
||||||
|
op.drop_column('purchase_item', 'cases_shipped')
|
||||||
|
|
||||||
|
# purchase_batch_row
|
||||||
|
op.drop_column('purchase_batch_row', 'units_shipped')
|
||||||
|
op.drop_column('purchase_batch_row', 'cases_shipped')
|
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""add purchase_batch_row_claim
|
||||||
|
|
||||||
|
Revision ID: b5aa18867ab3
|
||||||
|
Revises: 977a76f259bd
|
||||||
|
Create Date: 2018-05-17 11:43:14.639019
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b5aa18867ab3'
|
||||||
|
down_revision = u'977a76f259bd'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import rattail.db.types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
|
||||||
|
# purchase_batch_row_claim
|
||||||
|
op.create_table('purchase_batch_row_claim',
|
||||||
|
sa.Column('uuid', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('claiming_row_uuid', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('claimed_row_uuid', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('cases_received', sa.Numeric(precision=10, scale=4), nullable=True),
|
||||||
|
sa.Column('units_received', sa.Numeric(precision=10, scale=4), nullable=True),
|
||||||
|
sa.Column('cases_damaged', sa.Numeric(precision=10, scale=4), nullable=True),
|
||||||
|
sa.Column('units_damaged', sa.Numeric(precision=10, scale=4), nullable=True),
|
||||||
|
sa.Column('cases_expired', sa.Numeric(precision=10, scale=4), nullable=True),
|
||||||
|
sa.Column('units_expired', sa.Numeric(precision=10, scale=4), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['claimed_row_uuid'], [u'purchase_batch_row.uuid'], name=u'purchase_batch_row_claim_fk_claimed_row'),
|
||||||
|
sa.ForeignKeyConstraint(['claiming_row_uuid'], [u'purchase_batch_row.uuid'], name=u'purchase_batch_row_claim_fk_claiming_row'),
|
||||||
|
sa.PrimaryKeyConstraint('uuid')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
|
||||||
|
# purchase_batch_row_claim
|
||||||
|
op.drop_table('purchase_batch_row_claim')
|
|
@ -65,6 +65,6 @@ from .batch.handheld import HandheldBatch, HandheldBatchRow
|
||||||
from .batch.inventory import InventoryBatch, InventoryBatchRow
|
from .batch.inventory import InventoryBatch, InventoryBatchRow
|
||||||
from .batch.labels import LabelBatch, LabelBatchRow
|
from .batch.labels import LabelBatch, LabelBatchRow
|
||||||
from .batch.pricing import PricingBatch, PricingBatchRow
|
from .batch.pricing import PricingBatch, PricingBatchRow
|
||||||
from .batch.purchase import PurchaseBatch, PurchaseBatchRow, PurchaseBatchCredit
|
from .batch.purchase import PurchaseBatch, PurchaseBatchRow, PurchaseBatchRowClaim, PurchaseBatchCredit
|
||||||
from .batch.vendorcatalog import VendorCatalog, VendorCatalogRow
|
from .batch.vendorcatalog import VendorCatalog, VendorCatalogRow
|
||||||
from .batch.vendorinvoice import VendorInvoice, VendorInvoiceRow
|
from .batch.vendorinvoice import VendorInvoice, VendorInvoiceRow
|
||||||
|
|
|
@ -34,6 +34,7 @@ from sqlalchemy.ext.declarative import declared_attr
|
||||||
from rattail.db.model import (Base, uuid_column, BatchMixin, BatchRowMixin,
|
from rattail.db.model import (Base, uuid_column, BatchMixin, BatchRowMixin,
|
||||||
PurchaseBase, PurchaseItemBase, PurchaseCreditBase,
|
PurchaseBase, PurchaseItemBase, PurchaseCreditBase,
|
||||||
Purchase, PurchaseItem)
|
Purchase, PurchaseItem)
|
||||||
|
from rattail.db.model.batch import filename_column
|
||||||
from rattail.util import pretty_quantity
|
from rattail.util import pretty_quantity
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,14 +52,19 @@ class PurchaseBatch(BatchMixin, PurchaseBase, Base):
|
||||||
def __table_args__(cls):
|
def __table_args__(cls):
|
||||||
return cls.__batch_table_args__() + cls.__purchase_table_args__() + (
|
return cls.__batch_table_args__() + cls.__purchase_table_args__() + (
|
||||||
sa.ForeignKeyConstraint(['purchase_uuid'], ['purchase.uuid'], name='purchase_batch_fk_purchase'),
|
sa.ForeignKeyConstraint(['purchase_uuid'], ['purchase.uuid'], name='purchase_batch_fk_purchase'),
|
||||||
|
sa.ForeignKeyConstraint(['truck_dump_batch_uuid'], ['purchase_batch.uuid'], name='purchase_batch_fk_truck_dump_batch', use_alter=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
STATUS_OK = 1
|
STATUS_OK = 1
|
||||||
STATUS_UNKNOWN_PRODUCT = 2
|
STATUS_UNKNOWN_PRODUCT = 2
|
||||||
|
STATUS_TRUCKDUMP_UNCLAIMED = 3
|
||||||
|
STATUS_TRUCKDUMP_CLAIMED = 4
|
||||||
|
|
||||||
STATUS = {
|
STATUS = {
|
||||||
STATUS_OK: "ok",
|
STATUS_OK : "ok",
|
||||||
STATUS_UNKNOWN_PRODUCT: "has unknown product(s)",
|
STATUS_UNKNOWN_PRODUCT : "has unknown product(s)",
|
||||||
|
STATUS_TRUCKDUMP_UNCLAIMED : "not yet fully claimed",
|
||||||
|
STATUS_TRUCKDUMP_CLAIMED : "fully claimed by child(ren)",
|
||||||
}
|
}
|
||||||
|
|
||||||
purchase_uuid = sa.Column(sa.String(length=32), nullable=True)
|
purchase_uuid = sa.Column(sa.String(length=32), nullable=True)
|
||||||
|
@ -80,12 +86,35 @@ class PurchaseBatch(BatchMixin, PurchaseBase, Base):
|
||||||
Numeric "mode" for the purchase batch, to indicate new/receiving etc.
|
Numeric "mode" for the purchase batch, to indicate new/receiving etc.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
truck_dump = sa.Column(sa.Boolean(), nullable=True, doc="""
|
invoice_file = filename_column(doc="Base name for the associated invoice file, if any.")
|
||||||
|
|
||||||
|
invoice_parser_key = sa.Column(sa.String(length=100), nullable=True, doc="""
|
||||||
|
The key of the parser used to read the contents of the invoice file.
|
||||||
|
""")
|
||||||
|
|
||||||
|
truck_dump = sa.Column(sa.Boolean(), nullable=True, default=False, doc="""
|
||||||
Flag indicating whether a "receiving" batch is of the "truck dump"
|
Flag indicating whether a "receiving" batch is of the "truck dump"
|
||||||
persuasion, i.e. it does not correspond to a single purchase order but
|
persuasion, i.e. it does not correspond to a single purchase order but
|
||||||
rather is assumed to represent multiple orders.
|
rather is assumed to represent multiple orders.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
truck_dump_batch_uuid = sa.Column(sa.String(length=32), nullable=True)
|
||||||
|
truck_dump_batch = orm.relationship(
|
||||||
|
'PurchaseBatch',
|
||||||
|
remote_side='PurchaseBatch.uuid',
|
||||||
|
doc="""
|
||||||
|
Reference to the "truck dump" receiving batch, for which the current
|
||||||
|
batch represents a single invoice which partially "consumes" the truck
|
||||||
|
dump.
|
||||||
|
""",
|
||||||
|
backref=orm.backref(
|
||||||
|
'truck_dump_children',
|
||||||
|
order_by='PurchaseBatch.id',
|
||||||
|
doc="""
|
||||||
|
List of batches which are "children" of the current batch, which is
|
||||||
|
assumed to be a truck dump.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
|
||||||
class PurchaseBatchRow(BatchRowMixin, PurchaseItemBase, Base):
|
class PurchaseBatchRow(BatchRowMixin, PurchaseItemBase, Base):
|
||||||
"""
|
"""
|
||||||
|
@ -106,6 +135,9 @@ class PurchaseBatchRow(BatchRowMixin, PurchaseItemBase, Base):
|
||||||
STATUS_CASE_QUANTITY_UNKNOWN = 4
|
STATUS_CASE_QUANTITY_UNKNOWN = 4
|
||||||
STATUS_INCOMPLETE = 5
|
STATUS_INCOMPLETE = 5
|
||||||
STATUS_ORDERED_RECEIVED_DIFFER = 6
|
STATUS_ORDERED_RECEIVED_DIFFER = 6
|
||||||
|
STATUS_TRUCKDUMP_UNCLAIMED = 7
|
||||||
|
STATUS_TRUCKDUMP_CLAIMED = 8
|
||||||
|
STATUS_TRUCKDUMP_OVERCLAIMED = 9
|
||||||
|
|
||||||
STATUS = {
|
STATUS = {
|
||||||
STATUS_OK : "ok",
|
STATUS_OK : "ok",
|
||||||
|
@ -114,6 +146,9 @@ class PurchaseBatchRow(BatchRowMixin, PurchaseItemBase, Base):
|
||||||
STATUS_CASE_QUANTITY_UNKNOWN : "case quantity not known",
|
STATUS_CASE_QUANTITY_UNKNOWN : "case quantity not known",
|
||||||
STATUS_INCOMPLETE : "incomplete",
|
STATUS_INCOMPLETE : "incomplete",
|
||||||
STATUS_ORDERED_RECEIVED_DIFFER : "ordered / received differ",
|
STATUS_ORDERED_RECEIVED_DIFFER : "ordered / received differ",
|
||||||
|
STATUS_TRUCKDUMP_UNCLAIMED : "not yet fully claimed",
|
||||||
|
STATUS_TRUCKDUMP_CLAIMED : "fully claimed by child(ren)",
|
||||||
|
STATUS_TRUCKDUMP_OVERCLAIMED : "OVER claimed by child(ren)",
|
||||||
}
|
}
|
||||||
|
|
||||||
item_uuid = sa.Column(sa.String(length=32), nullable=True)
|
item_uuid = sa.Column(sa.String(length=32), nullable=True)
|
||||||
|
@ -126,6 +161,75 @@ class PurchaseBatchRow(BatchRowMixin, PurchaseItemBase, Base):
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseBatchRowClaim(Base):
|
||||||
|
"""
|
||||||
|
Represents the connection between a row(s) from a truck dump batch, and the
|
||||||
|
corresponding "child" batch row which claims it, as well as the claimed
|
||||||
|
quantities etc.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'purchase_batch_row_claim'
|
||||||
|
__table_args__ = (
|
||||||
|
sa.ForeignKeyConstraint(['claiming_row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_row_claim_fk_claiming_row'),
|
||||||
|
sa.ForeignKeyConstraint(['claimed_row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_row_claim_fk_claimed_row'),
|
||||||
|
)
|
||||||
|
|
||||||
|
uuid = uuid_column()
|
||||||
|
|
||||||
|
claiming_row_uuid = sa.Column(sa.String(length=32), nullable=False)
|
||||||
|
claiming_row = orm.relationship(
|
||||||
|
PurchaseBatchRow,
|
||||||
|
foreign_keys='PurchaseBatchRowClaim.claiming_row_uuid',
|
||||||
|
doc="""
|
||||||
|
Reference to the "child" row which is claiming some row from a truck
|
||||||
|
dump batch.
|
||||||
|
""",
|
||||||
|
backref=orm.backref(
|
||||||
|
'truck_dump_claims',
|
||||||
|
cascade='all, delete-orphan',
|
||||||
|
doc="""
|
||||||
|
List of claims which this "child" row makes against rows within a
|
||||||
|
truck dump batch.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
claimed_row_uuid = sa.Column(sa.String(length=32), nullable=False)
|
||||||
|
claimed_row = orm.relationship(
|
||||||
|
PurchaseBatchRow,
|
||||||
|
foreign_keys='PurchaseBatchRowClaim.claimed_row_uuid',
|
||||||
|
doc="""
|
||||||
|
Reference to the truck dump batch row which is claimed by the "child" row.
|
||||||
|
""",
|
||||||
|
backref=orm.backref(
|
||||||
|
'claims',
|
||||||
|
# cascade='all, delete-orphan',
|
||||||
|
doc="""
|
||||||
|
List of claims made by "child" rows against this truck dump batch row.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
cases_received = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
|
Number of cases of product which were ultimately received, and are involved in the claim.
|
||||||
|
""")
|
||||||
|
|
||||||
|
units_received = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
|
Number of units of product which were ultimately received, and are involved in the claim.
|
||||||
|
""")
|
||||||
|
|
||||||
|
cases_damaged = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
|
Number of cases of product which were shipped damaged, and are involved in the claim.
|
||||||
|
""")
|
||||||
|
|
||||||
|
units_damaged = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
|
Number of units of product which were shipped damaged, and are involved in the claim.
|
||||||
|
""")
|
||||||
|
|
||||||
|
cases_expired = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
|
Number of cases of product which were shipped expired, and are involved in the claim.
|
||||||
|
""")
|
||||||
|
|
||||||
|
units_expired = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
|
Number of units of product which were shipped expired, and are involved in the claim.
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
@six.python_2_unicode_compatible
|
@six.python_2_unicode_compatible
|
||||||
class PurchaseBatchCredit(PurchaseCreditBase, Base):
|
class PurchaseBatchCredit(PurchaseCreditBase, Base):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -235,6 +235,14 @@ class PurchaseItemBase(object):
|
||||||
Expected total cost for line item, as of initial order placement.
|
Expected total cost for line item, as of initial order placement.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
cases_shipped = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
|
Number of cases of product which were supposedly shipped by/from the vendor.
|
||||||
|
""")
|
||||||
|
|
||||||
|
units_shipped = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
|
Number of units of product which were supposedly shipped by/from the vendor.
|
||||||
|
""")
|
||||||
|
|
||||||
cases_received = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
cases_received = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
|
||||||
Number of cases of product which were ultimately received.
|
Number of cases of product which were ultimately received.
|
||||||
""")
|
""")
|
||||||
|
|
Loading…
Reference in a new issue