rattail.batch.purchase

Handler for “purchasing” batches

class rattail.batch.purchase.PurchaseBatchHandler(config, **kwargs)[source]

Handler for all “purchasing” batches, regardless of “mode”. The handler must inspect the mode attribute of each batch it deals with, in order to determine which logic to apply. Possible mode values are:

  • rattail.enum.PURCHASE_BATCH_MODE_ORDERING

  • rattail.enum.PURCHASE_BATCH_MODE_RECEIVING

  • rattail.enum.PURCHASE_BATCH_MODE_COSTING

Most of the interface can be seen in the documentation for the BatchHandler class. Additional or overridden attributes and methods provided by this PurchaseBatchHandler class are listed below.

after_add_row(batch, row)[source]

Implements an event hook with the following logic:

If the batch is for “receiving” then various invoice totals are updated, both for the row and batch. In particular:

assign_purchase_order(batch, purchase_key, session=None)[source]

This is mostly to assign the original PO for a new receiving batch.

The purchase order is located (in whatever system) and will be returned. The batch will be updated to include a reference to the PO. Note that which batch attribute is used to store the reference may vary depending on the system where the PO lives.

Returns:

The PO object if found, or None.

execute(batch, user, progress=None)[source]

Performs execution logic for the batch, as follows:

Ordering Mode

A new purchase is created via make_purchase(), and then update_order_counts() is invoked to keep the numbers straight. The new purchase is then returned.

Receiving Mode

If the batch does not yet have a receiving date, that is set to the current date.

If the batch is a truck dump parent, then execute_truck_dump() is invoked.

Otherwise, either a “traditional” receiving, or truck dump child batch is assumed, and receive_purchase() is invoked.

Costing Mode

This assumes an original purchase is attached to the batch. The invoice date for that purchase is updated according to the value in the batch, and the status for the purchase is set to “costed”. The purchase object is returned.

Note

Execution for a “costing” batch has yet to be fully implemented.

execute_truck_dump(batch, user, progress=None)[source]

Fully executes a truck dump parent batch. In reality nothing is done with the data from the parent; instead, each truck dump child batch is simply executed in sequence.

get_purchase_order(session, purchase_key, **kwargs)[source]

Retrieve the PO object represented by purchase_key. The default logic assumes the key is a UUID, and will try to locate the corresponding Purchase instance.

init_batch(batch, progress=None, **kwargs)[source]

If this is a receiving batch, try to assign the original PO for it, by invoking assign_purchase_order().

make_purchase(batch, user, ordered_only=False, progress=None)[source]

Effectively clones the given batch, creating a new Purchase in the Rattail system.

Parameters:

ordered_only – If true, only include rows which have an effective “ordered” quantity. If false (the default) then all rows will be cloned regardless of ordered quantity.

order_row(row, cases=None, units=None, **kwargs)[source]

This method is conceptually similar to receive_row() and, while the latter is more “necessary” than this one is, this method tries to match its style just for consistency. Callers may or may not need this method directly, but are welcome to use it.

Each call to this method must include the row to be updated, as well as the details of the update. These details should reflect “changes” which are to be made, as opposed to “final values” for the row. In other words if a row already has cases_ordered == 1 and the user is ordering a second case, this method should be called like so:

handler.order_row(row, cases=1)

The row will be updated such that cases_ordered == 2; the main point here is that the caller should not specify cases=2 because it is the handler’s job to “apply changes” from the caller. (If the caller speficies cases=2 then the row would end up with cases_ordered == 3.)

See also update_row_quantity() which allows the caller to specify the final values instead.

For “undo” type adjustments, caller can just send a negative amount, and the handler will apply the changes as expected:

handler.order_row(row, cases=-1)

Note that each call must specify either a (non-empty) cases or units value, but not both! If you need to adjust both then you must make two separate calls.

Parameters:
  • row (PurchaseBatchRow) – Batch row which is to be updated with the given order data. The row must exist, i.e. this method will not create a new row for you.

  • cases (Decimal) – Case quantity for the update, if applicable.

  • units (Decimal) – Unit quantity for the update, if applicable.

populate(batch, progress=None)[source]

Fill the batch with initial data, e.g. from data file or existing PO.

A receiving batch which is populated from PO/file will also have its order_quantities_known attribute set to True.

If the batch is a “truck dump child” and does not yet have a receiving date, it is given the same one as the parent batch.

receive_purchase(batch, progress=None)[source]

Update the purchase for the given batch, to indicate received status.

receive_row(row, mode='received', cases=None, units=None, update_credits=True, **kwargs)[source]

This method is arguably the workhorse of the whole process. Callers should invoke it as they receive input from the user during the receiving workflow.

Each call to this method must include the row to be updated, as well as the details of the update. These details should reflect “changes” which are to be made, as opposed to “final values” for the row. In other words if a row already has cases_received == 1 and the user is receiving a second case, this method should be called like so:

handler.receive_row(row, mode='received', cases=1)

The row will be updated such that cases_received == 2; the main point here is that the caller should not specify cases=2 because it is the handler’s job to “apply changes” from the caller. (If the caller speficies cases=2 then the row would end up with cases_received == 3.)

For “undo” type adjustments, caller can just send a negative amount, and the handler will apply the changes as expected:

handler.receive_row(row, mode='received', cases=-1)

Note that each call must specify either a (non-empty) cases or units value, but not both! If you need to adjust both then you must make two separate calls.

Parameters:
  • row (PurchaseBatchRow) – Batch row which is to be updated with the given receiving data. The row must exist, i.e. this method will not create a new row for you.

  • mode (str) –

    Must be one of the receiving modes which are “supported” according to the handler. Possible modes include:

    • 'received'

    • 'damaged'

    • 'expired'

    • 'mispick'

    • 'missing'

  • cases (Decimal) – Case quantity for the update, if applicable.

  • units (Decimal) – Unit quantity for the update, if applicable.

  • expiration_date (date) – Expiration date for the update, if applicable. Only used if mode='expired'.

