1 (edited by boxygen 06/01/2019 12:05:54 pm)

Topic: Anomaly created by Cost Update in Inventory Sales Report (rep304)

When Cost Update is executed it adjusts all previous transactions' standard_cost in stock_moves table. WHY? Any Idea?

Due to this the Inventory Sales Report is adversely affected because the Cost Column in this report is being picked from stock_moves table.

Hence the Contribution Margin of all Previous Entries are also affected.

Cost Update shall affect all future entries not previous entries.

In rep304 instead of taking standard_cost from the stock_moves table we can pick it from debtor_trans_detail table. I have modified the report as below

<?php
/**********************************************************************
    Copyright (C) FrontAccounting, LLC.
    Released under the terms of the GNU General Public License, GPL,
    as published by the Free Software Foundation, either version 3
    of the License, or (at your option) any later version.
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
***********************************************************************/
$page_security = 'SA_SALESANALYTIC';
// ----------------------------------------------------------------
// $ Revision:    2.0 $
// Creator:    Joe Hunt
// date_:    2005-05-19
// Title:    Inventory Sales Report
// ----------------------------------------------------------------
$path_to_root="..";

include_once($path_to_root . "/includes/session.inc");
include_once($path_to_root . "/includes/date_functions.inc");
include_once($path_to_root . "/includes/data_checks.inc");
include_once($path_to_root . "/includes/banking.inc");
include_once($path_to_root . "/gl/includes/gl_db.inc");
include_once($path_to_root . "/inventory/includes/db/items_category_db.inc");

//----------------------------------------------------------------------------------------------------

print_inventory_sales();

function getTransactions($category, $location, $fromcust, $from, $to, $show_service)
{
    $from = date2sql($from);
    $to = date2sql($to);

    $sql = "SELECT item.category_id,
            category.description AS cat_description,
            item.stock_id,
            item.description, item.inactive,
            item.mb_flag,
            move.loc_code,
            trans.debtor_no,
            debtor.name AS debtor_name,
            move.tran_date,
            SUM(-move.qty) AS qty,
            SUM(-move.qty*move.price) AS amt,
            -- SUM(-IF(move.standard_cost <> 0, move.qty * move.standard_cost, move.qty *item.material_cost)) AS cost
            SUM(-(move.qty *td.standard_cost)) AS cost
        FROM ".TB_PREF."stock_master item,
            ".TB_PREF."stock_category category,
            ".TB_PREF."debtor_trans trans,
            ".TB_PREF."debtor_trans_details td,
            ".TB_PREF."debtors_master debtor,
            ".TB_PREF."stock_moves move
        WHERE item.stock_id=move.stock_id
        AND item.category_id=category.category_id
        AND trans.debtor_no=debtor.debtor_no
        AND trans.trans_no = td.debtor_trans_no
        AND item.stock_id = td.stock_id
        AND trans.type = td.debtor_trans_type
        AND move.type=trans.type
        AND move.trans_no=trans.trans_no
        AND move.tran_date>='$from'
        AND move.tran_date<='$to'
        AND (trans.type=".ST_CUSTDELIVERY." OR move.type=".ST_CUSTCREDIT.")";

    if (!$show_service)
        $sql .= " AND (item.mb_flag='B' OR item.mb_flag='M')";
    else
        $sql .= " AND item.mb_flag<>'F'";
    if ($category != 0)
        $sql .= " AND item.category_id = ".db_escape($category);

    if ($location != '')
        $sql .= " AND move.loc_code = ".db_escape($location);

    if ($fromcust != '')
        $sql .= " AND debtor.debtor_no = ".db_escape($fromcust);

    $sql .= " GROUP BY item.stock_id, debtor.name ORDER BY item.category_id,
        item.stock_id, debtor.name";

    return db_query($sql,"No transactions were returned");

}

//----------------------------------------------------------------------------------------------------

