Movimentazioni (#766)

Aggiunto raggruppamento dei movimenti per documento, aggiornamento per causali personalizzate dei movimenti manuali e implementazione nuovo sistema di riferimento ai documenti.
This commit is contained in:
Thomas Zilio 2020-03-03 10:33:32 +01:00 committed by GitHub
parent 9216cd1288
commit fda1bca710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 663 additions and 266 deletions

View File

@ -5,6 +5,7 @@ namespace Common\Components;
use Common\Document;
use Illuminate\Database\Eloquent\Builder;
use Modules\Articoli\Articolo as Original;
use Modules\Articoli\Movimento;
use UnexpectedValueException;
abstract class Article extends Row
@ -49,7 +50,10 @@ abstract class Article extends Row
}
}
abstract public function getDirection();
public function getDirection()
{
return $this->parent->direzione;
}
/**
* Imposta i seriali collegati all'articolo.
@ -205,7 +209,24 @@ abstract class Article extends Row
return !empty($this->abilita_serial) && !empty($this->serialRowID);
}
abstract protected function movimentaMagazzino($qta);
protected function movimentaMagazzino($qta)
{
$documento = $this->fattura;
$data = $documento->data;
$qta_movimento = $documento->direzione == 'uscita' ? $qta : -$qta;
$movimento = Movimento::descrizioneMovimento($qta_movimento, $documento->direzione).' - '.$documento->getReference();
$partenza = $documento->direzione == 'uscita' ? $documento->idsede_destinazione : $documento->idsede_partenza;
$arrivo = $documento->direzione == 'uscita' ? $documento->idsede_partenza : $documento->idsede_destinazione;
$this->articolo->movimenta($qta_movimento, $movimento, $data, false, [
'reference_type' => get_class($documento),
'reference_id' => $documento->id,
'idsede_azienda' => $partenza,
'idsede_controparte' => $arrivo,
]);
}
protected static function boot()
{

View File

@ -4,7 +4,7 @@ namespace Common;
use Common\Components\Description;
abstract class Document extends Model
abstract class Document extends Model implements ReferenceInterface
{
/**
* Restituisce la collezione di righe e articoli con valori rilevanti per i conti.
@ -66,6 +66,8 @@ abstract class Document extends Model
abstract public function sconti();
abstract public function getDirezioneAttribute();
/**
* Calcola l'imponibile del documento.
*

View File

@ -0,0 +1,14 @@
<?php
namespace Common;
interface ReferenceInterface
{
public function getReferenceName();
public function getReferenceNumber();
public function getReferenceDate();
public function getReference();
}

View File

@ -285,3 +285,26 @@ function discountInfo(\Common\Components\Row $riga, $mostra_maggiorazione = true
'_TYPE_' => (!empty($riga->sconto_percentuale) ? '%' : currency()),
]);
}
function reference($document)
{
if (!empty($document) && !($document instanceof \Common\ReferenceInterface)) {
return;
}
$extra = '';
$module_id = null;
$document_id = null;
if (empty($document)) {
$description = tr('Documento di riferimento non disponibile');
$extra = 'class="disabled"';
} else {
$module_id = $document->module;
$document_id = $document->id;
$description = $document->getReference();
}
return Modules::link($module_id, $document_id, $description, $description, $extra);
}

View File

@ -243,7 +243,7 @@ switch ($resource) {
case 'sedi_azienda':
if (isset($superselect['idanagrafica'])) {
$user = Auth::user();
$id_azienda = get_var('Azienda predefinita');
$id_azienda = setting('Azienda predefinita');
$query = "SELECT * FROM (SELECT '0' AS id, CONCAT_WS(' - ', 'Sede legale' , (SELECT CONCAT (citta, ' (', ragione_sociale,')') FROM an_anagrafiche |where|)) AS descrizione UNION SELECT id, CONCAT_WS(' - ', nomesede, citta) FROM an_sedi |where|) AS tab |filter| ORDER BY descrizione";

View File

@ -205,6 +205,11 @@ class Anagrafica extends Model
// Attributi Eloquent
public function getModuleAttribute()
{
return 'Anagrafiche';
}
/**
* Restituisce l'identificativo.
*

View File

@ -7,6 +7,29 @@ echo '
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">'.tr('Movimenti').'</h3>
<div class="pull-right">';
if (empty($_GET['movimentazione_completa'])) {
echo '
<a class="btn btn-info btn-xs" href="'.$rootdir.'/editor.php?id_module='.$id_module.'&id_record='.$id_record.'&movimentazione_completa=1#tab_'.$id_plugin.'">
<i class="fa fa-eye"></i>
'.tr('Mostra tutti i movimenti').'
</a>';
} else {
echo '
<a class="btn btn-info btn-xs" href="'.$rootdir.'/editor.php?id_module='.$id_module.'&id_record='.$id_record.'&movimentazione_completa=0#tab_'.$id_plugin.'">
<i class="fa fa-eye-slash"></i>
'.tr('Mostra gli ultimi 20 movimenti').'
</a>';
}
echo '
<a class="btn btn-warning btn-xs" href="'.$rootdir.'/controller.php?id_module='.Modules::get('Movimenti')->id.'&search_Articolo='.($articolo->codice.' - '.$articolo->descrizione).'">
<i class="fa fa-external-link"></i>
'.tr('Visualizza dettagli').'
</a>';
echo '
</div>
</div>
<div class="box-body">';
@ -15,33 +38,27 @@ $qta_totale = $dbo->fetchOne('SELECT SUM(qta) AS qta FROM mg_movimenti WHERE ida
$qta_totale_attuale = $dbo->fetchOne('SELECT SUM(qta) AS qta FROM mg_movimenti WHERE idarticolo='.prepare($id_record).' AND data <= CURDATE()')['qta'];
echo '
<p>'.tr('Quantità calcolata dai movimenti').': <b>'.Translator::numberToLocale($qta_totale, 'qta').' '.$record['um'].'</b> <span class=\'tip\' title=\''.tr('Quantità calcolata da tutti i movimenti registrati').'.\' ><i class="fa fa-question-circle-o"></i></span></p>';
<p>'.tr('Quantità calcolata dai movimenti').': <b>'.Translator::numberToLocale($qta_totale, 'qta').' '.$record['um'].'</b> <span class="tip" title="'.tr('Quantità calcolata da tutti i movimenti registrati').'." ><i class="fa fa-question-circle-o"></i></span></p>';
echo '
<p>'.tr('Quantità calcolata attuale').': <b>'.Translator::numberToLocale($qta_totale_attuale, 'qta').' '.$record['um'].'</b> <span class=\'tip\' title=\''.tr('Quantità calcolata secondo i movimenti registrati con data oggi o date trascorse').'.\' ><i class="fa fa-question-circle-o"></i></span></p>';
<p>'.tr('Quantità calcolata attuale').': <b>'.Translator::numberToLocale($qta_totale_attuale, 'qta').' '.$record['um'].'</b> <span class="tip" title="'.tr('Quantità calcolata secondo i movimenti registrati con data oggi o date trascorse').'." ><i class="fa fa-question-circle-o"></i></span></p>';
// Elenco movimenti magazzino
$query = 'SELECT * FROM mg_movimenti WHERE idarticolo='.prepare($id_record).' ORDER BY data DESC, id DESC';
if (empty($_GET['show_all1'])) {
$query .= ' LIMIT 0, 20';
// Individuazione movimenti
$movimenti = $articolo->movimentiComposti()
->orderBy('data', 'id');
if (empty($_GET['movimentazione_completa'])) {
$movimenti->limit(20);
}
$movimenti = $dbo->fetchArray($query);
// Raggruppamento per documento
$movimenti = $movimenti->get();
if (!empty($movimenti)) {
if (empty($_GET['show_all1'])) {
echo '
<p><a href="'.$rootdir.'/editor.php?id_module='.$id_module.'&id_record='.$id_record.'&show_all1=1#tab_'.$id_plugin.'">[ '.tr('Mostra tutti i movimenti').' ]</a></p>';
} else {
echo '
<p><a href="'.$rootdir.'/editor.php?id_module='.$id_module.'&id_record='.$id_record.'&show_all1=0#tab_'.$id_plugin.'">[ '.tr('Mostra solo gli ultimi 20 movimenti').' ]</a></p>';
}
echo '
<table class="table table-striped table-condensed table-bordered">
<tr>
<th class="text-center">'.tr('Q.').'</th>
<th class="text-center">'.tr('Q. progressiva').'</th>
<th>'.tr('Causale').'</th>
<th>'.tr('Operazione').'</th>
<th class="text-center">'.tr('Data').'</th>
<th class="text-center" width="7%">#</th>
</tr>';
@ -53,7 +70,8 @@ if (!empty($movimenti)) {
} else {
$movimento['progressivo_finale'] = $movimenti[$i - 1]['progressivo_iniziale'];
}
$movimento['progressivo_iniziale'] = $movimento['progressivo_finale'] - $movimento['qta'];
$movimento['progressivo_iniziale'] = $movimento['progressivo_finale'] - $movimento->qta;
$movimento['progressivo_iniziale'] = $movimento['progressivo_finale'] - $movimento->qta;
$movimenti[$i]['progressivo_iniziale'] = $movimento['progressivo_iniziale'];
$movimenti[$i]['progressivo_finale'] = $movimento['progressivo_finale'];
@ -62,40 +80,32 @@ if (!empty($movimenti)) {
echo '
<tr>
<td class="text-center">
'.numberFormat($movimento['qta'], 'qta').' '.$record['um'].'
'.numberFormat($movimento->qta, 'qta').' '.$record['um'].'
</td>
<td class="text-center">
'.numberFormat($movimento['progressivo_iniziale'], 'qta').' '.$record['um'].'
<i class="fa fa-arrow-circle-right"></i>
'.numberFormat($movimento['progressivo_finale'], 'qta').' '.$record['um'].'
</td>
<td>
'.$movimento->descrizione.''.($movimento->hasDocument() ? ' - '.reference($movimento->getDocument()) : '').'
</td>';
// Causale
$dir = ($movimento['qta'] < 0) ? 'vendita' : 'acquisto';
if (!empty($movimento['iddocumento'])) {
$dir = $dbo->fetchArray('SELECT dir FROM co_tipidocumento WHERE id = (SELECT idtipodocumento FROM co_documenti WHERE id = '.prepare($movimento['iddocumento']).')')[0]['dir'] == 'entrata' ? 'vendita' : 'acquisto';
}
echo '
<td>'.$movimento['movimento'].'
'.((!empty($movimento['idintervento'])) ? Modules::link('Interventi', $movimento['idintervento']) : '').'
'.((!empty($movimento['idddt'])) ? (Modules::link('DDt di '.$dir, $movimento['idddt'], null, null, (intval($database->fetchOne('SELECT * FROM `dt_ddt` WHERE `id` ='.prepare($movimento['idddt'])))) ? '' : 'class="disabled"')) : '').'
'.((!empty($movimento['iddocumento'])) ? (Modules::link('Fatture di '.$dir, $movimento['iddocumento'], null, null, (intval($database->fetchOne('SELECT * FROM `co_documenti` WHERE `id` ='.prepare($movimento['iddocumento'])))) ? '' : 'class="disabled"')) : '').'
</td>';
// Data
echo '
<td class="text-center" >'.Translator::dateToLocale($movimento['data']).' <span class=\'tip\' title=\''.tr('Data del movimento: ').Translator::timestampToLocale($movimento['created_at']).'\' ><i class="fa fa-question-circle-o"></i></span> </td>';
<td class="text-center">'.dateFormat($movimento->data).' <span class="tip" title="'.tr('Data di creazione del movimento: _DATE_', [
'_DATE_' => timestampFormat($movimento->created_at),
]).'"><i class="fa fa-question-circle-o"></i></span> </td>';
// Operazioni
echo '
<td class="text-center">';
if (Auth::admin() && $movimento['manuale'] == '1') {
if (Auth::admin() && $movimento->isManuale()) {
echo '
<a class="btn btn-danger btn-sm ask" data-backto="record-edit" data-op="delmovimento" data-idmovimento="'.$movimento['id'].'">
<a class="btn btn-danger btn-xs ask" data-backto="record-edit" data-op="delmovimento" data-idmovimento="'.$movimento['id'].'">
<i class="fa fa-trash"></i>
</a>';
}

View File

@ -128,6 +128,28 @@ class Articolo extends Model
return $this->hasMany(ArticoloIntervento::class, 'idarticolo');
}
/**
* Restituisce i movimenti di magazzino dell'articolo.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Query\Builder
*/
public function movimenti()
{
return $this->hasMany(Movimento::class, 'idarticolo');
}
/**
* Restituisce i movimenti di magazzino dell'articolo raggruppati per documento relativo.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Query\Builder
*/
public function movimentiComposti()
{
return $this->movimenti()
->selectRaw('*, sum(qta) as qta_documento, IFNULL(reference_type, UUID()) as tipo_gruppo')
->groupBy('tipo_gruppo', 'reference_id');
}
public function categoria()
{
return $this->belongsTo(Categoria::class, 'id_categoria');

View File

@ -0,0 +1,127 @@
<?php
namespace Modules\Articoli;
use Common\Model;
class Movimento extends Model
{
protected $document;
protected $table = 'mg_movimenti';
public static function build(Articolo $articolo, $qta, $descrizone, $data, $document = null)
{
$model = parent::build();
$model->articolo()->associate($articolo);
$model->qta = $qta;
$model->descrizone = $descrizone;
$model->data = $data;
if (!empty($document)) {
$class = get_class($document);
$id = $document->id;
$model->reference_type = $class;
$model->reference_id = $id;
} else {
$model->manuale = true;
}
$model->save();
return $model;
}
public function getDescrizioneAttribute()
{
$descrizione = $this->movimento;
if ($this->hasDocument()) {
$documento = $this->getDocument();
$descrizione = $documento ? self::descrizioneMovimento($this->qta, $documento->direzione) : $descrizione;
}
return $descrizione;
}
public function getDataAttribute()
{
$data = $this->attributes['data'];
if ($this->hasDocument()) {
$documento = $this->getDocument();
$data = $documento ? $documento->getReferenceDate() : $data;
}
return $data;
}
public function getQtaAttribute()
{
if (isset($this->qta_documento)) {
return $this->qta_documento;
}
return $this->qta;
}
public function articolo()
{
return $this->hasOne(Articolo::class, 'idarticolo');
}
public function movimentiRelativi()
{
return $this->hasMany(Movimento::class, 'idarticolo', 'idarticolo')
->where('reference_type', $this->reference_type)
->where('reference_id', $this->reference_id);
}
public function hasDocument()
{
return isset($this->reference_type);
}
public function isManuale()
{
return !empty($this->manuale);
}
/**
* Restituisce il documento collegato al movimento.
*
* @return Model
*/
public function getDocument()
{
if ($this->hasDocument() && !isset($this->document)) {
$class = $this->reference_type;
$id = $this->reference_id;
$this->document = $class::find($id);
}
return $this->document;
}
public static function descrizioneMovimento($qta, $direzione = 'entrata')
{
if (empty($direzione)) {
$direzione = 'entrata';
}
$carico = ($direzione == 'entrata') ? tr('Ripristino articolo') : tr('Carico magazzino');
$scarico = ($direzione == 'entrata') ? tr('Scarico magazzino') : tr('Rimozione articolo');
$descrizione = $qta > 0 ? $carico : $scarico;
// Descrizione per vecchi documenti rimossi ma con movimenti azzerati
if ($qta == 0) {
$descrizione = tr('Nessun movimento');
}
return $descrizione;
}
}

View File

@ -25,7 +25,7 @@ switch (filter('op')) {
]));
}
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio.'));
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;

