sábado, 4 de agosto de 2012

Programacion PHP / Yii Framework / Cargos y Abonos a Cuentas

Los Cargos y Abonos.

Se conoce por "Sistema de Cargos y Abonos" a aquel componente de software encargado de proveer una interfaz de programación para registrar las entradas y/o salidas regularmente de dinero (pero pudiese ser cualquier valor) para una cuenta.  Para mayor profundidad sería de útil ayuda leer un libro de economía acerca de los conceptos de balance, cuenta, débito y crédito.

Normalmente un sistema de cargo y abono tiene tres funciones muy claras:
  1. Crear un cargo a una cuenta.
  2. Crear un abono a una cuenta.
  3. Consultar una cuenta.

Ejemplo:

Tienes un sistema de administración de condominios, a los propietarios se les hacen cargos mensuales, estos abonan o pagan completo y finalmente consultan cuando deben.

Otros casos, pueden ser muchos, en donde un cliente dice "voy a pagar esta factura...esta....y esta otra"...."cuanto es ?"...."ok aqui va el cheque.".  Quiza este haciendo pagos parciales o pagos totales, o mixtos.

Este sistema de Cargos y Abonos funciona para Yii Framework, el cual considero un muy buen framework PHP de cual estoy muy feliz de conocer.  A pesar de que la configuración es sencilla, la implementación es mas que todo larga, mas que complicada, pero en el fondo simple: Solo tienes que recibir peticiones muy basicas del sistema "guardar esto....insertar esto"...nada mas, no tienes que manejar logicas de negocio y calculos, ni verificaciones nada, todo eso lo hace el modulo, ni siquiera tienes que hacer formularios, estos también los hace el módulo.

Si ves las imagenes de muestra abajo, la interfaz esta hecha con bootstrap...y escribiendo este blog me acabo de acordar que debo documentar eso...porque si pretenden usarla sin bootstrap entonces recibiran un error porque algunos componentes de la UI (ninguno indispensable) estan hechos en base a bootstrap, pero es algo simple de resolver.

Cargo y Abono

sitio oficial:
https://bitbucket.org/christiansalazarh/cargoyabono

@author: Christian Salazar H. christiansalazarh@gmail.com @bluyell
Modulo para el manejo de cargos y abonos a una Persona (natural o juridica).
Es independiente del modelo de datos seleccionado, opera bajo interfaz.
ver diagramas adjuntos en carpeta diseno\
crearcargo crearabonoconsultacuenta

Cómo funciona.

Un sistema de manejo de cargos y abonos casi siempre es lo mismo, con varianzas que pueden abstraerse, dicho de otro modo todos tienen algo en comun: agregar cargos a una cuenta, agregar abonos que neutralizen los cargos y finalmente ver el saldo o listar las cuentas.
Lo que hace este modulo es dejarte a ti solo la responsabilidad de decir donde guardar y de donde leer, siendo el modulo capaz de manejar todo lo demás de forma encapsulada.

Instalación

en config/main.php
'modules'=>array(
    'cargoyabono'=>array(
        'debug'=>true,
        'layout'=>'//layouts/column1',
        'config'=>array(

            // esta es una cuenta, referenciada en el api bajo la palabra KEY
            // se hace asi para que se pueden tener distintos tipos de cuenta
            // en el mismo sistema, cada uno con su grupo de tablas y valores.
            // equivalente a un namespace para el sistema de cargo y abono.
            //
            'cuenta'=>array(
                'persona'=>'Persona',       
                'cuenta'=>'CuentaPersona',
                'historia'=>'HistoriaAbono',
                'cargo'=>1, // codigo que se usa para indicar que es cargo
                'abono'=>2, // codigo para abono
            ),

        ),
    ),
),
'components'=>array(
    'cyaui'=>array('class'=>'application.modules.cargoyabono.components.CyaUI'),
    'cyaApi'=>array('class'=>'application.modules.cargoyabono.components.CyaApi'),

    // necesario porque se usa el tipo de formato 'money' que esta hecho en la clase CyaFormat
    // del modulo cargoyabono
    'format' => array(
        'class'=>'application.modules.cargoyabono.components.CyaFormat',
        'datetimeFormat'=>"d M, Y h:m:s a",
        'dateFormat'=>"d-m-Y",
        'simboloMoneda'=>'Bsf.',
    ),
),