function print_inventory_sales()
{
    global $path_to_root;

    $from = $_POST['PARAM_0'];
    $to = $_POST['PARAM_1'];
    $category = $_POST['PARAM_2'];
    $location = $_POST['PARAM_3'];
    $fromcust = $_POST['PARAM_4'];
    $show_service = $_POST['PARAM_5'];
    $comments = $_POST['PARAM_6'];
    $orientation = $_POST['PARAM_7'];
    $destination = $_POST['PARAM_8'];
    if ($destination)
        include_once($path_to_root . "/reporting/includes/excel_report.inc");
    else
        include_once($path_to_root . "/reporting/includes/pdf_report.inc");

    $orientation = ($orientation ? 'L' : 'P');
    $dec = user_price_dec();

    if ($category == ALL_NUMERIC)
        $category = 0;
    if ($category == 0)
        $cat = _('All');
    else
        $cat = get_category_name($category);

    if ($location == '')
        $loc = _('All');
    else
        $loc = get_location_name($location);

    if ($fromcust == '')
        $fromc = _('All');
    else
        $fromc = get_customer_name($fromcust);
    if ($show_service) $show_service_items = _('Yes');
    else $show_service_items = _('No');

    $cols = array(0, 75, 175, 250, 300, 375, 450,    515);

    $headers = array(_('Category'), _('Description'), _('Customer'), _('Qty'), _('Sales'), _('Cost'), _('Contribution'));
    if ($fromcust != '')
        $headers[2] = '';

    $aligns = array('left',    'left',    'left', 'right', 'right', 'right', 'right');

    $params =   array(     0 => $comments,
                        1 => array('text' => _('Period'),'from' => $from, 'to' => $to),
                        2 => array('text' => _('Category'), 'from' => $cat, 'to' => ''),
                        3 => array('text' => _('Location'), 'from' => $loc, 'to' => ''),
                        4 => array('text' => _('Customer'), 'from' => $fromc, 'to' => ''),
                        5 => array('text' => _('Show Service Items'), 'from' => $show_service_items, 'to' => ''));

    $rep = new FrontReport(_('Inventory Sales Report'), "InventorySalesReport", user_pagesize(), 9, $orientation);
       if ($orientation == 'L')
        recalculate_cols($cols);

    $rep->Font();
    $rep->Info($params, $cols, $headers, $aligns);
    $rep->NewPage();

    $res = getTransactions($category, $location, $fromcust, $from, $to, $show_service);
    $total = $grandtotal = 0.0;
    $total1 = $grandtotal1 = 0.0;
    $total2 = $grandtotal2 = 0.0;
    $catt = '';
    while ($trans=db_fetch($res))
    {
        if ($catt != $trans['cat_description'])
        {
            if ($catt != '')
            {
                $rep->NewLine(2, 3);
                $rep->TextCol(0, 4, _('Total'));
                $rep->AmountCol(4, 5, $total, $dec);
                $rep->AmountCol(5, 6, $total1, $dec);
                $rep->AmountCol(6, 7, $total2, $dec);
                $rep->Line($rep->row - 2);
                $rep->NewLine();
                $rep->NewLine();
                $total = $total1 = $total2 = 0.0;
            }
            $rep->TextCol(0, 1, $trans['category_id']);
            $rep->TextCol(1, 6, $trans['cat_description']);
            $catt = $trans['cat_description'];
            $rep->NewLine();
        }

        $curr = get_customer_currency($trans['debtor_no']);
        $rate = get_exchange_rate_from_home_currency($curr, sql2date($trans['tran_date']));
        $trans['amt'] *= $rate;
        $cb = $trans['amt'] - $trans['cost'];
        $rep->NewLine();
        $rep->fontSize -= 2;
        $rep->TextCol(0, 1, $trans['stock_id']);
        if ($fromcust == ALL_TEXT)
        {
            $rep->TextCol(1, 2, $trans['description'].($trans['inactive']==1 ? " ("._("Inactive").")" : ""), -1);
            $rep->TextCol(2, 3, $trans['debtor_name']);
        }
        else
            $rep->TextCol(1, 3, $trans['description'].($trans['inactive']==1 ? " ("._("Inactive").")" : ""), -1);
        $rep->AmountCol(3, 4, $trans['qty'], get_qty_dec($trans['stock_id']));
        $rep->AmountCol(4, 5, $trans['amt'], $dec);
        if (is_service($trans['mb_flag']))
            $rep->TextCol(5, 6, "---");
        else
            $rep->AmountCol(5, 6, $trans['cost'], $dec);
        $rep->AmountCol(6, 7, $cb, $dec);
        $rep->fontSize += 2;
        $total += $trans['amt'];
        $total1 += $trans['cost'];
        $total2 += $cb;
        $grandtotal += $trans['amt'];
        $grandtotal1 += $trans['cost'];
        $grandtotal2 += $cb;
    }
    $rep->NewLine(2, 3);
    $rep->TextCol(0, 4, _('Total'));
    $rep->AmountCol(4, 5, $total, $dec);
    $rep->AmountCol(5, 6, $total1, $dec);
    $rep->AmountCol(6, 7, $total2, $dec);
    $rep->Line($rep->row - 2);
    $rep->NewLine();
    $rep->NewLine(2, 1);
    $rep->TextCol(0, 4, _('Grand Total'));
    $rep->AmountCol(4, 5, $grandtotal, $dec);
    $rep->AmountCol(5, 6, $grandtotal1, $dec);
    $rep->AmountCol(6, 7, $grandtotal2, $dec);

    $rep->Line($rep->row  - 4);
    $rep->NewLine();
    $rep->End();
}

