Mysql double join SUM invoice total and payments - mysql

I build an invoicing system, now i try to get an view with each invoice with the total price and the amount to pay.
I left joined the invoice_part table on the invoice and summed it up, now i want to also join the payments table but when i sum it sometimes takes the payment more than once. Is there a way i can accomplish the join to only take a payment once?
$stmt->from(array('x'=>'invoice'),array('x.id as id','x.ref_id as ref_id','x.start_date as start_date','x.regard as regard','x.project_code as project_code'));
$stmt->joinLeft(array('ip'=>'invoice_part'),'x.id=ip.invoice_id','SUM(ip.price*ip.amount*(100-ip.discount)/100) as price_ex');
$stmt->joinLeft(array('p'=>'payment'),'x.id=p.invoice_id','SUM(ip.price*ip.amount*(100-ip.discount)*(100+tax)/10000)-IFNULL(SUM(p.amount),0) as price_open');
//joins the payment multiple times if there are multiple invoice parts, payment should only be joined once
//note: there can be multiple payments for one invoice
$stmt->group('x.id');
result query:
SELECT `x`.`id` , `x`.`ref_id` , `x`.`start_date` , `x`.`regard` , `x`.`project_code` , `o`.`name` AS `contact` , `d`.`name` AS `department` , `c`.`name` AS `company` , `is`.`name` AS `status` , SUM( ip.price * ip.amount * ( 100 - ip.discount ) /100 ) AS `price_ex` , SUM( ip.price * ip.amount * ( 100 - ip.discount ) * ( 100 + tax ) /10000 ) - IFNULL( SUM( p.amount ) , 0 ) AS `price_open`
FROM `invoice` AS `x`
LEFT JOIN `invoice_part` AS `ip` ON x.id = ip.invoice_id
LEFT JOIN `payment` AS `p` ON x.id = p.invoice_id
GROUP BY `x`.`id`
so when i have 2 invoice parts and 1 payment for an invoice. the payment gets counted twice. how can i only let it be counted once?

You can divide the sum by the number of repetitions like this:
SUM(tbl.field_to_sum) / COUNT(tbl.primary_key) * COUNT(DISTINCT tbl.primary_key)
Also check out this question

Related

Optimizing MySQL query that includes a repeated subquery

I have the following query that is working perfectly right now, I have been trying to optimize it since I am using the same subquery 4 times. It will be great to come up with a better/smarter solution. Thank you
Here is the query:
select
invoices.invoice_id
,invoices.invoice_amount
,(
select SUM(invoice_payment_amount) as total
FROM invoice_payments
where invoice_payment_invoice_id = invoices.invoice_id
) as payments
,round((invoices.invoice_amount-(
select SUM(invoice_payment_amount) as total
FROM invoice_payments
where invoice_payment_invoice_id = invoices.invoice_id
)),2) as balance
from invoices
where (
round((invoices.invoice_amount -
(select SUM(invoice_payment_amount) as total
FROM invoice_payments
where invoice_payment_invoice_id = invoices.invoice_id)
),2)
) > 0
or (
round((invoices.invoice_amount -
(select SUM(invoice_payment_amount) as total
FROM invoice_payments
where invoice_payment_invoice_id = invoices.invoice_id)
),2)
) IS NULL
order by balance
SQL Fiddle: http://sqlfiddle.com/#!9/aecea/1
Just use a subquery:
select i.invoice_id, i.invoice_amount, i.payments,
round((i.invoice_amount- i.payments), 2) as balance
from (select i.*,
(select sum(ip.invoice_payment_amount)
from invoice_payments ip
where ip.invoice_payment_invoice_id = i.invoice_id
) as payments
from invoices i
) i
where round((i.invoice_amount- i.payments), 2) > 0 or
round((i.invoice_amount- i.payments), 2) is null
order by balance;
For better performance, you want an index on invoice_payments(invoice_payment_invoice_id, invoice_payment_amount).

Adding totals from multiple tables

