diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..7d257b5 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,39 @@ +name: PHP Composer + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" + # Docs: https://getcomposer.org/doc/articles/scripts.md + + # - name: Run test suite + # run: composer run-script test diff --git a/ChangeLog.md b/ChangeLog.md index 2970243..af22b78 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,12 +1,16 @@ -# CHANGELOG MODULE DYNAMICSPRICES FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
-
-##1.1.1
-- Correction du filtre pour ignorer les services, seul les produits sont pris en compte. (29/09/2025)
-
- -##1.1
-- Prise en charge des prix de revient pour l'actualisation des prix de vente. (17/09/2025)
-
-## 1.0
-
-Initial version
+# Changelog DynamicsPrices + +## 2.0.0 +- Ajout du support des kits : le prix d'un kit est recalculé après ses composants pour éviter les doublons et refléter le coût cumulé. / Added kit support: a kit price is recalculated after its components to avoid duplicates and reflect the cumulative cost. +- Correction des mises à jour intempestives des services : seuls les produits physiques sont recalculés (`fk_product_type = 0`). / Fixed unintended service updates: only physical products are recalculated (`fk_product_type = 0`). +- Extension de la couverture des triggers pour lancer les recalculs sur davantage d'événements Dolibarr. / Expanded trigger coverage to launch recalculations on more Dolibarr events. +- Calcul automatique des prix de revient via un dictionnaire dédié et la moyenne des prix d'achat. / Automatic cost-price computation via a dedicated dictionary and average purchase prices. + +## 1.1.1 +- Correction du filtre pour ignorer les services ; seuls les produits sont pris en compte. (29/09/2025) / Fixed the filter to ignore services; only products are taken into account. (29/09/2025) + +## 1.1 +- Prise en charge des prix de revient pour l'actualisation des prix de vente. (17/09/2025) / Added cost-price handling to refresh selling prices. (17/09/2025) + +## 1.0 +- Version initiale. / Initial release. diff --git a/README.md b/README.md index 528356b..21db353 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,153 @@ -# DYNAMICSPRICES FOR [DOLIBARR ERP & CRM](https://www.dolibarr.org) +# DynamicsPrices -## Features +## Présentation (FR) -Ce module permet de mettre à jour les prix de vente en fonction du prix d'achat moyen unitaire chez les fournisseurs et des coefficients de prix définis dans un dictionnaire dédié. +Module Dolibarr pour la mise à jour dynamique des prix de vente à partir des coûts d'achat et des coefficients configurables. - +### Aperçu -Other external modules are available on [Dolistore.com](https://www.dolistore.com). +DynamicsPrices automatise le recalcul des prix des produits en s'appuyant sur les prix d'achat moyens, les coefficients de marge et les relations entre produits (composants, kits). Les déclencheurs du module s'occupent d'appliquer les nouveaux prix de vente au bon moment, tout en respectant les spécificités des produits et services Dolibarr. -## Translations +### Fonctionnalités clés -Translations can be completed manually by editing files in the module directories under `langs`. +- Mise à jour automatique des prix de vente en fonction du prix d'achat moyen et d'un dictionnaire de coefficients dédié. +- Recalcul des kits après leurs composants pour éviter les doublons de prix de vente et refléter le coût cumulé des sous-produits et services. +- Filtrage des services : seuls les produits physiques (`fk_product_type = 0`) sont recalculés pour éviter les mises à jour intempestives. +- Plus grand nombre de triggers pour couvrir les actions courantes (création, modification, réception d'achat, etc.). +- Calcul automatique des prix de revient à partir des nouveaux dictionnaires et de la moyenne des prix d'achat. - +### Installation +#### Depuis une archive ZIP -## Installation +1. Télécharger l'archive `module_dynamicsprices-x.y.z.zip`. +2. Déployer l'archive via le menu **Accueil > Configuration > Modules > Déployer un module externe**. +3. Activer le module **DynamicsPrices** dans **Configuration > Modules/Applications**. -Prerequisites: You must have Dolibarr ERP & CRM software installed. You can download it from [Dolistore.org](https://www.dolibarr.org). -You can also get a ready-to-use instance in the cloud from https://saas.dolibarr.org +#### Depuis un dépôt Git +```bash +cd htdocs/custom +git clone git@github.com:gitlogin/dynamicsprices.git dynamicsprices +``` + +Puis activer le module dans Dolibarr comme décrit ci-dessus. + +### Mise à jour + +1. Sauvegarder la base de données et le répertoire du module. +2. Installer la nouvelle version (ZIP ou Git) dans `htdocs/custom/dynamicsprices`. +3. Lancer les scripts de migration proposés par Dolibarr si nécessaire. + +### Configuration + +- **Dictionnaire des coefficients** : définir les coefficients de marge dans **Dictionnaires > Coefficients DynamicsPrices**. +- **Triggers** : les déclencheurs DynamicsPrices mettent à jour les prix lors des actions standards (création de produit, réception fournisseur, modification de prix, etc.). +- **Kits** : le prix de vente d'un kit est recalculé uniquement après mise à jour des prix de ses composants pour éviter toute duplication. + +### Utilisation + +- Créer ou mettre à jour un produit avec un prix d'achat renseigné. +- Les triggers calculent automatiquement le prix de revient et le prix de vente suivant le coefficient applicable. +- Les services et produits non physiques (`fk_product_type != 0`) sont ignorés par les mises à jour automatiques. + +### Permissions et sécurité + +- Les actions de mise à jour sont soumises aux permissions Dolibarr standard sur les produits et dictionnaires. +- Les écrans du module masquent automatiquement les actions non autorisées. -### From the ZIP file and GUI interface +### Traductions -If the module is a ready-to-deploy zip file, so with a name `module_xxx-version.zip` (e.g., when downloading it from a marketplace like [Dolistore](https://www.dolistore.com)), -go to menu `Home> Setup> Modules> Deploy external module` and upload the zip file. +Les fichiers de langue sont disponibles dans `langs/`. Complétez ou ajustez les traductions en `en_US` et `fr_FR` pour tout nouveau libellé. - +### Summary - +Then enable the module in Dolibarr as described above. + +### Upgrade + +1. Back up the database and the module directory. +2. Install the new version (ZIP or Git) in `htdocs/custom/dynamicsprices`. +3. Run any migration scripts proposed by Dolibarr if needed. + +### Configuration + +- **Coefficient dictionary**: define margin coefficients in **Dictionaries > DynamicsPrices Coefficients**. +- **Triggers**: DynamicsPrices triggers update prices during standard actions (product creation, supplier receipt, price edits, etc.). +- **Kits**: a kit's selling price is recalculated only after updating its component prices to prevent duplication. -### Final steps +### Usage -Using your browser: +- Create or update a product with a purchase price filled in. +- Triggers automatically compute cost price and selling price using the applicable coefficient. +- Services and non-physical products (`fk_product_type != 0`) are ignored by automated updates. - - Log into Dolibarr as a super-administrator - - Go to "Setup"> "Modules" - - You should now be able to find and enable the module +### Permissions and security +- Update actions follow Dolibarr standard permissions for products and dictionaries. +- Module screens automatically hide actions that the user is not allowed to perform. +### Translations -## Licenses +Language files live under `langs/`. Complete or adjust translations in `en_US` and `fr_FR` for any new labels. -### Main code +### Support -GPLv3 or (at your option) any later version. See file COPYING for more information. +- Dolibarr documentation and support: [https://wiki.dolibarr.org](https://wiki.dolibarr.org) +- Other external modules: [Dolistore.com](https://www.dolistore.com) -### Documentation +### License -All texts and readme's are licensed under [GFDL](https://www.gnu.org/licenses/fdl-1.3.en.html). +- Code: GPLv3 or later (see `COPYING`). +- Documentation: GFDL (see the corresponding license). diff --git a/admin/setup.php b/admin/setup.php index e6d32bc..6c6df35 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -1,155 +1,182 @@ - - * Copyright (C) 2024 Frédéric France - * Copyright (C) 2025 Pierre ARDOIN - * - * 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 . - */ - -/** - * \file dynamicsprices/admin/setup.php - * \ingroup dynamicsprices - * \brief DynamicsPrices setup page. - */ - -// Load Dolibarr environment -$res = 0; -// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined) -if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { - $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; -} -// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME -$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; -$tmp2 = realpath(__FILE__); -$i = strlen($tmp) - 1; -$j = strlen($tmp2) - 1; -while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { - $i--; - $j--; -} -if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { - $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; -} -if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { - $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; -} -// Try main.inc.php using relative path -if (!$res && file_exists("../../main.inc.php")) { - $res = @include "../../main.inc.php"; -} -if (!$res && file_exists("../../../main.inc.php")) { - $res = @include "../../../main.inc.php"; -} -if (!$res) { - die("Include of main fails"); -} - -// Libraries -require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php"; -require_once '../lib/dynamicsprices.lib.php'; -//require_once "../class/myclass.class.php"; - -/** - * @var Conf $conf - * @var DoliDB $db - * @var HookManager $hookmanager - * @var Translate $langs - * @var User $user - */ - -// Translations -$langs->loadLangs(array("admin", "dynamicsprices@dynamicsprices")); - -// Initialize a technical object to manage hooks of page. Note that conf->hooks_modules contains an array of hook context -/** @var HookManager $hookmanager */ -$hookmanager->initHooks(array('dynamicspricessetup', 'globalsetup')); - -// Parameters -$action = GETPOST('action', 'aZ09'); -$backtopage = GETPOST('backtopage', 'alpha'); -$modulepart = GETPOST('modulepart', 'aZ09'); // Used by actions_setmoduleoptions.inc.php - -$value = GETPOST('value', 'alpha'); -$label = GETPOST('label', 'alpha'); -$scandir = GETPOST('scan_dir', 'alpha'); -$type = 'myobject'; - -$error = 0; -$setupnotempty = 1; - -// Access control -if (!$user->admin) { - accessforbidden(); -} - - -// Set this to 1 to use the factory to manage constants. Warning, the generated module will be compatible with version v15+ only -$useFormSetup = 1; - -if (!class_exists('FormSetup')) { - require_once DOL_DOCUMENT_ROOT.'/core/class/html.formsetup.class.php'; -} -$formSetup = new FormSetup($db); - -// Access control -if (!$user->admin) { - accessforbidden(); -} - - -$action = 'edit'; - - -/* - * View - */ - -$form = new Form($db); - -$help_url = ''; -$title = "DynamicsPricesSetup"; - -llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-dynamicsprices page-admin'); - -// Subheader -$linkback = ''.$langs->trans("BackToModuleList").''; - -print load_fiche_titre($langs->trans($title), $linkback, 'title_setup'); - -// Configuration header -$head = dynamicspricesAdminPrepareHead(); -print dol_get_fiche_head($head, 'settings', $langs->trans($title), -1, "dynamicsprices@dynamicsprices"); - -// Setup page goes here -echo ''.$langs->trans("DynamicsPricesSetupPage").'