View File

@ -0,0 +1,41 @@
<?php
include_once __DIR__.'/../../core.php';
switch (filter('op')) {
case 'update':
if (isset($id_record)) {
$database->update('mg_causali_movimenti', [
'nome' => post('nome'),
'movimento_carico' => post('movimento_carico'),
'descrizione' => post('descrizione'),
], [
'id' => $id_record,
]);
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;
case 'add':
$database->insert('mg_causali_movimenti', [
'nome' => post('nome'),
'movimento_carico' => post('movimento_carico'),
'descrizione' => post('descrizione'),
]);
$id_record = $database->lastInsertedID();
break;
case 'delete':
if (isset($id_record)) {
$dbo->query('DELETE FROM `mg_causali_movimenti` WHERE `id`='.prepare($id_record));
flash()->info(tr('Tipologia di _TYPE_ eliminata con successo!', [
'_TYPE_' => 'movimento predefinito',
]));
}
break;
}

View File

@ -0,0 +1,31 @@
<?php
include_once __DIR__.'/../../core.php';
?><form action="" method="post" id="add-form">
<input type="hidden" name="op" value="add">
<input type="hidden" name="backto" value="record-edit">
<div class="row">
<div class="col-md-9">
{[ "type": "text", "label": "<?php echo tr('Nome'); ?>", "name": "nome", "required": 1 ]}
</div>
<div class="col-md-3">
{[ "type": "checkbox", "label": "<?php echo tr('Movimento di carico'); ?>", "name": "movimento_carico" ]}
</div>
</div>
<div class="row">
<div class="col-md-12">
{[ "type": "textarea", "label": "<?php echo tr('Descrizione'); ?>", "name": "descrizione", "required": 1 ]}
</div>
</div>
<!-- PULSANTI -->
<div class="row">
<div class="col-md-12 text-right">
<button type="submit" class="btn btn-primary"><i class="fa fa-plus"></i> <?php echo tr('Aggiungi'); ?></button>
</div>
</div>
</form>

View File

@ -0,0 +1,37 @@
<?php
include_once __DIR__.'/../../core.php';
?><form action="" method="post" id="edit-form">
<input type="hidden" name="backto" value="record-edit">
<input type="hidden" name="op" value="update">
<!-- DATI -->
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title"><?php echo tr('Dati'); ?></h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-9">
{[ "type": "text", "label": "<?php echo tr('Nome'); ?>", "name": "nome", "required": 1, "value": "$nome$" ]}
</div>
<div class="col-md-3">
{[ "type": "checkbox", "label": "<?php echo tr('Movimento di carico'); ?>", "name": "movimento_carico", "value": "$movimento_carico$" ]}
</div>
</div>
<div class="row">
<div class="col-md-12">
{[ "type": "textarea", "label": "<?php echo tr('Descrizione'); ?>", "name": "descrizione", "required": 1, "value": "$descrizione$" ]}
</div>
</div>
</div>
</div>
</form>
<a class="btn btn-danger ask" data-backto="record-list">
<i class="fa fa-trash"></i> <?php echo tr('Elimina'); ?>
</a>

View File

@ -0,0 +1,7 @@
<?php
include_once __DIR__.'/../../core.php';
if (isset($id_record)) {
$record = $dbo->fetchOne('SELECT * FROM `mg_causali_movimenti` WHERE id='.prepare($id_record));
}

View File

@ -25,14 +25,4 @@ class Articolo extends Article
return $model;
}
public function movimentaMagazzino($qta)
{
return;
}
public function getDirection()
{
return $this->contratto->tipo->dir;
}
}