Here is the Comparison of InventorySales Report Before and After Modification

https://prnt.sc/nw9n9t


PS: The Affect of Cost Update in Standard Cost of stock_move table is explained here

https://prnt.sc/nvkezx

Post's attachments

InvSales_StdCostUpd.zip 508 kb, 3 downloads since 2019-06-04 

You don't have the permssions to download the attachments of this post.
www.boxygen.pk

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

When we record Customer Credit Note, the standard cost is not being updated in debtor_trans_details table.

Please add this line at Line # 117 on /sales/includes/db/sales_credit_db.inc

        $credit_line->standard_cost = get_unit_cost($credit_line->stock_id); //Added by Faisal 

I request other senior members specially @joe and @jansuz to check this Cost Update anomaly being created.

Regards.

www.boxygen.pk

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

Any Comment? Feedback? May be I am taking this as Anomaly but any explanation for this behaviour of Cost Update would be grateful.

www.boxygen.pk

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

Yes, you are right, the transaction update was improperly added some times ago. I have rolled it back. Also the other your fix has been added to the repo. Thank you for poitning this problems.
J.

5 (edited by boxygen 06/15/2019 07:22:45 pm)

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

Thanks @itronics, I had some more observations but I held them because of no feedback.

In stock_moves table we have 2 fields price and standard_cost.

Ideally price is the factor affecting the resulting standard_cost. But this is not happening for transactions done through Inventory Adjustment.

Ideally Inventory Adjustment shall always be done at Prevailing Avg Cost of the Item but since the Input Field is open for Unit Cost on this page /inventory/adjustments.php?NewAdjustment=1 and if user input different Unit Cost then Prevailing then it affects the Avg Cost.

For e.g Item A's Avg Cost in the system is 20 with QOH=40 and while recording Adjustment the User Inputs 30 for +10 quantity then Price Affecting the Avg Cost is 30 while Resulting Avg Cost is 22

In this scenario currently in FA the standard_cost field is updated with 30 instead of 22 while price field is updated with 1 instead of 30.

I think this should be Fixed.

POST FIX PROBLEM

In rep301 and rep308 the following function

function get_domestic_price($myrow, $stock_id)
{
    if ($myrow['type'] == ST_SUPPRECEIVE || $myrow['type'] == ST_SUPPCREDIT)
    {
        $price = $myrow['price'];
        if ($myrow['person_id'] > 0)
        {
            // Do we have foreign currency?
            $supp = get_supplier($myrow['person_id']);
            $currency = $supp['curr_code'];
            $ex_rate = get_exchange_rate_to_home_currency($currency, sql2date($myrow['tran_date']));
            $price /= $ex_rate;
        }    
    }
    else
        $price = $myrow['standard_cost']; // Item Adjustments just have the real cost
    return $price;
}    

returns standard_cost for inventory adjustment transactions.

This can be changed as follows

function get_domestic_price($myrow, $stock_id)
{
        $price = $myrow['price'];
        if ($myrow['person_id'] > 0)
        {
            // Do we have foreign currency?
            $supp = get_supplier($myrow['person_id']);
            $currency = $supp['curr_code'];
            $ex_rate = get_exchange_rate_to_home_currency($currency, sql2date($myrow['tran_date']));
            $price /= $ex_rate;
        }    
    return $price;
}    

In both reports this function returns the Price Affecting the Avg Cost.

If we do this than these two fields will become a Log of Changing Avg Cost with Price Affecting it.

www.boxygen.pk

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@boxygen.
This has been fixed and pushed to stable repo. Thanks for helping with this.

/Joe

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

There are at lest two fixes still needed:
. changing item cost in Cost Update has no track in stock_moves;
. the price is in transaction currency, but there is lack  of currency exchange rate recorded in stock_stock_moves, so logging in stock_moves is unreliable (you can at any time change currency exchange rate in exchange_rates table);

We will refine the logging feature of stock_moves in the next days, however the changes will go probably to unstable branch. for next 2.5 release.

J.

8 (edited by boxygen 06/16/2019 10:22:56 am)

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@joe,
This commit needs one more commit in the function add_stock_adjustment_item on /inventory/includes/db/items_adjust_db.inc as follows

function add_stock_adjustment_item($adj_id, $stock_id, $location, $date_, $reference,
    $quantity, $price, $memo_)
{
    $mb_flag = get_mb_flag($stock_id);
    if (is_service($mb_flag))
    {
        display_db_error("Cannot do inventory adjustment for Service item : $stock_id", "");
    }
    update_average_material_cost(null, $stock_id, $price, $quantity, $date_);
    if (is_fixed_asset($mb_flag)) {
        $sql = "UPDATE ".TB_PREF."stock_master SET inactive=1
            WHERE stock_id=".db_escape($stock_id);
        db_query($sql,"The inactive flag for the fixed asset could not be updated");
    }
  $standard_cost = get_unit_cost($stock_id); //added by faisal
    add_stock_move(ST_INVADJUST, $stock_id, $adj_id, $location,
        $date_, $reference, $quantity, $standard_cost, $price);
    $inv_value = $price * $quantity;
    $adj_value = $price * -($quantity);
    if (is_fixed_asset($mb_flag)) {
        // get the initial value of the fixed assset.
        $row = get_fixed_asset_move($stock_id, ST_SUPPRECEIVE);
        $inv_value = $row['price'] * $quantity;
        $adj_value = (-($row['price']) + $price) * $quantity;
    }
    if ($price > 0 || is_fixed_asset($mb_flag))
    {
        $stock_gl_codes = get_stock_gl_code($stock_id);
        add_gl_trans_std_cost(ST_INVADJUST, $adj_id, $date_,
            $stock_gl_codes['adjustment_account'], $stock_gl_codes['dimension_id'], $stock_gl_codes['dimension2_id'], $memo_, $adj_value  );
        add_gl_trans_std_cost(ST_INVADJUST, $adj_id, $date_, $stock_gl_codes['inventory_account'], 0, 0, $memo_, $inv_value);
    }
    if (is_fixed_asset($mb_flag)) {
        // Additional gl entry for fixed asset.
        $grn_act = get_company_pref('default_loss_on_asset_disposal_act');
        add_gl_trans_std_cost(ST_INVADJUST, $adj_id, $date_, $grn_act, 0, 0, $memo_, ($price * -($quantity)));
    }
}

FOR LEGACY DATA ACCURACY

In order to keep rep301 and rep308 workable with older data than this commit one update query needs to be executed to bring standard_cost values in the price field for all transactions through inventory_adjustment.

UPDATE stock_moves SET price=standard_cost WHERE type=17

WORKAROUND FOR COST UPDATE


@itronics for logging Cost Update tracking in stock_moves I have a workaround solution. May be you endorse it. Just sharing the idea. If approved I can work on code.

We can execute 2 Inventory Adjustments per location for that particular item when cost update is executed.

#1. This will bring the QOH to ZERO even for -ve inventory on the prevailing cost price in the system.
#2. This will bring the QOH on the same position either +ve or -ve at the New Cost give through Cost Update.

With this workaround we will be able to bring correct results in rep301 and rep308 as well.

EXCHANGE RATE AFFECT

In this function get_domestic_price the exchange rate factor is applied when person_id > 0. But for Inventory Adjustments person_id is always 0

However you are right that there shall be a column to record Exchange Rate of that transaction so that any change in exchange_rates shall not affect the transaction.

www.boxygen.pk

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@boxygen,
Yes, this is how I plan to implement cost update logging.

Regarding database update for inventory adjustment, we will have to add this in upgrade procedure to release 2.5. We do not have updating procedure for minor releases, so implementing this in 2.4.* would require run the query outside user's awareness. Skilled user suffered with this problem can run the query manually anyway.

J.

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

Thanks for your endorsement. I agree with your plan

www.boxygen.pk

11 (edited by boxygen 06/16/2019 09:19:16 pm)

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@joe,

function get_domestic_price needs some more adjustments. But it requires changes in post#8 to be committed first.