'; - -print ''; - -// Réglage - setup_print_title($langs->trans("LMDB_UpdateOptions")); - setup_print_on_off('LMDB_COST_PRICE_ONLY'); - setup_print_on_off('LMDB_SUPPLIER_BUYPRICE_ALTERED'); - - - -print '
'; -if (empty($setupnotempty)) { - print '
'.$langs->trans("NothingToSetup"); -} - -// Page end -print dol_get_fiche_end(); - -llxFooter(); -$db->close(); + +* Copyright (C) 2024 Frédéric France +* Copyright (C) 2025 Pierre ARDOIN +* +* 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 . +*/ + +/** +* \file dynamicsprices/admin/setup.php +* \ingroup dynamicsprices +* \brief DynamicsPrices setup page. +*/ + +// Load Dolibarr environment +$res = 0; +// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined) +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +// Try main.inc.php using relative path +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} + +// Libraries +require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php"; +require_once '../lib/dynamicsprices.lib.php'; +//require_once "../class/myclass.class.php"; +require_once __DIR__.'/../core/modules/modDynamicsPrices.class.php'; + +/** +* @var Conf $conf +* @var DoliDB $db +* @var HookManager $hookmanager +* @var Translate $langs +* @var User $user +*/ + +// Translations +$langs->loadLangs(array("admin", "dynamicsprices@dynamicsprices")); + +// Initialize a technical object to manage hooks of page. Note that conf->hooks_modules contains an array of hook context +/** @var HookManager $hookmanager */ +$hookmanager->initHooks(array('dynamicspricessetup', 'globalsetup')); + +// Parameters +$action = GETPOST('action', 'aZ09'); +$backtopage = GETPOST('backtopage', 'alpha'); +$modulepart = GETPOST('modulepart', 'aZ09'); // Used by actions_setmoduleoptions.inc.php + +$value = GETPOST('value', 'alpha'); +$label = GETPOST('label', 'alpha'); +$scandir = GETPOST('scan_dir', 'alpha'); +$type = 'myobject'; + +$error = 0; +$setupnotempty = 1; + +// Access control +if (!$user->admin) { + accessforbidden(); +} + +// Actions on module constants +include DOL_DOCUMENT_ROOT.'/core/actions_setmoduleoptions.inc.php'; + +// Load dictionary definitions +$module = new modDynamicsPrices($db); +$taborder = empty($module->dictionaries['taborder']) ? array() : $module->dictionaries['taborder']; +$tabname = empty($module->dictionaries['tabname']) ? array() : $module->dictionaries['tabname']; +$tablib = empty($module->dictionaries['tablib']) ? array() : $module->dictionaries['tablib']; +$tabsql = empty($module->dictionaries['tabsql']) ? array() : $module->dictionaries['tabsql']; +$tabsqlsort = empty($module->dictionaries['tabsqlsort']) ? array() : $module->dictionaries['tabsqlsort']; +$tabfield = empty($module->dictionaries['tabfield']) ? array() : $module->dictionaries['tabfield']; +$tabfieldvalue = empty($module->dictionaries['tabfieldvalue']) ? array() : $module->dictionaries['tabfieldvalue']; +$tabfieldinsert = empty($module->dictionaries['tabfieldinsert']) ? array() : $module->dictionaries['tabfieldinsert']; +$tabrowid = empty($module->dictionaries['tabrowid']) ? array() : $module->dictionaries['tabrowid']; +$tabcond = empty($module->dictionaries['tabcond']) ? array() : $module->dictionaries['tabcond']; +$tabhelp = empty($module->dictionaries['tabhelp']) ? array() : $module->dictionaries['tabhelp']; +$tabsave = empty($module->dictionaries['tabsave']) ? array() : $module->dictionaries['tabsave']; +$dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']); + +include DOL_DOCUMENT_ROOT.'/core/actions_dictionnaire.inc.php'; + + +// Set this to 1 to use the factory to manage constants. Warning, the generated module will be compatible with version v15+ only +$useFormSetup = 1; + +if (!class_exists('FormSetup')) { + require_once DOL_DOCUMENT_ROOT.'/core/class/html.formsetup.class.php'; +} +$formSetup = new FormSetup($db); + +// Access control +if (!$user->admin) { + accessforbidden(); +} + + +$action = 'edit'; + + +/* +* View +*/ + +$form = new Form($db); + +$help_url = ''; +$title = "DynamicsPricesSetup"; + +llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-dynamicsprices page-admin'); + +// Subheader +$linkback = ''.$langs->trans("BackToModuleList").''; + +print load_fiche_titre($langs->trans($title), $linkback, 'title_setup'); + +// Configuration header +$head = dynamicspricesAdminPrepareHead(); +print dol_get_fiche_head($head, 'settings', $langs->trans($title), -1, "dynamicsprices@dynamicsprices"); + +// Setup page goes here +echo ''.$langs->trans("DynamicsPricesSetupPage").'

'; + +print ''; + +// Settings +setup_print_title($langs->trans("LMDB_UpdateOptions")); +setup_print_on_off('LMDB_COST_PRICE_ONLY'); +setup_print_on_off('LMDB_SUPPLIER_BUYPRICE_ALTERED'); +setup_print_on_off('LMDB_KIT_PRICE_FROM_COMPONENTS'); + +print '
'; + +print '
'; + +// Dictionary management +include DOL_DOCUMENT_ROOT.'/core/tpl/admin/dict.tpl.php'; + +if (empty($setupnotempty)) { +print '
'.$langs->trans("NothingToSetup"); +} + +// Page end +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..48fffde --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "dolibarr/dynamicprices", + "description": "Dynamicprices module for Dolibarr ERP & CRM.", + "type": "dolibarr-module", + "license": "GPL-3.0-or-later", + "keywords": [ + "dolibarr", + "module", + "dynamicprices" + ], + "homepage": "https://www.dolibarr.org", + "support": { + "docs": "https://wiki.dolibarr.org/" + }, + "require": { + "php": ">=7.0" + }, + "extra": { + "dolibarr": { + "module-name": "dynamicprices" + } + } +} diff --git a/core/modules/modDynamicsPrices.class.php b/core/modules/modDynamicsPrices.class.php index f3aaf19..4b2304b 100644 --- a/core/modules/modDynamicsPrices.class.php +++ b/core/modules/modDynamicsPrices.class.php @@ -54,7 +54,7 @@ public function __construct($db) // Family can be 'base' (core modules),'crm','financial','hr','projects','products','ecm','technic' (transverse modules),'interface' (link with external tools),'other','...' // It is used to group modules by family in module setup page - $this->family = 'products'; + $this->family = 'Les Métiers du Bâtiment'; // Module position in the family on 2 digits ('01', '10', '20', ...) $this->module_position = '90'; @@ -75,8 +75,8 @@ public function __construct($db) $this->editor_url = 'lesmetiersdubatiment.fr'; // Must be an external online web site $this->editor_squarred_logo = 'logo.png@dynamicsprices'; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@dynamicsprices' - // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '1.1.1'; +// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' +$this->version = '2.0.0'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -87,7 +87,7 @@ public function __construct($db) // If file is in theme/yourtheme/img directory under name object_pictovalue.png, use this->picto='pictovalue' // If file is in module/img directory under name object_pictovalue.png, use this->picto='pictovalue@module' // To use a supported fa-xxx css style of font awesome, use this->picto='xxx' - $this->picto = 'margin'; + $this->picto = 'margin'; // Define some features supported by module (triggers, login, substitutions, menus, css, etc...) $this->module_parts = array( @@ -253,40 +253,48 @@ public function __construct($db) 'tabhelp' => array(array('code' => $langs->trans('CodeTooltipHelp'), 'field2' => 'field2tooltip'), array('code' => $langs->trans('CodeTooltipHelp'), 'field2' => 'field2tooltip'), ...), ); */ - /* BEGIN MODULEBUILDER DICTIONARIES */ - $this->dictionaries = array( - 'langs'=>'dynamicsprices@dynamicsprices', - 'tabname'=>array(MAIN_DB_PREFIX."c_coefprice"), - 'tablib'=>array("LMDB_coefprice"), - 'tabsql'=>array( - 'SELECT t.rowid as rowid, t.entity, t.code, t.fk_nature, t.pricelevel, t.minrate, t.targetrate, t.active FROM '.MAIN_DB_PREFIX.'c_coefprice AS t WHERE t.entity = '.((int) $conf->entity), - ), - 'tabsqlsort'=>array( - "code ASC", - ), - 'tabfield'=>array( - "code,fk_nature,pricelevel,targetrate,minrate", - ), - 'tabfieldvalue'=>array( - "code,entity,fk_nature,pricelevel,targetrate,minrate", - ), - 'tabfieldinsert'=>array( - "code,entity,fk_nature,pricelevel,targetrate,minrate", - ), - 'tabrowid'=>array('rowid'), - 'tabcond'=>array( - isModEnabled('dynamicsprices'), - ), - 'tabhelp' => array( - 'code' => $langs->trans('LMDB_CodeTooltipHelp'), - 'entity' => $langs->trans('LMDB_ENtityTooltipHelp'), - 'fk_nature' => $langs->trans('LMDB_FkNatureTooltipHelp'), - 'pricelevel' => $langs->trans('LMDB_PriceLevelTooltipHelp'), - 'targetrate' => $langs->trans('LMDB_TargetRateTooltipHelp'), - 'minrate' => $langs->trans('LMDB_MinRateTooltipHelp'), - ), - ); - /* END MODULEBUILDER DICTIONARIES */ +/* BEGIN MODULEBUILDER DICTIONARIES */ +$this->dictionaries = array( +'langs'=>'dynamicsprices@dynamicsprices', +'tabname'=>array(MAIN_DB_PREFIX."c_coefprice", MAIN_DB_PREFIX."c_margin_on_cost"), +'tablib'=>array("LMDB_coefprice", "LMDB_marginoncost"), +'tabsql'=>array( +'SELECT t.rowid as rowid, t.entity, t.code, t.fk_nature, t.pricelevel, t.minrate, t.targetrate, t.active FROM '.MAIN_DB_PREFIX.'c_coefprice AS t WHERE t.entity = '.((int) $conf->entity), +'SELECT t.rowid as rowid, t.entity, t.code, t.code_nature, t.margin_on_cost_percent, t.active FROM '.MAIN_DB_PREFIX.'c_margin_on_cost AS t WHERE t.entity = '.((int) $conf->entity), +), +'tabsqlsort'=>array( +"code ASC", +"code ASC", +), +'tabfield'=>array( +"code,fk_nature,pricelevel,targetrate,minrate", +"code,code_nature,margin_on_cost_percent", +), +'tabfieldvalue'=>array( +"code,entity,fk_nature,pricelevel,targetrate,minrate", +"code,entity,code_nature,margin_on_cost_percent", +), +'tabfieldinsert'=>array( +"code,entity,fk_nature,pricelevel,targetrate,minrate", +"code,entity,code_nature,margin_on_cost_percent", +), +'tabrowid'=>array('rowid', 'rowid'), +'tabcond'=>array( +isModEnabled('dynamicsprices'), +isModEnabled('dynamicsprices'), +), +'tabhelp' => array( +'code' => $langs->trans('LMDB_CodeTooltipHelp'), +'entity' => $langs->trans('LMDB_ENtityTooltipHelp'), +'fk_nature' => $langs->trans('LMDB_FkNatureTooltipHelp'), +'pricelevel' => $langs->trans('LMDB_PriceLevelTooltipHelp'), +'targetrate' => $langs->trans('LMDB_TargetRateTooltipHelp'), +'minrate' => $langs->trans('LMDB_MinRateTooltipHelp'), +'code_nature' => $langs->trans('LMDB_CodeNatureTooltipHelp'), +'margin_on_cost_percent' => $langs->trans('LMDB_MarginOnCostTooltipHelp'), +), +); +/* END MODULEBUILDER DICTIONARIES */ // Boxes/Widgets // Add here list of php file(s) stored in dynamicsprices/core/boxes that contains a class to show a widget. @@ -593,3 +601,4 @@ public function remove($options = '') return $this->_remove($sql, $options); } } + diff --git a/core/triggers/interface_99_modDynamicsPrices_DynamicsPricesTriggers.class.php b/core/triggers/interface_99_modDynamicsPrices_DynamicsPricesTriggers.class.php index c8c9ad5..93962d2 100644 --- a/core/triggers/interface_99_modDynamicsPrices_DynamicsPricesTriggers.class.php +++ b/core/triggers/interface_99_modDynamicsPrices_DynamicsPricesTriggers.class.php @@ -1,6 +1,6 @@ - * Copyright (C) ---Replace with your own copyright and developer email--- + * Copyright (C) 2025 Pierre Ardoin * * 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 @@ -37,301 +37,302 @@ */ class InterfaceDynamicsPricesTriggers extends DolibarrTriggers { - /** - * Constructor - * - * @param DoliDB $db Database handler - */ - public function __construct($db) - { - parent::__construct($db); - $this->family = "product"; - $this->description = "Dynamicsprices triggers."; - $this->version = self::VERSIONS['dev']; - $this->picto = 'logo.png@dynamicsprices'; - } - - /** - * Function called when a Dolibarr business event is done. - * All functions "runTrigger" are triggered if the file is inside the directory core/triggers - * - * @param string $action Event action code - * @param CommonObject $object Object - * @param User $user Object user - * @param Translate $langs Object langs - * @param Conf $conf Object conf - * @return int Return integer <0 if KO, 0 if no triggered ran, >0 if OK - */ - public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf) - { - if (!isModEnabled('dynamicsprices')) { - return 0; // If module is not enabled, we do nothing - } - - // Put here code you want to execute when a Dolibarr business events occurs. - // Data and type of action are stored into $object and $action - - global $db; - - if (!getDolGlobalString('LMDB_COST_PRICE_ONLY')) { - //var_dump(getDolGlobalString('LMDB_COST_PRICE_ONLY')); - var_dump($action); - if ($action === 'SUPPLIER_PRODUCT_BUYPRICE_CREATE' || $action === 'SUPPLIER_PRODUCT_BUYPRICE_MODIFY' || $action == 'SUPPLIER_PRODUCT_BUYPRICE_DELETE' || $action === 'PRODUCT_BUYPRICE_MODIFY' || $action === 'PRODUCT_BUYPRICE_DELETE') { - require_once __DIR__.'/../../lib/dynamicsprices.lib.php'; - var_dump(getDolGlobalString('LMDB_SUPPLIER_BUYPRICE_ALTERED')); - if (getDolGlobalString('LMDB_SUPPLIER_BUYPRICE_ALTERED')) { - var_dump(getDolGlobalString('LMDB_SUPPLIER_BUYPRICE_ALTERED')); - $results = update_customer_prices_from_suppliers($db, $user, $langs, $conf, $object->fk_product); - } - //var_dump($results); - } - } else { - if ($action === 'PRODUCT_MODIFY') { - require_once __DIR__.'/../../lib/dynamicsprices.lib.php'; - //var_dump(getDolGlobalString('LMDB_COST_PRICE_ONLY')); - if (getDolGlobalString('LMDB_SUPPLIER_BUYPRICE_ALTERED')) { - $results = update_customer_prices_from_cost_price($db, $user, $langs, $conf, $object->fk_product); - } - } - } - - // You can isolate code for each action in a separate method: this method should be named like the trigger in camelCase. - // For example : COMPANY_CREATE => public function companyCreate($action, $object, User $user, Translate $langs, Conf $conf) - $methodName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($action))))); - $callback = array($this, $methodName); - if (is_callable($callback)) { - dol_syslog( - "Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id - ); - - return call_user_func($callback, $action, $object, $user, $langs, $conf); - - - } - - // Or you can execute some code here - switch ($action) { // @phan-suppress-current-line PhanNoopSwitchCases - // Users - //case 'USER_CREATE': - //case 'USER_MODIFY': - //case 'USER_NEW_PASSWORD': - //case 'USER_ENABLEDISABLE': - //case 'USER_DELETE': - - // Actions - //case 'ACTION_MODIFY': - //case 'ACTION_CREATE': - //case 'ACTION_DELETE': - - // Groups - //case 'USERGROUP_CREATE': - //case 'USERGROUP_MODIFY': - //case 'USERGROUP_DELETE': - - // Companies - //case 'COMPANY_CREATE': - //case 'COMPANY_MODIFY': - //case 'COMPANY_DELETE': - - // Contacts - //case 'CONTACT_CREATE': - //case 'CONTACT_MODIFY': - //case 'CONTACT_DELETE': - //case 'CONTACT_ENABLEDISABLE': - - // Products - //case 'PRODUCT_CREATE': - //case 'PRODUCT_MODIFY': - //case 'PRODUCT_DELETE': - //case 'PRODUCT_PRICE_MODIFY': - //case 'PRODUCT_SET_MULTILANGS': - //case 'PRODUCT_DEL_MULTILANGS': - - //Stock movement - //case 'STOCK_MOVEMENT': - - //MYECMDIR - //case 'MYECMDIR_CREATE': - //case 'MYECMDIR_MODIFY': - //case 'MYECMDIR_DELETE': - - // Sales orders - //case 'ORDER_CREATE': - //case 'ORDER_MODIFY': - //case 'ORDER_VALIDATE': - //case 'ORDER_DELETE': - //case 'ORDER_CANCEL': - //case 'ORDER_SENTBYMAIL': - //case 'ORDER_CLASSIFY_BILLED': // TODO Replace it with ORDER_BILLED - //case 'ORDER_CLASSIFY_UNBILLED': // TODO Replace it with ORDER_UNBILLED - //case 'ORDER_SETDRAFT': - //case 'LINEORDER_INSERT': - //case 'LINEORDER_MODIFY': - //case 'LINEORDER_DELETE': - - // Supplier orders - //case 'ORDER_SUPPLIER_CREATE': - //case 'ORDER_SUPPLIER_MODIFY': - //case 'ORDER_SUPPLIER_VALIDATE': - //case 'ORDER_SUPPLIER_DELETE': - //case 'ORDER_SUPPLIER_APPROVE': - //case 'ORDER_SUPPLIER_CLASSIFY_BILLED': // TODO Replace with ORDER_SUPPLIER_BILLED - //case 'ORDER_SUPPLIER_CLASSIFY_UNBILLED': // TODO Replace with ORDER_SUPPLIER_UNBILLED - //case 'ORDER_SUPPLIER_REFUSE': - //case 'ORDER_SUPPLIER_CANCEL': - //case 'ORDER_SUPPLIER_SENTBYMAIL': - //case 'ORDER_SUPPLIER_RECEIVE': - //case 'LINEORDER_SUPPLIER_DISPATCH': - //case 'LINEORDER_SUPPLIER_CREATE': - //case 'LINEORDER_SUPPLIER_MODIFY': - //case 'LINEORDER_SUPPLIER_DELETE': - - // Proposals - //case 'PROPAL_CREATE': - //case 'PROPAL_MODIFY': - //case 'PROPAL_VALIDATE': - //case 'PROPAL_SENTBYMAIL': - //case 'PROPAL_CLASSIFY_BILLED': // TODO Replace it with PROPAL_BILLED - //case 'PROPAL_CLASSIFY_UNBILLED': // TODO Replace it with PROPAL_UNBILLED - //case 'PROPAL_CLOSE_SIGNED': - //case 'PROPAL_CLOSE_REFUSED': - //case 'PROPAL_DELETE': - //case 'LINEPROPAL_INSERT': - //case 'LINEPROPAL_MODIFY': - //case 'LINEPROPAL_DELETE': - - // SupplierProposal - //case 'SUPPLIER_PROPOSAL_CREATE': - //case 'SUPPLIER_PROPOSAL_MODIFY': - //case 'SUPPLIER_PROPOSAL_VALIDATE': - //case 'SUPPLIER_PROPOSAL_SENTBYMAIL': - //case 'SUPPLIER_PROPOSAL_CLOSE_SIGNED': - //case 'SUPPLIER_PROPOSAL_CLOSE_REFUSED': - //case 'SUPPLIER_PROPOSAL_DELETE': - //case 'LINESUPPLIER_PROPOSAL_INSERT': - //case 'LINESUPPLIER_PROPOSAL_MODIFY': - //case 'LINESUPPLIER_PROPOSAL_DELETE': - - // Contracts - //case 'CONTRACT_CREATE': - //case 'CONTRACT_MODIFY': - //case 'CONTRACT_ACTIVATE': - //case 'CONTRACT_CANCEL': - //case 'CONTRACT_CLOSE': - //case 'CONTRACT_DELETE': - //case 'LINECONTRACT_INSERT': - //case 'LINECONTRACT_MODIFY': - //case 'LINECONTRACT_DELETE': - - // Bills - //case 'BILL_CREATE': - //case 'BILL_MODIFY': - //case 'BILL_VALIDATE': - //case 'BILL_UNVALIDATE': - //case 'BILL_SENTBYMAIL': - //case 'BILL_CANCEL': - //case 'BILL_DELETE': - //case 'BILL_PAYED': - //case 'LINEBILL_INSERT': - //case 'LINEBILL_MODIFY': - //case 'LINEBILL_DELETE': - - // Recurring Bills - //case 'BILLREC_MODIFY': - //case 'BILLREC_DELETE': - //case 'BILLREC_AUTOCREATEBILL': - //case 'LINEBILLREC_MODIFY': - //case 'LINEBILLREC_DELETE': - - //Supplier Bill - //case 'BILL_SUPPLIER_CREATE': - //case 'BILL_SUPPLIER_MODIFY': - //case 'BILL_SUPPLIER_DELETE': - //case 'BILL_SUPPLIER_PAYED': - //case 'BILL_SUPPLIER_UNPAYED': - //case 'BILL_SUPPLIER_VALIDATE': - //case 'BILL_SUPPLIER_UNVALIDATE': - //case 'LINEBILL_SUPPLIER_CREATE': - //case 'LINEBILL_SUPPLIER_MODIFY': - //case 'LINEBILL_SUPPLIER_DELETE': - - // Payments - //case 'PAYMENT_CUSTOMER_CREATE': - //case 'PAYMENT_SUPPLIER_CREATE': - //case 'PAYMENT_ADD_TO_BANK': - //case 'PAYMENT_DELETE': - - // Online - //case 'PAYMENT_PAYBOX_OK': - //case 'PAYMENT_PAYPAL_OK': - //case 'PAYMENT_STRIPE_OK': - - // Donation - //case 'DON_CREATE': - //case 'DON_MODIFY': - //case 'DON_DELETE': - - // Interventions - //case 'FICHINTER_CREATE': - //case 'FICHINTER_MODIFY': - //case 'FICHINTER_VALIDATE': - //case 'FICHINTER_CLASSIFY_BILLED': // TODO Replace it with FICHINTER_BILLED - //case 'FICHINTER_CLASSIFY_UNBILLED': // TODO Replace it with FICHINTER_UNBILLED - //case 'FICHINTER_DELETE': - //case 'LINEFICHINTER_CREATE': - //case 'LINEFICHINTER_MODIFY': - //case 'LINEFICHINTER_DELETE': - - // Members - //case 'MEMBER_CREATE': - //case 'MEMBER_VALIDATE': - //case 'MEMBER_SUBSCRIPTION': - //case 'MEMBER_MODIFY': - //case 'MEMBER_NEW_PASSWORD': - //case 'MEMBER_RESILIATE': - //case 'MEMBER_DELETE': - - // Categories - //case 'CATEGORY_CREATE': - //case 'CATEGORY_MODIFY': - //case 'CATEGORY_DELETE': - //case 'CATEGORY_SET_MULTILANGS': - - // Projects - //case 'PROJECT_CREATE': - //case 'PROJECT_MODIFY': - //case 'PROJECT_DELETE': - - // Project tasks - //case 'TASK_CREATE': - //case 'TASK_MODIFY': - //case 'TASK_DELETE': - - // Task time spent - //case 'TASK_TIMESPENT_CREATE': - //case 'TASK_TIMESPENT_MODIFY': - //case 'TASK_TIMESPENT_DELETE': - //case 'PROJECT_ADD_CONTACT': - //case 'PROJECT_DELETE_CONTACT': - //case 'PROJECT_DELETE_RESOURCE': - - // Shipping - //case 'SHIPPING_CREATE': - //case 'SHIPPING_MODIFY': - //case 'SHIPPING_VALIDATE': - //case 'SHIPPING_SENTBYMAIL': - //case 'SHIPPING_BILLED': - //case 'SHIPPING_CLOSED': - //case 'SHIPPING_REOPEN': - //case 'SHIPPING_DELETE': - - // and more... - - default: - dol_syslog("Trigger '".$this->name."' for action '".$action."' launched by ".__FILE__.". id=".$object->id); - break; - } - - return 0; - } + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + parent::__construct($db); + $this->family = "product"; + $this->description = "Dynamicsprices triggers."; + $this->version = self::VERSIONS['dev']; + $this->picto = 'logo.png@dynamicsprices'; + } + + /** + * Function called when a Dolibarr business event is done. + * All functions "runTrigger" are triggered if the file is inside the directory core/triggers + * + * @param string $action Event action code + * @param CommonObject $object Object + * @param User $user Object user + * @param Translate $langs Object langs + * @param Conf $conf Object conf + * @return int Return integer <0 if KO, 0 if no triggered ran, >0 if OK + */ + public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf) + { + if (!isModEnabled('dynamicsprices')) { + return 0; // If module is not enabled, we do nothing + } + + // Put here code you want to execute when a Dolibarr business events occurs. + // Data and type of action are stored into $object and $action + + global $db; + + require_once __DIR__.'/../../lib/dynamicsprices.lib.php'; + $updateFunction = getDolGlobalString('LMDB_COST_PRICE_ONLY') ? 'update_customer_prices_from_cost_price' : 'update_customer_prices_from_suppliers'; + $affectedActions = array('SUPPLIER_PRODUCT_BUYPRICE_CREATE', 'SUPPLIER_PRODUCT_BUYPRICE_MODIFY', 'SUPPLIER_PRODUCT_BUYPRICE_DELETE', 'PRODUCT_MODIFY', 'PRODUCT_CREATE', 'PRODUCT_CLONE', 'PRODUCT_PRICE_CREATE', 'PRODUCT_PRICE_MODIFY', 'PRODUCT_PRICE_DELETE', 'PRODUCT_BUYPRICE_CREATE', 'PRODUCT_BUYPRICE_MODIFY', 'PRODUCT_BUYPRICE_DELETE', 'PRODUCT_SUBPRODUCT_ADD', 'PRODUCT_SUBPRODUCT_UPDATE', 'PRODUCT_SUBPRODUCT_DELETE'); + //var_dump($updateFunction); + //var_dump($action); + if (getDolGlobalString('LMDB_SUPPLIER_BUYPRICE_ALTERED') && in_array($action, $affectedActions, true)) { + dol_include_once('/product/class/product.class.php'); + $productId = !empty($object->fk_product) ? $object->fk_product : (isset($object->id) ? $object->id : 0); + $product = new Product($db); + if ($productId > 0 && $product->fetch($productId) > 0) { + if ((int) $product->type !== Product::TYPE_PRODUCT) { + return 0; + } + } + if ($productId > 0) { + call_user_func($updateFunction, $db, $user, $langs, $conf, $productId); + $parentKits = dynamicsprices_get_parent_kits($db, $productId); + foreach ($parentKits as $kitId) { + call_user_func($updateFunction, $db, $user, $langs, $conf, $kitId); + } + } + } + + // You can isolate code for each action in a separate method: this method should be named like the trigger in camelCase. + // For example : COMPANY_CREATE => public function companyCreate($action, $object, User $user, Translate $langs, Conf $conf) + $methodName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($action))))); + $callback = array($this, $methodName); + if (is_callable($callback)) { + dol_syslog( + "Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id + ); + + return call_user_func($callback, $action, $object, $user, $langs, $conf); + + + } + + // Or you can execute some code here + switch ($action) { // @phan-suppress-current-line PhanNoopSwitchCases + // Users + //case 'USER_CREATE': + //case 'USER_MODIFY': + //case 'USER_NEW_PASSWORD': + //case 'USER_ENABLEDISABLE': + //case 'USER_DELETE': + + // Actions + //case 'ACTION_MODIFY': + //case 'ACTION_CREATE': + //case 'ACTION_DELETE': + + // Groups + //case 'USERGROUP_CREATE': + //case 'USERGROUP_MODIFY': + //case 'USERGROUP_DELETE': + + // Companies + //case 'COMPANY_CREATE': + //case 'COMPANY_MODIFY': + //case 'COMPANY_DELETE': + + // Contacts + //case 'CONTACT_CREATE': + //case 'CONTACT_MODIFY': + //case 'CONTACT_DELETE': + //case 'CONTACT_ENABLEDISABLE': + + // Products + //case 'PRODUCT_CREATE': + //case 'PRODUCT_MODIFY': + //case 'PRODUCT_DELETE': + //case 'PRODUCT_PRICE_MODIFY': + //case 'PRODUCT_SET_MULTILANGS': + //case 'PRODUCT_DEL_MULTILANGS': + + //Stock movement + //case 'STOCK_MOVEMENT': + + //MYECMDIR + //case 'MYECMDIR_CREATE': + //case 'MYECMDIR_MODIFY': + //case 'MYECMDIR_DELETE': + + // Sales orders + //case 'ORDER_CREATE': + //case 'ORDER_MODIFY': + //case 'ORDER_VALIDATE': + //case 'ORDER_DELETE': + //case 'ORDER_CANCEL': + //case 'ORDER_SENTBYMAIL': + //case 'ORDER_CLASSIFY_BILLED': // TODO Replace it with ORDER_BILLED + //case 'ORDER_CLASSIFY_UNBILLED': // TODO Replace it with ORDER_UNBILLED + //case 'ORDER_SETDRAFT': + //case 'LINEORDER_INSERT': + //case 'LINEORDER_MODIFY': + //case 'LINEORDER_DELETE': + + // Supplier orders + //case 'ORDER_SUPPLIER_CREATE': + //case 'ORDER_SUPPLIER_MODIFY': + //case 'ORDER_SUPPLIER_VALIDATE': + //case 'ORDER_SUPPLIER_DELETE': + //case 'ORDER_SUPPLIER_APPROVE': + //case 'ORDER_SUPPLIER_CLASSIFY_BILLED': // TODO Replace with ORDER_SUPPLIER_BILLED + //case 'ORDER_SUPPLIER_CLASSIFY_UNBILLED': // TODO Replace with ORDER_SUPPLIER_UNBILLED + //case 'ORDER_SUPPLIER_REFUSE': + //case 'ORDER_SUPPLIER_CANCEL': + //case 'ORDER_SUPPLIER_SENTBYMAIL': + //case 'ORDER_SUPPLIER_RECEIVE': + //case 'LINEORDER_SUPPLIER_DISPATCH': + //case 'LINEORDER_SUPPLIER_CREATE': + //case 'LINEORDER_SUPPLIER_MODIFY': + //case 'LINEORDER_SUPPLIER_DELETE': + + // Proposals + //case 'PROPAL_CREATE': + //case 'PROPAL_MODIFY': + //case 'PROPAL_VALIDATE': + //case 'PROPAL_SENTBYMAIL': + //case 'PROPAL_CLASSIFY_BILLED': // TODO Replace it with PROPAL_BILLED + //case 'PROPAL_CLASSIFY_UNBILLED': // TODO Replace it with PROPAL_UNBILLED + //case 'PROPAL_CLOSE_SIGNED': + //case 'PROPAL_CLOSE_REFUSED': + //case 'PROPAL_DELETE': + //case 'LINEPROPAL_INSERT': + //case 'LINEPROPAL_MODIFY': + //case 'LINEPROPAL_DELETE': + + // SupplierProposal + //case 'SUPPLIER_PROPOSAL_CREATE': + //case 'SUPPLIER_PROPOSAL_MODIFY': + //case 'SUPPLIER_PROPOSAL_VALIDATE': + //case 'SUPPLIER_PROPOSAL_SENTBYMAIL': + //case 'SUPPLIER_PROPOSAL_CLOSE_SIGNED': + //case 'SUPPLIER_PROPOSAL_CLOSE_REFUSED': + //case 'SUPPLIER_PROPOSAL_DELETE': + //case 'LINESUPPLIER_PROPOSAL_INSERT': + //case 'LINESUPPLIER_PROPOSAL_MODIFY': + //case 'LINESUPPLIER_PROPOSAL_DELETE': + + // Contracts + //case 'CONTRACT_CREATE': + //case 'CONTRACT_MODIFY': + //case 'CONTRACT_ACTIVATE': + //case 'CONTRACT_CANCEL': + //case 'CONTRACT_CLOSE': + //case 'CONTRACT_DELETE': + //case 'LINECONTRACT_INSERT': + //case 'LINECONTRACT_MODIFY': + //case 'LINECONTRACT_DELETE': + + // Bills + //case 'BILL_CREATE': + //case 'BILL_MODIFY': + //case 'BILL_VALIDATE': + //case 'BILL_UNVALIDATE': + //case 'BILL_SENTBYMAIL': + //case 'BILL_CANCEL': + //case 'BILL_DELETE': + //case 'BILL_PAYED': + //case 'LINEBILL_INSERT': + //case 'LINEBILL_MODIFY': + //case 'LINEBILL_DELETE': + + // Recurring Bills + //case 'BILLREC_MODIFY': + //case 'BILLREC_DELETE': + //case 'BILLREC_AUTOCREATEBILL': + //case 'LINEBILLREC_MODIFY': + //case 'LINEBILLREC_DELETE': + + //Supplier Bill + //case 'BILL_SUPPLIER_CREATE': + //case 'BILL_SUPPLIER_MODIFY': + //case 'BILL_SUPPLIER_DELETE': + //case 'BILL_SUPPLIER_PAYED': + //case 'BILL_SUPPLIER_UNPAYED': + //case 'BILL_SUPPLIER_VALIDATE': + //case 'BILL_SUPPLIER_UNVALIDATE': + //case 'LINEBILL_SUPPLIER_CREATE': + //case 'LINEBILL_SUPPLIER_MODIFY': + //case 'LINEBILL_SUPPLIER_DELETE': + + // Payments + //case 'PAYMENT_CUSTOMER_CREATE': + //case 'PAYMENT_SUPPLIER_CREATE': + //case 'PAYMENT_ADD_TO_BANK': + //case 'PAYMENT_DELETE': + + // Online + //case 'PAYMENT_PAYBOX_OK': + //case 'PAYMENT_PAYPAL_OK': + //case 'PAYMENT_STRIPE_OK': + + // Donation + //case 'DON_CREATE': + //case 'DON_MODIFY': + //case 'DON_DELETE': + + // Interventions + //case 'FICHINTER_CREATE': + //case 'FICHINTER_MODIFY': + //case 'FICHINTER_VALIDATE': + //case 'FICHINTER_CLASSIFY_BILLED': // TODO Replace it with FICHINTER_BILLED + //case 'FICHINTER_CLASSIFY_UNBILLED': // TODO Replace it with FICHINTER_UNBILLED + //case 'FICHINTER_DELETE': + //case 'LINEFICHINTER_CREATE': + //case 'LINEFICHINTER_MODIFY': + //case 'LINEFICHINTER_DELETE': + + // Members + //case 'MEMBER_CREATE': + //case 'MEMBER_VALIDATE': + //case 'MEMBER_SUBSCRIPTION': + //case 'MEMBER_MODIFY': + //case 'MEMBER_NEW_PASSWORD': + //case 'MEMBER_RESILIATE': + //case 'MEMBER_DELETE': + + // Categories + //case 'CATEGORY_CREATE': + //case 'CATEGORY_MODIFY': + //case 'CATEGORY_DELETE': + //case 'CATEGORY_SET_MULTILANGS': + + // Projects + //case 'PROJECT_CREATE': + //case 'PROJECT_MODIFY': + //case 'PROJECT_DELETE': + + // Project tasks + //case 'TASK_CREATE': + //case 'TASK_MODIFY': + //case 'TASK_DELETE': + + // Task time spent + //case 'TASK_TIMESPENT_CREATE': + //case 'TASK_TIMESPENT_MODIFY': + //case 'TASK_TIMESPENT_DELETE': + //case 'PROJECT_ADD_CONTACT': + //case 'PROJECT_DELETE_CONTACT': + //case 'PROJECT_DELETE_RESOURCE': + + // Shipping + //case 'SHIPPING_CREATE': + //case 'SHIPPING_MODIFY': + //case 'SHIPPING_VALIDATE': + //case 'SHIPPING_SENTBYMAIL': + //case 'SHIPPING_BILLED': + //case 'SHIPPING_CLOSED': + //case 'SHIPPING_REOPEN': + //case 'SHIPPING_DELETE': + + // and more... + + default: + dol_syslog("Trigger '".$this->name."' for action '".$action."' launched by ".__FILE__.". id=".$object->id); + break; + } + + return 0; + } } diff --git a/langs/en_US/dynamicsprices.lang b/langs/en_US/dynamicsprices.lang index 3087700..c802fde 100644 --- a/langs/en_US/dynamicsprices.lang +++ b/langs/en_US/dynamicsprices.lang @@ -12,13 +12,14 @@ ModuleDynamicsPricesDesc = This module allows you to update sales prices based o #
# Admin page
#
-DynamicsPricesSetup = Dynamic sales price settings
-Settings = Settings
-DynamicsPricesSetupPage = Dynamic sales price module settings page
-LMDB_UpdateOptions = Sales price update options
-LMDB_COST_PRICE_ONLY = Update sales prices based on cost prices only. -LMDB_SUPPLIER_BUYPRICE_ALTERED = Update sales prices when creating/updating/deleting a purchase price, or when updating the cost price if cost-based calculation is enabled. -
+DynamicsPricesSetup = Dynamic sales price settings
+Settings = Settings
+DynamicsPricesSetupPage = Dynamic sales price module settings page
+LMDB_UpdateOptions = Sales price update options
+LMDB_KIT_PRICE_FROM_COMPONENTS = Calculate Kit prices from component prices
+LMDB_COST_PRICE_ONLY = Update sales prices based on cost prices only. +LMDB_SUPPLIER_BUYPRICE_ALTERED = Update sales prices when creating/updating/deleting a purchase price, or when updating the cost price if cost-based calculation is enabled. +
#
# About page
#
@@ -49,8 +50,11 @@ Pricelevel = Price level ID
Targetrate = Target margin rate (in %)
Minrate = Minimum margin rate (in %)

