Increase and decrease in stock quantity

We implemented simple create and save in Create and save invoices, but it does not satisfy the requirements. Let's add them one by one.

In this chapter, you can learn about the following contents:

Mechanism of increase and decrease

The stock number decreases when the product are sold, and increases with arrival or returns. This example does not have arrival process, we will implement sales, returns and deleting invoice.

It is linked with saving and deleting of InvoiceItem. Let's write increase/decrease processes. After that we call it from event handers of InvoiceItem.

Define support class InvoiceItemSaveHandler.

class InvoiceItemSaveHandler
{
}

Safe and fast processing with server cursor

onSaveRow decreases the stock number if this row is sales line. If there is a stock record of the product, this method updates it. Otherwise, this method save a new record. It also updates the object's cache.

Use serverCursor for fast searching and saving.

class InvoiceItemSaveHandler
{
    public function onSaveRow(InvoiceItem $row)
    {
        if ($row->line_type === InvoiceItem::SALES) {
            // Get server cursor
            $it = Stock::keyValue($row->product_code)->serverCursor();

            // Check whether it can be read
            if ($it->valid()) {
                // If there is a stock record of the product, get Stock object.
                // This object is the most recent and locked by transaction on the server.
                $stock = $it->current();
                $stock->quantity -= $row->quantity;
                $it->update($stock);
            } else {
                // If there is no stock record of the product, create Stock object.
                $stock = new Stock(['code' => $row->product_code]);
                $stock->quantity = 0 - $row->quantity;
                $it->insert($stock);
            }

            // Update Stock cache
            $stock->updateCache();
        }
    }
}

Note: Server cursor is very powerful and easy to use.

In transaction, the current record of the server cursor is locked, so its value is the most recent. Increasing or decreasing the value can safely be updated with this value.

Out of transaction, reading does not lock any records without $lockBias parameter.

Define onDeleteRow in the same way.

class InvoiceItemSaveHandler
{
    // (omitted)

    public function onDeleteRow(InvoiceItem $row)
    {
        if ($row->line_type === InvoiceItem::SALES) {
            $it = Stock::keyValue($row->product_code)->serverCursor();

            // The stock record of the product must exists when the invoice is deleted.
            // If there is no records, throw exception.
            $it->validOrFail();

            // This object is the most recent and locked by transaction on the server.
            $stock = $it->current();
            $stock->quantity += $row->quantity;
            $it->update($stock);
            $stock->updateCache();
        }
    }
}

Binding event handler

Define save and delete handlers on InvoiceItem and call above methods.

Define hanlders

In this case, we use handlers saving and deleting. If you define following methods, they will be called automatically when saving or deleting.

class InvoiceItem extends Model
{
    // (omitted)

    public static function deleting(InvoiceItem $row)
    {
        return true;
    }

    public static function saving(InvoiceItem $row)
    {
        return true;
    }
}

Add some process

These handlers are static method. We need to them to be able to use InvoiceItemSaveHandler instance. Define a static variable and set instance to it before using. Then we can call onDeleteRow or onSaveRow.

class InvoiceItem extends Model
{
    // (omitted)

    public static $handler = null;

    public static function deleting(InvoiceItem $row)
    {
        self::$handler->onDeleteRow($row);
        return true;
    }

    public static function saving(InvoiceItem $row)
    {
        self::$handler->onSaveRow($row);
        return true;
    }
}

Note: Although you can write the increase or decrease process directly in deleting or saving handler, we made it to another class in this case. Because it is an event process which spans two classes of InvoiceItem and Stock.

This is also useful when you want to add related processing with another class.

The finishing touch

Override save and delete in Invoice, to create InvoiceItemSaveHandler instance and set it to InvoiceItem::$handler.

class Invoice extends Model
{
    // (omitted)

    public function save($options = 0, $forceInsert = false)
    {
        InvoiceItem::$handler = new InvoiceItemSaveHandler();
        return parent::save($options, $forceInsert);
    }

    public function delete($options = null)
    {
        InvoiceItem::$handler = new InvoiceItemSaveHandler();
        return parent::delete($options);
    }

The reason to create an instance of the handler class each time is that the handler will hold member variables in the next step. It is created every time to prevent bugs such as invalid initialization of member variables.

Now, the stock will be increased or decreased automatically when you call $invoice->saveWithTransaction().

Source code

The source code of this chapter can be downloaded from GitHub Gist.

  1. Create and save invoices
  2. Calculate and save sales invoices