This method exists mostly to consolidate the various logical steps which must be taken for each new receiving input from the user. Under the hood it delegates to a few other methods:

receiving_find_best_child_row(row, mode, cases, units)[source]

Locate and return the “best match” child row, for the given parent row and receiving update details. The idea here is that the parent row will represent the “receiving” side of things, whereas the child row will be the “ordering” side.

For instance if the update is for say, “received 2 CS” and there are two child rows, one of which is for 1 CS and the other 2 CS, the latter will be returned. This logic is capable of “splitting” a case where necessary, in order to find a partial match etc.

receiving_update_row_attrs(row, mode, cases, units)[source]

Apply a receiving update to the row’s attributes.

Note that this should not be called directly; it is invoked as part of receive_row().

receiving_update_row_child(parent_row, child_row, mode, cases, units, **kwargs)[source]

Update the given child row attributes, as well as the “claim” record which ties it to the parent, as well as any credit(s) which may apply.

Ideally the child row can accommodate the “full” case/unit amounts given, but if not then it must do as much as it can. Note that the child row should have been located via receiving_find_best_child_row() and therefore should be able to accommodate something at least.

This method returns a 2-tuple of (cases, units) which reflect the amounts it was not able to claim (or relinquish, if incoming amounts are negative). In other words these are the “leftovers” which still need to be dealt with somehow.

receiving_update_row_children(row, mode, cases, units, **kwargs)[source]

Apply a receiving update to the row’s “children”, if applicable.

Note that this should not be called directly; it is invoked as part of receive_row().

This logic only applies to a “truck dump parent” row, since that is the only type which can have “children”. Also this logic is assumed only to apply if using the “children first” workflow. If these criteria are not met then nothing is done.

This method is ultimately responsible for updating “everything” (relevant) about the children of the given parent row. This includes updating the child row(s) as well as the “claim” records used for reconciliation, as well as any child credit(s). However most of the heavy lifting is done by receiving_update_row_child().

receiving_update_row_credits(row, mode, cases, units, **kwargs)[source]

Apply a receiving update to the row’s credits, if applicable.

Note that this should not be called directly; it is invoked as part of receive_row().

refresh(batch, progress=None)[source]

Supplements the default logic as follows:

First, the default refresh logic runs. And if the batch is not part of a truck dump, nothing more happens. But if it is truck dump…

Basically whether the given batch is a truck dump parent, or truck dump child batch, the goal here is the same. We must locate any rows in the given batch which are not yet “fully claimed / complete” with regard to the “other” (parent/child) batch.

For any rows which are not yet fully resolved between parent and child, an attempt is then made to add new row “claims” where possible, to eliminate the gap.

The status of the given batch is then updated. If the batch is a truck dump child then the status of its parent batch will also be updated. If the given batch is truck dump parent, then status for each of its children will also be updated. See refresh_batch_status() for more on that.

refresh_batch_status(batch)[source]

Logic for updating the status attribute(s) for the given batch.

This primarily tries to see if there are any “unknown” items in the batch, and set status accordingly if some are found.

But it also is responsible for setting “truck dump” status, for a truck dump parent batch, based on whether or not its children have fully claimed all of its items etc.

refresh_row(row, **kwargs)[source]

Refreshing a row will A) assume that row.product is already set to a valid product, or else will attempt to locate the product, and B) update various other fields on the row (description, size, etc.) to reflect the current product data. It also will adjust the batch PO total per the row PO total.

remove_row(row)[source]

Overrides the default logic as follows:

In all cases, the row is deleted outright from the batch, instead of simply marking its removed flag. Then refresh_batch_status() is invoked.

However, before those things happen, we may do some other steps based on the batch mode:

Ordering Mode

If the row has a po_total_calculated amount, then the batch’s po_total_calculated is decreased by that amount.

Receiving Mode

If the row has a invoice_total_calculated amount, then the batch’s invoice_total_calculated is decreased by that amount.

should_populate(batch)[source]

Must populate when e.g. making new receiving batch from PO or invoice, but otherwise not, e.g. receiving from scratch.

update_order_counts(purchase, progress=None)[source]

Update the “on order” counts for all items on the given purchase. Obviously this assumes that the purchase was just “ordered” from the vendor.

update_row_cost(row, **kwargs)[source]

Update the cost value(s) for the given row, and calculate new totals accordingly. This will handle updating the row as well as the batch, as necessary.

Note that thus far, it is assumed the given row is for a “receiving” batch, and the logic does not provide special handling for truck dump. The only cost kwargs supported are:

  • catalog_unit_cost

  • invoice_unit_cost

update_row_quantity(row, **kwargs)[source]

Update quantity value(s) for the given row, and calculate new totals accordingly. This will handle updating the row as well as the batch, as necessary. Which kwargs this method accepts, and which values are updated, will depend on the batch mode.

Ordering Mode

Possible kwargs are:

  • cases_ordered

  • units_ordered

Logic will figure out the “diff” between the given quantites, and the row’s existing values at the time. It then invokes order_row() with the diff values.

why_not_execute(batch, **kwargs)[source]

This makes the following checks, but only for “receiving” batches:

If it is a truck dump parent batch, then its truck_dump_status must be “claimed” or else execution is not allowed. This is for simplicity, to require the truck dump parent and child batches to be “fully” on the same page, and nothing accidentally left behind.

If it is a truck dump child, then execution is “never” allowed. (At least, that’s what we want to tell the user, so they’re forced to execute the parent batch. Technically the handler does know how to execute a child batch; see execute() and execute_truck_dump() for more info.)