Skip to content

Instantly share code, notes, and snippets.

@klette
Created November 12, 2009 01:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save klette/232497 to your computer and use it in GitHub Desktop.
Save klette/232497 to your computer and use it in GitHub Desktop.
! Billig::Economy::Settlement
Denne noden forklarer de forskjellige aspektene av Billig sin støtte for å håndtere kasseoppgjør.
!! Databasen
Lagringen av kasseoppgjør gjøre etter skjemaet som finnes i sql/settlement.sql
i kode-treet.
Et kasseoppgjør har et starttidspunkt og et slutttidspunkt som viser når
kasseoppgjøret ble åpnet og eventuelt lukket. Kasseoppgjøret har en eier som
er brukeren som åpnet kassen. Tidligere hadde billig et konsept om hvilken
kasse og bankterminal ble benyttet, men dette ble fjernet da dette er
overflødig da kassekontoret (under UKA i alle fall) har denne informasjonen.
Denne informasjonen knyttes opp via feltet cash_king_id. Dette er et løpetall
fra kassekontoret sin side for hver transaksjon de har i cash_king. (I vårt
tilfelle er det kasse ut med veksel).
Et kasseoppgjør får for hver billett som blir berørt av selgeren så lenge
kasseoppgjøret er åpent, en ny transaksjon tilknyttet seg. Dvs om selgeren
selger et syv billetter til en kunde registreres dette som syv
salgstransaksjoner. Dette er for å gjøre rapporteringlogikken mindre kompleks
da det samme kjøpet kan bli berørt av andre kasseoppgjør innenfor den samme
perioden. En transaksjon kan oppre i to typer, salg og refundering. Dvs om noen
gjør om en billett fra medlem til ikke-medlem (f.eks) blir det registrert som
en refundering og ett salg. En transaksjon har også et konsept om
betalingsmiddel, vi støtter i dag to stykk, kontanter og kreditt
(bankterminal).
Databasen har også to hjelpe-views. settlement_report og settlement_tickets.
settlement_report returnerer feltene i settlement-tabellen (starttid osv) samt
en summering av hvor mye pengestrømmen var (i følge det som ble rapportert inn
om kasseoppgjøret er avsluttet, og hvor mye billig mener det skulle ha vært.
settlement_tickets gir en liste over billetter som i følge kasseoppgjøret er
gyldige, altså billetter som ble solgt og ikke refundert innenfor det samme
kasseoppgjøret. Merk at billettene kan ha blitt refundert i et annet
kasseoppgjør. Dette viewet hjelper oss å lage økonomiske rapporter for hvert
kasseoppgjør.
!! Bruk - fra en selgers synspunkt
Når en vanlig bruker prøver å selge billetter og ikke har et aktivt
kasseoppgjør blir han spurt om han vil åpne et nytt kasseoppgjør. Om han har et
tidligere kasseoppgjør som ikke er avsluttet vil han også få valget om å
fortsette på det eksisterende kasseoppgjøret. Her må han samtidig taste inn
hvor han selger fra og hvilken billettprinter han evt bruker.
Selgeren selger så billetter som vanlig.
Når selgeren er ferdig for dagen, går han inn på kasseoppgjøret sitt (link
finnes i hovedmenyen). Her blir han spurt om å taste inn hvor mye penger han
har i kassen og hvor mye han har fått inn på bankterminalen sin. Hadde han
veksel med seg ut, må dette trekkes fra summen før han taster den inn. Om
kasseoppgjøret stemmer overrens med det billig mener han burde ha fått inn,
avsluttes oppgjøret og brukeren sendes til startsiden. Om det er en differanse
på over en gitt grense ($settlement_diff_limit i config.pm) får han opp en
advarsel om at det er for stor differanse for å avslutte kasseoppgjøret
direkte. Meningen er da at selgeren skal telle over kassen sin på nytt. Om det
fortsatt er for stor differanse kan selgeren krysse av for å avslutte
kasseoppgjøret med differanse og har da også en mulighet for å legge ved et
notis om hvorfor han har differanse. Når dette kasseoppgjøret avsluttes sendes
det en mail til en gitt adresse med beskjed om at det er levert inn et
kasseoppgjør med høy differanse.
Selgeren er nå ferdig for dagen.
!! Bruk - fra en administrators synspunkt
Administratorer har tilgang til å se alle detaljer om et kasseoppgjør via
rapport-siden. Her kan han også søke opp gamle kasseoppgjør på selger og/eller
dato. Inne på detaljersiden kan han se alle detaljer billig har om
kasseoppgjøret. I fremtiden er det meningen at han skal kunne legge inn
justeringstransaksjoner, og evt endre på detaljene på kasseoppgjøret (men ikke
endre på noen av transaksjonene).
Disse rapportene har et printstyle som gjør de egnet for å printe ut og levere
til økonomi som skal ha papirutgaver av alle rapportene.
!! Automatiske rapporter
Under UKA-09 ble det skrevet et script for å gi CSV-rapporter pr oppgjør som
var egnet for import til visma. Dette skriptet finnes i bin/report.pl. Dette
scriptet lager en csv-fil pr oppgjør med et linje for hvert UKE-prosjektnummer
(knyttet mot arrangementer) eksl. billettavgift, og en linje for hvert prosjekt
med kun billettavgiften (da de skal inn på forskjellige kontoer).
Disse rapportene ble dog aldri eksportert automatisk under UKA da det ofte måtte
gjøres korreksjoner av ITK på oppgjørene, og dette er litt problematisk å rette på i
Visma om rapporten allerede er lagt inn.
Scriptet ble kjørt som brukeren billig-web, da billig-web hadde skrivetilgang på et fellesområde
som økonomi hadde for rapporter av denne typen.
TODO: Format på CSV eksport, samt håndtering av MVA og debet/kredit-kontoer.
!! Kravspec
* Et kasseoppgjør skal være knyttet mot en ansvarlig selger.
* Et kasseoppgjør skal inneholde alle økonomiske transaksjoner som er blitt gjor av selgeren innenfor kasseoppgjøret.
* Kasseoppgjør er obligatorisk for vanlige selgere, med unntak for Samfundets (org.nr 1)
* Et kasseoppgjør skal kunne regne ut kassedifferanse og lagre dette for fremtidig bruk ved avslutting av et kasseoppgjør.
* Et kasseoppgjør skal kunne knyttes opp mot en fysisk kasse (skaffes fra CashKing).
* En transaksjon skal ha informasjon om retning på pengene, tidspunkt, brukeren som gjennomførte transaksjonen og transaksjonssummen.
* Det skal kunne genereres automatiske rapporter fra kasseoppgjørene i CSV-format.
use strict;
use warnings;
use utf8;
use Test::More tests => 12;
use Billig::Economy::Settlement;
#FIXME
my $dbh = undef;
$dbh->{'AutoCommit'} = 0;
sub clean_db {
my ($dbh) = @_;
$dbh->rollback;
#TODO: Insert fixture data
}
# Test that the module compiles and that we can use it.
BEGIN { use_ok ('Billig::Economy::Settlement'); }
require_ok('Billig::Economy::Settlement');
my $large_diff_settlement = 100;
# Test current_settlement's two known outputs
isnt(undef, Billig::Economy::Settlement::current_settlement('klette', $dbh), 'current_settlement existing');
clean_db($dbh);
is(undef, Billig::Economy::Settlement::current_settlement('idontexist', $dbh), 'current_settlement non-existing');
clean_db($dbh);
# Test creation of settlement
is(Billig::Economy::Settlement::ERROR_OPEN_SETTLEMENT_EXISTS,
Billig::Economy::Settlement::new_settlement('klette',2, $dbh), 'new_settlement exists');
clean_db($dbh);
isnt(undef, Billig::Economy::Settlement::new_settlement('foobar', 2, $dbh), 'new_settlement ok');
clean_db($dbh);
# Test diff threshold calculation
is(1, Billig::Economy::Settlement::_exceeds_diff_threshold($large_diff_settlement, 1000, 1000, $dbh), '_exceed_diff_threshold positive');
clean_db($dbh);
is(1, Billig::Economy::Settlement::_exceeds_diff_threshold($large_diff_settlement, -1000, -1000, $dbh), '_exceed_diff_threshold negative');
clean_db($dbh);
is(1, Billig::Economy::Settlement::_exceeds_diff_threshold($large_diff_settlement, undef, undef, $dbh), '_exceed_diff_threshold undef');
clean_db($dbh);
is(0, Billig::Economy::Settlement::_exceeds_diff_threshold($large_diff_settlement, 0, 0, $dbh), '_exceed_diff_threshold ok');
clean_db($dbh);
# Test closing of an settlement
is($Billig::Economy::Settlement::LARGE_DIFF_ERROR,
Billig::Economy::Settlement::close_settlement('klette', '2', 2, 1000, 1000, $dbh),
'close_settlement large diff');
clean_db($dbh);
is(1, Billig::Economy::Settlement::close_settlement('klette', '2', 2, 0, 0, $dbh), 'close_settlement ok');
clean_db($dbh);
package Billig::Economy::Settlement;
use strict;
use warnings;
use utf8;
use Carp;
# Return constants
our $ERROR_OPEN_SETTLEMENT_EXISTS = -2;
our $ERROR_LARGE_DIFF = -3;
our $ERROR_NOT_CLOSED = -4;
sub current_settlement {
my ($seller, $organisation, $dbh) = @_;
my ($settlement) = $dbh->fetchrow('
SELECT settlement FROM settlement
WHERE owner = ? AND organisation = ?
AND endtime IS NULL
LIMIT 1',
undef, $seller, $organisation);
return $settlement;
}
sub new_settlement {
my ($seller, $organisation, $cash_king_id, $dbh) = @_;
if (defined(Billig::Economy::Settlement::current_settlement($seller, $organisation, $dbh))){
return $ERROR_OPEN_SETTLEMENT_EXISTS;
}
my ($settlement) = $dbh->fetchrow('
INSERT INTO settlement (owner, organisation, cash_king_id)
VALUES (?,?,?)', undef, $seller, $organisation, $cash_king_id);
return $settlement;
}
sub close_settlement {
my ($seller, $organisation, $settlement, $cash_in, $credit_in, $override, $dbh) = @_;
if (!$override and _exceeds_diff_threshold($settlement, $cash_in, $credit_in, $dbh)){
return $ERROR_LARGE_DIFF;
}
$dbh->execute('
UPDATE settlement
SET cash_in = ?, credit_in = ?, endtime = NOW()
WHERE settlement = ?', undef, $cash_in, $credit_in, $settlement);
return $dbh->rows;
}
sub new_ticket_transaction {
my ($settlement, $seller, $ticket, $transaction_type, $payment_method, $dbh) = @_;
$dbh->execute('INSERT INTO transaction (settlement, seller, ticket, transaction_type, payment_method)
VALUES (?,?,?,?,?)', undef, $settlement, $seller, $ticeket, $transaction_type, $payment_method);
return $dbh->rows;
}
#
# Internal functions
#
sub _exceeds_diff_threshold {
my ($settlement, $cash_in, $credit_in, $dbh) = @_;
my ($cash, $credit, $_cash_in, $_credit_in) = $dbh->fetchrow('
SELECT cash, credit, cash_in, credit_in
FROM settlement_report
WHERE settlement = ?', undef, $settlement);
# Use values stored in database if parameters are undefined.
if (!defined($cash_in) || !defined($credit_in)){
$cash_in = $_cash_in;
$credit_in = $_credit_in;
}
my $total = $cash_in + $credit_in;
if (abs($total - ($cash + $credit)) > $Billig::Config::settlement_diff_threshold){
return 1;
}
return 0;
}
1;
__END__
=head1 NAME
Billig::Economy::Settlement - Settlement package for Billig
=head1 VERSION
2.0
=head1 SYNOPSIS
use Billig::Economy::Settlement;
my $seller = Billig::get_seller($dbh);
my $settlement = Billig::Economy::Settlement::current_settlement($seller, $dbh);
if (!defined($settlement)){
$settlement = Billig::Economy::Settlement::new_settlement($seller, ..);
}
=head1 DESCRIPTION
This module handles almost all aspects of Billig's support for handling settlements.
Some of the logic however resides in the database. See sql/settlement.sql for details.
=head1 DEVELOPING
Adding or modifying features in this module is fairly easy, but please follow these basic guidelines:
* Keep your functions small and consise. If it gets too complex, split the logic up in multiple functions.
* Test your functions. Tests go into the t/settlement.pl.
* Embrace code reuse, but use as few external dependencies as possible (this includes Billig::*-stuff).
* NEVER make your functions commit or rollback anything on the current DB-connection. NEVER!.
=head2 CONSTANTS
ERROR_LARGE_DIFF
ERROR_OPEN_SETTLEMENT_EXISTS
=head2 API
This section documents the exported methods from this module.
=over 4
=item C<current_settlement($seller, $organisation, $dbh)>
Returns an open settlement owned by the seller given. If no open settlements exists
undef is returned.
=item C<new_settlement($seller, $organisation, $cash_king_id, $dbh)>
Returns the settlement-id for a newly created settlement.
Takes four parameters:
$seller - The owner of the settlement
$organisation - The organisation responsible for the settlement
$cash_king_id - Optional Cash King ID for tracking settlements
$dbh - The current database connection (created by Billig::db_connect())
=item C<close_settlement($seller, $organisation, $settlement, $cash_in, $credit_in, $override, $dbh)>
Tries to close an existing settlement. If the the the accumelated cashflow is above the
set threshold, Billig::Economy::Settlement::ERROR_LARGE_DIFF is returned. Returns 1 on success.
If $override is true (1), the settlement is closed with no regard to the settlement diff threshold.
=item C<new_ticket_transaction($settlement, $seller, $ticket, $transaction_type, $payment_method, $dbh)>
Creates a new transaction connected to the settlement. Sanity checks are performed by the database.
See the database schema for details (sql/settlement.sql).
Returns 1 on success.
=back
=head2 Internal functions
=over 4
=item C<_exceeds_diff_threshold($settlement, $cash_in, $credit_in, $dbh)>
Tests whether a settlement has a larger difference than the allowed threshold. If
the $cash_in and $credit_in parameters are undefined it uses the values stored in the
database.
Returns 1 if the settlement exceeds the threshold, 0 if its lower than the threshold.
=back
--
-- Billig::Economy::Settlement SQL (2.0)
--
-- Changes from 1.0 (UKA-09):
-- - Settlement does not contain information about the physical cash register and terminal.
-- - No 'change' transaction type - handled as one refund and one sale pr ticket affected.
-- - No 'reservation' payment method. Reservation payments are not handled in billig unless
-- they are paid through NetAxcept, which makes it a normal netsale (not handled by this module).
-- - Transactions are now made per ticket and not per purchase, which removes the need for the old
-- ticket_transaction-table and decreases the reporting complexity quite a bit.
-- - Transactions do not have the transaction_sum-field anymore as its redundant and a violation of 3NF,
-- join with the price_group-table instead to find the sum.
-- - Added the settlement_report-view for moving a lot of client-side code (TODO: Analyze performance)
--
-- TODO:
-- - Include muliple seller support from eide
CREATE TABLE settlement (
settlement SERIAL PRIMARY KEY NOT NULL,
owner VARCHAR NOT NULL,
organisation INTEGER REFERENCES (organisation) NOT NULL,
starttime TIMESTAMP NOT NULL DEFAULT NOW(),
endtime TIMESTAMP,
cash_in INTEGER, -- Cash contained in the register at endtime
credit_in INTEGER, -- Credit in as reported by the terminal
cash_king_id INTEGER, -- Export ID from CashKing (used under UKA) (Renamed from uka_transaction_id in old version)
);
CREATE TABLE transaction_type (
transaction_type VARCHAR PRIMARY KEY NOT NULL
);
INSERT INTO transaction_type VALUES ('sale');
INSERT INTO transaction_type VALUES ('refund');
CREATE TABLE payment_method (
payment_method VARCHAR PRIMARY KEY NOT NULL
);
INSERT INTO payment_method VALUES ('cash');
INSERT INTO payment_method VALUES ('credit');
CREATE TABLE transaction (
transaction SERIAL PRIMARY KEY NOT NULL,
settlement INTEGER REFERENCES (settlement) NOT NULL,
ticket INTEGER REFERENCES (ticket) NOT NULL,
seller VARCHAR NOT NULL,
transaction_type VARCHAR REFERENCES (transaction_type)
transaction_time TIMESTAMP NOT NULL DEFAULT NOW(),
payment_method VARCHAR REFERENCES (payment_method),
-- Don't allow multiple transactions of the same type on one ticket
UNIQUE (ticket, transaction_type),
-- We only support refunds in cash
CHECK (transaction_type = 'sale' OR (transaction_type = 'refund' AND payment_type = 'cash'))
);
CREATE VIEW settlement_report (
SELECT settlement, seller, starttime, endtime, cash_in, credit_in,
(cash_sale.cash_sale - cash_sale.cash_refund) AS cash,
credit_sale.credit_sale AS credit
FROM settlement
NATURAL JOIN transaction
NATURAL JOIN (
SELECT settlement, SUM(price) AS cash_sale
FROM transaction
NATURAL JOIN ticket
NATURAL JOIN price_group
WHERE payment_type = 'cash' AND transaction_type = 'sale'
GROUP BY settlement
) AS cash_sale,
NATURAL JOIN (
SELECT settlement, SUM(price) AS cash_refund
FROM transaction
NATURAL JOIN ticket
NATURAL JOIN price_group
WHERE payment_type = 'cash' AND transaction_type = 'refund'
GROUP BY settlement
) AS cash_refund,
NATURAL JOIN (
SELECT settlement, SUM(price) AS credit_sale
FROM transaction
NATURAL JOIN ticket
NATURAL JOIN price_group
WHERE payment_type = 'credit' AND transaction_type = 'sale'
GROUP BY settlement
) AS credit_sale
);
--
-- Returns the sold tickets that were not refunded within the same settlement. Returns the settlement value and the ticket.
-- Ex: SELECT SUM(price) FROM ticket NATURAL JOIN price_group WHERE ticket in (SELECT ticket FROM settlement_tickets WHERE settlement = 100);
--
CREATE VIEW settlement_tickets (
SELECT settlement, ticket FROM (
SELECT settlement, ticket, refunds.ticket
FROM transaction
NATURAL LEFT JOIN (
SELECT settlement, ticket
FROM transaction
WHERE transaction_type = 'refund') AS refunds
WHERE transaction_type = 'sale'
AND refunds.ticket IS NULL) AS collector
ORDER BY settlement, ticket
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment