简体   繁体   中英

Calculate the exact margin from sales orders and purchase orders

I am trying to generate a report which calculates the margin from the below database. The problem is that the cost (existing in purchase_order_products table) of the product may change.

The cost of product with id 4022 on 2017-06-08 is 1110, however its cost is 1094 on 2017-07-25. This is confusing. I am unable to get the exact cost for each product sold.

I wrote a PHP algorithm which loops through all orders and purchase orders and used the oldest cost to newest cost. but the algorithm has a very high time complexity. Can this be done just using mysql query?

Please check below scenario:

Company created a purchase order for product X: quantity 3, cost 10. on day 1

Customers bought 2 product X sell price: 12 on day 1 (still have 1 item in inventory with cost 10)

Company created a purchase order for product X : quantity 4, cost 9. on day 2

Customers bought 3 product X sell price: 12 on day 2

Customers bought 2 product X sell price: 12 on day 3

Company created a purchase order for product X : quantity 2, cost 11. on day 3

Customers bought 2 product X sell price: 12 on day 3

The report:

day 1: sold 2 product X for 12 , cost 10 , profit: 2 * (12 - 10)

day 2: sold 3 product X for 12 , 1 item has a cost of 10, 2 items have a cost of 9 ,

profit: 1 * (12 - 10) + 2 * (12 - 9)

day 3: sold 2 product X for 12 , cost 9 , profit: 2 * (12 - 9)

sold 2 product X for 12 , cost 11 , profit: 2 * (12 - 11)

Therefor the profit of newly sold products is calculated using the their corresponding cost. Hope you got my point.

Databse Structure: 数据库表

4 Products From Database

在此输入图像描述

Products Purchase orders for the above products

在此输入图像描述

Sold Products 在此输入图像描述

Dump File Attached here

Why don't you just take it easy and add a profit column to the orders table that is calculated in real time when a customer buys a product.This way you can calculate your marging solely from the sales orders given the fact that this is actualy already calculated somehow in order to generate the selling price. Of course this will work only for future sales but you can use your existing code with little modification to populate the profit column for old records and you will run this code only one time for the old transactions before the update.
To elaborate more:
Alter the table "sales_order" adding "profit" column . This way you can calculate the sum using the other related columns (total_paid, total_refund, total_due, grand_total) because you may want have more control over the report by including those monetary fields as needed in your calculation for example generating a report using total_payed only excluding tota_due or encluding it for different type of reports, in other words you can generate multiple reports types only from this table without overwhelming the DB system by adding this one column only.
Edit:
You can also add a cost column to this table for fast retrieving purpose and minimize joins and queries to other tables and if you want to take it a step further you can add a dedicated table for reports and it will be very helpful for example to generate a missing report from last month and checking old order status.

Some disclaimers:

  • this is an attempt to assist with the logic, so it's rough code(open to SQL injection attacks, so don't copy and paste this)
  • I can't test this query so there's probably mistakes in it, just trying to get you on the right track (and/or will make follow up edits)
  • This won't work if you need profit per order, only for profit per product. You could probably get a date range with a BETWEEN clause if needed.

That being said, I think something like this should work for you:

    $productsIds = array('4022', '4023', '4160', '4548', '4601');
    foreach($productIds as $pid){
        $sql = "SELECT (soi.revenue - sum(pop.cost)) AS profit, sum(pop.cost) AS total_cost, sum(pop.quantity) AS total_purchased, soi.revenue, soi.total_sold 
                    FROM purchase_order_products pop 
                    JOIN (SELECT sum(price) AS revenue, sum(quantity_ordred) AS total_sold FROM sales_order_item WHERE product_id = ".$pid.") AS soi ON soi.product_id = pop.product_id
                    WHERE pop.product_id = ".$pid." GROUP BY pop.product_id HAVING sum(pop.quantity) < soi.total_sold ORDER BY pop.created_at ASC;";
        $conn->query($sql);
        //do what you want with results
    }