Explicacion de la configuración.

Layout:
Indica que layout se usara para presentar los formularios.
Config:
Es un array, presenta los tipos de cuenta a manejarse.  en una misma aplicacion pueden
haber distintos tipos de cuenta.
    a) El argumento 'persona'=>'Persona' indica que la clase "Persona" sera el objetivo de la 
    cuenta, es decir a quien se le haran los cargos o abonos.
    b) El argumento 'cuenta'=>'CuentaPersona' indica donde se haran los cargos y los abonos relativos
    a la persona seleccionada.

Usandolo

Empiezo por mostrar un sistema de ejemplo que tiene dos tablas
CREATE TABLE zlm_persona (
  idpersona serial,
  rifced VARCHAR(20) NULL ,
  nombre VARCHAR(250) NULL ,
  direccion VARCHAR(100) NULL ,
  telefonos VARCHAR(100) NULL ,
  tipopersona integer,

  PRIMARY KEY (idpersona) )
;

CREATE TABLE zlm_cuentapersona (
  idcuentapersona serial,
  fechahora bigint,
  fecha bigint,
  monto float,
  concepto varchar(512),
  itemno varchar(20),
  idpersona int not null,
  tipocuenta integer,
  idcuentapersonapagada int,
  estatuscuenta integer DEFAULT 0,
  montoabonado double precision DEFAULT 0,

  -- estatus de como se hizo el pago --
  docnum varchar(45),       -- numero del cheque o transfer -- 
  doctipo varchar(20),      -- CHECK, TRANS, DEPOS --
  docentidad varchar(45),   -- nombre de la entidad, nombre del banco --

  CONSTRAINT fk_cuentapersona_persona
    FOREIGN KEY (idpersona )
    REFERENCES zlm_persona (idpersona )
    ON DELETE RESTRICT
    ON UPDATE NO ACTION,

  PRIMARY KEY (idcuentapersona) )
;

CREATE TABLE zlm_historiaabono (
  idhistoriaabono serial,
  idcargo int not null,
  idabono int not null,
  monto float,
  fechahora bigint,

  CONSTRAINT fk_historia_cargo
    FOREIGN KEY (idcargo )
    REFERENCES zlm_cuentapersona (idcuentapersona)
    ON DELETE CASCADE
    ON UPDATE NO ACTION,

  CONSTRAINT fk_historia_abono
    FOREIGN KEY (idabono )
    REFERENCES zlm_cuentapersona (idcuentapersona)
    ON DELETE CASCADE
    ON UPDATE NO ACTION,

  PRIMARY KEY (idhistoriaabono) )
;
Pues bien, pueden ser usadas cualquier tipo de tablas ya que el modulo cargoyabono es abstracto.
En el modelo Persona.php que representa al modelo de datos: persona (arriba sql), se debe implementar una interfaz que el modulo cargoyabono provee:
class Persona extends CActiveRecord implements IcyaPersona
{
    public function cya_buscarPersonas($texto){
        return 
        Yii::app()->db->createCommand()
            ->select()
            ->from($this->tableName())
            ->where("nombre like :patron", array(
                ':patron'=>"%".$texto."%",
            ))
            ->queryAll();
        ;
    }
    public function cya_getobject($obj){
        return array('id'=>$obj['idpersona'],'label'=>$obj['nombre'],'extra'=>$obj['rifced']);
    }
    public function cya_buscarPersona($id){
        return self::model()->findByPk($id);
    }

    ..
    ..
}
En el modelo CuentaPersona.php, se implementan los siguientes metodos de la interfaz:
class CuentaPersona extends CActiveRecord implements IcyaCuenta
{
    const CUENTA_CARGO = 1;
    const CUENTA_ABONO = 2;

    const ESTATUSCUENTA_PENDIENTE = 0;
    const ESTATUSCUENTA_PARCIAL = 1;
    const ESTATUSCUENTA_TOTAL = 2;
    const ESTATUSCUENTA_NOAPLICA = 3;

