Merge branch 'develop' of git@github.com:Dolibarr/dolibarr.git into develop
This commit is contained in:
commit
ad0ed2bb19
@ -104,7 +104,7 @@ $formaccounting = new FormAccounting($db);
|
||||
$formother = new FormOther($db);
|
||||
$form = new Form($db);
|
||||
|
||||
if (! in_array($action, array('export_file', 'delmouv', 'delmouvconfirm')) && ! isset($_POST['begin']) && ! isset($_GET['begin']) && ! isset($_POST['formfilteraction']) && GETPOST('page', 'int') == '' && ! GETPOST('noreset', 'int'))
|
||||
if (! in_array($action, array('export_file', 'delmouv', 'delmouvconfirm')) && ! isset($_POST['begin']) && ! isset($_GET['begin']) && ! isset($_POST['formfilteraction']) && GETPOST('page', 'int') == '' && ! GETPOST('noreset', 'int') && $user->rights->accounting->mouvements->export)
|
||||
{
|
||||
if (empty($search_date_start) && empty($search_date_end) && ! GETPOSTISSET('restore_lastsearch_values'))
|
||||
{
|
||||
@ -300,7 +300,7 @@ if (! empty($search_lettering_code)) {
|
||||
}
|
||||
|
||||
|
||||
if ($action == 'delbookkeeping') {
|
||||
if ($action == 'delbookkeeping' && $user->rights->accounting->mouvements->supprimer) {
|
||||
|
||||
$import_key = GETPOST('importkey', 'alpha');
|
||||
|
||||
@ -315,7 +315,7 @@ if ($action == 'delbookkeeping') {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
if ($action == 'delbookkeepingyearconfirm') {
|
||||
if ($action == 'delbookkeepingyearconfirm' && $user->rights->accounting->mouvements->supprimer_tous) {
|
||||
|
||||
$delyear = GETPOST('delyear', 'int');
|
||||
if ($delyear==-1) {
|
||||
@ -346,7 +346,7 @@ if ($action == 'delbookkeepingyearconfirm') {
|
||||
setEventMessages("NoRecordDeleted", null, 'warnings');
|
||||
}
|
||||
}
|
||||
if ($action == 'delmouvconfirm') {
|
||||
if ($action == 'delmouvconfirm' && $user->rights->accounting->mouvements->supprimer) {
|
||||
|
||||
$mvt_num = GETPOST('mvt_num', 'int');
|
||||
|
||||
@ -366,7 +366,8 @@ if ($action == 'delmouvconfirm') {
|
||||
}
|
||||
|
||||
// Export into a file with format defined into setup (FEC, CSV, ...)
|
||||
if ($action == 'export_file') {
|
||||
if ($action == 'export_file' && $user->rights->accounting->mouvements->export) {
|
||||
|
||||
$result = $object->fetchAll($sortorder, $sortfield, 0, 0, $filter, 'AND', $conf->global->ACCOUNTING_REEXPORT);
|
||||
|
||||
if ($result < 0)
|
||||
@ -514,11 +515,11 @@ if (! empty($conf->global->ACCOUNTING_REEXPORT)) {
|
||||
}
|
||||
$newcardbutton.= '<span class="valignmiddle marginrightonly">'.$langs->trans("IncludeDocsAlreadyExported").'</span>';
|
||||
|
||||
$newcardbutton.= dolGetButtonTitle($buttonLabel, $langs->trans("ExportFilteredList").' ('.$listofformat[$conf->global->ACCOUNTING_EXPORT_MODELCSV].')', 'fa fa-file-export paddingleft', $_SERVER["PHP_SELF"].'?action=export_file'.($param?'&'.$param:''));
|
||||
$newcardbutton.= dolGetButtonTitle($buttonLabel, $langs->trans("ExportFilteredList").' ('.$listofformat[$conf->global->ACCOUNTING_EXPORT_MODELCSV].')', 'fa fa-file-export paddingleft', $_SERVER["PHP_SELF"].'?action=export_file'.($param?'&'.$param:''), $user->rights->accounting->mouvements->export);
|
||||
|
||||
$newcardbutton.= dolGetButtonTitle($langs->trans('GroupByAccountAccounting'), '', 'fa fa-stream paddingleft', DOL_URL_ROOT.'/accountancy/bookkeeping/listbyaccount.php?'.$param);
|
||||
|
||||
$newcardbutton.= dolGetButtonTitle($langs->trans('NewAccountingMvt'), '', 'fa fa-plus-circle paddingleft', './card.php?action=create');
|
||||
$newcardbutton.= dolGetButtonTitle($langs->trans('NewAccountingMvt'), '', 'fa fa-plus-circle paddingleft', './card.php?action=create', '', $user->rights->accounting->mouvements->creer);
|
||||
|
||||
print_barre_liste($title_page, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $result, $nbtotalofrecords, 'title_accountancy', 0, $newcardbutton, '', $limit);
|
||||
|
||||
@ -818,12 +819,17 @@ if ($num > 0)
|
||||
|
||||
// Action column
|
||||
print '<td class="nowraponall center">';
|
||||
if(empty($line->date_export)) {
|
||||
print '<a href="'.DOL_URL_ROOT.'/accountancy/bookkeeping/card.php?piece_num=' . urlencode($line->piece_num) . $param . '&page=' . $page . ($sortfield ? '&sortfield='.$sortfield : '') . ($sortorder ? '&sortorder='.$sortorder : '') . '">' . img_edit() . '</a> ';
|
||||
print '<a href="' . $_SERVER['PHP_SELF'] . '?action=delmouv&mvt_num=' . urlencode($line->piece_num) . $param . '&page=' . $page . ($sortfield ? '&sortfield='.$sortfield : '') . ($sortorder ? '&sortorder='.$sortorder : '') . '">' . img_delete() . '</a>';
|
||||
if (empty($line->date_export)) {
|
||||
if ($user->rights->accounting->mouvements->creer) {
|
||||
print '<a href="' . DOL_URL_ROOT . '/accountancy/bookkeeping/card.php?piece_num=' . $line->piece_num . $param . '&page=' . $page . ($sortfield ? '&sortfield=' . $sortfield : '') . ($sortorder ? '&sortorder=' . $sortorder : '') . '">' . img_edit() . '</a>';
|
||||
}
|
||||
if ($user->rights->accounting->mouvements->supprimer) {
|
||||
print ' <a href="' . $_SERVER['PHP_SELF'] . '?action=delmouv&mvt_num=' . $line->piece_num . $param . '&page=' . $page . ($sortfield ? '&sortfield=' . $sortfield : '') . ($sortorder ? '&sortorder=' . $sortorder : '') . '">' . img_delete() . '</a>';
|
||||
}
|
||||
}
|
||||
print '</td>';
|
||||
if (! $i) $totalarray['nbfield']++;
|
||||
print '</td>';
|
||||
|
||||
if (! $i) $totalarray['nbfield']++;
|
||||
|
||||
print "</tr>\n";
|
||||
|
||||
@ -855,10 +861,11 @@ print "</table>";
|
||||
print '</div>';
|
||||
|
||||
// TODO Replace this with mass delete action
|
||||
print '<div class="tabsAction tabsActionNoBottom">' . "\n";
|
||||
print '<a class="butActionDelete" name="button_delmvt" href="'.$_SERVER["PHP_SELF"].'?action=delbookkeepingyear'.($param?'&'.$param:'').'">' . $langs->trans("DeleteMvt") . '</a>';
|
||||
print '</div>';
|
||||
|
||||
if ($user->rights->accounting->mouvements->supprimer_tous) {
|
||||
print '<div class="tabsAction tabsActionNoBottom">' . "\n";
|
||||
print '<a class="butActionDelete" name="button_delmvt" href="' . $_SERVER["PHP_SELF"] . '?action=delbookkeepingyear' . ($param ? '&' . $param : '') . '">' . $langs->trans("DeleteMvt") . '</a>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
print '</form>';
|
||||
|
||||
|
||||
@ -1059,11 +1059,17 @@ else
|
||||
|
||||
// Other attributes
|
||||
include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_add.tpl.php';
|
||||
|
||||
print '<tbody>';
|
||||
//Hooks here
|
||||
$reshook=$hookmanager->executeHooks('formObjectOptions', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
|
||||
print $hookmanager->resPrint;
|
||||
if (empty($reshook))
|
||||
{
|
||||
print $object->showOptionals($extrafields, 'edit');
|
||||
}
|
||||
|
||||
print '<tbody>';
|
||||
print "</table>\n";
|
||||
|
||||
dol_fiche_end();
|
||||
dol_fiche_end();
|
||||
|
||||
print '<div class="center">';
|
||||
print '<input type="submit" name="button" class="button" value="'.$langs->trans("AddMember").'">';
|
||||
@ -1357,9 +1363,15 @@ else
|
||||
|
||||
// Other attributes
|
||||
include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_add.tpl.php';
|
||||
|
||||
//Hooks here
|
||||
$reshook=$hookmanager->executeHooks('formObjectOptions', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
|
||||
print $hookmanager->resPrint;
|
||||
if (empty($reshook))
|
||||
{
|
||||
print $object->showOptionals($extrafields, 'edit');
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
|
||||
dol_fiche_end();
|
||||
|
||||
print '<div class="center">';
|
||||
|
||||
@ -6381,6 +6381,7 @@ abstract class CommonObject
|
||||
$out .= '<!-- showOptionalsInput --> ';
|
||||
$out .= "\n";
|
||||
|
||||
$extrafields_collapse_num = '';
|
||||
$e = 0;
|
||||
foreach($extrafields->attributes[$this->table_element]['label'] as $key=>$label)
|
||||
{
|
||||
@ -6440,6 +6441,20 @@ abstract class CommonObject
|
||||
|
||||
if ($extrafields->attributes[$this->table_element]['type'][$key] == 'separate')
|
||||
{
|
||||
$extrafields_collapse_num = '';
|
||||
$extrafield_param = $extrafields->attributes[$this->table_element]['param'][$key];
|
||||
if (!empty($extrafield_param) && is_array($extrafield_param)) {
|
||||
$extrafield_param_list = array_keys($extrafield_param['options']);
|
||||
|
||||
if (count($extrafield_param_list)>0) {
|
||||
$extrafield_collapse_display_value = intval($extrafield_param_list[0]);
|
||||
|
||||
if ($extrafield_collapse_display_value==1 || $extrafield_collapse_display_value==2) {
|
||||
$extrafields_collapse_num = $extrafields->attributes[$this->table_element]['pos'][$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$out .= $extrafields->showSeparator($key, $this);
|
||||
}
|
||||
else
|
||||
@ -6459,7 +6474,7 @@ abstract class CommonObject
|
||||
|
||||
$html_id = !empty($this->id) ? 'extrarow-'.$this->element.'_'.$key.'_'.$this->id : '';
|
||||
|
||||
$out .= '<tr id="'.$html_id.'" '.$csstyle.' class="'.$class.$this->element.'_extras_'.$key.'" '.$domData.' >';
|
||||
$out .= '<tr id="'.$html_id.'" '.$csstyle.' class="'.$class.$this->element.'_extras_'.$key.' trextrafields_collapse'.$extrafields_collapse_num.'" '.$domData.' >';
|
||||
|
||||
if (! empty($conf->global->MAIN_EXTRAFIELDS_USE_TWO_COLUMS) && ($e % 2) == 0)
|
||||
{
|
||||
|
||||
@ -1956,9 +1956,43 @@ class ExtraFields
|
||||
{
|
||||
global $langs;
|
||||
|
||||
$out = '<tr class="trextrafieldseparator trextrafieldseparator'.$key.'"><td colspan="2"><strong>';
|
||||
$out = '<tr id="trextrafieldseparator'.$key.'" class="trextrafieldseparator trextrafieldseparator'.$key.'"><td colspan="2"><strong>';
|
||||
$out.= $langs->trans($this->attributes[$object->table_element]['label'][$key]);
|
||||
$out.= '</strong></td></tr>';
|
||||
|
||||
$extrafield_param = $this->attributes[$object->table_element]['param'][$key];
|
||||
if (!empty($extrafield_param) && is_array($extrafield_param)) {
|
||||
$extrafield_param_list = array_keys($extrafield_param['options']);
|
||||
|
||||
if (count($extrafield_param_list) > 0) {
|
||||
$extrafield_collapse_display_value = intval($extrafield_param_list[0]);
|
||||
if ($extrafield_collapse_display_value == 1 || $extrafield_collapse_display_value == 2) {
|
||||
$collapse_display = ($extrafield_collapse_display_value == 2 ? false : true);
|
||||
$extrafields_collapse_num = $this->attributes[$object->table_element]['pos'][$key];
|
||||
|
||||
$out .= '<script type="text/javascript">';
|
||||
$out .= 'jQuery(document).ready(function(){';
|
||||
if ($collapse_display === false) {
|
||||
$out .= ' jQuery("#trextrafieldseparator' . $key . ' td").prepend("<span class=\"cursorpointer fa fa-plus-square\"></span> ");';
|
||||
$out .= ' jQuery(".trextrafields_collapse' . $extrafields_collapse_num . '").hide();';
|
||||
} else {
|
||||
$out .= ' jQuery("#trextrafieldseparator' . $key . ' td").prepend("<span class=\"cursorpointer fa fa-minus-square\"></span> ");';
|
||||
}
|
||||
$out .= ' jQuery("#trextrafieldseparator' . $key . '").click(function(){';
|
||||
$out .= ' jQuery(".trextrafields_collapse' . $extrafields_collapse_num . '").toggle("slow", function(){';
|
||||
$out .= ' if (jQuery(".trextrafields_collapse' . $extrafields_collapse_num . '").is(":hidden")) {';
|
||||
$out .= ' jQuery("#trextrafieldseparator' . $key . ' td span").addClass("fa-plus-square").removeClass("fa-minus-square");';
|
||||
$out .= ' } else {';
|
||||
$out .= ' jQuery("#trextrafieldseparator' . $key . ' td span").addClass("fa-minus-square").removeClass("fa-plus-square");';
|
||||
$out .= ' }';
|
||||
$out .= ' });';
|
||||
$out .= ' });';
|
||||
$out .= '});';
|
||||
$out .= '</script>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
@ -170,15 +170,23 @@ class modAccounting extends DolibarrModules
|
||||
$this->rights = array(); // Permission array used by this module
|
||||
$r = 0;
|
||||
|
||||
$this->rights[$r][0] = 50440;
|
||||
$this->rights[$r][1] = 'Manage chart of accounts, setup of accountancy';
|
||||
$this->rights[$r][0] = 50440;
|
||||
$this->rights[$r][1] = 'Manage chart of accounts, setup of accountancy';
|
||||
$this->rights[$r][2] = 'r';
|
||||
$this->rights[$r][3] = 0;
|
||||
$this->rights[$r][4] = 'chartofaccount';
|
||||
$this->rights[$r][5] = '';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = 50430;
|
||||
$this->rights[$r][1] = 'Define and close a fiscal year';
|
||||
$this->rights[$r][2] = 'r';
|
||||
$this->rights[$r][3] = 0;
|
||||
$this->rights[$r][4] = 'chartofaccount';
|
||||
$this->rights[$r][4] = 'fiscalyear';
|
||||
$this->rights[$r][5] = '';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = 50401;
|
||||
$this->rights[$r][0] = 50401;
|
||||
$this->rights[$r][1] = 'Bind products and invoices with accounting accounts';
|
||||
$this->rights[$r][2] = 'r';
|
||||
$this->rights[$r][3] = 0;
|
||||
@ -212,6 +220,30 @@ class modAccounting extends DolibarrModules
|
||||
$this->rights[$r][5] = 'creer';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = 50414;
|
||||
$this->rights[$r][1] = 'Delete operations in Ledger';
|
||||
$this->rights[$r][2] = 'd';
|
||||
$this->rights[$r][3] = 0;
|
||||
$this->rights[$r][4] = 'mouvements';
|
||||
$this->rights[$r][5] = 'supprimer';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = 50415;
|
||||
$this->rights[$r][1] = 'Delete all operations by year and journal in Ledger';
|
||||
$this->rights[$r][2] = 'd';
|
||||
$this->rights[$r][3] = 0;
|
||||
$this->rights[$r][4] = 'mouvements';
|
||||
$this->rights[$r][5] = 'supprimer_tous';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = 50418;
|
||||
$this->rights[$r][1] = 'Export operations of the Ledger';
|
||||
$this->rights[$r][2] = 'r';
|
||||
$this->rights[$r][3] = 0;
|
||||
$this->rights[$r][4] = 'mouvements';
|
||||
$this->rights[$r][5] = 'export';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = 50420;
|
||||
$this->rights[$r][1] = 'Report and export reports (turnover, balance, journals, ledger)';
|
||||
$this->rights[$r][2] = 'r';
|
||||
@ -220,14 +252,6 @@ class modAccounting extends DolibarrModules
|
||||
$this->rights[$r][5] = 'lire';
|
||||
$r++;
|
||||
|
||||
$this->rights[$r][0] = 50430;
|
||||
$this->rights[$r][1] = 'Define and close a fiscal year';
|
||||
$this->rights[$r][2] = 'r';
|
||||
$this->rights[$r][3] = 0;
|
||||
$this->rights[$r][4] = 'fiscalyear';
|
||||
$this->rights[$r][5] = '';
|
||||
$r++;
|
||||
|
||||
|
||||
// Menus
|
||||
//-------
|
||||
|
||||
@ -107,7 +107,7 @@ $langs->load("modulebuilder");
|
||||
else if (type == 'link') { size.val('').prop('disabled', true); unique.removeAttr('disabled'); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").show();jQuery("#helppassword").hide();}
|
||||
else if (type == 'separate') {
|
||||
langfile.val('').prop('disabled',true);size.val('').prop('disabled', true); unique.removeAttr('checked').prop('disabled', true); required.val('').prop('disabled', true);
|
||||
jQuery("#value_choice").hide();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();
|
||||
jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();jQuery("#helppassword").hide();
|
||||
}
|
||||
else { // type = string
|
||||
size.val('').prop('disabled', true);
|
||||
|
||||
@ -103,7 +103,7 @@ $langs->load("modulebuilder");
|
||||
else if (type == 'checkbox') { size.val('').prop('disabled', true); unique.removeAttr('checked').prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").show();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();jQuery("#helppassword").hide();}
|
||||
else if (type == 'chkbxlst') { size.val('').prop('disabled', true); unique.removeAttr('checked').prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").show();jQuery("#helplink").hide();jQuery("#helppassword").hide();}
|
||||
else if (type == 'link') { size.val('').prop('disabled', true); unique.removeAttr('disabled'); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").show();jQuery("#helppassword").hide();}
|
||||
else if (type == 'separate') { size.val('').prop('disabled', true); unique.removeAttr('checked').prop('disabled', true); required.val('').prop('disabled', true); default_value.val('').prop('disabled', true); jQuery("#value_choice").hide();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();jQuery("#helppassword").hide();}
|
||||
else if (type == 'separate') { size.val('').prop('disabled', true); unique.removeAttr('checked').prop('disabled', true); required.val('').prop('disabled', true); default_value.val('').prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();jQuery("#helppassword").hide();}
|
||||
else { // type = string
|
||||
size.val('').prop('disabled', true);
|
||||
unique.removeAttr('disabled');
|
||||
@ -173,7 +173,7 @@ if((($type == 'select') || ($type == 'checkbox') || ($type == 'radio')) && is_ar
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (($type== 'sellist') || ($type == 'chkbxlst') || ($type == 'link') || ($type == 'password'))
|
||||
elseif (($type== 'sellist') || ($type == 'chkbxlst') || ($type == 'link') || ($type == 'password') || ($type == 'separate'))
|
||||
{
|
||||
$paramlist=array_keys($param['options']);
|
||||
$param_chain = $paramlist[0];
|
||||
|
||||
@ -48,8 +48,8 @@ if ($reshook < 0) setEventMessages($hookmanager->error, $hookmanager->errors, 'e
|
||||
|
||||
//var_dump($extrafields->attributes[$object->table_element]);
|
||||
if (empty($reshook) && is_array($extrafields->attributes[$object->table_element]['label']))
|
||||
|
||||
{
|
||||
$extrafields_collapse_num = '';
|
||||
foreach ($extrafields->attributes[$object->table_element]['label'] as $key => $label)
|
||||
{
|
||||
// Discard if extrafield is a hidden field on form
|
||||
@ -86,11 +86,25 @@ if (empty($reshook) && is_array($extrafields->attributes[$object->table_element]
|
||||
}
|
||||
if ($extrafields->attributes[$object->table_element]['type'][$key] == 'separate')
|
||||
{
|
||||
$extrafields_collapse_num = '';
|
||||
$extrafield_param = $extrafields->attributes[$object->table_element]['param'][$key];
|
||||
if (!empty($extrafield_param) && is_array($extrafield_param)) {
|
||||
$extrafield_param_list = array_keys($extrafield_param['options']);
|
||||
|
||||
if (count($extrafield_param_list)>0) {
|
||||
$extrafield_collapse_display_value = intval($extrafield_param_list[0]);
|
||||
|
||||
if ($extrafield_collapse_display_value==1 || $extrafield_collapse_display_value==2) {
|
||||
$extrafields_collapse_num = $extrafields->attributes[$object->table_element]['pos'][$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print $extrafields->showSeparator($key, $object);
|
||||
}
|
||||
else
|
||||
{
|
||||
print '<tr>';
|
||||
print '<tr class="trextrafields_collapse'.$extrafields_collapse_num.'">';
|
||||
print '<td class="titlefield">';
|
||||
print '<table width="100%" class="nobordernopadding">';
|
||||
print '<tr>';
|
||||
|
||||
@ -907,6 +907,9 @@ Permission50202=Import transactions
|
||||
Permission50401=Bind products and invoices with accounting accounts
|
||||
Permission50411=Read operations in ledger
|
||||
Permission50412=Write/Edit operations in ledger
|
||||
Permission50414=Delete operations in ledger
|
||||
Permission50415=Delete all operations by year and journal in ledger
|
||||
Permission50418=Export operations of the ledger
|
||||
Permission50420=Report and export reports (turnover, balance, journals, ledger)
|
||||
Permission50430=Define and close a fiscal year
|
||||
Permission50440=Manage chart of accounts, setup of accountancy
|
||||
|
||||
Loading…
Reference in New Issue
Block a user