function get_domestic_price($myrow, $stock_id)
{
    if ($myrow['type'] == ST_SUPPRECEIVE || $myrow['type'] == ST_SUPPCREDIT || $myrow['type'] == ST_INVADJUST)
     {
        $price = $myrow['price'];
        if ($myrow['person_id'] > 0)
        {
            // Do we have foreign currency?
            $supp = get_supplier($myrow['person_id']);
            $currency = $supp['curr_code'];
            $ex_rate = $myrow['ex_rate'];
            $price *= $ex_rate;
        }
    }
    else
        $price = $myrow['standard_cost']; //pick standard_cost for sales deliveries

    return $price;
}

Explanation:

1. We need price Value only for ST_SUPPRECEIVE, ST_SUPPCREDIT AND ST_INVADUST. For Sales Deliveries we need the standard_cost for Average cost calculation in rep301, rep308.

2. $price *= $ex_rate is currently $price /= $ex_rate which is a logical mistake.

3. We can pick Exchange Rate directly from Supplier Transaction for GRN and Supplier Credit Note. For this we need some more changes in rep301 and rep308

Following partial code in the SQL Query shall be replaced with

IF(ISNULL(supplier.supplier_id), debtor.debtor_no, supplier.supplier_id) person_id

This code

supplier.supplier_id person_id, IF(ISNULL(grn.rate), credit.rate, grn.rate) ex_rate

Explanation
1. We need person_id only for suppliers to get exchange rate of the transaction in get_domestic_price function
2. ex_rate is directly picked from either GRN or Supplier Credit


I have tested all this after creating a fresh item and executing all type of transactions on it as in this image

There is one understanding that ST_CUSTCREDIT transactions will always pick the prevailing unit_cost.

Regards.

www.boxygen.pk

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@boxygen

Should we change back to the former functions, get_domestic_price, in rep301 and rep308, but wich these changed rows?
And post #8 should be changed? and the wrongly calculated domestic price?

/Joe

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@joe,

Should we change back to the former functions, get_domestic_price, in rep301 and rep308, but wich these changed rows?

You can do either, or but I think better keep the one in post # 11. I have tested it. It has changed conditions as well as some removal of logical bugs and

And post #8 should be changed? and the wrongly calculated domestic price?

Post #8 should be committed otherwise domestic_price will not calculate correctly.

With my finally suggested codes in post#8 and post#11 domestic_price is calculating correctly the average cost.

Regards.

www.boxygen.pk

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@boxygen.

I think I can follow you now. I will just test this tomorrow to ensure that it is right. If the reports take the domestic price from the underlying ex_rate values in the grn and sub credit transaction lines.

Joe

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@boxygen.

I have tested your changes in /inventory/includes/db/items_adjust_db.inc, /reporting/rep301.php and /reporting/rep308.php.
And the test passed ok.
These changes has been committed to stable repo.
Do you have more files to change, @boxygen, because we plan to ship a minor release 2.4.7 next week?
Major changes will wait to the upcoming release 2.5 unstable that also are about to ship.

/Joe

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@boxygen.

Slightly update of add_stock_adjustment_item by @itronics. Committed to stable repo. Only keeping the parameter names.

/Joe

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@joe, not for this topic now. But I have few suggestions for 2.5. This post https://frontaccounting.com/punbb/viewtopic.php?id=8137 doesn't allow us to post. Where can we post for suggestions for 2.5.

www.boxygen.pk

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@boxygen

Please try again. For some reason you were not upgraded internally as Senior Member. Even if it was shown.

Joe

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

It would be nice to preserve the whitespace in the commits so that effective changes can be discerned. The whitespace cleanup can be in a separate commit.

The recent changes on 2019-06-17 (1, 2) have been assimilated in my repo separating the actual changes and the whitespace synch.

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@boxygen
I have to reopen this thread for a moment.

Finally to maintain database consistency on existing installations I have reverted the changes in add_stock_adjustment_item() and related fixes in reports rep301, rep308.  We will address all this in forthgoing 2.5 release. We plan to change stock_moves table structure to better upport inventory cost tracking. All comments regarding this feature are welcome  in this thread.

As far as I understand, after revertion all should still work as expected, as suggested changes where mostly technical. If I'm wrong, please do not hesitate to reopen the thread and explain the lasting problems (if any) once again.
J.

Re: Anomaly created by Cost Update in Inventory Sales Report (rep304)

@itronics,  yes database consistency requires some update queries to run.

I agree with you. I will check in detail later

www.boxygen.pk