View File

@ -11,10 +11,12 @@ use Modules\TipiIntervento\Tipo as TipoSessione;
use Plugins\PianificazioneFatturazione\Pianificazione;
use Plugins\PianificazioneInterventi\Promemoria;
use Traits\RecordTrait;
use Traits\ReferenceTrait;
use Util\Generator;
class Contratto extends Document
{
use ReferenceTrait;
use RecordTrait;
protected $table = 'co_contratti';
@ -223,4 +225,21 @@ class Contratto extends Document
return $numero;
}
// Opzioni di riferimento
public function getReferenceName()
{
return 'Contratto';
}
public function getReferenceNumber()
{
return $this->numero;
}
public function getReferenceDate()
{
return $this->data_bozza;
}
}

View File

@ -315,8 +315,6 @@ switch (post('op')) {
try {
$ddt->delete();
$dbo->query('DELETE FROM mg_movimenti WHERE idddt = '.prepare($id_record));
flash()->info(tr('Ddt eliminato!'));
} catch (InvalidArgumentException $e) {
flash()->error(tr('Sono stati utilizzati alcuni serial number nel documento: impossibile procedere!'));

View File

@ -104,9 +104,11 @@ switch (post('op')) {
case 'delete-bulk':
foreach ($id_records as $id) {
$dbo->query('DELETE FROM dt_ddt WHERE id = '.prepare($id).Modules::getAdditionalsQuery($id_module));
$dbo->query('DELETE FROM dt_righe_ddt WHERE idddt='.prepare($id).Modules::getAdditionalsQuery($id_module));
$dbo->query('DELETE FROM mg_movimenti WHERE idddt='.prepare($id).Modules::getAdditionalsQuery($id_module));
$documento = DDT::find($id);
try {
$documento->delete();
} catch (InvalidArgumentException $e) {
}
}
flash()->info(tr('Ddt eliminati!'));

View File

@ -63,11 +63,9 @@ foreach ($righe as $riga) {
}
// Aggiunta dei riferimenti ai documenti
$ref = doc_references($r, $dir, ['idddt']);
if (!empty($ref)) {
if ($riga->hasOriginal()) {
echo '
<br>'.Modules::link($ref['module'], $ref['id'], $ref['description'], $ref['description']);
<br>'.reference($riga->getOriginal()->parent);
}
echo '

View File

@ -24,38 +24,4 @@ class Articolo extends Article
return $model;
}
public function movimentaMagazzino($qta)
{
$ddt = $this->ddt;
$tipo = $ddt->tipo;
$numero = $ddt->numero_esterno ?: $ddt->numero;
$data = $ddt->data;
$carico = ($tipo->dir == 'entrata') ? tr('Ripristino articolo da _TYPE_ numero _NUM_') : tr('Carico magazzino da _TYPE_ numero _NUM_');
$scarico = ($tipo->dir == 'entrata') ? tr('Scarico magazzino per _TYPE_ numero _NUM_') : tr('Rimozione articolo da _TYPE_ numero _NUM_');
$qta = ($tipo->dir == 'uscita') ? -$qta : $qta;
$movimento = ($qta < 0) ? $carico : $scarico;
$movimento = replace($movimento, [
'_TYPE_' => $tipo->descrizione,
'_NUM_' => $numero,
]);
$partenza = $ddt->direzione == 'uscita' ? $ddt->idsede_destinazione : $ddt->idsede_partenza;
$arrivo = $ddt->direzione == 'uscita' ? $ddt->idsede_partenza : $ddt->idsede_destinazione;
$this->articolo->movimenta(-$qta, $movimento, $data, false, [
'idddt' => $ddt->id,
'idsede_azienda' => $partenza,
'idsede_controparte' => $arrivo,
]);
}
public function getDirection()
{
return $this->ddt->tipo->dir;
}
}

View File

@ -7,10 +7,12 @@ use Common\Components\Description;
use Common\Document;
use Modules\Anagrafiche\Anagrafica;
use Traits\RecordTrait;
use Traits\ReferenceTrait;
use Util\Generator;
class DDT extends Document
{
use ReferenceTrait;
use RecordTrait;
protected $table = 'dt_ddt';
@ -205,4 +207,21 @@ class DDT extends Document
return $numero;
}
// Opzioni di riferimento
public function getReferenceName()
{
return $this->tipo->descrizione;
}
public function getReferenceNumber()
{
return $this->numero_esterno ?: $this->numero;
}
public function getReferenceDate()
{
return $this->data;
}
}

View File

@ -45,19 +45,6 @@ switch (post('op')) {
break;
case 'delete-bulk':
foreach ($id_records as $id) {
$dbo->query('DELETE FROM co_documenti WHERE id = '.prepare($id).Modules::getAdditionalsQuery($id_module));
$dbo->query('DELETE FROM co_righe_documenti WHERE iddocumento='.prepare($id).Modules::getAdditionalsQuery($id_module));
$dbo->query('DELETE FROM co_scadenziario WHERE iddocumento='.prepare($id).Modules::getAdditionalsQuery($id_module));
$dbo->query('DELETE FROM mg_movimenti WHERE iddocumento='.prepare($id).Modules::getAdditionalsQuery($id_module));
}
flash()->info(tr('Fatture eliminate!'));
break;
case 'genera-xml':
$failed = [];
$added = [];
@ -247,11 +234,23 @@ switch (post('op')) {
flash()->info(tr('Fatture duplicate correttamente!'));
break;
case 'delete-bulk':
foreach ($id_records as $id) {
$documento = Fattura::find($id);
try {
$documento->delete();
} catch (InvalidArgumentException $e) {
}
}
flash()->info(tr('Fatture eliminate!'));
break;
}
if (App::debug()) {
$operations = [
'delete-bulk' => '<span><i class="fa fa-trash"></i> '.tr('Elimina selezionati').'</span>',
'delete-bulk' => tr('Elimina selezionati'),
];
}

View File

@ -142,10 +142,10 @@ foreach ($righe as $riga) {
<br>'.Modules::link($id_module, $record['ref_documento'], $text, $text);
}
$ref = doc_references($r, $dir, ['iddocumento']);
if (!empty($ref)) {
// Aggiunta dei riferimenti ai documenti
if ($riga->hasOriginal()) {
echo '
<br>'.Modules::link($ref['module'], $ref['id'], $ref['description'], $ref['description']);
<br>'.reference($riga->getOriginal()->parent);
}
echo '

View File

@ -24,38 +24,4 @@ class Articolo extends Article
return $model;
}
public function movimentaMagazzino($qta)
{
$fattura = $this->fattura;
$tipo = $fattura->tipo;
$numero = $fattura->numero_esterno ?: $fattura->numero;
$data = $fattura->data;
$carico = ($tipo->dir == 'entrata') ? tr('Ripristino articolo da _TYPE_ numero _NUM_') : tr('Carico magazzino da _TYPE_ numero _NUM_');
$scarico = ($tipo->dir == 'entrata') ? tr('Scarico magazzino per _TYPE_ numero _NUM_') : tr('Rimozione articolo da _TYPE_ numero _NUM_');
$qta = ($tipo->dir == 'uscita') ? -$qta : $qta;
$movimento = ($qta < 0) ? $carico : $scarico;
$movimento = replace($movimento, [
'_TYPE_' => $tipo->descrizione,
'_NUM_' => $numero,
]);
$partenza = $fattura->direzione == 'uscita' ? $fattura->idsede_destinazione : $fattura->idsede_partenza;
$arrivo = $fattura->direzione == 'uscita' ? $fattura->idsede_partenza : $fattura->idsede_destinazione;
$this->articolo->movimenta(-$qta, $movimento, $data, false, [
'iddocumento' => $fattura->id,
'idsede_azienda' => $partenza,
'idsede_controparte' => $arrivo,
]);
}
public function getDirection()
{
return $this->fattura->tipo->dir;
}
}

View File

@ -16,11 +16,13 @@ use Modules\Scadenzario\Scadenza;
use Plugins\DichiarazioniIntento\Dichiarazione;
use Plugins\ExportFE\FatturaElettronica;
use Traits\RecordTrait;
use Traits\ReferenceTrait;
use Util\Generator;
class Fattura extends Document
{
use RecordTrait;
use ReferenceTrait;
protected $table = 'co_documenti';
@ -749,4 +751,21 @@ class Fattura extends Document
return $numero;
}
// Opzioni di riferimento
public function getReferenceName()
{
return $this->tipo->descrizione;
}
public function getReferenceNumber()
{
return $this->numero_esterno ?: $this->numero;
}
public function getReferenceDate()
{
return $this->data;
}
}