The key thing here is using the HAVING clause after GROUP BY to determine where you cut off finding the sum of the purchase costs. You can sum them all as long as they're within that range, and you get the right dates ordering by created_at.

Again, I can't test this, and I wouldn't recommend using this code as is, just hoping this helps from a "here's a general idea of how to make this happen".

If I had time to recreate your databases I would, or if you provide sql dump files with example data, I could try to get you a working example.

The price of an object on a given time is given by the formula : "total price of the stock / total number in the stock".

To get this, you have two queries to execute :

  • The first one to know the amount of sold items (total price and quantities) :

sql:

SELECT SUM(row_total) sale_total_cost, SUM(quantity_ordered) sale_total_number
    FROM sales_order_item soi
    JOIN sales_order so ON soi.sales_order_id=so.id
    WHERE so.purchase_date<'2017-06-07 15:03:30'
    AND soi.product_id=4160;
  • the second one to know how much you have bought the products

sql:

SELECT SUM(pop.cost * pop.quantity) purchase_total_price, SUM(pop.quantity) purchase_total_number
    FROM purchase_order_products pop
    JOIN purchase_order po ON pop.purchase_order_id=po.id
    WHERE po.created_at<'2017-06-07 15:03:30'
        AND pop.product_id=4160;
  • The price of the product 4160 at 2017-02-01 14:23:35 is:

(purchase_total_price - sale_total_cost) / (purchase_total_number - sale_total_number)

The problem is that your "sales_order" table start on 2017-02-01 14:23:35 , while your "purchase_order" table start on 2017-06-07 08:55:48 . So the result will be incoherent as long as you can't track all your purchases from the start.


EDIT:

If you can modify you table structure and are only interested in future sells.

Adding the number of items sold in the purchase_order_products table

You have to modify purchase_order_products to have the consumption for each product:

ALTER TABLE `purchase_order_products` ADD COLUMN sold_items INT DEFAULT 0;

Initializing the data

In order for it to work, you have to make the sold_items column reflect your real stock

You should initialize your table with the following request UPDATE `purchase_order_products` SET sold_items=quantity;

and then manually update the table with your exact stock for each product (which means that quantity_ordered-sold_items must reflect your real stock.

This has to be done only once.

adding the purchase price to the sales_order_item table

ALTER TABLE sales_order_item ADD total_purchase_price INT DEFAULT NULL

entering new sales order

When you enter new sales order, you will have to get the oldest purchase order with remaining items using the following command:

SELECT * FROM `purchase_order_products` WHERE quantity!=sold_items where product_id=4160 ORDER BY `purchase_order_id` LIMIT 1;

You will have then to increment the sold_items value, and calculate the total purchase price (sum) to fill the total_purchase_price column.

calculating margin

The margin will be easily calculated with the difference between row_total and total_purchase_price in the sales_order_item table

I appreciate you are using the FIFO method for the management of stock. However, this does not mean you need to use FIFO to calculate margins. The article https://en.wikipedia.org/wiki/Inventory_valuation gives an overview of the options. (Regulations in your country may exclude some options.)

I believe a repeatable solution for FIFO margin calculation of an individual sale is complex. There are complexities of opening balances, returns, partial deliveries, partial shipments, out-of-order processing, stock-take adjustments, damaged goods etc.

These issues do not seem to be addressed by the database structures in your question.

Typically, these issues are addressed by computing the margin/profit of a period (day, month etc) by calculating the change in value of the inventory over the period.

If you can use the average cost method, you can calculate the margin with pure SQL. I believe other methods would seem to require some iteration as there is no inherent order in SQL. (Your could improve performance by creating a new table and storing the previous period values.)

I would not be too worried about putting the whole solution in SQL as this would not appear to reduce the computational complexity of the problem. Still there might be speed advantages in doing as much of the calculation in the database engine, particularly if the data-set is large.

You might find this article interesting: Set-based Speed Phreakery: The FIFO Stock Inventory SQL Problem . (There are some clever people out there!)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM