Snowflake 8 files
dbt_project.yml
name: receivables
version: '1.0'
config-version: 2
profile: dai
# Written by DuctileAI from this project's models; edit the models, not these files.
models/staging/stg_invoices.sql
-- Invoices, with the day the books are closed as of.
{{ config(materialized='view') }}
select
invoices.id as invoice_id,
invoices.customer_ref as client_id,
invoices.txn_date as invoice_day,
invoices.due_date as due_day,
(ROUND((invoices.total_amt), 2)) as amount_due,
invoices.close_date as as_of
from {{ source('quickbooks', 'invoices') }} as invoices
models/staging/stg_payments.sql
{{ config(materialized='view') }}
select
payments.invoice_ref as invoice_id,
(ROUND((payments.total_amt), 2)) as amount_paid
from {{ source('quickbooks', 'payments') }} as payments
models/intermediate/open_invoices.sql
-- Each invoice against what was paid: bucket, status, fee and letter.
{{ config(materialized='view') }}
select
stg_invoices.invoice_id as invoice_id,
stg_invoices.client_id as client_id,
stg_invoices.amount_due as amount_due,
(COALESCE((paid_by_invoice.amount_paid), 0)) as amount_paid,
(CASE WHEN CAST((stg_invoices.due_day) AS DATE) >= CAST((stg_invoices.as_of) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '61-90' ELSE '90+' END) as bucket,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'Paid' WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) > 0 THEN CASE WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Overdue' ELSE 'Open' END) as status,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 0 ELSE ROUND(((stg_invoices.amount_due) - ((COALESCE((paid_by_invoice.amount_paid), 0)))) * CASE WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END) as late_fee,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END) as dunning
from {{ ref('stg_invoices') }} as stg_invoices
left join {{ ref('paid_by_invoice') }} as paid_by_invoice on stg_invoices.invoice_id = paid_by_invoice.invoice_id
models/intermediate/paid_by_invoice.sql
-- Every payment against an invoice, added up.
{{ config(materialized='view') }}
select
stg_payments.invoice_id as invoice_id,
SUM(stg_payments.amount_paid) as amount_paid
from {{ ref('stg_payments') }} as stg_payments
group by stg_payments.invoice_id
models/marts/ar_aging.sql
-- What is owed, by how late.
{{ config(materialized='table') }}
select
open_invoices.bucket as bucket,
COUNT(open_invoices.invoice_id) as invoices,
SUM(open_invoices.amount_due - open_invoices.amount_paid) as outstanding,
SUM(open_invoices.late_fee) as late_fees
from {{ ref('open_invoices') }} as open_invoices
where open_invoices.status <> 'Paid'
group by open_invoices.bucket
models/sources.yml
version: 2
sources:
- name: quickbooks
schema: quickbooks
tables:
- name: invoices
- name: payments
macros/dai_formulas.sql
{# The project's reusable formulas as dbt macros, for snowflake.
Written by DuctileAI for this run; edit the formulas, not this file. #}
{% macro AgingBucket(due_day, as_of) -%}
CASE WHEN CAST({{ due_day }} AS DATE) >= CAST({{ as_of }} AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '61-90' ELSE '90+' END
{%- endmacro %}
{% macro DunningStage(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END
{%- endmacro %}
{% macro LateFee(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 0 ELSE ROUND(({{ amount_due }} - {{ amount_paid }}) * CASE WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END
{%- endmacro %}
{% macro PaymentStatus(amount_due, amount_paid, due_day, as_of) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'Paid' WHEN {{ amount_paid }} > 0 THEN CASE WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Overdue' ELSE 'Open' END
{%- endmacro %}
{% macro RoundMoney(amount) -%}
ROUND({{ amount }}, 2)
{%- endmacro %}
{% macro ZeroIfBlank(amount) -%}
COALESCE({{ amount }}, 0)
{%- endmacro %}
BigQuery 8 files
dbt_project.yml
name: receivables
version: '1.0'
config-version: 2
profile: dai
# Written by DuctileAI from this project's models; edit the models, not these files.
models/staging/stg_invoices.sql
-- Invoices, with the day the books are closed as of.
{{ config(materialized='view') }}
select
invoices.id as invoice_id,
invoices.customer_ref as client_id,
invoices.txn_date as invoice_day,
invoices.due_date as due_day,
(ROUND((invoices.total_amt), 2)) as amount_due,
invoices.close_date as as_of
from {{ source('quickbooks', 'invoices') }} as invoices
models/staging/stg_payments.sql
{{ config(materialized='view') }}
select
payments.invoice_ref as invoice_id,
(ROUND((payments.total_amt), 2)) as amount_paid
from {{ source('quickbooks', 'payments') }} as payments
models/intermediate/open_invoices.sql
-- Each invoice against what was paid: bucket, status, fee and letter.
{{ config(materialized='view') }}
select
stg_invoices.invoice_id as invoice_id,
stg_invoices.client_id as client_id,
stg_invoices.amount_due as amount_due,
(COALESCE((paid_by_invoice.amount_paid), 0)) as amount_paid,
(CASE WHEN CAST((stg_invoices.due_day) AS DATE) >= CAST((stg_invoices.as_of) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST((stg_invoices.due_day) AS DATE), INTERVAL 30 DAY) >= CAST((stg_invoices.as_of) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST((stg_invoices.due_day) AS DATE), INTERVAL 60 DAY) >= CAST((stg_invoices.as_of) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST((stg_invoices.due_day) AS DATE), INTERVAL 90 DAY) >= CAST((stg_invoices.as_of) AS DATE) THEN '61-90' ELSE '90+' END) as bucket,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'Paid' WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) > 0 THEN CASE WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Overdue' ELSE 'Open' END) as status,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 0 ELSE ROUND(((stg_invoices.amount_due) - ((COALESCE((paid_by_invoice.amount_paid), 0)))) * CASE WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN BIGNUMERIC '0.015' ELSE BIGNUMERIC '0.03' END, 2) END) as late_fee,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END) as dunning
from {{ ref('stg_invoices') }} as stg_invoices
left join {{ ref('paid_by_invoice') }} as paid_by_invoice on stg_invoices.invoice_id = paid_by_invoice.invoice_id
models/intermediate/paid_by_invoice.sql
-- Every payment against an invoice, added up.
{{ config(materialized='view') }}
select
stg_payments.invoice_id as invoice_id,
SUM(stg_payments.amount_paid) as amount_paid
from {{ ref('stg_payments') }} as stg_payments
group by stg_payments.invoice_id
models/marts/ar_aging.sql
-- What is owed, by how late.
{{ config(materialized='table') }}
select
open_invoices.bucket as bucket,
COUNT(open_invoices.invoice_id) as invoices,
SUM(open_invoices.amount_due - open_invoices.amount_paid) as outstanding,
SUM(open_invoices.late_fee) as late_fees
from {{ ref('open_invoices') }} as open_invoices
where open_invoices.status <> 'Paid'
group by open_invoices.bucket
models/sources.yml
version: 2
sources:
- name: quickbooks
schema: quickbooks
tables:
- name: invoices
- name: payments
macros/dai_formulas.sql
{# The project's reusable formulas as dbt macros, for bigquery.
Written by DuctileAI for this run; edit the formulas, not this file. #}
{% macro AgingBucket(due_day, as_of) -%}
CASE WHEN CAST({{ due_day }} AS DATE) >= CAST({{ as_of }} AS DATE) THEN 'Current' WHEN DATE_ADD(CAST({{ due_day }} AS DATE), INTERVAL 30 DAY) >= CAST({{ as_of }} AS DATE) THEN '1-30' WHEN DATE_ADD(CAST({{ due_day }} AS DATE), INTERVAL 60 DAY) >= CAST({{ as_of }} AS DATE) THEN '31-60' WHEN DATE_ADD(CAST({{ due_day }} AS DATE), INTERVAL 90 DAY) >= CAST({{ as_of }} AS DATE) THEN '61-90' ELSE '90+' END
{%- endmacro %}
{% macro DunningStage(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END
{%- endmacro %}
{% macro LateFee(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 0 ELSE ROUND(({{ amount_due }} - {{ amount_paid }}) * CASE WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN BIGNUMERIC '0.015' ELSE BIGNUMERIC '0.03' END, 2) END
{%- endmacro %}
{% macro PaymentStatus(amount_due, amount_paid, due_day, as_of) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'Paid' WHEN {{ amount_paid }} > 0 THEN CASE WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Overdue' ELSE 'Open' END
{%- endmacro %}
{% macro RoundMoney(amount) -%}
ROUND({{ amount }}, 2)
{%- endmacro %}
{% macro ZeroIfBlank(amount) -%}
COALESCE({{ amount }}, 0)
{%- endmacro %}
Databricks 8 files
dbt_project.yml
name: receivables
version: '1.0'
config-version: 2
profile: dai
# Written by DuctileAI from this project's models; edit the models, not these files.
models/staging/stg_invoices.sql
-- Invoices, with the day the books are closed as of.
{{ config(materialized='view') }}
select
invoices.id as invoice_id,
invoices.customer_ref as client_id,
invoices.txn_date as invoice_day,
invoices.due_date as due_day,
(ROUND((invoices.total_amt), 2)) as amount_due,
invoices.close_date as as_of
from {{ source('quickbooks', 'invoices') }} as invoices
models/staging/stg_payments.sql
{{ config(materialized='view') }}
select
payments.invoice_ref as invoice_id,
(ROUND((payments.total_amt), 2)) as amount_paid
from {{ source('quickbooks', 'payments') }} as payments
models/intermediate/open_invoices.sql
-- Each invoice against what was paid: bucket, status, fee and letter.
{{ config(materialized='view') }}
select
stg_invoices.invoice_id as invoice_id,
stg_invoices.client_id as client_id,
stg_invoices.amount_due as amount_due,
(COALESCE((paid_by_invoice.amount_paid), 0)) as amount_paid,
(CASE WHEN CAST((stg_invoices.due_day) AS DATE) >= CAST((stg_invoices.as_of) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '61-90' ELSE '90+' END) as bucket,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'Paid' WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) > 0 THEN CASE WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Overdue' ELSE 'Open' END) as status,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 0 ELSE ROUND(((stg_invoices.amount_due) - ((COALESCE((paid_by_invoice.amount_paid), 0)))) * CASE WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END) as late_fee,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END) as dunning
from {{ ref('stg_invoices') }} as stg_invoices
left join {{ ref('paid_by_invoice') }} as paid_by_invoice on stg_invoices.invoice_id = paid_by_invoice.invoice_id
models/intermediate/paid_by_invoice.sql
-- Every payment against an invoice, added up.
{{ config(materialized='view') }}
select
stg_payments.invoice_id as invoice_id,
SUM(stg_payments.amount_paid) as amount_paid
from {{ ref('stg_payments') }} as stg_payments
group by stg_payments.invoice_id
models/marts/ar_aging.sql
-- What is owed, by how late.
{{ config(materialized='table') }}
select
open_invoices.bucket as bucket,
COUNT(open_invoices.invoice_id) as invoices,
SUM(open_invoices.amount_due - open_invoices.amount_paid) as outstanding,
SUM(open_invoices.late_fee) as late_fees
from {{ ref('open_invoices') }} as open_invoices
where open_invoices.status <> 'Paid'
group by open_invoices.bucket
models/sources.yml
version: 2
sources:
- name: quickbooks
schema: quickbooks
tables:
- name: invoices
- name: payments
macros/dai_formulas.sql
{# The project's reusable formulas as dbt macros, for databricks.
Written by DuctileAI for this run; edit the formulas, not this file. #}
{% macro AgingBucket(due_day, as_of) -%}
CASE WHEN CAST({{ due_day }} AS DATE) >= CAST({{ as_of }} AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '61-90' ELSE '90+' END
{%- endmacro %}
{% macro DunningStage(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END
{%- endmacro %}
{% macro LateFee(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 0 ELSE ROUND(({{ amount_due }} - {{ amount_paid }}) * CASE WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END
{%- endmacro %}
{% macro PaymentStatus(amount_due, amount_paid, due_day, as_of) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'Paid' WHEN {{ amount_paid }} > 0 THEN CASE WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Overdue' ELSE 'Open' END
{%- endmacro %}
{% macro RoundMoney(amount) -%}
ROUND({{ amount }}, 2)
{%- endmacro %}
{% macro ZeroIfBlank(amount) -%}
COALESCE({{ amount }}, 0)
{%- endmacro %}
Redshift 8 files
dbt_project.yml
name: receivables
version: '1.0'
config-version: 2
profile: dai
# Written by DuctileAI from this project's models; edit the models, not these files.
models/staging/stg_invoices.sql
-- Invoices, with the day the books are closed as of.
{{ config(materialized='view') }}
select
invoices.id as invoice_id,
invoices.customer_ref as client_id,
invoices.txn_date as invoice_day,
invoices.due_date as due_day,
(ROUND((invoices.total_amt), 2)) as amount_due,
invoices.close_date as as_of
from {{ source('quickbooks', 'invoices') }} as invoices
models/staging/stg_payments.sql
{{ config(materialized='view') }}
select
payments.invoice_ref as invoice_id,
(ROUND((payments.total_amt), 2)) as amount_paid
from {{ source('quickbooks', 'payments') }} as payments
models/intermediate/open_invoices.sql
-- Each invoice against what was paid: bucket, status, fee and letter.
{{ config(materialized='view') }}
select
stg_invoices.invoice_id as invoice_id,
stg_invoices.client_id as client_id,
stg_invoices.amount_due as amount_due,
(COALESCE((paid_by_invoice.amount_paid), 0)) as amount_paid,
(CASE WHEN CAST((stg_invoices.due_day) AS DATE) >= CAST((stg_invoices.as_of) AS DATE) THEN 'Current' WHEN (CAST((stg_invoices.due_day) AS DATE) + (30) * INTERVAL '1 day') >= CAST((stg_invoices.as_of) AS DATE) THEN '1-30' WHEN (CAST((stg_invoices.due_day) AS DATE) + (60) * INTERVAL '1 day') >= CAST((stg_invoices.as_of) AS DATE) THEN '31-60' WHEN (CAST((stg_invoices.due_day) AS DATE) + (90) * INTERVAL '1 day') >= CAST((stg_invoices.as_of) AS DATE) THEN '61-90' ELSE '90+' END) as bucket,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'Paid' WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) > 0 THEN CASE WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Overdue' ELSE 'Open' END) as status,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 0 ELSE ROUND(((stg_invoices.amount_due) - ((COALESCE((paid_by_invoice.amount_paid), 0)))) * CASE WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END) as late_fee,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END) as dunning
from {{ ref('stg_invoices') }} as stg_invoices
left join {{ ref('paid_by_invoice') }} as paid_by_invoice on stg_invoices.invoice_id = paid_by_invoice.invoice_id
models/intermediate/paid_by_invoice.sql
-- Every payment against an invoice, added up.
{{ config(materialized='view') }}
select
stg_payments.invoice_id as invoice_id,
SUM(stg_payments.amount_paid) as amount_paid
from {{ ref('stg_payments') }} as stg_payments
group by stg_payments.invoice_id
models/marts/ar_aging.sql
-- What is owed, by how late.
{{ config(materialized='table') }}
select
open_invoices.bucket as bucket,
COUNT(open_invoices.invoice_id) as invoices,
SUM(open_invoices.amount_due - open_invoices.amount_paid) as outstanding,
SUM(open_invoices.late_fee) as late_fees
from {{ ref('open_invoices') }} as open_invoices
where open_invoices.status <> 'Paid'
group by open_invoices.bucket
models/sources.yml
version: 2
sources:
- name: quickbooks
schema: quickbooks
tables:
- name: invoices
- name: payments
macros/dai_formulas.sql
{# The project's reusable formulas as dbt macros, for redshift.
Written by DuctileAI for this run; edit the formulas, not this file. #}
{% macro AgingBucket(due_day, as_of) -%}
CASE WHEN CAST({{ due_day }} AS DATE) >= CAST({{ as_of }} AS DATE) THEN 'Current' WHEN (CAST({{ due_day }} AS DATE) + (30) * INTERVAL '1 day') >= CAST({{ as_of }} AS DATE) THEN '1-30' WHEN (CAST({{ due_day }} AS DATE) + (60) * INTERVAL '1 day') >= CAST({{ as_of }} AS DATE) THEN '31-60' WHEN (CAST({{ due_day }} AS DATE) + (90) * INTERVAL '1 day') >= CAST({{ as_of }} AS DATE) THEN '61-90' ELSE '90+' END
{%- endmacro %}
{% macro DunningStage(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END
{%- endmacro %}
{% macro LateFee(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 0 ELSE ROUND(({{ amount_due }} - {{ amount_paid }}) * CASE WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END
{%- endmacro %}
{% macro PaymentStatus(amount_due, amount_paid, due_day, as_of) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'Paid' WHEN {{ amount_paid }} > 0 THEN CASE WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Overdue' ELSE 'Open' END
{%- endmacro %}
{% macro RoundMoney(amount) -%}
ROUND({{ amount }}, 2)
{%- endmacro %}
{% macro ZeroIfBlank(amount) -%}
COALESCE({{ amount }}, 0)
{%- endmacro %}
Postgres 8 files
dbt_project.yml
name: receivables
version: '1.0'
config-version: 2
profile: dai
# Written by DuctileAI from this project's models; edit the models, not these files.
models/staging/stg_invoices.sql
-- Invoices, with the day the books are closed as of.
{{ config(materialized='view') }}
select
invoices.id as invoice_id,
invoices.customer_ref as client_id,
invoices.txn_date as invoice_day,
invoices.due_date as due_day,
(ROUND(CAST((invoices.total_amt) AS NUMERIC), 2)) as amount_due,
invoices.close_date as as_of
from {{ source('quickbooks', 'invoices') }} as invoices
models/staging/stg_payments.sql
{{ config(materialized='view') }}
select
payments.invoice_ref as invoice_id,
(ROUND(CAST((payments.total_amt) AS NUMERIC), 2)) as amount_paid
from {{ source('quickbooks', 'payments') }} as payments
models/intermediate/open_invoices.sql
-- Each invoice against what was paid: bucket, status, fee and letter.
{{ config(materialized='view') }}
select
stg_invoices.invoice_id as invoice_id,
stg_invoices.client_id as client_id,
stg_invoices.amount_due as amount_due,
(COALESCE((paid_by_invoice.amount_paid), 0)) as amount_paid,
(CASE WHEN CAST((stg_invoices.due_day) AS DATE) >= CAST((stg_invoices.as_of) AS DATE) THEN 'Current' WHEN (CAST((stg_invoices.due_day) AS DATE) + (30) * INTERVAL '1 day') >= CAST((stg_invoices.as_of) AS DATE) THEN '1-30' WHEN (CAST((stg_invoices.due_day) AS DATE) + (60) * INTERVAL '1 day') >= CAST((stg_invoices.as_of) AS DATE) THEN '31-60' WHEN (CAST((stg_invoices.due_day) AS DATE) + (90) * INTERVAL '1 day') >= CAST((stg_invoices.as_of) AS DATE) THEN '61-90' ELSE '90+' END) as bucket,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'Paid' WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) > 0 THEN CASE WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Overdue' ELSE 'Open' END) as status,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 0 ELSE ROUND(CAST(((stg_invoices.amount_due) - ((COALESCE((paid_by_invoice.amount_paid), 0)))) * CASE WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END AS NUMERIC), 2) END) as late_fee,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (30) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (60) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + (90) * INTERVAL '1 day') >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END) as dunning
from {{ ref('stg_invoices') }} as stg_invoices
left join {{ ref('paid_by_invoice') }} as paid_by_invoice on stg_invoices.invoice_id = paid_by_invoice.invoice_id
models/intermediate/paid_by_invoice.sql
-- Every payment against an invoice, added up.
{{ config(materialized='view') }}
select
stg_payments.invoice_id as invoice_id,
SUM(stg_payments.amount_paid) as amount_paid
from {{ ref('stg_payments') }} as stg_payments
group by stg_payments.invoice_id
models/marts/ar_aging.sql
-- What is owed, by how late.
{{ config(materialized='table') }}
select
open_invoices.bucket as bucket,
COUNT(open_invoices.invoice_id) as invoices,
SUM(open_invoices.amount_due - open_invoices.amount_paid) as outstanding,
SUM(open_invoices.late_fee) as late_fees
from {{ ref('open_invoices') }} as open_invoices
where open_invoices.status <> 'Paid'
group by open_invoices.bucket
models/sources.yml
version: 2
sources:
- name: quickbooks
schema: quickbooks
tables:
- name: invoices
- name: payments
macros/dai_formulas.sql
{# The project's reusable formulas as dbt macros, for postgres.
Written by DuctileAI for this run; edit the formulas, not this file. #}
{% macro AgingBucket(due_day, as_of) -%}
CASE WHEN CAST({{ due_day }} AS DATE) >= CAST({{ as_of }} AS DATE) THEN 'Current' WHEN (CAST({{ due_day }} AS DATE) + (30) * INTERVAL '1 day') >= CAST({{ as_of }} AS DATE) THEN '1-30' WHEN (CAST({{ due_day }} AS DATE) + (60) * INTERVAL '1 day') >= CAST({{ as_of }} AS DATE) THEN '31-60' WHEN (CAST({{ due_day }} AS DATE) + (90) * INTERVAL '1 day') >= CAST({{ as_of }} AS DATE) THEN '61-90' ELSE '90+' END
{%- endmacro %}
{% macro DunningStage(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END
{%- endmacro %}
{% macro LateFee(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 0 ELSE ROUND(CAST(({{ amount_due }} - {{ amount_paid }}) * CASE WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + (30) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + (60) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + (90) * INTERVAL '1 day') >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END AS NUMERIC), 2) END
{%- endmacro %}
{% macro PaymentStatus(amount_due, amount_paid, due_day, as_of) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'Paid' WHEN {{ amount_paid }} > 0 THEN CASE WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Overdue' ELSE 'Open' END
{%- endmacro %}
{% macro RoundMoney(amount) -%}
ROUND(CAST({{ amount }} AS NUMERIC), 2)
{%- endmacro %}
{% macro ZeroIfBlank(amount) -%}
COALESCE({{ amount }}, 0)
{%- endmacro %}
SQL Server, Azure SQL, Synapse, Fabric 8 files
dbt_project.yml
name: receivables
version: '1.0'
config-version: 2
profile: dai
# Written by DuctileAI from this project's models; edit the models, not these files.
models/staging/stg_invoices.sql
-- Invoices, with the day the books are closed as of.
{{ config(materialized='view') }}
select
invoices.id as invoice_id,
invoices.customer_ref as client_id,
invoices.txn_date as invoice_day,
invoices.due_date as due_day,
(ROUND((invoices.total_amt), 2)) as amount_due,
invoices.close_date as as_of
from {{ source('quickbooks', 'invoices') }} as invoices
models/staging/stg_payments.sql
{{ config(materialized='view') }}
select
payments.invoice_ref as invoice_id,
(ROUND((payments.total_amt), 2)) as amount_paid
from {{ source('quickbooks', 'payments') }} as payments
models/intermediate/open_invoices.sql
-- Each invoice against what was paid: bucket, status, fee and letter.
{{ config(materialized='view') }}
select
stg_invoices.invoice_id as invoice_id,
stg_invoices.client_id as client_id,
stg_invoices.amount_due as amount_due,
(COALESCE((paid_by_invoice.amount_paid), 0)) as amount_paid,
(CASE WHEN CAST((stg_invoices.due_day) AS DATE) >= CAST((stg_invoices.as_of) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST((stg_invoices.due_day) AS DATE)) >= CAST((stg_invoices.as_of) AS DATE) THEN '61-90' ELSE '90+' END) as bucket,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'Paid' WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) > 0 THEN CASE WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Overdue' ELSE 'Open' END) as status,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 0 ELSE ROUND(((stg_invoices.amount_due) - ((COALESCE((paid_by_invoice.amount_paid), 0)))) * CASE WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END) as late_fee,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(((stg_invoices.due_day)) AS DATE)) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END) as dunning
from {{ ref('stg_invoices') }} as stg_invoices
left join {{ ref('paid_by_invoice') }} as paid_by_invoice on stg_invoices.invoice_id = paid_by_invoice.invoice_id
models/intermediate/paid_by_invoice.sql
-- Every payment against an invoice, added up.
{{ config(materialized='view') }}
select
stg_payments.invoice_id as invoice_id,
SUM(stg_payments.amount_paid) as amount_paid
from {{ ref('stg_payments') }} as stg_payments
group by stg_payments.invoice_id
models/marts/ar_aging.sql
-- What is owed, by how late.
{{ config(materialized='table') }}
select
open_invoices.bucket as bucket,
COUNT(open_invoices.invoice_id) as invoices,
SUM(open_invoices.amount_due - open_invoices.amount_paid) as outstanding,
SUM(open_invoices.late_fee) as late_fees
from {{ ref('open_invoices') }} as open_invoices
where open_invoices.status <> 'Paid'
group by open_invoices.bucket
models/sources.yml
version: 2
sources:
- name: quickbooks
schema: quickbooks
tables:
- name: invoices
- name: payments
macros/dai_formulas.sql
{# The project's reusable formulas as dbt macros, for tsql.
Written by DuctileAI for this run; edit the formulas, not this file. #}
{% macro AgingBucket(due_day, as_of) -%}
CASE WHEN CAST({{ due_day }} AS DATE) >= CAST({{ as_of }} AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST({{ due_day }} AS DATE)) >= CAST({{ as_of }} AS DATE) THEN '61-90' ELSE '90+' END
{%- endmacro %}
{% macro DunningStage(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END
{%- endmacro %}
{% macro LateFee(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 0 ELSE ROUND(({{ amount_due }} - {{ amount_paid }}) * CASE WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATEADD(day, 30, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATEADD(day, 60, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATEADD(day, 90, CAST(({{ due_day }}) AS DATE)) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END
{%- endmacro %}
{% macro PaymentStatus(amount_due, amount_paid, due_day, as_of) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'Paid' WHEN {{ amount_paid }} > 0 THEN CASE WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Overdue' ELSE 'Open' END
{%- endmacro %}
{% macro RoundMoney(amount) -%}
ROUND({{ amount }}, 2)
{%- endmacro %}
{% macro ZeroIfBlank(amount) -%}
COALESCE({{ amount }}, 0)
{%- endmacro %}
Oracle 8 files
dbt_project.yml
name: receivables
version: '1.0'
config-version: 2
profile: dai
# Written by DuctileAI from this project's models; edit the models, not these files.
models/staging/stg_invoices.sql
-- Invoices, with the day the books are closed as of.
{{ config(materialized='view') }}
select
invoices.id as invoice_id,
invoices.customer_ref as client_id,
invoices.txn_date as invoice_day,
invoices.due_date as due_day,
(ROUND((invoices.total_amt), 2)) as amount_due,
invoices.close_date as as_of
from {{ source('quickbooks', 'invoices') }} as invoices
models/staging/stg_payments.sql
{{ config(materialized='view') }}
select
payments.invoice_ref as invoice_id,
(ROUND((payments.total_amt), 2)) as amount_paid
from {{ source('quickbooks', 'payments') }} as payments
models/intermediate/open_invoices.sql
-- Each invoice against what was paid: bucket, status, fee and letter.
{{ config(materialized='view') }}
select
stg_invoices.invoice_id as invoice_id,
stg_invoices.client_id as client_id,
stg_invoices.amount_due as amount_due,
(COALESCE((paid_by_invoice.amount_paid), 0)) as amount_paid,
(CASE WHEN CAST((stg_invoices.due_day) AS DATE) >= CAST((stg_invoices.as_of) AS DATE) THEN 'Current' WHEN (CAST((stg_invoices.due_day) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST((stg_invoices.as_of) AS DATE) THEN '1-30' WHEN (CAST((stg_invoices.due_day) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST((stg_invoices.as_of) AS DATE) THEN '31-60' WHEN (CAST((stg_invoices.due_day) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST((stg_invoices.as_of) AS DATE) THEN '61-90' ELSE '90+' END) as bucket,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'Paid' WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) > 0 THEN CASE WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Overdue' ELSE 'Open' END) as status,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 0 ELSE ROUND(((stg_invoices.amount_due) - ((COALESCE((paid_by_invoice.amount_paid), 0)))) * CASE WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END) as late_fee,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN (CAST(((stg_invoices.due_day)) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END) as dunning
from {{ ref('stg_invoices') }} as stg_invoices
left join {{ ref('paid_by_invoice') }} as paid_by_invoice on stg_invoices.invoice_id = paid_by_invoice.invoice_id
models/intermediate/paid_by_invoice.sql
-- Every payment against an invoice, added up.
{{ config(materialized='view') }}
select
stg_payments.invoice_id as invoice_id,
SUM(stg_payments.amount_paid) as amount_paid
from {{ ref('stg_payments') }} as stg_payments
group by stg_payments.invoice_id
models/marts/ar_aging.sql
-- What is owed, by how late.
{{ config(materialized='table') }}
select
open_invoices.bucket as bucket,
COUNT(open_invoices.invoice_id) as invoices,
SUM(open_invoices.amount_due - open_invoices.amount_paid) as outstanding,
SUM(open_invoices.late_fee) as late_fees
from {{ ref('open_invoices') }} as open_invoices
where open_invoices.status <> 'Paid'
group by open_invoices.bucket
models/sources.yml
version: 2
sources:
- name: quickbooks
schema: quickbooks
tables:
- name: invoices
- name: payments
macros/dai_formulas.sql
{# The project's reusable formulas as dbt macros, for oracle.
Written by DuctileAI for this run; edit the formulas, not this file. #}
{% macro AgingBucket(due_day, as_of) -%}
CASE WHEN CAST({{ due_day }} AS DATE) >= CAST({{ as_of }} AS DATE) THEN 'Current' WHEN (CAST({{ due_day }} AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST({{ as_of }} AS DATE) THEN '1-30' WHEN (CAST({{ due_day }} AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST({{ as_of }} AS DATE) THEN '31-60' WHEN (CAST({{ due_day }} AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST({{ as_of }} AS DATE) THEN '61-90' ELSE '90+' END
{%- endmacro %}
{% macro DunningStage(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END
{%- endmacro %}
{% macro LateFee(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 0 ELSE ROUND(({{ amount_due }} - {{ amount_paid }}) * CASE WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(30, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(60, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN (CAST(({{ due_day }}) AS DATE) + NUMTODSINTERVAL(90, 'DAY')) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END
{%- endmacro %}
{% macro PaymentStatus(amount_due, amount_paid, due_day, as_of) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'Paid' WHEN {{ amount_paid }} > 0 THEN CASE WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Overdue' ELSE 'Open' END
{%- endmacro %}
{% macro RoundMoney(amount) -%}
ROUND({{ amount }}, 2)
{%- endmacro %}
{% macro ZeroIfBlank(amount) -%}
COALESCE({{ amount }}, 0)
{%- endmacro %}
MySQL 8 files
dbt_project.yml
name: receivables
version: '1.0'
config-version: 2
profile: dai
# Written by DuctileAI from this project's models; edit the models, not these files.
models/staging/stg_invoices.sql
-- Invoices, with the day the books are closed as of.
{{ config(materialized='view') }}
select
invoices.id as invoice_id,
invoices.customer_ref as client_id,
invoices.txn_date as invoice_day,
invoices.due_date as due_day,
(ROUND((invoices.total_amt), 2)) as amount_due,
invoices.close_date as as_of
from {{ source('quickbooks', 'invoices') }} as invoices
models/staging/stg_payments.sql
{{ config(materialized='view') }}
select
payments.invoice_ref as invoice_id,
(ROUND((payments.total_amt), 2)) as amount_paid
from {{ source('quickbooks', 'payments') }} as payments
models/intermediate/open_invoices.sql
-- Each invoice against what was paid: bucket, status, fee and letter.
{{ config(materialized='view') }}
select
stg_invoices.invoice_id as invoice_id,
stg_invoices.client_id as client_id,
stg_invoices.amount_due as amount_due,
(COALESCE((paid_by_invoice.amount_paid), 0)) as amount_paid,
(CASE WHEN CAST((stg_invoices.due_day) AS DATE) >= CAST((stg_invoices.as_of) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST((stg_invoices.due_day) AS DATE), INTERVAL 30 DAY) >= CAST((stg_invoices.as_of) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST((stg_invoices.due_day) AS DATE), INTERVAL 60 DAY) >= CAST((stg_invoices.as_of) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST((stg_invoices.due_day) AS DATE), INTERVAL 90 DAY) >= CAST((stg_invoices.as_of) AS DATE) THEN '61-90' ELSE '90+' END) as bucket,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'Paid' WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) > 0 THEN CASE WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST((stg_invoices.due_day) AS DATE) < CAST((stg_invoices.as_of) AS DATE) THEN 'Overdue' ELSE 'Open' END) as status,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 0 ELSE ROUND(((stg_invoices.amount_due) - ((COALESCE((paid_by_invoice.amount_paid), 0)))) * CASE WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END) as late_fee,
(CASE WHEN ((COALESCE((paid_by_invoice.amount_paid), 0))) >= (stg_invoices.amount_due) THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(((stg_invoices.due_day)) AS DATE) >= CAST(((stg_invoices.as_of)) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 30 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 60 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(((stg_invoices.due_day)) AS DATE), INTERVAL 90 DAY) >= CAST(((stg_invoices.as_of)) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END) as dunning
from {{ ref('stg_invoices') }} as stg_invoices
left join {{ ref('paid_by_invoice') }} as paid_by_invoice on stg_invoices.invoice_id = paid_by_invoice.invoice_id
models/intermediate/paid_by_invoice.sql
-- Every payment against an invoice, added up.
{{ config(materialized='view') }}
select
stg_payments.invoice_id as invoice_id,
SUM(stg_payments.amount_paid) as amount_paid
from {{ ref('stg_payments') }} as stg_payments
group by stg_payments.invoice_id
models/marts/ar_aging.sql
-- What is owed, by how late.
{{ config(materialized='table') }}
select
open_invoices.bucket as bucket,
COUNT(open_invoices.invoice_id) as invoices,
SUM(open_invoices.amount_due - open_invoices.amount_paid) as outstanding,
SUM(open_invoices.late_fee) as late_fees
from {{ ref('open_invoices') }} as open_invoices
where open_invoices.status <> 'Paid'
group by open_invoices.bucket
models/sources.yml
version: 2
sources:
- name: quickbooks
schema: quickbooks
tables:
- name: invoices
- name: payments
macros/dai_formulas.sql
{# The project's reusable formulas as dbt macros, for mysql.
Written by DuctileAI for this run; edit the formulas, not this file. #}
{% macro AgingBucket(due_day, as_of) -%}
CASE WHEN CAST({{ due_day }} AS DATE) >= CAST({{ as_of }} AS DATE) THEN 'Current' WHEN DATE_ADD(CAST({{ due_day }} AS DATE), INTERVAL 30 DAY) >= CAST({{ as_of }} AS DATE) THEN '1-30' WHEN DATE_ADD(CAST({{ due_day }} AS DATE), INTERVAL 60 DAY) >= CAST({{ as_of }} AS DATE) THEN '31-60' WHEN DATE_ADD(CAST({{ due_day }} AS DATE), INTERVAL 90 DAY) >= CAST({{ as_of }} AS DATE) THEN '61-90' ELSE '90+' END
{%- endmacro %}
{% macro DunningStage(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 'None' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 'Reminder' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '31-60' THEN 'Second notice' WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '61-90' THEN 'Final notice' ELSE 'Collections' END
{%- endmacro %}
{% macro LateFee(due_day, as_of, amount_due, amount_paid) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 0 ELSE ROUND(({{ amount_due }} - {{ amount_paid }}) * CASE WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = 'Current' THEN 0 WHEN (CASE WHEN CAST(({{ due_day }}) AS DATE) >= CAST(({{ as_of }}) AS DATE) THEN 'Current' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 30 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '1-30' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 60 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '31-60' WHEN DATE_ADD(CAST(({{ due_day }}) AS DATE), INTERVAL 90 DAY) >= CAST(({{ as_of }}) AS DATE) THEN '61-90' ELSE '90+' END) = '1-30' THEN 0.015 ELSE 0.03 END, 2) END
{%- endmacro %}
{% macro PaymentStatus(amount_due, amount_paid, due_day, as_of) -%}
CASE WHEN {{ amount_paid }} >= {{ amount_due }} THEN 'Paid' WHEN {{ amount_paid }} > 0 THEN CASE WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Partial, overdue' ELSE 'Partial' END WHEN CAST({{ due_day }} AS DATE) < CAST({{ as_of }} AS DATE) THEN 'Overdue' ELSE 'Open' END
{%- endmacro %}
{% macro RoundMoney(amount) -%}
ROUND({{ amount }}, 2)
{%- endmacro %}
{% macro ZeroIfBlank(amount) -%}
COALESCE({{ amount }}, 0)
{%- endmacro %}