    // recibe un array con atributos para crear una cuenta nueva de tipo cargo
    //  ejemplo:
    //      [idpersona,1][concepto,hola][fecha,01-08-2012][monto,2000][itemno,4555][key,cuenta]
    public function cya_crearcargo($campos){

        $cargo = new CuentaPersona();
        $cargo->tipocuenta = self::CUENTA_CARGO;
        $cargo->fechahora = time();
        $cargo->fecha = time($campos['fecha']);
        $cargo->monto = 1*($campos['monto']);
        $cargo->concepto = $campos['concepto'];
        $cargo->itemno = $campos['itemno'];
        $cargo->idpersona = $campos['idpersona'];
        $cargo->estatuscuenta = self::ESTATUSCUENTA_PENDIENTE;
        if($cargo->insert()){
            return $cargo->getPrimaryKey();
        }else{
            return null;
        }
    }
    // recibe un array con atributos para crear una cuenta nueva de tipo cargo
    //  ejemplo:
    //      [idpersona,1][concepto,hola][fecha,01-08-2012][monto,2000][itemno,4555][key,cuenta]
    //  por ser un abono, recibe tres campos mas: (a diferencia de crearcargo)
    //      [docnum,1298918291][doctipo,CHECK][docentidad,banco mercantil]
    public function cya_crearabono($campos){

        $cargo = new CuentaPersona();
        $cargo->tipocuenta = self::CUENTA_ABONO;
        $cargo->fechahora = time();
        $cargo->fecha = time($campos['fecha']);
        $cargo->monto = 1*($campos['monto']);
        $cargo->concepto = $campos['concepto'];
        $cargo->itemno = $campos['itemno'];
        $cargo->idpersona = $campos['idpersona'];
        $cargo->estatuscuenta = self::ESTATUSCUENTA_NOAPLICA;

        $cargo->docnum = $campos['docnum'];
        $cargo->doctipo = $campos['doctipo'];
        $cargo->docentidad = $campos['docentidad'];

        if($cargo->insert()){
            return $cargo->getPrimaryKey();
        }else{
            return null;
        }
    }
    /** Lista las cuenta de la persona seleccionada.

        $params:
            es un array de parametros que el API envia a la clase host.
            se cuenta con:
                'pagadas'=>true o false,  
                    para indicar que entrege solo las cuentas pagadas o no.

        se espera que retorne:

            return  self::model()->findAllByAttributes(array('idpersona'=>$idpersona));
    */
    public function cya_listarcuentas($idpersona,$params=array()){
        if(isset($params['pagadas'])){

            if($params['pagadas'] == true){
                $criteria=new CDbCriteria();
                $criteria->compare('idpersona',$idpersona);
                $criteria->compare('tipocuenta',self::CUENTA_CARGO);
                $criteria->compare('estatuscuenta',self::ESTATUSCUENTA_TOTAL,false);
                return self::model()->findAll($criteria);
            }else{
                return self::model()->findAll(
                         'idpersona = '.$idpersona.' and tipocuenta = '.self::CUENTA_CARGO.' and '
                        .'(estatuscuenta = '.self::ESTATUSCUENTA_PENDIENTE.') or '
                        .'(estatuscuenta = '.self::ESTATUSCUENTA_PARCIAL.')'
                    );
            }
        }else{
            return self::model()->findAllByAttributes(array('idpersona'=>$idpersona));
        }
    }
    /** pide al modelo host que devuelva un array con los campos solicitados.

        array('id'=>'x','fecha'=>'x','tipo'=>'x','concepto'=>'x','monto'=>1000,'idpersona'=>1
            ,'tipocuenta'=>'1','tipocuentatxt'=>'CARGO','estatus'=>1,'estatustxt'=>'pendiente'
            ,'montoabonado'=>900,'montopendiente'=>100,'refno'=>'12287',
            ,'docnum'=>'19289812', 'doctipo'=>'check', 'docentidad'=>'banco mercantil')     
    */
    public function cya_getobject($obj){
        return array(
            'id'=>$obj->getPrimaryKey(),
            'fecha'=>$obj->fecha,
            'tipo'=>$obj->tipocuenta,
            'concepto'=>$obj->concepto,
            'monto'=>$obj->monto,
            'refno'=>$obj->itemno,
            'idpersona'=>$obj->idpersona,
            'tipocuenta'=>$obj->tipocuenta,
            'tipocuentatxt'=>$obj->tipocuenta==self::CUENTA_CARGO ? "CARGO" : "ABONO",
            'estatus'=>$obj->estatuscuenta,
            'estatustxt'=>self::etiquetarEstatus($obj->estatuscuenta),
            'montoabonado'=>$obj->montoabonado,
            'montopendiente'=>$obj->monto-$obj->montoabonado,
            'docnum'=>$obj->docnum,
            'doctipo'=>$obj->doctipo,
            'docentidad'=>$obj->docentidad,
        );
    }

