Merge pull request #17020 from Hystepik/develop#1

Close #4951 : New module Inventory
This commit is contained in:
Laurent Destailleur 2021-03-30 20:43:52 +02:00 committed by GitHub
commit 38f1710b43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 549 additions and 32 deletions

View File

@ -172,6 +172,12 @@ class modStock extends DolibarrModules
$this->rights[9][3] = 0; // Permission by default for new user (0/1)
$this->rights[9][4] = 'inventory_advance'; // In php code, permission will be checked by test if ($user->rights->permkey->level1->level2)
$this->rights[9][5] = 'changePMP'; // In php code, permission will be checked by test if ($user->rights->permkey->level1->level2)
$this->rights[10][0] = 1016;
$this->rights[10][1] = 'inventoryDeletePermission'; // Permission label
$this->rights[10][3] = 0; // Permission by default for new user (0/1)
$this->rights[10][4] = 'inventory_advance'; // In php code, permission will be checked by test if ($user->rights->permkey->level1->level2)
$this->rights[10][5] = 'delete'; // In php code, permission will be checked by test if ($user->rights->permkey->level1->level2)
}
// Main menu entries

View File

@ -185,6 +185,7 @@ inventoryCreatePermission=Create new inventory
inventoryReadPermission=View inventories
inventoryWritePermission=Update inventories
inventoryValidatePermission=Validate inventory
inventoryDeletePermission=Delete inventory
inventoryTitle=Inventory
inventoryListTitle=Inventories
inventoryListEmpty=No inventory in progress
@ -243,4 +244,7 @@ InventoryRealQtyHelp=Set value to 0 to reset qty<br>Keep field empty, or remove
UpdateByScaning=Update by scaning
UpdateByScaningProductBarcode=Update by scan (product barcode)
UpdateByScaningLot=Update by scan (lot|serial barcode)
DisableStockChangeOfSubProduct=Deactivate the stock change for all the subproducts of this Kit during this movement.
DisableStockChangeOfSubProduct=Deactivate the stock change for all the subproducts of this Kit during this movement.
LabelOfInventoryMovemement=Inventory %s
ReOpen=Reopen
ConfirmFinish=Confirm closing

View File