I'm using Laravel and Eloquent on MySQL. Essentially I am trying to get results from invoices, including their 'total' and 'paid' amounts.
I have 4 tables:
invoices
id int(10),
date_due date,
client_id int(10),
deleted_at datetime
invoices_items
id int(10),
invoice_id int(10),
price decimal(15,2),
quantity int(10)
invoices_payments (this is a pivot table as payments can apply to other invoices too)
payment_id int(10),
invoice_id int(10),
amount decimal(15,2)
payments
id int(10),
payment_date date,
total decimal(15,2)
(there are other fields but they are not relevant)
I've been using this query, based on a few other answers and other research:
select
`invoices`.*,
SUM(
invoices_items.price * invoices_items.quantity
) as total ,
SUM(
invoices_payments.amount
) as paid
from
`invoices`
left join `invoices_items` on `invoices`.`id` = `invoices_items`.`invoice_id`
left join `invoices_payments` on `invoices`.`id` = `invoices_payments`.`invoice_id`
where
`invoices`.`deleted_at` is null
limit
25
The problem I am having is that the result always only returns 1 row (there are 5 invoices in the test db), and the amount for 'total' or 'paid' is not correct.
I'd like to add that there may not be any records in invoices_payments
-- SOLUTION --
Here is the final query in case anyone runs into similar situation
select
`invoices`.*,
COALESCE(SUM(
invoices_items.price * invoices_items.quantity
),0) as total,
COALESCE(SUM(invoices_payments.amount),0) as paid,
COALESCE(SUM(
invoices_items.price * invoices_items.quantity
),0) - COALESCE(SUM(invoices_payments.amount),0) as balance
from
`invoices`
left join `invoices_items` on `invoices`.`id` = `invoices_items`.`invoice_id`
left join `invoices_payments` on `invoices`.`id` = `invoices_payments`.`invoice_id`
where
`invoices`.`deleted_at` is null
group by
`invoices`.`id`
order by
`balance` desc
limit
25
Add a GROUP BY invoices.id after the WHERE

MySQL Use of Having and Where Clause

Having difficulty returning the desired results.
Here is my query:
SELECT
DATABASE()AS INVOICES_Range_$0,
count(
DISTINCT invoice_lines.invoice_header_id
)AS 'Invoice Header Count',
sum(invoice_lines.accounting_total)AS 'Dollar_Value',
100 * COUNT(
DISTINCT invoice_lines.invoice_header_id
)/(
SELECT
COUNT(
DISTINCT invoice_lines.invoice_header_id
)
FROM
invoice_lines
WHERE
(
invoice_lines. STATUS NOT LIKE '%new%'
)
AND(
invoice_lines. STATUS NOT LIKE '%voided%'
)
)AS 'Percent of All Invoices',
COUNT(approvals.approvable_id)/ count(
DISTINCT invoice_lines.invoice_header_id
)AS 'AVG_APPROVALS'
FROM
invoice_lines
LEFT JOIN approvals ON invoice_lines.invoice_header_id = approvals.approvable_id
WHERE
(
invoice_lines.accounting_total = 0
)
AND(
invoice_lines. STATUS NOT LIKE '%new%'
)
AND(
invoice_lines. STATUS NOT LIKE '%voided%'
);
This query returns results where any invoice line has a value of $0.
For reference, we may have an invoice where one line is $0 but the other lines total $600.
I am wanting to only include in the above query where the total of all the invoice lines equal $0.
I've tried:
SELECT
DATABASE()AS INVOICES_Range_$0,
count(
DISTINCT invoice_lines.invoice_header_id
)AS 'Invoice Header Count',
sum(invoice_lines.accounting_total)AS 'Dollar_Value',
100 * COUNT(
DISTINCT invoice_lines.invoice_header_id
)/(
SELECT
COUNT(
DISTINCT invoice_lines.invoice_header_id
)
FROM
invoice_lines
WHERE
(
invoice_lines. STATUS NOT LIKE '%new%'
)
AND(
invoice_lines. STATUS NOT LIKE '%voided%'
)
)AS 'Percent of All Invoices',
COUNT(approvals.approvable_id)/ count(
DISTINCT invoice_lines.invoice_header_id
)AS 'AVG_APPROVALS'
FROM
invoice_lines
LEFT JOIN approvals ON invoice_lines.invoice_header_id = approvals.approvable_id
WHERE
(
invoice_lines. STATUS NOT LIKE '%new%'
)
AND(
invoice_lines. STATUS NOT LIKE '%voided%'
)
HAVING
SUM(
invoice_lines.accounting_total = 0
);
However, that returns the same results. Also, when modified to
HAVING (SUM(invoice_lines.accounting_total) < 500 )
It returns all invoices and the total amount.
Any assistance would be greatly appreciated, as I cannot determine the proper method for limiting my results to those invoice_header_id to only count those invoices where the sum of all lines is equal to 0.
HAVING
SUM(
invoice_lines.accounting_total = 0
);
probably wants to be
HAVING
SUM(
invoice_lines.accounting_total
) = 0
The solution was to evaluate in the WHERE clause using the 'Dollar Value' identified earlier in the query.
I changed the SUM(invoice_lines.accounting_total) as TOTAL
and then in the WHERE clause I added AND (TOTAL = 0);
Worked like a champ.