    /** registra una historia de abono a un cargo por un valor especifico.

        sirve para registrar que abonos se le hicieron a cual cargo y viceversa.

        idAbono:    
            el identificador primario del abono obtenido con cyaApi.crearAbono

        idCargo:
            el identificador primario del cargo a ser abonado.

        montoAbonado:   
            el valor que se le quiere abonar al cargo

        returns:
            nada
    */
    public function cya_crearhistoriaabono($idAbono,$idCargo,$montoAbonado){
        Yii::app()->db->createCommand()
            ->insert("zlm_historiaabono", array(
                'idabono'=>$idAbono,
                'idcargo'=>$idCargo,
                'fechahora'=>time(),
                'monto'=>$montoAbonado
            ));
    }

    /** suma el monto al cargo indicado, para ser acumulado en cargo.montoabonado.

        deberia ser usada en conjunto con crearHistoriaAbono, para que quede historia
        de los abonos realizados a un cargo.

        idcargo:
            el identificador primario del cargo a ser abonado.
        monto:
            el valor que se quiere sumar a cargo.montoabonado

        returns:
            nada.
    */
    public function cya_actualizarcargo($idcargo,$monto){
        $cargoInst = self::model()->findByPk($idcargo);
        $cargoInst->montoabonado = $cargoInst->montoabonado + $monto;
        $cargoInst->estatuscuenta = self::ESTATUSCUENTA_PARCIAL;
        if($cargoInst->montoabonado >= $cargoInst->monto)
            $cargoInst->estatuscuenta = self::ESTATUSCUENTA_TOTAL;
        $cargoInst->update();
    }

    public function etiquetarEstatus($valor){
        if($valor == self::ESTATUSCUENTA_PENDIENTE)
            return "pendiente";
        if($valor == self::ESTATUSCUENTA_PARCIAL)
            return "parcial";
        if($valor == self::ESTATUSCUENTA_TOTAL)
            return "total";
        if($valor == self::ESTATUSCUENTA_NOAPLICA)
            return "...";

        return "estatus desconocido";
    }

    ...
    ...
}
finalmente la tabla de historia queda asi:
class HistoriaAbono extends CActiveRecord implements IcyaHistoria
{

    /** lista los ABONOS que se hicieron para este CARGO (id)

        ejemplo:
            valor que retorna model()->findAllByAttributes()

        returns:
            array de instancias de clase del modelo de datos
    */
    public function cya_listarhistoriacargo($id){
        return self::model()->findAllByAttributes(array('idcargo'=>$id));
    }

    /** lista los CARGOS que se abonaron con el ABONO indicado (id)

        ejemplo:
            valor que retorna model()->findAllByAttributes()

        returns:
            array de instancias de clase del modelo de datos
    */
    public function cya_listarhistoriaabono($id){
        return self::model()->findAllByAttributes(array('idabono'=>$id));
    }

    /** obtiene los valores del objeto en forma de array.

        obj:
            instancia del modelo de datos recibida por funciones cya_listarhistoriaXXXX()

        returns:
            array con lista de campos:

            id:         identificador primario del registro de historia
            idcargo:    identificador primario de la cuenta cargo
            idabono:    identificador primario de la cuenta abono
            fechahora:  valor numerico del timestamp de fechahora
            monto:      valor float del monto abonado
            adata:      data (string) serializada del registro abono referenciado
            cdata:      data (string) serializada del registro cargo referenciado
    */
    public function cya_getobject($obj){
        return array(
            'id'=>$obj->getPrimaryKey(),
            'idcargo'=>$obj->idcargo,
            'idabono'=>$obj->idabono,
            'fechahora'=>$obj->fechahora,
            'monto'=>$obj->monto,
            'adata'=>serialize($obj->idabono0),
            'cdata'=>serialize($obj->idcargo0),
        );
    }

