Created
November 12, 2009 01:10
-
-
Save klette/232497 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
! 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- | |
-- 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