Showing taxes per invoice items from database

I have problem with getting tax values from database. I will simplify it as possible i can.
First table is
Invoices
(
`Id`,
`Date`,
`InvoiceNumber`,
`Total`
)
Second table is
`InvoiceItems`
(
`Id`,
`Total`,
`TotalWithoutTax`,
`TotalTax`,
`InvoiceId`
)
InvoiceId is a foreign key for Id column from previous table Invoices
Third table is
`InvoiceItemTaxes`
(
`Id`,
`TaxAmmount`,
`InvoiceItemId`,
`TaxId`
)
and fourth table
`Taxes`
(
`Id`,
`Value`
)
This last table contains three taxes, let's say 3, 10 and 15 percent.
I am trying to get something like this - table with columns InvoiceNumber, Total without taxes, Tax1, Tax2, Tax3 and Total with taxes.
I tried a lot of different approaches and i simply cannot get tax amount for every invoice. End result would be table where i can see every invoice with specified amounts of every tax (sum of each tax amount for every invoice item).
If I'm understanding correctly, you can use conditional aggregation with sum and case to get the breakdown by tax group:
select i.id, i.invoicenumber, i.total as pretaxtotal,
sum(case when t.value = 3 then iit.TaxAmmount end) taxes_3,
sum(case when t.value = 10 then iit.TaxAmmount end) taxes_10,
sum(case when t.value = 15 then iit.TaxAmmount end) taxes_15,
sum(ii.Total) as overalltotal
from invoices i
join InvoiceItems ii on i.id = ii.invoiceid
join InvoiceItemTaxes iit on ii.id = iit.InvoiceItemId
join Taxes t on t.id = iit.taxid
group by i.id, i.invoicenumber, i.total
Some of the fields may be a little off -- the sample data was not complete.

SQL Group By Number Of Users Within Range