    ...
    ...
}

Muy bien todo esta configurado, pero ahora cómo lo uso ?

Existen dos componentes configurados, estos podrian servirte para hacer consultas a bajo nivel.
<?php 
    echo CHtml::link("crear cargo"  
    ,Yii::app()->cyaui->getCrearCargoUrl('cuenta'))); ?>

usalo para que se cree un enlace "crear cargo" que lanzara el formulario de nuevo cargo. El argumento "cuenta" hace referencia a la entrada en config main llamada 'cuenta', la cual indica cuales seran las clases
involucradas.

<?php 
    echo CHtml::link("crear abono"  
    ,Yii::app()->cyaui->getCrearAbonoUrl('cuenta'))); ?>

usalo para que se cree un enlace "crear abono" que lanzara el formulario de nuevo abono. El argumento "cuenta" hace referencia a la entrada en config main llamada 'cuenta', la cual indica cuales seran las clases
involucradas.

<?php 
    echo CHtml::link("consultar"    
    ,Yii::app()->cyaui->getConsultarUrl('cuenta'))); ?>

usalo para que se cree un enlace que muestra la consulta de la cuenta.
El componente Yii::app()->cyaApi te provee funciones de acceso de bajo nivel, no todas son de uso libre, algunas son usadas internamente por el modulo, pero una como esta puede servir:
<?php 
    $saldo = Yii::app()->cyaApi->calculaSaldo('cuenta',$idpersona,&$totalCargos,&$totalAbonos); 
?>
Puedes hallar documentacion de cada metodo del api con un ejemplo en components/CyaApi.php

Resumen

  • Como muestro arriba, la unica responsabilidad del modelo de datos, de tu aplicacion, es decir donde y como vas a guardar la informacion que el modulo requiere.
  • Si te das cuenta ambas interfaces proveen un metodo llamado "public function cya_getobject($obj)", este funciona asi:
    cuando el api interna de CargoyAbono quiere conocer digamos, la lista de cuentas, o una lista de personas invoca a tus metodos implementados en tus modelos para traerse la lista original de instancias,pero no toca ninguna..porque el modulo no conoce y no debe saber que campos tienes ahi, por tanto de nuevo, te pregunta a ti mediante cya_getobject($obj) para que tu devuelvas lo que el modulo requiere para la instancia especifica $obj.
  • Como un ejemplo en pseudocodigo, lo que haria seria como esto:
    modulo_pidiendo_lista_de_cuentas:
    
        $lista = interfaz.cya_listarcuentas(idpersona);
        // lista trae un monton de instancias que tu devolviste en crudo con findAllByAttributes(..)
    
        $item = array();
    
        foreach($lista as $obj)
            $item[] = interfaz.cya_getobject($obj)
    
        listo, ahora item tiene lo que tu indicas que debe haber para, por ejemplo , renderizar 
        una lista de cuentas. En algunos casos cya_getobject te indica en la documentacion que 
        es lo que se espera que tu devuelvas.
    

UML Diseńo

casouso buscarpersona componentes dc-crearcargo dc-crearabono

6 comentarios:

  1. Buen Trabajo Cristian si quieres agregame al g+ podemos intercambiar modulos un saludo

    ResponderEliminar
  2. Hola, tenes un demo o podrias facilitar el codigo, estoy trabajando en un proyecto y me gustaria integrarlo como modulo de pagos y abonos de pensiones para un sistema academico.

    Saludos

    ResponderEliminar
    Respuestas
    1. hola, no dispongo de un demo listo para usar. tendrias que crear una app yii en blanco, implementar el modulo y en tus modelos de datos insertar las dependencias aqui expuestas, de resto el modulo lo hace todo, proveyendote api.

      Eliminar
  3. gracias por los comentarios, espero les sirva de ayuda

    ResponderEliminar