View File

@ -30,36 +30,4 @@ class Articolo extends Article
return $model;
}
public function movimentaMagazzino($qta)
{
$intervento = $this->intervento;
$numero = $intervento->codice;
$data = database()->fetchOne('SELECT MAX(orario_fine) AS data FROM in_interventi_tecnici WHERE idintervento = :id_intervento', [
':id_intervento' => $intervento->id,
])['data'];
$data = $data ?: $intervento->data_richiesta;
$descrizione = ($qta < 0) ? tr('Ripristino articolo da Attività numero _NUM_', [
'_NUM_' => $numero,
]) : tr('Scarico magazzino per intervento _NUM_', [
'_NUM_' => $numero,
]);
$partenza = $intervento->idsede_partenza;
$arrivo = $intervento->idsede_destinazione;
$this->articolo->movimenta(-$qta, $descrizione, $data, false, [
'idintervento' => $intervento->id,
'idsede_azienda' => $partenza,
'idsede_controparte' => $arrivo,
]);
}
public function getDirection()
{
return 'entrata';
}
}

View File

@ -8,10 +8,13 @@ use Modules\Contratti\Contratto;
use Modules\Preventivi\Preventivo;
use Modules\TipiIntervento\Tipo as TipoSessione;
use Traits\RecordTrait;
use Traits\ReferenceTrait;
use Util\Generator;
class Intervento extends Document
{
use ReferenceTrait;
use RecordTrait;
protected $table = 'in_interventi';
@ -90,6 +93,11 @@ class Intervento extends Document
return 'Interventi';
}
public function getDirezioneAttribute()
{
return 'entrata';
}
/**
* Restituisce la collezione di righe e articoli con valori rilevanti per i conti.
*
@ -198,4 +206,21 @@ class Intervento extends Document
return $numero;
}
// Opzioni di riferimento
public function getReferenceName()
{
return 'Attività';
}
public function getReferenceNumber()
{
return $this->codice;
}
public function getReferenceDate()
{
return $this->data_richiesta;
}
}

View File

@ -16,7 +16,7 @@ switch (filter('op')) {
]));
}
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio.'));
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;
@ -43,7 +43,7 @@ switch (filter('op')) {
]));
}
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio.'));
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;

View File

@ -8,9 +8,10 @@ switch (post('op')) {
case 'add':
$idsede_partenza = post('idsede_partenza');
$idsede_destinazione = post('idsede_destinazione');
$qta = (post('direzione') == 'Carico manuale') ? post('qta') : -post('qta');
$direzione = post('direzione');
if (post('direzione') == 'Carico manuale') {
$qta = !empty($direzione) ? post('qta') : -post('qta');
if (!empty($direzione)) {
if ($idsede_partenza == 0 && $idsede_destinazione != 0) {
$qta = -post('qta');
} elseif ($idsede_partenza != 0 && $idsede_destinazione == 0) {

View File

@ -3,7 +3,7 @@
include_once __DIR__.'/../../core.php';
// Imposto come azienda l'azienda predefinita per selezionare le sedi a cui ho accesso
$_SESSION['superselect']['idanagrafica'] = get_var('Azienda predefinita');
$_SESSION['superselect']['idanagrafica'] = setting('Azienda predefinita');
// Azzero le sedi selezionate
unset($_SESSION['superselect']['idsede_partenza']);
@ -19,35 +19,37 @@ $_SESSION['superselect']['idsede_destinazione'] = 0;
<div class="row">
<div class="col-md-4">
{["type":"select", "label":"<?php echo tr('Articolo'); ?>", "name":"idarticolo", "ajax-source":"articoli", "value":"", "required":1]}
{["type": "select", "label": "<?php echo tr('Articolo'); ?>", "name": "idarticolo", "ajax-source": "articoli", "value": "", "required": 1 ]}
</div>
<div class="col-md-2">
{["type":"number", "label":"<?php echo tr('Quantità'); ?>", "name":"qta", "decimals":"2", "value":"1", "required":1]}
{["type": "number", "label": "<?php echo tr('Quantità'); ?>", "name": "qta", "decimals": "2", "value": "1", "required": 1 ]}
</div>
<div class="col-md-2">
{["type":"date", "label":"<?php echo tr('Data'); ?>", "name":"data", "value":"-now-", "required":1]}
{["type": "date", "label": "<?php echo tr('Data'); ?>", "name": "data", "value": "-now-", "required": 1 ]}
</div>
<div class="col-md-4">
{["type":"select", "label":"<?php echo tr('Causale'); ?>", "name":"direzione", "values":"list=\"Carico manuale\":\"Carico\", \"Scarico manuale\":\"Scarico\" ", "value":"Carico manuale", "required":1]}
{["type": "select", "label": "<?php echo tr('Causale'); ?>", "name": "causale", "values": "query=SELECT id, nome as text, descrizione, movimento_carico FROM mg_causali_movimenti", "value": 1, "required": 1 ]}
<input type="hidden" name="direzione" id="direzione">
</div>
</div>
<div class="row">
<div class="col-md-12">
{["type":"textarea", "label":"<?php echo tr('Descrizione movimento'); ?>", "name":"movimento", "required":1]}
{["type": "textarea", "label": "<?php echo tr('Descrizione movimento'); ?>", "name": "movimento", "required": 1 ]}
</div>
</div>
<div class="row">
<div class="col-md-6">
{[ "type": "select", "label": "<?php echo tr('Sede'); ?>", "name": "idsede_destinazione", "ajax-source": "sedi_azienda", "value": "0", "required":1 ]}
{[ "type": "select", "label": "<?php echo tr('Sede'); ?>", "name": "idsede_destinazione", "ajax-source": "sedi_azienda", "value": "0", "required": 1 ]}
</div>
<div class="col-md-6">
{[ "type": "select", "label": "<?php echo tr('Partenza merce'); ?>", "name": "idsede_partenza", "ajax-source": "sedi_azienda", "value": "0", "required":1 ]}
{[ "type": "select", "label": "<?php echo tr('Partenza merce'); ?>", "name": "idsede_partenza", "ajax-source": "sedi_azienda", "value": "0", "required": 1 ]}
</div>
</div>
@ -64,11 +66,15 @@ $_SESSION['superselect']['idsede_destinazione'] = 0;
<script>
$('#modals > div').on('shown.bs.modal', function(){
$('#direzione').on('change', function(){
$('#movimento').val( $(this).val() );
$('#causale').on('change', function() {
var data = $(this).selectData();
if (data) {
$('#movimento').val(data.descrizione);
$('#direzione').val(data.movimento_carico);
}
});
$('#direzione').trigger('change');
$('#causale').trigger('change');
// Lettura codici da lettore barcode
var keys = '';
@ -97,7 +103,7 @@ $_SESSION['superselect']['idsede_destinazione'] = 0;
$('#idarticolo').selectSetNew( record.id, record.text );
ajax_submit( record );
}
// Articolo non trovato
else {
$('#messages').html( '<hr><div class="alert alert-danger text-center"><big>Articolo <b>' + search + '</b> non trovato!</big></div>' );
@ -149,26 +155,26 @@ $_SESSION['superselect']['idsede_destinazione'] = 0;
text = 'Scarico';
qta_rimanente = parseFloat(articolo.qta)-parseFloat(qta_movimento);
}
if( articolo.descrizione != '' ){
$('#messages').html(
$('#messages').html(
'<hr>'+
'<div class="row">'+
'<div class="col-md-6">'+
'<div class="alert alert-info text-center" style="line-height: 1.6;">'+
'<b style="font-size:14pt;"><i class="fa fa-barcode"></i> ' + articolo.barcode + ' - ' + articolo.descrizione + '</b><br>'+
'<b>Prezzo acquisto:</b> ' + prezzo_acquisto.toLocale() + " " + globals.currency + '<br><b>Prezzo vendita:</b> ' + prezzo_vendita.toLocale() + " " + globals.currency +
'</div>'+
'</div>'+
'</div>'+
'</div>'+
'<div class="col-md-6">'+
'<div class="alert '+alert+' text-center">'+
'<p style="font-size:14pt;">'+icon+' '+text+' '+qta_movimento.toLocale()+' '+articolo.um+' <i class="fa fa-arrow-circle-right"></i> '+qta_rimanente.toLocale()+' '+articolo.um+' rimanenti</p>'+
'</div>'+
'</div>'+
'</div>'+
'</div>'+
'</div>'
);
}
$("#qta").val(1);
}
}
@ -176,16 +182,15 @@ $_SESSION['superselect']['idsede_destinazione'] = 0;
<?php
if (setting('Attiva scorciatoie da tastiera')) {
echo '
<script>
hotkeys(\'f8\', \'carico\', function(event, handler){
$("#modals > div #direzione").val("Carico manuale").change();
});
hotkeys.setScope(\'carico\');
<script>
hotkeys(\'f8\', \'carico\', function(event, handler){
$("#modals > div #direzione").val(1).change();
});
hotkeys.setScope(\'carico\');
hotkeys(\'f9\', \'carico\', function(event, handler){
$("#modals > div #direzione").val("Scarico manuale").change();
});
hotkeys.setScope(\'carico\');
</script>';
hotkeys(\'f9\', \'carico\', function(event, handler){
$("#modals > div #direzione").val(2).change();
});
hotkeys.setScope(\'carico\');
</script>';
}

View File

@ -63,11 +63,9 @@ foreach ($righe as $riga) {
}
// Aggiunta dei riferimenti ai documenti
$ref = doc_references($r, $dir, ['idordine']);
if (!empty($ref)) {
if ($riga->hasOriginal()) {
echo '
<br>'.Modules::link($ref['module'], $ref['id'], $ref['description'], $ref['description']);
<br>'.reference($riga->getOriginal()->parent);
}
echo '

View File

@ -26,14 +26,4 @@ class Articolo extends Article
return $model;
}
public function movimentaMagazzino($qta)
{
return;
}
public function getDirection()
{
return $this->ordine->tipo->dir;
}
}

View File

@ -7,10 +7,12 @@ use Common\Document;
use Modules\Anagrafiche\Anagrafica;
use Modules\DDT\DDT;
use Traits\RecordTrait;
use Traits\ReferenceTrait;
use Util\Generator;
class Ordine extends Document
{
use ReferenceTrait;
use RecordTrait;
protected $table = 'or_ordini';
@ -200,4 +202,21 @@ class Ordine extends Document
return $numero;
}
// Opzioni di riferimento
public function getReferenceName()
{
return $this->tipo->descrizione;
}
public function getReferenceNumber()
{
return $this->numero_esterno ?: $this->numero;
}
public function getReferenceDate()
{
return $this->data;
}
}

View File

@ -25,14 +25,4 @@ class Articolo extends Article
return $model;
}
public function movimentaMagazzino($qta)
{
return;
}
public function getDirection()
{
return $this->preventivo->tipo->dir;
}
}

View File

@ -10,10 +10,12 @@ use Modules\Interventi\Intervento;
use Modules\Ordini\Ordine;
use Modules\TipiIntervento\Tipo as TipoSessione;
use Traits\RecordTrait;
use Traits\ReferenceTrait;
use Util\Generator;
class Preventivo extends Document
{
use ReferenceTrait;
use RecordTrait;
protected $table = 'co_preventivi';
@ -245,4 +247,21 @@ class Preventivo extends Document
return $numero;
}
// Opzioni di riferimento
public function getReferenceName()
{
return 'Preventivo';
}
public function getReferenceNumber()
{
return $this->numero;
}
public function getReferenceDate()
{
return $this->data_bozza;
}
}

View File

@ -104,7 +104,7 @@ foreach ($id_documenti as $id_documento) {
// Predisposizione prima riga
$conto_field = 'idconto_'.($dir == 'entrata' ? 'vendite' : 'acquisti');
$id_conto_aziendale = $fattura->pagamento[$conto_field] ?: get_var('Conto aziendale predefinito');
$id_conto_aziendale = $fattura->pagamento[$conto_field] ?: setting('Conto aziendale predefinito');
// Predisposizione conto crediti clienti
$conto_field = 'idconto_'.($dir == 'entrata' ? 'cliente' : 'fornitore');
@ -334,7 +334,7 @@ include $structure->filepath('movimenti.php');
$('#modals > div table.scadenze > tbody').append( '<tr>' + $new_tr + '</tr>' );
$('#modals > div table.scadenze > tbody tr').last().find('select').attr('id', 'conto' + i).attr('name', 'idconto[' + i + ']');
}
$('#modals > div #conto' + i).append(option);
}
} else {

View File

@ -17,7 +17,7 @@ switch (filter('op')) {
]));
}
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio.'));
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;
@ -45,7 +45,7 @@ switch (filter('op')) {
]));
}
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio.'));
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;

View File

@ -36,7 +36,7 @@ switch (filter('op')) {
]));
}
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio.'));
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;

View File

@ -1167,12 +1167,9 @@ class FatturaElettronica
$descrizione = str_replace('…', '...', $descrizione);
$descrizione = str_replace('', ' ', $descrizione);
if (setting('Riferimento dei documenti in Fattura Elettronica')) {
$ref = doc_references($riga->toArray(), 'entrata', ['iddocumento']);
if (!empty($ref)) {
$descrizione .= "\n".$ref['description'];
}
// Aggiunta dei riferimenti ai documenti
if (setting('Riferimento dei documenti in Fattura Elettronica') && $riga->hasOriginal()) {
$descrizione .= "\n".$riga->getOriginal()->parent->getReference();
}
$dettaglio['Descrizione'] = $descrizione;

View File

@ -26,14 +26,4 @@ class Articolo extends Article
return $model;
}
public function movimentaMagazzino($qta)
{
return;
}
public function getDirection()
{
return $this->contratto->tipo->dir;
}
}

View File

@ -7,6 +7,8 @@ use Models\Plugin;
trait RecordTrait
{
abstract public function getModuleAttribute();
public function getModule()
{
return !empty($this->module) ? Module::get($this->module) : null;

View File

@ -0,0 +1,48 @@
<?php
namespace Traits;
use Stringy\Stringy;
trait ReferenceTrait
{
abstract public function getReferenceName();
abstract public function getReferenceNumber();
abstract public function getReferenceDate();
public function getReference()
{
$name = $this->getReferenceName();
$number = $this->getReferenceNumber();
$date = $this->getReferenceDate();
// Testo relativo
$name = Stringy::create($name)->toLowerCase();
if (!empty($date) && !empty($number)) {
$description = tr('Rif. _DOC_ num. _NUM_ del _DATE_', [
'_DOC_' => $name,
'_NUM_' => $number,
'_DATE_' => dateFormat($date),
]);
} elseif (!empty($number)) {
$description = tr('Rif. _DOC_ num. _NUM_', [
'_DOC_' => $name,
'_NUM_' => $number,
]);
} elseif (!empty($date)) {
$description = tr('Rif. _DOC_ del _DATE_', [
'_DOC_' => $name,
'_DATE_' => dateFormat($date),
]);
} else {
$description = tr('Rif. _DOC_', [
'_DOC_' => $name,
]);
}
return $description;
}
}

View File

@ -65,14 +65,14 @@ foreach ($righe as $riga) {
}
// Aggiunta dei riferimenti ai documenti
if (setting('Riferimento dei documenti nelle stampe')) {
$ref = doc_references($r, $record['dir'], ['idddt']);
if (setting('Riferimento dei documenti nelle stampe') && $riga->hasOriginal()) {
$ref = $riga->getOriginal()->parent->getReference();
if (!empty($ref)) {
echo '
<br><small>'.$ref['description'].'</small>';
<br><small>'.$ref.'</small>';
$autofill->count($ref['description'], true);
$autofill->count($ref, true);
}
}

View File

@ -80,14 +80,14 @@ foreach ($righe as $riga) {
}
// Aggiunta dei riferimenti ai documenti
if (setting('Riferimento dei documenti nelle stampe')) {
$ref = doc_references($r, $record['dir'], ['iddocumento']);
if (setting('Riferimento dei documenti nelle stampe') && $riga->hasOriginal()) {
$ref = $riga->getOriginal()->parent->getReference();
if (!empty($ref)) {
echo '
<br><small>'.$ref['description'].'</small>';
<br><small>'.$ref.'</small>';
$autofill->count($ref['description'], true);
$autofill->count($ref, true);
}
}

View File

@ -59,7 +59,7 @@ if (!empty($search['barcode'])) {
$period_end = $_SESSION['period_end'];
$query = 'SELECT *,
(SELECT SUM(qta) FROM mg_movimenti WHERE mg_movimenti.idarticolo=mg_articoli.id AND (mg_movimenti.idintervento IS NULL) AND data <= '.prepare($period_end).') AS qta
(SELECT SUM(qta) FROM mg_movimenti WHERE mg_movimenti.idarticolo=mg_articoli.id AND data <= '.prepare($period_end).') AS qta
FROM mg_articoli LEFT OUTER JOIN (SELECT id, nome FROM mg_categorie) AS categoria ON mg_articoli.id_categoria = categoria.id WHERE 1=1
ORDER BY codice ASC';
@ -83,7 +83,7 @@ echo '
<th class="text-center" width="90">'.tr('Valore totale', [], ['upper' => true]).'</th>
</tr>
</thead>
<tbody>';
$totali = [];

View File

@ -392,6 +392,34 @@ UPDATE `zz_views` SET `query` = 'righe.totale_imponibile' WHERE `id_module` = (S
INSERT INTO `zz_views` (`id_module`, `name`, `query`, `order`, `search`, `format`, `default`, `visible`) VALUES
((SELECT `id` FROM `zz_modules` WHERE `name` = 'Ordini fornitore'), 'Totale ivato', 'righe.totale', 5, 1, 1, 1, 1);
-- Aggiunta gestione dinamica dei movimenti degli Articoli
ALTER TABLE `mg_movimenti` ADD `reference_id` int(11), ADD `reference_type` varchar(255);
UPDATE `mg_movimenti` SET `reference_id` = `iddocumento`, `reference_type` = 'Modules\\Fatture\\Fattura' WHERE `iddocumento` IS NOT NULL AND `iddocumento` != 0;
UPDATE `mg_movimenti` SET `reference_id` = `idintervento`, `reference_type` = 'Modules\\Interventi\\Intervento' WHERE `idintervento` IS NOT NULL AND `idintervento` != 0;
UPDATE `mg_movimenti` SET `reference_id` = `idddt`, `reference_type` = 'Modules\\DDT\\DDT' WHERE `idddt` IS NOT NULL AND `idddt` != 0;
-- Descrizioni movimenti predefinite per l'aggiunta dal modulo Movimenti
CREATE TABLE IF NOT EXISTS `mg_causali_movimenti` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nome` varchar(255) NOT NULL,
`descrizione` varchar(255) NOT NULL,
`movimento_carico` BOOLEAN DEFAULT TRUE,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
INSERT INTO `mg_causali_movimenti` (`id`, `nome`, `descrizione`, `movimento_carico`) VALUES
(NULL, 'Carico', 'Carico manuale', '1'),
(NULL, 'Scarico', 'Scarico manuale', '0');
-- Introduzione modulo Movimenti predefiniti
INSERT INTO `zz_modules` (`id`, `name`, `title`, `directory`, `options`, `options2`, `icon`, `version`, `compatibility`, `order`, `parent`, `default`, `enabled`) VALUES (NULL, 'Causali movimenti', 'Causali movimenti', 'causali_movimenti', 'SELECT |select| FROM `mg_causali_movimenti` WHERE 1=1 HAVING 2=2', '', 'fa fa-truck', '2.4.14', '2.4.14', '1', (SELECT `id` FROM `zz_modules` t WHERE t.`name` = 'Tabelle'), '1', '1');
INSERT INTO `zz_views` (`id_module`, `name`, `query`, `order`, `search`, `slow`, `default`, `visible`) VALUES
((SELECT `id` FROM `zz_modules` WHERE `name` = 'Causali movimenti'), 'Movimento di carico', 'IF(movimento_carico, ''Si'', ''No'')', 4, 1, 0, 0, 1),
((SELECT `id` FROM `zz_modules` WHERE `name` = 'Causali movimenti'), 'Descrizione', 'descrizione', 3, 1, 0, 0, 1),
((SELECT `id` FROM `zz_modules` WHERE `name` = 'Causali movimenti'), 'Nome', 'nome', 2, 1, 0, 0, 1),
((SELECT `id` FROM `zz_modules` WHERE `name` = 'Causali movimenti'), 'id', 'id', 1, 1, 0, 0, 0);
-- Miglioramento della cache interna
CREATE TABLE IF NOT EXISTS `zz_cache` (
`id` int(11) NOT NULL AUTO_INCREMENT,

View File

@ -79,6 +79,7 @@ return [
'in_vociservizio',
'mg_articoli',
'mg_categorie',
'mg_causali_movimenti',
'mg_listini',
'mg_movimenti',
'mg_prodotti',