I have the following query which will return the number of users in table transactions who have earned between $100 and $200
SELECT COUNT(users.id)
FROM transactions
LEFT JOIN users ON users.id = transactions.user_id
WHERE transactions.amount > 100 AND transactions.amount < 200
The above query returns the correct result below:
COUNT(users.id)
559
I would like to extend it so that the query can return data in the following format:
COUNT(users.id) : amount
1678 : 0-100
559 : 100-200
13 : 200-300
How can I do this?
You can use a CASE expression inside of your aggregate function which will get the result in columns:
SELECT
COUNT(case when amount >= 0 and amount <= 100 then users.id end) Amt0_100,
COUNT(case when amount >= 101 and amount <= 200 then users.id end) Amt101_200,
COUNT(case when amount >= 201 and amount <= 300 then users.id end) Amt201_300
FROM transactions
LEFT JOIN users
ON users.id = transactions.user_id;
See SQL Fiddle with Demo
You will notice that I altered the ranges from 0-100, 101-200, 201-300 otherwise you will have user ids being counted twice on the 100, 200 values.
If you want the values in rows, then you can use:
select count(u.id),
CASE
WHEN amount >=0 and amount <=100 THEN '0-100'
WHEN amount >=101 and amount <=200 THEN '101-200'
WHEN amount >=201 and amount <=300 THEN '101-300'
END Amount
from transactions t
left join users u
on u.id = t.user_id
group by
CASE
WHEN amount >=0 and amount <=100 THEN '0-100'
WHEN amount >=101 and amount <=200 THEN '101-200'
WHEN amount >=201 and amount <=300 THEN '101-300'
END
See SQL Fiddle with Demo
But if you have many ranges that you need to calculate the counts on, then you might want to consider creating a table with the ranges, similar to the following:
create table report_range
(
start_range int,
end_range int
);
insert into report_range values
(0, 100),
(101, 200),
(201, 300);
Then you can use this table to join to your current tables and group by the range values:
select count(u.id) Total, concat(start_range, '-', end_range) amount
from transactions t
left join users u
on u.id = t.user_id
left join report_range r
on t.amount >= r.start_range
and t.amount<= r.end_range
group by concat(start_range, '-', end_range);
See SQL Fiddle with Demo.
If you don't want to create a new table with the ranges, then you can always use a derived table to get the same result:
select count(u.id) Total, concat(start_range, '-', end_range) amount
from transactions t
left join users u
on u.id = t.user_id
left join
(
select 0 start_range, 100 end_range union all
select 101 start_range, 200 end_range union all
select 201 start_range, 300 end_range
) r
on t.amount >= r.start_range
and t.amount<= r.end_range
group by concat(start_range, '-', end_range);
See SQL Fiddle with Demo
One way to do this would be to use a case/when statement in your group by.
SELECT
-- NB this must match your group by statement exactly
-- otherwise you will get an error
CASE
WHEN amount <= 100
THEN '0-100'
WHEN amount <= 200
THEN '100-200'
ELSE '201+'
END Amount,
COUNT(*)
FROM
transactions
GROUP BY
CASE
WHEN amount <= 100
THEN '0-100'
WHEN amount <= 200
THEN '100-200'
ELSE '201+'
END
If you plan on using the grouping elsewhere, it probably makes sense to define it as a scalar function (it will also look cleaner)
e.g.
SELECT
AmountGrouping(amount),
COUNT(*)
FROM
transactions
GROUP BY
AmountGrouping(amount)
If you want to be fully generic:
SELECT
concat(((amount DIV 100) * 100),'-',(((amount DIV 100) + 1) * 100)) AmountGroup,
COUNT(*)
FROM
transactions
GROUP BY
AmountGroup
Sql Fiddle
Bilbo, I tried to be creative and found a very nice solution [ for those who love math (like me) ]
It's always surprising when MySQL integer division operator solves our problems.
DROP SCHEMA IF EXISTS `stackoverflow3`;
CREATE SCHEMA `stackoverflow3`;
USE `stackoverflow3`;
CREATE TABLE users (
id INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
name VARCHAR(25) NOT NULL DEFAULT "-");
CREATE TABLE transactions(
id INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
user_id INT UNSIGNED NOT NULL,
amount INT UNSIGNED DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id));
INSERT users () VALUES (),(),();
INSERT transactions (user_id,amount)
VALUES (1,120),(2,270),(3, 350),
(2,500), (1,599), (1,550), (3,10),
(3,20), (3,30), (3,50), (3,750);
SELECT
COUNT(t.id),
CONCAT(
((t.amount DIV 100)*100)," to ",((t.amount DIV 100 + 1)*100-1)
) AS amount_range
FROM transactions AS t
GROUP BY amount_range;
Awaiting your questions, Mr. Baggins.