-LMDB_CommentAutoUpdateSellPrice = Sales prices are updated based on the price coefficients given in the dictionary and the average of the Supplier purchase unit prices.
-LMDB_LabelAutoUpdateSellPrice = Automatic update of sales prices
-LMDB_coefprice = Margin rates on sales prices
- -DynamicsPrices = Dynamic sales prices +LMDB_CommentAutoUpdateSellPrice = Sales prices are updated based on the price coefficients given in the dictionary and the average of the Supplier purchase unit prices.
+LMDB_LabelAutoUpdateSellPrice = Automatic update of sales prices
+LMDB_coefprice = Margin rates on sales prices
+LMDB_marginoncost = Margin rate on cost prices
+ +DynamicsPrices = Dynamic sales prices +LMDB_CodeNatureTooltipHelp = Product nature code +LMDB_MarginOnCostTooltipHelp = Margin rate applied on the average purchase cost price diff --git a/langs/fr_FR/dynamicsprices.lang b/langs/fr_FR/dynamicsprices.lang index e5c65e6..5320608 100644 --- a/langs/fr_FR/dynamicsprices.lang +++ b/langs/fr_FR/dynamicsprices.lang @@ -15,10 +15,11 @@ ModuleDynamicsPricesDesc = Ce module permet de mettre à jour les prix de vente DynamicsPricesSetup = Réglages des prix de vente dynamiques Settings = Réglages DynamicsPricesSetupPage = Page de réglage du module de Prix de vente dynamiques -LMDB_UpdateOptions=Options de mise à jour des prix de vente - -LMDB_COST_PRICE_ONLY = Ne mettre à jour les prix de vente que sur la base des prix de revient. -LMDB_SUPPLIER_BUYPRICE_ALTERED = Actualisation de prix de vente à la création/mise à jour/suppression d'un prix d'achat ou l'actualisation du prix de revient si le calcul sur le prix de revient est activé. +LMDB_UpdateOptions=Options de mise à jour des prix de vente +LMDB_KIT_PRICE_FROM_COMPONENTS = Calculer les prix des Kits à partir des composants + +LMDB_COST_PRICE_ONLY = Ne mettre à jour les prix de vente que sur la base des prix de revient. +LMDB_SUPPLIER_BUYPRICE_ALTERED = Actualisation de prix de vente à la création/mise à jour/suppression d'un prix d'achat ou l'actualisation du prix de revient si le calcul sur le prix de revient est activé. @@ -53,8 +54,11 @@ Targetrate = Taux de marge cible (en %) Minrate = Taux de marge minimum (en %) Entity = Entité -LMDB_CommentAutoUpdateSellPrice = Les prix de vente sont mis à jour en fonction des coefficients de prix donnés dans le dictionnaire, et de la moyenne des prix unitaires d'achats fournisseurs. -LMDB_LabelAutoUpdateSellPrice = Mise à jour automatique des prix de vente -LMDB_coefprice = Taux de marges sur les prix de vente -DynamicsPrices = Prix de vente dynamiques +LMDB_CommentAutoUpdateSellPrice = Les prix de vente sont mis à jour en fonction des coefficients de prix donnés dans le dictionnaire, et de la moyenne des prix unitaires d'achats fournisseurs. +LMDB_LabelAutoUpdateSellPrice = Mise à jour automatique des prix de vente +LMDB_coefprice = Taux de marges sur les prix de vente +LMDB_marginoncost = Taux de marge sur les prix de revient +DynamicsPrices = Prix de vente dynamiques +LMDB_CodeNatureTooltipHelp = Code de la nature de produit +LMDB_MarginOnCostTooltipHelp = Taux de marge appliqué sur le prix de revient moyen d'achat diff --git a/lib/dynamicsprices.lib.php b/lib/dynamicsprices.lib.php index dcfaae0..2daa7af 100644 --- a/lib/dynamicsprices.lib.php +++ b/lib/dynamicsprices.lib.php @@ -1,5 +1,5 @@ * * 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 @@ -86,343 +86,509 @@ function dynamicspricesAdminPrepareHead() } -function update_customer_prices_from_suppliers($db, $user, $langs, $conf, $productid = 0) +// Check if a product is a Kit using product associations +function dynamicsprices_is_kit($db, $productId) +{ + $sql = "SELECT COUNT(rowid) as nb"; + $sql .= " FROM ".MAIN_DB_PREFIX."product_association"; + $sql .= " WHERE fk_product_pere = ".((int) $productId); + //$sql .= " AND entity IN (".getEntity('product').")"; + //$sql .= " AND (type IS NULL OR type IN (0,1))"; + + $resql = $db->query($sql); + if ($resql === false) { + return false; + } + + $obj = $db->fetch_object($resql); + return (!empty($obj->nb) && (int) $obj->nb > 0); +} + +// Get Kit components with quantities +function dynamicsprices_get_kit_components($db, $productId) +{ + $components = array(); + + $sql = "SELECT fk_product_fils as fk_product, qty"; + $sql .= " FROM ".MAIN_DB_PREFIX."product_association"; + $sql .= " WHERE fk_product_pere = ".((int) $productId); + //$sql .= " AND entity IN (".getEntity('product').")"; + //$sql .= " AND (type IS NULL OR type IN (0,1))"; + + $resql = $db->query($sql); + if ($resql === false) { + return $components; + } + + while ($obj = $db->fetch_object($resql)) { + $components[] = array('id' => (int) $obj->fk_product, 'qty' => (float) $obj->qty); + } + + return $components; +} + +// Get parent Kits containing a component +function dynamicsprices_get_parent_kits($db, $productId) +{ + $parents = array(); + + $sql = "SELECT fk_product_pere as fk_parent"; + $sql .= " FROM ".MAIN_DB_PREFIX."product_association"; + $sql .= " WHERE fk_product_fils = ".((int) $productId); + //$sql .= " AND entity IN (".getEntity('product').")"; + //$sql .= " AND (type IS NULL OR type IN (0,1))"; + + $resql = $db->query($sql); + if ($resql === false) { + return $parents; + } + + while ($obj = $db->fetch_object($resql)) { + $parents[] = (int) $obj->fk_parent; + } + + return $parents; +} + +// Compute average supplier price +function dynamicsprices_get_average_supplier_price($db, $productId) +{ + $sql = "SELECT unitprice"; + $sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price"; + $sql .= " WHERE fk_product = ".((int) $productId); + $sql .= " AND entity IN (".getEntity('product_fournisseur_price').")"; + + $resql = $db->query($sql); + if ($resql === false) { + return null; + } + + $prices = array(); + while ($obj = $db->fetch_object($resql)) { + $prices[] = (float) $obj->unitprice; + } + + if (count($prices) === 0) { + return null; + } + + return array_sum($prices) / count($prices); +} + +// Get margin on cost percent for a nature +function dynamicsprices_get_margin_on_cost_percent($db, $natureId) +{ + $sql = "SELECT margin_on_cost_percent"; + $sql .= " FROM ".MAIN_DB_PREFIX."c_margin_on_cost"; + $sql .= " WHERE code_nature = '".$db->escape($natureId)."'"; + $sql .= " AND entity IN (".getEntity('entity').")"; + $sql .= " AND active = 1"; + $sql .= " ORDER BY rowid DESC"; + $sql .= " LIMIT 1"; + + $resql = $db->query($sql); + if ($resql === false) { + return 0; + } + + $obj = $db->fetch_object($resql); + return $obj ? (float) $obj->margin_on_cost_percent : 0; +} + +// Fetch selling price rules from dictionary +function dynamicsprices_get_price_rules($db, $natureId) +{ + $rules = array(); + + $sql = "SELECT pricelevel, minrate, targetrate"; + $sql .= " FROM ".MAIN_DB_PREFIX."c_coefprice"; + $sql .= " WHERE fk_nature = ".((int) $natureId); + $sql .= " AND entity IN (".getEntity('entity').")"; + $sql .= " AND active = 1"; + + $resql = $db->query($sql); + if ($resql === false) { + return $rules; + } + + while ($obj = $db->fetch_object($resql)) { + $rules[(int) $obj->pricelevel] = array('minrate' => (float) $obj->minrate, 'targetrate' => (float) $obj->targetrate); + } + + return $rules; +} + +// Save cost price on product table +function dynamicsprices_save_cost_price($db, $productId, $costPrice) +{ + $sql = "UPDATE ".MAIN_DB_PREFIX."product"; + $sql .= " SET cost_price = ".price2num($costPrice, 'MU'); + $sql .= " WHERE rowid = ".((int) $productId); + $sql .= " AND entity IN (".getEntity('product').")"; + + return $db->query($sql); +} + +// Calculate and persist Kit cost price based on components +function dynamicsprices_update_kit_cost_price($db, $productId) { - dol_include_once('/product/class/product.class.php'); - - global $conf; - - $products = []; - $nb_line = 0; - $entity = $conf->entity; - - if ($productid > 0) { - $products[] = $productid; - } else { - $sql = "SELECT rowid, finished"; - $sql.= " FROM ".MAIN_DB_PREFIX."product"; - $sql.= " WHERE tosell = 1 "; - $sql.= " AND entity IN (".getEntity('product').")"; - - //var_dump($sql.'
'); - - $resql = $db->query($sql); - if ($resql === false) { - dol_print_error($db); - return; + $components = dynamicsprices_get_kit_components($db, $productId); + $totalCost = 0; + + foreach ($components as $component) { + $avg = dynamicsprices_get_average_supplier_price($db, $component['id']); + $avg = ($avg === null) ? 0 : $avg; + $totalCost += $avg * (float) $component['qty']; + } + + dynamicsprices_save_cost_price($db, $productId, $totalCost); + + return $totalCost; +} + +// Update selling prices from a base cost and rules +function dynamicsprices_update_prices_from_base($db, $user, $product, $basePrice, $rules, $tvaTx, $entity) +{ + $nb_line = 0; + $now = $db->idate(dol_now()); + + foreach ($rules as $level => $rule) { + $price = $basePrice * (1 + ((float) $rule['targetrate'] / 100)); + $price_ttc = $price * (1 + ((float) $tvaTx / 100)); + $price_min = $basePrice * (1 + ((float) $rule['minrate'] / 100)); + $price_min_ttc = $price_min * (1 + ((float) $tvaTx / 100)); + $current = dynamicsprices_get_latest_price_for_level($db, $product->id, $level); + +if (!$current || price2num($current->price, 2) != price2num($price, 2) || price2num($current->price_min, 2) != price2num($price_min, 2) || price2num($current->price_ttc, 2) != price2num($price_ttc, 2) || price2num($current->price_min_ttc, 2) != price2num($price_min_ttc, 2)) { +$sqlp = "INSERT INTO ".MAIN_DB_PREFIX."product_price (entity, fk_product, price_level, fk_user_author, price, price_ttc, price_min, price_min_ttc, date_price, tva_tx)"; +$sqlp .= " VALUES (".((int) $entity).", ".((int) $product->id).", ".((int) $level).", ".((int) $user->id).", ".price2num($price, 2).", ".price2num($price_ttc, 2).", ".price2num($price_min, 2).", ".price2num($price_min_ttc, 2).", '".$now."', ".((float) $tvaTx).")"; +$sqlp .= " ON DUPLICATE KEY UPDATE price = VALUES(price), price_ttc = VALUES(price_ttc), price_min = VALUES(price_min), price_min_ttc = VALUES(price_min_ttc), date_price = VALUES(date_price), tva_tx = VALUES(tva_tx)"; + $db->query($sqlp); + $nb_line++; } + } - while ($obj = $db->fetch_object($resql)) { - $products[] = array('id'=>$obj->rowid, 'nature'=>$obj->finished); - } - } + return $nb_line; +} + +// Fetch latest component prices per level +function dynamicsprices_get_component_prices_by_level($db, $productId) +{ + $prices = array(); + + $sql = "SELECT price_level, price, price_min"; + $sql .= " FROM ".MAIN_DB_PREFIX."product_price"; + $sql .= " WHERE fk_product = ".((int) $productId); + $sql .= " AND entity IN (".getEntity('productprice').")"; + $sql .= " ORDER BY date_price DESC"; + + $resql = $db->query($sql); + if ($resql === false) { + return $prices; + } + + while ($obj = $db->fetch_object($resql)) { + $level = (int) $obj->price_level; + if (!isset($prices[$level])) { + $prices[$level] = array('price' => (float) $obj->price, 'price_min' => (float) $obj->price_min); + } + } - foreach ($products as $prod) { - $prodid = is_array($prod) ? $prod['id'] : $prod; - //var_dump('ID Produit = '.$prodid.'
'); - $natureid = is_array($prod) ? $prod['nature'] : 0; - //var_dump('Nature = '.$natureid.'
'); - $product = new Product($db); - $product->fetch($prodid); - - $tva_tx = (float) $product->tva_tx; - - //var_dump('$tva_tx = '.price($tva_tx).'
'); - - // Prix fournisseurs - $sqlf = "SELECT price FROM ".MAIN_DB_PREFIX."product_fournisseur_price - WHERE fk_product = ".((int) $prodid); - $sqlf.= " AND entity IN (".getEntity('product_fournisseur_price').")"; - - //var_dump('$sqfl = '.$sqlf.'
'); - - $resqlf = $db->query($sqlf); - - $prices_fourn = []; - while ($objf = $db->fetch_object($resqlf)) { - $prices_fourn[] = (float) $objf->price; - } - - if (!count($prices_fourn)) continue; - - $moyenne = array_sum($prices_fourn) / count($prices_fourn); - - //var_dump('moyenne = '.$moyenne.'
'); - - // Coefficients par nature - $sqlc = "SELECT code, pricelevel, minrate, targetrate - FROM ".MAIN_DB_PREFIX."c_coefprice - WHERE fk_nature = ".((int) $natureid); - $sqlc.= " AND entity IN (".getEntity('entity').")"; - - //var_dump('$sqlc = '.$sqlc.'
'); - - $resqlc = $db->query($sqlc); - - while ($objc = $db->fetch_object($resqlc)) { - $level = (int) $objc->pricelevel; - //var_dump('$level = '.$level.'
'); - $minrate = (float) $objc->minrate; - $targetrate = (float) $objc->targetrate; - - $price = $moyenne * (1 + $targetrate/100); - - //var_dump('$price = '.price($price).'
'); - - $price_ttc = $price * (1 + $tva_tx/100); - - //var_dump('$price_ttc = '.price($price_ttc).'
'); - - $price_min = $moyenne * (1 + $minrate/100); - - //var_dump('$price_min = '.price($price_min).'
'); - - $price_min_ttc = $price_min * (1 + $tva_tx/100); - - //var_dump('$price_min_ttc = '.price($price_min_ttc).'
'); - - $now = $db->idate(dol_now()); - - $sqlv = "SELECT price_level, price, price_ttc, price_min, price_min_ttc, tva_tx "; - $sqlv.= " FROM ".MAIN_DB_PREFIX."product_price"; - $sqlv.= " WHERE fk_product = ".((int) $prodid) ; - $sqlv.= " AND price_level = ".$level; - $sqlv.= " AND entity IN (".getEntity('productprice').")"; - $sqlv.= " ORDER BY date_price DESC LIMIT 1"; - //var_dump('$sqlv = '.$sqlv.'
'); - - $resqlv = $db->query($sqlv); - - while ($objv = $db->fetch_object($resqlv)) { - $price_v = price2num($objv->price,2); - $price_ttc_v = price2num($objv->price_ttc,2); - $price_min_v = price2num($objv->price_min,2); - $price_min_ttc_v = price2num($objv->price_min_ttc,2); - //$tva_tx_v = $objv->tva_tx; - - if (price2num($price,2)!=$price_v || price2num($price_min,2)!=$price_min_v || price2num($price_ttc,2)!=$price_ttc_v || price2num($price_min_ttc,2)!=$price_min_ttc_v) { - $sqlp = "INSERT INTO ".MAIN_DB_PREFIX."product_price - (entity, fk_product, price_level, fk_user_author, price, price_ttc, price_min, price_min_ttc, date_price, tva_tx) - VALUES (".((int )$entity).", - ".((int) $prodid).", - ".$level.", - ".$user->id.", - ".price2num($price,2).", - ".price2num($price_ttc,2).", - ".price2num($price_min,2).", - ".price2num($price_min_ttc,2).", - '".$now."', - ".((float) $tva_tx).") - ON DUPLICATE KEY UPDATE - price = VALUES(price), - price_ttc = VALUES(price_ttc), - price_min = VALUES(price_min), - price_min_ttc = VALUES(price_min_ttc), - date_price = VALUES(date_price), - tva_tx = VALUES(tva_tx)"; - $db->query($sqlp); - - $nb_line++ ; - //var_dump('$nb_line = '.$nb_line.'
'); - } - //var_dump('$nb_line2 = '.$nb_line.'
'); - } - //var_dump('$nb_line3 = '.$nb_line.'
'); - } - } - //var_dump('$nb_line4 = '.$nb_line.'
'); - - return $nb_line; + return $prices; } -function update_customer_prices_from_cost_price($db, $user, $langs, $conf, $productid = 0) +// Get latest price line for a product and level +function dynamicsprices_get_latest_price_for_level($db, $productId, $level) { - dol_include_once('/product/class/product.class.php'); - - global $conf; - - $products = []; - $nb_line = 0; - $entity = $conf->entity; - - if ($productid > 0) { - $products[] = $productid; - } else { - $sql = "SELECT rowid, finished, cost_price"; - $sql.= " FROM ".MAIN_DB_PREFIX."product"; - $sql.= " WHERE tosell = 1 "; - $sql.= " AND entity IN (".getEntity('product').")"; - - //var_dump($sql.'
'); - - $resql = $db->query($sql); - if ($resql === false) { - dol_print_error($db); - return; - } - - while ($obj = $db->fetch_object($resql)) { - $products[] = array('id'=>$obj->rowid, 'nature'=>$obj->finished, 'cost_price'=>$obj->cost_price); - } - } - - foreach ($products as $prod) { - $prodid = is_array($prod) ? $prod['id'] : $prod; - //var_dump('ID Produit = '.$prodid.'
'); - $natureid = is_array($prod) ? $prod['nature'] : 0; - //var_dump('Nature = '.$natureid.'
'); - $cost = is_array($prod) ? $prod['cost_price'] : 0; - //var_dump('Prix de Revient = '.$cost.'
'); - $product = new Product($db); - $product->fetch($prodid); - - $tva_tx = (float) $product->tva_tx; - - // Coefficients par nature - $sqlc = "SELECT code, pricelevel, minrate, targetrate - FROM ".MAIN_DB_PREFIX."c_coefprice - WHERE fk_nature = ".((int) $natureid); - $sqlc.= " AND entity IN (".getEntity('entity').")"; - - //var_dump('$sqlc = '.$sqlc.'
'); - - $resqlc = $db->query($sqlc); - - ////var_dump($resqlc.'
'); - - while ($objc = $db->fetch_object($resqlc)) { - $level = (int) $objc->pricelevel; - //var_dump('$level = '.$level.'
'); - $minrate = (float) $objc->minrate; - $targetrate = (float) $objc->targetrate; - - $price = $cost * (1 + $targetrate/100); - - //var_dump('$price = '.price($price).'
'); - - $price_ttc = $price * (1 + $tva_tx/100); - - //var_dump('$price_ttc = '.price($price_ttc).'
'); - - $price_min = $cost * (1 + $minrate/100); - - //var_dump('$price_min = '.price($price_min).'
'); - - $price_min_ttc = $price_min * (1 + $tva_tx/100); - - //var_dump('$price_min_ttc = '.price($price_min_ttc).'
'); - - $now = $db->idate(dol_now()); - - $sqlv = "SELECT price_level, price, price_ttc, price_min, price_min_ttc, tva_tx "; - $sqlv.= " FROM ".MAIN_DB_PREFIX."product_price"; - $sqlv.= " WHERE fk_product = ".((int) $prodid) ; - $sqlv.= " AND price_level = ".$level; - $sqlv.= " AND entity IN (".getEntity('productprice').")"; - $sqlv.= " ORDER BY date_price DESC LIMIT 1"; - //var_dump('$sqlv = '.$sqlv.'
'); - - $resqlv = $db->query($sqlv); - - while ($objv = $db->fetch_object($resqlv)) { - $price_v = price2num($objv->price,2); - $price_ttc_v = price2num($objv->price_ttc,2); - $price_min_v = price2num($objv->price_min,2); - $price_min_ttc_v = price2num($objv->price_min_ttc,2); - //$tva_tx_v = $objv->tva_tx; - - if (price2num($price,2)!=$price_v || price2num($price_min,2)!=$price_min_v || price2num($price_ttc,2)!=$price_ttc_v || price2num($price_min_ttc,2)!=$price_min_ttc_v) { - $sqlp = "INSERT INTO ".MAIN_DB_PREFIX."product_price - (entity, fk_product, price_level, fk_user_author, price, price_ttc, price_min, price_min_ttc, date_price, tva_tx) - VALUES (".$entity.", - ".((int) $prodid).", - ".$level.", - ".$user->id.", - ".price2num($price,2).", - ".price2num($price_ttc,2).", - ".price2num($price_min,2).", - ".price2num($price_min_ttc,2).", - '".$now."', - ".((float) $tva_tx).") - ON DUPLICATE KEY UPDATE - price = VALUES(price), - price_ttc = VALUES(price_ttc), - price_min = VALUES(price_min), - price_min_ttc = VALUES(price_min_ttc), - date_price = VALUES(date_price), - tva_tx = VALUES(tva_tx)"; - $db->query($sqlp); - - $nb_line++ ; - } - } - } - } - - return $nb_line; + $sql = "SELECT price, price_ttc, price_min, price_min_ttc"; + $sql .= " FROM ".MAIN_DB_PREFIX."product_price"; + $sql .= " WHERE fk_product = ".((int) $productId); + $sql .= " AND price_level = ".((int) $level); + $sql .= " AND entity IN (".getEntity('productprice').")"; + $sql .= " ORDER BY date_price DESC, rowid DESC"; + $sql .= " LIMIT 1"; + + $resql = $db->query($sql); + if ($resql === false) { + return null; + } + + return $db->fetch_object($resql); } +// Update Kit prices by summing component prices +function dynamicsprices_update_kit_prices_from_components($db, $user, $product, $components, $tvaTx, $entity) +{ + $levelTotals = array(); + $nb_line = 0; + $now = $db->idate(dol_now()); + + foreach ($components as $component) { + $componentPrices = dynamicsprices_get_component_prices_by_level($db, $component['id']); + foreach ($componentPrices as $level => $values) { + if (!isset($levelTotals[$level])) { + $levelTotals[$level] = array('price' => 0, 'price_min' => 0); + } + $levelTotals[$level]['price'] += ((float) $values['price']) * (float) $component['qty']; + $levelTotals[$level]['price_min'] += ((float) $values['price_min']) * (float) $component['qty']; + } + } -/** - * Display title - * @param string $title - */ -function setup_print_title($title="Parameter", $width = 300) + foreach ($levelTotals as $level => $values) { + $price = (float) $values['price']; + $price_min = (float) $values['price_min']; + $price_ttc = $price * (1 + ((float) $tvaTx / 100)); + $price_min_ttc = $price_min * (1 + ((float) $tvaTx / 100)); + $current = dynamicsprices_get_latest_price_for_level($db, $product->id, $level); + +if (!$current || price2num($current->price, 2) != price2num($price, 2) || price2num($current->price_min, 2) != price2num($price_min, 2) || price2num($current->price_ttc, 2) != price2num($price_ttc, 2) || price2num($current->price_min_ttc, 2) != price2num($price_min_ttc, 2)) { +$sqlp = "INSERT INTO ".MAIN_DB_PREFIX."product_price (entity, fk_product, price_level, fk_user_author, price, price_ttc, price_min, price_min_ttc, date_price, tva_tx)"; +$sqlp .= " VALUES (".((int) $entity).", ".((int) $product->id).", ".((int) $level).", ".((int) $user->id).", ".price2num($price, 2).", ".price2num($price_ttc, 2).", ".price2num($price_min, 2).", ".price2num($price_min_ttc, 2).", '".$now."', ".((float) $tvaTx).")"; +$sqlp .= " ON DUPLICATE KEY UPDATE price = VALUES(price), price_ttc = VALUES(price_ttc), price_min = VALUES(price_min), price_min_ttc = VALUES(price_min_ttc), date_price = VALUES(date_price), tva_tx = VALUES(tva_tx)"; + $db->query($sqlp); + $nb_line++; + } + } + + return $nb_line; +} + +function update_customer_prices_from_suppliers($db, $user, $langs, $conf, $productid = 0) { - global $langs; - print ''; - print ''.$langs->trans($title) . ''; - print ' '; - print ''.$langs->trans('Value').''; - print ''; + + + + + dol_include_once('/product/class/product.class.php'); + + global $conf; + + $products = array(); + $kits = array(); + $nb_line = 0; + $entity = $conf->entity; + + if ($productid > 0) { + $product = new Product($db); + if ($product->fetch($productid) > 0 && (int) $product->type !== Product::TYPE_PRODUCT) { + return 0; + } + $products[] = $productid; + } else { + $sql = "SELECT rowid, finished"; + $sql .= " FROM ".MAIN_DB_PREFIX."product"; + $sql .= " WHERE tosell = 1"; + $sql .= " AND fk_product_type = 0"; + $sql .= " AND entity IN (".getEntity('product').")"; + + $resql = $db->query($sql); + if ($resql === false) { + dol_print_error($db); + return 0; + } + + while ($obj = $db->fetch_object($resql)) { + $products[] = array('id' => $obj->rowid, 'nature' => $obj->finished); + } + } + + foreach ($products as $prod) { + $prodid = is_array($prod) ? $prod['id'] : $prod; + $natureid = is_array($prod) ? $prod['nature'] : 0; + $product = new Product($db); + $product->fetch($prodid); + if ((int) $product->type !== Product::TYPE_PRODUCT) { + continue; + } + $tva_tx = (float) $product->tva_tx; + + if (dynamicsprices_is_kit($db, $prodid)) { + $kits[] = array('id' => $prodid, 'nature' => $natureid, 'tva' => $tva_tx); + continue; + } + + $avgPrice = dynamicsprices_get_average_supplier_price($db, $prodid); + if ($avgPrice === null) { + continue; + } + + $marginPercent = dynamicsprices_get_margin_on_cost_percent($db, $natureid); + $costPrice = $avgPrice * (1 + ($marginPercent / 100)); + dynamicsprices_save_cost_price($db, $prodid, $costPrice); + + $rules = dynamicsprices_get_price_rules($db, $natureid); + $nb_line += dynamicsprices_update_prices_from_base($db, $user, $product, $avgPrice, $rules, $tva_tx, $entity); + } + + foreach ($kits as $kit) { + $prodid = $kit['id']; + $natureid = $kit['nature']; + $tva_tx = $kit['tva']; + $product = new Product($db); + $product->fetch($prodid); + + $costPrice = dynamicsprices_update_kit_cost_price($db, $prodid); + $rules = dynamicsprices_get_price_rules($db, $natureid); + if (getDolGlobalInt('LMDB_KIT_PRICE_FROM_COMPONENTS')) { + $components = dynamicsprices_get_kit_components($db, $prodid); + $nb_line += dynamicsprices_update_kit_prices_from_components($db, $user, $product, $components, $tva_tx, $entity); + } else { + $nb_line += dynamicsprices_update_prices_from_base($db, $user, $product, $costPrice, $rules, $tva_tx, $entity); + } + } + + return $nb_line; + + + + +} + +function update_customer_prices_from_cost_price($db, $user, $langs, $conf, $productid = 0) +{ + + + + + dol_include_once('/product/class/product.class.php'); + + global $conf; + + $products = array(); + $kits = array(); + $nb_line = 0; + $entity = $conf->entity; + + if ($productid > 0) { + $product = new Product($db); + if ($product->fetch($productid) > 0 && (int) $product->type !== Product::TYPE_PRODUCT) { + return 0; + } + $products[] = $productid; + } else { + $sql = "SELECT rowid, finished, cost_price"; + $sql .= " FROM ".MAIN_DB_PREFIX."product"; + $sql .= " WHERE tosell = 1"; + $sql .= " AND fk_product_type = 0"; + $sql .= " AND entity IN (".getEntity('product').")"; + + $resql = $db->query($sql); + if ($resql === false) { + dol_print_error($db); + return 0; + } + + while ($obj = $db->fetch_object($resql)) { + $products[] = array('id' => $obj->rowid, 'nature' => $obj->finished, 'cost_price' => $obj->cost_price); + } + } + + foreach ($products as $prod) { + $prodid = is_array($prod) ? $prod['id'] : $prod; + $natureid = is_array($prod) ? $prod['nature'] : 0; + $currentCost = is_array($prod) ? $prod['cost_price'] : 0; + $product = new Product($db); + $product->fetch($prodid); + if ((int) $product->type !== Product::TYPE_PRODUCT) { + continue; + } + $tva_tx = (float) $product->tva_tx; + + if (dynamicsprices_is_kit($db, $prodid)) { + $kits[] = array('id' => $prodid, 'nature' => $natureid, 'tva' => $tva_tx); + continue; + } + + $avgPrice = dynamicsprices_get_average_supplier_price($db, $prodid); + if ($avgPrice !== null) { + $marginPercent = dynamicsprices_get_margin_on_cost_percent($db, $natureid); + $currentCost = $avgPrice * (1 + ($marginPercent / 100)); + dynamicsprices_save_cost_price($db, $prodid, $currentCost); + } + + $rules = dynamicsprices_get_price_rules($db, $natureid); + $nb_line += dynamicsprices_update_prices_from_base($db, $user, $product, $currentCost, $rules, $tva_tx, $entity); + } + + foreach ($kits as $kit) { + $prodid = $kit['id']; + $natureid = $kit['nature']; + $tva_tx = $kit['tva']; + $product = new Product($db); + $product->fetch($prodid); + + $costPrice = dynamicsprices_update_kit_cost_price($db, $prodid); + $rules = dynamicsprices_get_price_rules($db, $natureid); + if (getDolGlobalInt('LMDB_KIT_PRICE_FROM_COMPONENTS')) { + $components = dynamicsprices_get_kit_components($db, $prodid); + $nb_line += dynamicsprices_update_kit_prices_from_components($db, $user, $product, $components, $tva_tx, $entity); + } else { + $nb_line += dynamicsprices_update_prices_from_base($db, $user, $product, $costPrice, $rules, $tva_tx, $entity); + } + } + + return $nb_line; + + + + + } + + /** + * Print a table section title. + * + * @param string $title Title key to translate + * @param int $width Width of the column + * @return void + */ + function setup_print_title($title = "Parameter", $width = 300) + { + global $langs; + + print ''; + print ''.$langs->trans($title).''; + print ' '; + print ''.$langs->trans('Value').''; + print ''; } -/** - * yes / no select - * @param string $confkey - * @param string $title - * @param string $desc - * @param $ajaxConstantOnOffInput will be send to ajax_constantonoff() input param - * - * exemple _print_on_off('CONSTNAME', 'ParamLabel' , 'ParamDesc'); - */ function setup_print_on_off($confkey, $title = false, $desc ='', $help = false, $width = 300, $forcereload = false, $ajaxConstantOnOffInput = array()) { - global $var, $bc, $langs, $conf, $form; - $var=!$var; + global $var, $bc, $langs, $conf, $form; + $var=!$var; - print ''; - print ''; + print ''; + print ''; if(empty($help) && !empty($langs->tab_translate[$confkey . '_HELP'])){ $help = $confkey . '_HELP'; } - if(!empty($help)){ - print $form->textwithtooltip( ($title?$title:$langs->trans($confkey)) , $langs->trans($help),2,1,img_help(1,'')); - } - else { - print $title?$title:$langs->trans($confkey); - } - - if(!empty($desc)) - { - print '
'.$langs->trans($desc).''; - } - print ''; - print ' '; - print ''; - - if($forcereload){ - $link = $_SERVER['PHP_SELF'].'?action=set_'.$confkey.'&token='. newToken() .'&'.$confkey.'='.intval((empty($conf->global->{$confkey}))); - $toggleClass = empty($conf->global->{$confkey})?'fa-toggle-off':'fa-toggle-on font-status4'; - print ''; - } - else{ - print ajax_constantonoff($confkey, $ajaxConstantOnOffInput); - } - print ''; + if(!empty($help)){ + print $form->textwithtooltip( ($title?$title:$langs->trans($confkey)) , $langs->trans($help),2,1,img_help(1,'')); + } + else { + print $title?$title:$langs->trans($confkey); + } + + if(!empty($desc)) + { + print '
'.$langs->trans($desc).''; + } + print ''; + print ' '; + print ''; + + if($forcereload){ + $link = $_SERVER['PHP_SELF'].'?action=set_'.$confkey.'&token='. newToken() .'&'.$confkey.'='.intval((empty($conf->global->{$confkey}))); + $toggleClass = empty($conf->global->{$confkey})?'fa-toggle-off':'fa-toggle-on font-status4'; + print ''; + } + else{ + print ajax_constantonoff($confkey, $ajaxConstantOnOffInput); + } + print ''; } /** @@ -437,53 +603,53 @@ function setup_print_on_off($confkey, $title = false, $desc ='', $help = false, */ function setup_print_input_form_part($confkey, $title = false, $desc ='', $metas = array(), $type='input', $help = false, $width = 300) { - global $var, $bc, $langs, $conf, $db; - $var=!$var; + global $var, $bc, $langs, $conf, $db; + $var=!$var; if(empty($help) && !empty($langs->tab_translate[$confkey . '_HELP'])){ $help = $confkey . '_HELP'; } - $form=new Form($db); + $form=new Form($db); - $defaultMetas = array( - 'name' => $confkey - ); + $defaultMetas = array( + 'name' => $confkey + ); - if($type!='textarea'){ - $defaultMetas['type'] = 'text'; - $defaultMetas['value'] = isset($conf->global->{$confkey}) ? $conf->global->{$confkey} : ''; - } + if($type!='textarea'){ + $defaultMetas['type'] = 'text'; + $defaultMetas['value'] = isset($conf->global->{$confkey}) ? $conf->global->{$confkey} : ''; + } - $metas = array_merge ($defaultMetas, $metas); - $metascompil = ''; - foreach ($metas as $key => $values) - { - $metascompil .= ' '.$key.'="'.$values.'" '; - } + $metas = array_merge ($defaultMetas, $metas); + $metascompil = ''; + foreach ($metas as $key => $values) + { + $metascompil .= ' '.$key.'="'.$values.'" '; + } - print ''; - print ''; + print ''; + print ''; - if(!empty($help)){ - print $form->textwithtooltip( ($title?$title:$langs->trans($confkey)) , $langs->trans($help),2,1,img_help(1,'')); - } - else { - print $title?$title:$langs->trans($confkey); - } + if(!empty($help)){ + print $form->textwithtooltip( ($title?$title:$langs->trans($confkey)) , $langs->trans($help),2,1,img_help(1,'')); + } + else { + print $title?$title:$langs->trans($confkey); + } - if(!empty($desc)) - { - print '
'.$langs->trans($desc).''; - } + if(!empty($desc)) + { + print '
'.$langs->trans($desc).''; + } - print ''; - print ' '; - print ''; - print '
'; - print ''; - print ''; + print ''; + print ' '; + print ''; + print ''; + print ''; + print ''; if($type=='textarea'){ print ''; @@ -496,7 +662,7 @@ function setup_print_input_form_part($confkey, $title = false, $desc ='', $metas print $type; } - print ''; - print '
'; - print ''; + print ''; + print ''; + print ''; } diff --git a/sql/dolibarr_allversions.sql b/sql/dolibarr_allversions.sql index 98cf761..96dfd7b 100644 --- a/sql/dolibarr_allversions.sql +++ b/sql/dolibarr_allversions.sql @@ -1,13 +1,25 @@ -- -- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version. -- -CREATE TABLE IF NOT EXISTS `llx_c_coefprice`( - `rowid` int(11) AUTO_INCREMENT, - `code` VARCHAR(255) NOT NULL UNIQUE, - `label` VARCHAR(255) NOT NULL, - `targetrate` FLOAT(24.8) NOT NULL, - `minrate` FLOAT(24.8) NOT NULL, - `active` TINYINT(4) NOT NULL DEFAULT 1, +CREATE TABLE IF NOT EXISTS `llx_c_coefprice`( +`rowid`int(11) AUTO_INCREMENT, +`code`VARCHAR(255) NOT NULL UNIQUE, +`label`VARCHAR(255) NOT NULL, +`targetrate`FLOAT(24.8) NOT NULL, +`minrate`FLOAT(24.8) NOT NULL, +`active`TINYINT(4)NOT NULL DEFAULT 1, - PRIMARY KEY (`rowid`) -)ENGINE=innodb DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; \ No newline at end of file +PRIMARY KEY (`rowid`) +)ENGINE=innodb DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + +CREATE TABLE IF NOT EXISTS `llx_c_margin_on_cost`( +`rowid`INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, +`entity`INTEGER NOT NULL DEFAULT '1', +`code`VARCHAR(50) NOT NULL, +`code_nature`VARCHAR(10) DEFAULT NULL, +`datec`DATETIME NULL, +`tms`TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +`margin_on_cost_percent`FLOAT NOT NULL DEFAULT '0', +`import_key`VARCHAR(14) NULL, +`active`TINYINT NOT NULL DEFAULT '1' +)ENGINE=innodb DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; diff --git a/sql/llx_c_margin_on_cost.sql b/sql/llx_c_margin_on_cost.sql new file mode 100644 index 0000000..b544ad8 --- /dev/null +++ b/sql/llx_c_margin_on_cost.sql @@ -0,0 +1,14 @@ +-- +-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version. +-- +CREATE TABLE IF NOT EXISTS `llx_c_margin_on_cost`( +`rowid` integer AUTO_INCREMENT PRIMARY KEY NOT NULL, +`entity` integer NOT NULL DEFAULT '1', +`code` varchar(50) NOT NULL, +`code_nature` varchar(10) DEFAULT NULL, +`datec` datetime NULL, +`tms` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +`margin_on_cost_percent` float NOT NULL DEFAULT '0', +`import_key` varchar(14) NULL, +`active` tinyint NOT NULL DEFAULT '1' +)ENGINE=innodb;