Overview

Cargo y Abono

Author James: 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 crearabono consultacuenta

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: aunque el api admite indicar el layout en cualquier momento aqui se puede
        // indicar cual sera el layout en forma global. leer mas abajo en este mismo documento
        '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);



    Yii::app()->cyaApi->crearCargo('cuenta',
            array(  
                'idpersona'=>'1',
                'concepto'=>'hola',
                'fecha'=>'01-08-2012',
                'monto'=>'2000',
                'itemno'=>'4555',
                'key'=>'cuenta'
            )
        );      
?>

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.
    

Layouts

Hay dos maneras de manejar el layout: global y especifica.

global: se hace indicando en config/main el layout indicado. ver mas arriba la configuracion.

especifica: se hace agregando el argumento 'layout' a la URL, ejemplo: Yii::app()->cyaui->getCrearCargoUrl('cuenta','//layouts/otro'); esta llamada hara que los formularios implicados usen el layout solicitado en argumento, en vez del layout especificado en forma global.

UML Diseńo

casouso buscarpersona componentes dc-crearcargo dc-crearabono