@ -407,6 +407,12 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=confirm_setdraft&confirm=yes">'.$langs->trans("SetToDraft").'</a>';
}
}
// Back to validate
if ($object->status == $object::STATUS_RECORDED) {
if ($permissiontoadd) {
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=confirm_validate&confirm=yes">'.$langs->trans("ReOpen").'</a>';
}
}
// Modify
if ($object->status == $object::STATUS_DRAFT) {

View File

@ -52,7 +52,7 @@ class Inventory extends CommonObject
/**
* @var int Does object support extrafields ? 0=No, 1=Yes
*/
public $isextrafieldmanaged = 1;
public $isextrafieldmanaged = 0;
/**
* @var string String with name of icon for inventory
@ -254,6 +254,7 @@ class Inventory extends CommonObject
*/
public function validate(User $user, $notrigger = false)
{
global $conf;
$this->db->begin();
$result = 0;
@ -328,6 +329,7 @@ class Inventory extends CommonObject
} else {
$this->db->rollback();
}
return $result;
}
/**
@ -357,6 +359,51 @@ class Inventory extends CommonObject
} else {
$this->db->rollback();
}
return $result;
}
/**
* Set to Recorded
*
* @param User $user User that creates
* @param bool $notrigger false=launch triggers after, true=disable triggers
* @return int <0 if KO, Id of created object if OK
*/
public function setRecorded(User $user, $notrigger = false)
{
$this->db->begin();
$result = $this->setStatut($this::STATUS_RECORDED, null, '', 'INVENTORY_RECORDED');
if ($result > 0) {
$this->db->commit();
} else {
$this->db->rollback();
return -1;
}
return $result;
}
/**
* Set to Canceled
*
* @param User $user User that creates
* @param bool $notrigger false=launch triggers after, true=disable triggers
* @return int <0 if KO, Id of created object if OK
*/
public function setCanceled(User $user, $notrigger = false)
{
$this->db->begin();
$result = $this->setStatut($this::STATUS_CANCELED, null, '', 'INVENTORY_CANCELED');
if ($result > 0) {
$this->db->commit();
} else {
$this->db->rollback();
return -1;
}
return $result;
}
/**
@ -566,9 +613,11 @@ class Inventory extends CommonObject
$labelStatus[self::STATUS_DRAFT] = $langs->trans('Draft');
$labelStatus[self::STATUS_VALIDATED] = $langs->trans('Validated').' ('.$langs->trans('Started').')';
$labelStatus[self::STATUS_CANCELED] = $langs->trans('Canceled');
$labelStatus[self::STATUS_RECORDED] = $langs->trans('Closed');
$labelStatusShort[self::STATUS_DRAFT] = $langs->trans('Draft');
$labelStatusShort[self::STATUS_VALIDATED] = $langs->trans('Started');
$labelStatusShort[self::STATUS_CANCELED] = $langs->trans('Canceled');
$labelStatusShort[self::STATUS_RECORDED] = $langs->trans('Closed');
return dolGetStatus($labelStatus[$status], $labelStatusShort[$status], '', 'status'.$status, $mode);
}
@ -628,6 +677,7 @@ class Inventory extends CommonObject
public function initAsSpecimen()
{
$this->initAsSpecimenCommon();
$this->title = '';
}
}

View File

@ -27,6 +27,7 @@ include_once DOL_DOCUMENT_ROOT.'/product/class/html.formproduct.class.php';
include_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
include_once DOL_DOCUMENT_ROOT.'/product/inventory/class/inventory.class.php';
include_once DOL_DOCUMENT_ROOT.'/product/inventory/lib/inventory.lib.php';
include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
// Load translation files required by the page
$langs->loadLangs(array("stocks", "other", "productbatch"));
@ -98,6 +99,71 @@ $now = dol_now();
* Actions
*/
if ($action == 'cancel_record' && $permissiontoadd) {
$object->setCanceled($user);
}
if ($action == 'update' && $user->rights->stock->mouvement->creer) {
$stockmovment = new MouvementStock($db);
$stockmovment->origin = $object;
$sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
$sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated';
$sql .= ' FROM '.MAIN_DB_PREFIX.'inventorydet as id';
$sql .= ' WHERE id.fk_inventory = '.$object->id;
$resql = $db->query($sql);
if ($resql) {
$num = $db->num_rows($resql);
$i = 0;
$totalarray = array();
while ($i < $num) {
$line = $db->fetch_object($resql);
$qty_view = $line->qty_view;
$qty_stock = $line->qty_stock;
$stock_movement_qty = $qty_view - $qty_stock;
if ($stock_movement_qty != 0) {
if ($stock_movement_qty < 0) {
$movement_type = 1;
} else {
$movement_type = 0;
}
$idstockmove = $stockmovment->_create($user, $line->fk_product, $line->fk_warehouse, $stock_movement_qty, $movement_type, 0, $langs->trans('LabelOfInventoryMovemement', $object->id), 'INV'.$object->id);
if ($idstockmove < 0) {
$error++;
setEventMessages($stockmovment->error, $stockmovment->errors, 'errors');
}
}
$i++;
}
if (!$error) {
$object->setRecorded($user);
}
}
}
if ($action =='updateinventorylines' && $permissiontoadd) {
$sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
$sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated';
$sql .= ' FROM '.MAIN_DB_PREFIX.'inventorydet as id';
$sql .= ' WHERE id.fk_inventory = '.$object->id;
$resql = $db->query($sql);
if ($resql) {
$num = $db->num_rows($resql);
$i = 0;
$totalarray = array();
while ($i < $num) {
$line = $db->fetch_object($resql);
$lineid = $line->rowid;
$inventoryline = new InventoryLine($db);
$inventoryline->fetch($lineid);
$inventoryline->qty_view = GETPOST("id_".$inventoryline->id);
$inventoryline->update($user);
$i++;
}
}
}
$parameters = array();
$reshook = $hookmanager->executeHooks('doActions', $parameters, $object, $action); // Note that $action and $object may have been modified by some hooks
if ($reshook < 0) {
@ -223,6 +289,18 @@ if ($object->id > 0) {
$formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('ToClone'), $langs->trans('ConfirmCloneMyObject', $object->ref), 'confirm_clone', $formquestion, 'yes', 1);
}
// Confirmation to close
if ($action == 'record') {
$formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('Close'), $langs->trans('ConfirmFinish'), 'update', '', 0, 1);
$action = 'view';
}
// Confirmation to close
if ($action == 'confirm_cancel') {
$formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('Cancel'), $langs->trans('ConfirmCancel'), 'cancel_record', '', 0, 1);
$action = 'view';
}
// Call Hook formConfirm
$parameters = array('formConfirm' => $formconfirm, 'lineid' => $lineid);
$reshook = $hookmanager->executeHooks('formConfirm', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
@ -307,24 +385,7 @@ if ($object->id > 0) {
// Buttons for actions
if ($action == 'record') {
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="update">';
print '<input type="hidden" name="id" value="'.$object->id.'">';
if ($backtopage) {
print '<input type="hidden" name="backtopage" value="'.$backtopage.'">';
}
print '<div class="center">';
print '<span class="opacitymedium">'.$langs->trans("InventoryDesc").'</span><br>';
print '<input type="submit" class="button button-save" name="save" value="'.$langs->trans("Save").'">';
print ' &nbsp; ';
print '<input type="submit" class="button button-cancel" name="cancel" value="'.$langs->trans("Cancel").'">';
print '</div>';
print '<br>';
print '</form>';
} else {
if ($action != 'record') {
print '<div class="tabsAction">'."\n";
$parameters = array();
$reshook = $hookmanager->executeHooks('addMoreActionsButtons', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
@ -360,7 +421,8 @@ if ($object->id > 0) {
if ($object->status == Inventory::STATUS_VALIDATED) {
if ($permissiontoadd) {
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&amp;action=record">'.$langs->trans("Finish").'</a>'."\n";
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&amp;action=confirm_cancel">'.$langs->trans("Cancel").'</a>'."\n";
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&amp;action=record">'.$langs->trans("Close").'</a>'."\n";
} else {
print '<a class="butActionRefused classfortooltip" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans('Finish').'</a>'."\n";
}
@ -419,10 +481,12 @@ if ($object->id > 0) {
print '<td class="center">';
print $form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp"));
print '</td>';
// Actions
print '<td class="center">';
print '</td>';
print '</tr>';
if ($object->status == $object::STATUS_VALIDATED) {
// Actions
print '<td class="center">';
print '</td>';
print '</tr>';
}
// Line to add a new line in inventory
if ($object->status == $object::STATUS_VALIDATED) {
@ -505,15 +569,20 @@ if ($object->id > 0) {
}
print '<td class="right">';
print 'TODO';
print $obj->qty_stock;
print '</td>';
print '<td class="center">';
print '<input type="text" class="maxwidth75" name="id_'.$obj->rowid.' value="'.GETPOST("id_".$obj->rowid).'">';
print '</td>';
print '<td class="right">';
print '<a class="reposition" href="'.DOL_URL_ROOT.'/product/inventory/inventory.php?id='.$object->id.'&lineid='.$obj->rowid.'&action=deleteline&token='.newToken().'">'.img_delete().'</a>';
print '</td>';
if ($object->status == $object::STATUS_VALIDATED) {
$qty_view = GETPOST("id_".$obj->rowid) ? GETPOST("id_".$obj->rowid) : $obj->qty_view;
print '<input type="text" class="maxwidth75" name="id_'.$obj->rowid.'" value="'.$qty_view.'">';
print '</td>';
print '<td class="right">';
print '<a class="reposition" href="'.DOL_URL_ROOT.'/product/inventory/inventory.php?id='.$object->id.'&lineid='.$obj->rowid.'&action=deleteline&token='.newToken().'">'.img_delete().'</a>';
print '</td>';
} else {
print $obj->qty_view;
print '</td>';
}
print '</tr>';
$i++;

View File

@ -0,0 +1,382 @@
<?php
/* Copyright (C) 2010 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2018 Frédéric France <frederic.france@netlogic.fr>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* or see https://www.gnu.org/
*/
/**
* \file test/phpunit/InventoryTest.php
* \ingroup test
* \brief PHPUnit test
* \remarks To run this script as CLI: phpunit filename.php
*/
global $conf,$user,$langs,$db;
//define('TEST_DB_FORCE_TYPE','mysql'); // This is to force using mysql driver
//require_once 'PHPUnit/Autoload.php';
require_once dirname(__FILE__).'/../../htdocs/master.inc.php';
require_once dirname(__FILE__).'/../../htdocs/product/inventory/class/inventory.class.php';
if (empty($user->id)) {
print "Load permissions for admin user nb 1\n";
$user->fetch(1);
$user->getrights();
}
$conf->global->MAIN_DISABLE_ALL_MAILS=1;
/**
* Class for PHPUnit tests
*
* @backupGlobals disabled
* @backupStaticAttributes enabled
* @remarks backupGlobals must be disabled to have db,conf,user and lang not erased.
*/
class InventoryTest extends PHPUnit\Framework\TestCase
{
protected $savconf;
protected $savuser;
protected $savlangs;
protected $savdb;
/**
* Constructor
* We save global variables into local variables
*
* @return InventoryTest
*/
public function __construct()
{
parent::__construct();
//$this->sharedFixture
global $conf,$user,$langs,$db;
$this->savconf=$conf;
$this->savuser=$user;
$this->savlangs=$langs;
$this->savdb=$db;
print __METHOD__." db->type=".$db->type." user->id=".$user->id;
//print " - db ".$db->db;
print "\n";
}
/**
* setUpBeforeClass
*
* @return void
*/
public static function setUpBeforeClass():void
{
global $conf,$user,$langs,$db;
$db->begin(); // This is to have all actions inside a transaction even if test launched without suite.
print __METHOD__."\n";
}
/**
* tearDownAfterClass
*
* @return void
*/
public static function tearDownAfterClass():void
{
global $conf,$user,$langs,$db;
$db->rollback();
print __METHOD__."\n";
}
/**
* Init phpunit tests
*
* @return void
*/
protected function setUp():void
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
print __METHOD__."\n";
}
/**
* End phpunit tests
*
* @return void
*/
protected function tearDown():void
{
print __METHOD__."\n";
}
/**
* testInventoryCreate
*
* @return int
*/
public function testInventoryCreate()
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$localobject=new Inventory($db);
$localobject->initAsSpecimen();
$result=$localobject->create($user);
$this->assertLessThan($result, 0);
print __METHOD__." result=".$result."\n";
return $result;
}
/**
* testInventoryFetch
*
* @param int $id Id invoice
* @return int
*
* @depends testInventoryCreate
* The depends says test is run only if previous is ok
*/
public function testInventoryFetch($id)
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$localobject=new Inventory($this->savdb);
$result=$localobject->fetch($id);
$this->assertLessThan($result, 0);
print __METHOD__." id=".$id." result=".$result."\n";
return $localobject;
}
/**
* testInventoryUpdate
*
* @param Inventory $localobject Invoice
* @return int
*
* @depends testInventoryFetch
* The depends says test is run only if previous is ok
*/
public function testInventoryUpdate($localobject)
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$localobject->status = 9;
$localobject->title = 'test';
$result=$localobject->update($user, $user);
print __METHOD__." id=".$localobject->id." result=".$result."\n";
$this->assertLessThan($result, 0);
return $localobject;
}
/**
* testInventoryValidate
*
* @param Inventory $localobject Invoice
* @return void
*
* @depends testInventoryUpdate
* The depends says test is run only if previous is ok
*/
public function testInventoryValidate($localobject)
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$result=$localobject->validate($user);
print __METHOD__." id=".$localobject->id." result=".$result."\n";
$this->assertLessThan($result, 0);
$this->assertEquals($localobject->status, '1');
return $localobject;
}
/**
* testInventorySetDraft
*
* @param Inventory $localobject Invoice
* @return void
*
* @depends testInventoryValidate
* The depends says test is run only if previous is ok
*/
public function testInventorySetDraft($localobject)
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$result=$localobject->setDraft($user);
print __METHOD__." id=".$localobject->id." result=".$result."\n";
$this->assertLessThan($result, 0);
$this->assertEquals($localobject->status, '0');
return $localobject;
}
/**
* testInventorySetRecorded
*
* @param Inventory $localobject Invoice
* @return void
*
* @depends testInventorySetDraft
* The depends says test is run only if previous is ok
*/
public function testInventorySetRecorded($localobject)
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$result=$localobject->setRecorded($user);
print __METHOD__." id=".$localobject->id." result=".$result."\n";
$this->assertLessThan($result, 0);
$this->assertEquals($localobject->status, '2');
return $localobject;
}
/**
* testInventorySetCanceled
*
* @param Inventory $localobject Invoice
* @return void
*
* @depends testInventorySetRecorded
* The depends says test is run only if previous is ok
*/
public function testInventorySetCanceled($localobject)
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$result=$localobject->setCanceled($user);
print __METHOD__." id=".$localobject->id." result=".$result."\n";
$this->assertLessThan($result, 0);
$this->assertEquals($localobject->status, '9');
return $localobject;
}
/**
* testInventoryOther
*
* @param Inventory $localobject Invoice
* @return int
* @depends testInventorySetRecorded
* The depends says test is run only if previous is ok
*/
public function testInventoryOther($localobject)
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$localobject->info($localobject->id);
print __METHOD__." localobject->date_creation=".$localobject->date_creation."\n";
$this->assertNotEquals($localobject->date_creation, '');
return $localobject->id;
}
/**
* testInventoryDelete
*
* @param int $id Id of invoice
* @return int
* @depends testInventoryOther
* The depends says test is run only if previous is ok
*/
public function testInventoryDelete($id)
{
global $conf,$user,$langs,$db;
$conf=$this->savconf;
$user=$this->savuser;
$langs=$this->savlangs;
$db=$this->savdb;
$localobject=new Inventory($this->savdb);
$result=$localobject->fetch($id);
$result=$localobject->delete($user);
print __METHOD__." id=".$id." result=".$result."\n";
$this->assertLessThan($result, 0);
return $result;
}
/**
* Compare all public properties values of 2 objects
*
* @param Object $oA Object operand 1
* @param Object $oB Object operand 2
* @param boolean $ignoretype False will not report diff if type of value differs
* @param array $fieldstoignorearray Array of fields to ignore in diff
* @return array Array with differences
*/
public function objCompare($oA, $oB, $ignoretype = true, $fieldstoignorearray = array('id'))
{
$retAr=array();
if (get_class($oA) !== get_class($oB)) {
$retAr[]="Supplied objects are not of same class.";
} else {
$oVarsA=get_object_vars($oA);
$oVarsB=get_object_vars($oB);
$aKeys=array_keys($oVarsA);
foreach ($aKeys as $sKey) {
if (in_array($sKey, $fieldstoignorearray)) {
continue;
}
if (! $ignoretype && ($oVarsA[$sKey] !== $oVarsB[$sKey])) {
$retAr[]=$sKey.' : '.(is_object($oVarsA[$sKey])?get_class($oVarsA[$sKey]):$oVarsA[$sKey]).' <> '.(is_object($oVarsB[$sKey])?get_class($oVarsB[$sKey]):$oVarsB[$sKey]);
}
if ($ignoretype && ($oVarsA[$sKey] != $oVarsB[$sKey])) {
$retAr[]=$sKey.' : '.(is_object($oVarsA[$sKey])?get_class($oVarsA[$sKey]):$oVarsA[$sKey]).' <> '.(is_object($oVarsB[$sKey])?get_class($oVarsB[$sKey]):$oVarsB[$sKey]);
}
}
}
return $retAr;
}
}