Magento: Bug With Editing Date & Time, Time Custom Options

Using Magento 1.5.0.1, I noticed an interesting bug this afternoon. If you have setup any Date & Time or Time custom attributes, and have made them NOT required, here’s something you may notice:

When initially adding the product to the cart, and you leave the Date & Time or Time custom options NOT filled out, your product will be added to the cart just as you would expect. The untouched custom options are not applied to the product in your cart. However, in the rare case where a user is clicking the “Edit” link in the cart to edit it, and you still leave those custom options untouched, the validation will bark at you saying “Field is not complete” on those custom options. The only way to edit that product then is to put values in for those fields. Magento updates the product as it should, but you now have those other custom options in play, which could screw things up.

I have submitted the bug to Magento: http://www.magentocommerce.com/bug-tracking/issue?issue=12183

Posted in Magento | Leave a comment

Disappointed with Magento Module Sales/Distribution

Magento, from the beginning, has offered a system of packaging extensions, and installing those packages. It was a smart move, and one that I really appreciated. There are a lot of developers out there who properly package their extensions (everything that you can get directly off Magento Connect is packaged). However, there are still loads of developers who do not package their extensions. This is especially true with developers who sell their extensions.

One particular developer that I’d like to point out my frustration with is AITOC. Before I get started though, they ARE talented developers, and they make good functioning modules. However, I can’t stand their pricing and distributing philosophies.

Most of their modules are based off of how many enabled products you have in your catalog, how many stores/websites you have setup in Magento, or how many admin accounts you have setup. Let’s say you want to use one of their modules for 50 of your products, but you have 10,000 products in your catalog. Or, you want to use one of their modules for one of your websites, but you have 10 websites setup in Magento. With both of these scenarios, prepare to spend big bucks. You can easily go from a $99 module, to a $599 module. I hate that.

Also, let’s say you have 950 products in your catalog, and you buy a module from them and you get the license that allows up to 1000 products. When you hit 1001, the functionality of that module will disable itself.

Now to their distribution. When you buy one of their modules, prepare for a headache! They are loaded and bloated full of files. It’s a mess. You cannot install their module with a simple command line. Instead, you need to upload a ton of files, set special permissions, and then walk through a somewhat complicated installation process. There’s even a module in there just to install/manage the module you want to install, loading up your Magento install with over 100 additional files. They override a few Core Magento files and send out a web service request, checking your license to make sure it’s valid before it will fully install your module. Almost every single file in their module is wrapped in a PHP call that checks to make sure you have properly installed it, and that you are not exceeding the license rule (# of products, stores, admin accounts, etc.).

They make it a headache to install, a headache to maintain, and a nightmare to fully uninstall and remove all the files. If they properly packaged their extension, it would be cakewalk to install, update, and uninstall.

Why do they do it? It’s simple. As for their pricing, they want to make more money off you. They tease you with a price, but many people will not pay the base price for their modules. Why do they not package their extension? They want more control over you and your store, and more control ensuring people aren’t abusing their license (because of the dumb limitations they enforce) or re-using it over and over, or passing it along to other people to use.

They are smart in that regard, but when it become a total pain for the end-user, you should re-think the process. I think they need to ditch the limitations. And, when it comes to the modules, properly package them up so that they can be installed with one command or using Magneto Connect. There are other ways of protecting a module from being re-used or re-distributed like IonCube that are far less of a headache to deal with.

So, if you are a Magento developer distributing or selling modules, please, make them into a proper Magento extension. It doesn’t take long, it’s not hard, and it works way, way better. And, don’t put annoying limitations on them like AITOC does.

Posted in Magento | 40 Comments

Magento: Revised Multiple Image Import Module (1.5 +)

Back in 2009, I made this post about my first multiple image import module. It was quite popular, and worked all the way up through Magento 1.4.x. With the release of 1.5, it no longer worked due to core changes Magento made.

We finally ran into a client who needed this functionality on 1.5, so I have re-factored the module, and added a couple new features. Instead of just packaging up the module though, like last time, I will walk you through how to create it yourself (so that hopefully some of you will learn a little more about how to create modules).

Note: This will not work on Magento 1.4.x and lower. For a version that works on 1.4.x and lower (which is a little more basic too), please see this post.

What does this module do?

  • Imports multiple images (of course) in addition to the 3 main product images (image, small_image, thumbnail)
  • Allows you to enable or disable (in the system config) excluding any main product images (image, small_image, thumbnail) from showing up in the gallery (the exclude checkbox)
  • Allows you to specify the column name for your multiple images in the system config

How it works: When you do a product import, all you have to do is add a new column to your CSV with the header of your choice (which you can change in the system config under Catalog > Catalog > Import Image Options – By default it’s ‘additional_images’). In that column, you can have as many images as you’d like. Simply separate the image names with a semi-colon: ‘image1.jpg;image2.jpg;image3.jpg’. Just be sure you’ve put the images in /media/import/ like you would normally do.

Enough talk, let’s get to building the module. In my example, I’m using the ‘Prattski’ namespace for my module. You can certainly change that if you would like, just make sure to change it in ever place you see it.

Step 1

Create the file ‘/app/etc/modules/Prattski_ImageImport.xml’. This file tells Magento that your module exists, if it’s enabled or not, and where to find it.

<?xml version="1.0"?>
<config>
    <modules>
        <Prattski_ImageImport>
            <active>true</active>
            <codePool>local</codePool>
        </Prattski_ImageImport>
    </modules>
</config>

Step 2

Now we create our module directory. Create ‘/app/code/local/Prattski/ImageImport/’. This is where our module will reside. Inside that directory, create this file: ‘etc/config.xml’. This file does a number of things: It tells Magento which version your module is, defines your model namespace and directory, rewrites a core model with your own, defines your helper namespace and directory, and sets default values in the system config.

<?xml version="1.0"?>
<config>
    <modules>
        <Prattski_ImageImport>
            <version>0.1.0</version>
        </Prattski_ImageImport>
    </modules>
    <global>
        <models>
            <imageimport>
                <class>Prattski_ImageImport_Model</class>
            </imageimport>
            <catalog>
                <rewrite>
                    <convert_adapter_product>Prattski_ImageImport_Model_Catalog_Convert_Adapter_Product</convert_adapter_product>
                </rewrite>
            </catalog>
        </models>
        <helpers>
            <imageimport>
                <class>Prattski_ImageImport_Helper</class>
            </imageimport>
        </helpers>
    </global>
    <default>
        <catalog>
            <catalog>
                <image_exclude_enabled>1</image_exclude_enabled>
                <multiple_image_column_name>additional_images</multiple_image_column_name>
            </catalog>
        </catalog>
    </default>
</config>

Step 3

Let’s create our module’s helper. This isn’t necessary, but always a good practice to include it, as it is required for locale stuff, and it’s a ready utility if ever needed. In our case, we’ll just create a blank helper. Create this file in your module directory ‘Helper/Data.php’:

<?php
/**
 * Prattski Import Images Module
 *
 * @category    Prattski
 * @package     Prattski_ImageImport
 * @copyright   Copyright (c) 2011 Prattski (http://prattski.com)
 * @author      Prattski (Josh Pratt)
 */
 
/**
 * Helper
 *
 * @category    Prattski
 * @package     Prattski_ImageImport
 */
class Prattski_ImageImport_Helper_Data extends Mage_Core_Helper_Abstract
{
 
}

Step 4

Now we will create our system config. This will give us the ability to turn on or off excluding the main product images, and specify the column in our CSV for the additional images. Note: In our config.xml file that we created, you’ll see the ‘default’ tag near the bottom. There we pre-populate our system config fields that we are creating here with data so that they aren’t blank upon module installation. Create this file in your module directory: ‘etc/system.xml’:

<?xml version="1.0"?>
<config>
    <sections>
        <catalog>
            <groups>
                <catalog>
                    <label>Import Image Options</label>
                    <frontend_type>text</frontend_type>
                    <sort_order>999</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>0</show_in_store>
                    <fields>
                        <image_exclude_enabled translate="label" module="imageimport">
                            <label>Enable Excluded Images</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>10</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                            <comment>If the product image is a main product image (image, small_image, or thumbnail), automatically check the exlcude checkbox.</comment>
                        </image_exclude_enabled>
                        <multiple_image_column_name translate="label" module="imageimport">
                            <label>Multiple Image Column Name</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>20</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                            <comment>The header in your CSV where you will put your additional images to import. When importing multiple images, separate with a semi-colon: 'image1.jpg;image2.jpg;image3.jpg'</comment>
                        </multiple_image_column_name>
                    </fields>
                </catalog>
            </groups>
        </catalog>
    </sections>
</config>

Step 5

Finally, we’ll create the model that will rewrite the core module to add in our new functionality. You’ll see towards the bottom (at line 230) that I’ve added two code blocks. The first block will exclude (or not) based on the system configuration setting we setup. The second is what imports all the additional images. Create this file in your module directory ‘Model/Catalog/Convert/Adapter/Product.php’:

<?php
/**
 * Prattski Import Images Module
 *
 * @category    Prattski
 * @package     Prattski_ImageImport
 * @copyright   Copyright (c) 2011 Prattski (http://prattski.com)
 * @author      Prattski (Josh Pratt)
 */
 
/**
 * Product Import Adapter
 *
 * @category    Prattski
 * @package     Prattski_ImageImport
 */
class Prattski_ImageImport_Model_Catalog_Convert_Adapter_Product extends Mage_Catalog_Model_Convert_Adapter_Product
{
    /**
     * Save product (import)
     *
     * @param array $importData
     * @throws Mage_Core_Exception
     * @return bool
     */
    public function saveRow(array $importData)
    {
        $product = $this->getProductModel()
            ->reset();
 
        if (empty($importData['store'])) {
            if (!is_null($this->getBatchParams('store'))) {
                $store = $this->getStoreById($this->getBatchParams('store'));
            } else {
                $message = Mage::helper('catalog')->__(
                    'Skipping import row, required field "%s" is not defined.',
                    'store'
                );
                Mage::throwException($message);
            }
        }
        else {
            $store = $this->getStoreByCode($importData['store']);
        }
 
        if ($store === false) {
            $message = Mage::helper('catalog')->__(
                'Skipping import row, store "%s" field does not exist.',
                $importData['store']
            );
            Mage::throwException($message);
        }
 
        if (empty($importData['sku'])) {
            $message = Mage::helper('catalog')->__('Skipping import row, required field "%s" is not defined.', 'sku');
            Mage::throwException($message);
        }
        $product->setStoreId($store->getId());
        $productId = $product->getIdBySku($importData['sku']);
 
        if ($productId) {
            $product->load($productId);
        }
        else {
            $productTypes = $this->getProductTypes();
            $productAttributeSets = $this->getProductAttributeSets();
 
            /**
             * Check product define type
             */
            if (empty($importData['type']) || !isset($productTypes[strtolower($importData['type'])])) {
                $value = isset($importData['type']) ? $importData['type'] : '';
                $message = Mage::helper('catalog')->__(
                    'Skip import row, is not valid value "%s" for field "%s"',
                    $value,
                    'type'
                );
                Mage::throwException($message);
            }
            $product->setTypeId($productTypes[strtolower($importData['type'])]);
            /**
             * Check product define attribute set
             */
            if (empty($importData['attribute_set']) || !isset($productAttributeSets[$importData['attribute_set']])) {
                $value = isset($importData['attribute_set']) ? $importData['attribute_set'] : '';
                $message = Mage::helper('catalog')->__(
                    'Skip import row, the value "%s" is invalid for field "%s"',
                    $value,
                    'attribute_set'
                );
                Mage::throwException($message);
            }
            $product->setAttributeSetId($productAttributeSets[$importData['attribute_set']]);
 
            foreach ($this->_requiredFields as $field) {
                $attribute = $this->getAttribute($field);
                if (!isset($importData[$field]) && $attribute && $attribute->getIsRequired()) {
                    $message = Mage::helper('catalog')->__(
                        'Skipping import row, required field "%s" for new products is not defined.',
                        $field
                    );
                    Mage::throwException($message);
                }
            }
        }
 
        $this->setProductTypeInstance($product);
 
        if (isset($importData['category_ids'])) {
            $product->setCategoryIds($importData['category_ids']);
        }
 
        foreach ($this->_ignoreFields as $field) {
            if (isset($importData[$field])) {
                unset($importData[$field]);
            }
        }
 
        if ($store->getId() != 0) {
            $websiteIds = $product->getWebsiteIds();
            if (!is_array($websiteIds)) {
                $websiteIds = array();
            }
            if (!in_array($store->getWebsiteId(), $websiteIds)) {
                $websiteIds[] = $store->getWebsiteId();
            }
            $product->setWebsiteIds($websiteIds);
        }
 
        if (isset($importData['websites'])) {
            $websiteIds = $product->getWebsiteIds();
            if (!is_array($websiteIds) || !$store->getId()) {
                $websiteIds = array();
            }
            $websiteCodes = explode(',', $importData['websites']);
            foreach ($websiteCodes as $websiteCode) {
                try {
                    $website = Mage::app()->getWebsite(trim($websiteCode));
                    if (!in_array($website->getId(), $websiteIds)) {
                        $websiteIds[] = $website->getId();
                    }
                }
                catch (Exception $e) {}
            }
            $product->setWebsiteIds($websiteIds);
            unset($websiteIds);
        }
 
        foreach ($importData as $field => $value) {
            if (in_array($field, $this->_inventoryFields)) {
                continue;
            }
            if (is_null($value)) {
                continue;
            }
 
            $attribute = $this->getAttribute($field);
            if (!$attribute) {
                continue;
            }
 
            $isArray = false;
            $setValue = $value;
 
            if ($attribute->getFrontendInput() == 'multiselect') {
                $value = explode(self::MULTI_DELIMITER, $value);
                $isArray = true;
                $setValue = array();
            }
 
            if ($value && $attribute->getBackendType() == 'decimal') {
                $setValue = $this->getNumber($value);
            }
 
            if ($attribute->usesSource()) {
                $options = $attribute->getSource()->getAllOptions(false);
 
                if ($isArray) {
                    foreach ($options as $item) {
                        if (in_array($item['label'], $value)) {
                            $setValue[] = $item['value'];
                        }
                    }
                } else {
                    $setValue = false;
                    foreach ($options as $item) {
                        if ($item['label'] == $value) {
                            $setValue = $item['value'];
                        }
                    }
                }
            }
 
            $product->setData($field, $setValue);
        }
 
        if (!$product->getVisibility()) {
            $product->setVisibility(Mage_Catalog_Model_Product_Visibility::VISIBILITY_NOT_VISIBLE);
        }
 
        $stockData = array();
        $inventoryFields = isset($this->_inventoryFieldsProductTypes[$product->getTypeId()])
            ? $this->_inventoryFieldsProductTypes[$product->getTypeId()]
            : array();
        foreach ($inventoryFields as $field) {
            if (isset($importData[$field])) {
                if (in_array($field, $this->_toNumber)) {
                    $stockData[$field] = $this->getNumber($importData[$field]);
                }
                else {
                    $stockData[$field] = $importData[$field];
                }
            }
        }
        $product->setStockData($stockData);
 
        $mediaGalleryBackendModel = $this->getAttribute('media_gallery')->getBackend();
 
        $arrayToMassAdd = array();
 
        foreach ($product->getMediaAttributes() as $mediaAttributeCode => $mediaAttribute) {
                if (isset($importData[$mediaAttributeCode])) {
                $file = trim($importData[$mediaAttributeCode]);
                if (!empty($file) && !$mediaGalleryBackendModel->getImage($product, $file)) {
                    $arrayToMassAdd[] = array('file' => trim($file), 'mediaAttribute' => $mediaAttributeCode);
                }
            }
        }
 
        /**
         * Added code to enable/disable the exclude checkbox for main product images
         */
 
        $_exclude = (Mage::getStoreConfig('catalog/catalog/image_exclude_enabled')) ? true : false;
 
        $addedFilesCorrespondence = $mediaGalleryBackendModel->addImagesWithDifferentMediaAttributes(
            $product,
            $arrayToMassAdd, Mage::getBaseDir('media') . DS . 'import',
            false,
            $_exclude
        );
 
        /**
         * End new code
         */
 
        /**
         * Added code to allow the import of additional product images
         */
        $_column = Mage::getStoreConfig('catalog/catalog/multiple_image_column_name');
        if ($importData[$_column]) {
            $additionalImages = explode(';',$importData[$_column]);
            foreach ($additionalImages as $image) {
                if (!empty($image) && !$mediaGalleryBackendModel->getImage($product, $image)) {
                    $arrayToMassAdd2[] = array('file' => trim($image));
                }
            }
        }
 
        $addedFilesCorrespondence .= $mediaGalleryBackendModel->addImagesWithDifferentMediaAttributes(
            $product,
            $arrayToMassAdd2, Mage::getBaseDir('media') . DS . 'import',
            false,
            false
        );
        /**
         * End new code
         */
 
        foreach ($product->getMediaAttributes() as $mediaAttributeCode => $mediaAttribute) {
            $addedFile = '';
            if (isset($importData[$mediaAttributeCode . '_label'])) {
                $fileLabel = trim($importData[$mediaAttributeCode . '_label']);
                if (isset($importData[$mediaAttributeCode])) {
                    $keyInAddedFile = array_search($importData[$mediaAttributeCode],
                        $addedFilesCorrespondence['alreadyAddedFiles']);
                    if ($keyInAddedFile !== false) {
                        $addedFile = $addedFilesCorrespondence['alreadyAddedFilesNames'][$keyInAddedFile];
                    }
                }
 
                if (!$addedFile) {
                    $addedFile = $product->getData($mediaAttributeCode);
                }
                if ($fileLabel && $addedFile) {
                    $mediaGalleryBackendModel->updateImage($product, $addedFile, array('label' => $fileLabel));
                }
            }
        }
 
        $product->setIsMassupdate(true);
        $product->setExcludeUrlRewrite(true);
 
        $product->save();
 
        return true;
    }
}

Step 6

To recap all the files we have made:

  • /app/etc/modules/Prattski_ImageImport.xml
  • /app/code/local/Prattski/ImageImport/etc/config.xml
  • /app/code/local/Prattski/ImageImport/etc/system.xml
  • /app/code/local/Prattski/ImageImport/Helper/Data.php
  • /app/code/local/Prattski/ImageImport/Model/Catalog/Convert/Adapter/Product.php

Test it out! You’ll be able to see if Magento recognizes your module by going to System > Configuration > Advanced. If you see the module in the list, Magento knows it’s there. You can also go to System > Configuration > Catalog > Catalog > Import Image Options to see the configuration options we setup there.

If all looks good, then you are ready for a test. Setup your CSV import file, including a new column called ‘additional_images’ (or whatever you changed it to in the system config) and populate it with images to import. When ready, go to System > Import/Export > Dataflow – Profiles and select the Import All Products and give it a go!

I hope you enjoyed learning how to build this module. If you find any issues or bugs, please let me know. Remember, this is for Magento 1.5.x and probably will not work on 1.4.x and lower.

Posted in Magento | 30 Comments

Magento API: V2 API Does Not Return Custom Order Attributes

I am working with a 3rd party integrator to make an integration between Magento and Sage AccPac. We planned from the beginning to pass them custom order attributes and custom order-item attributes, which I verified come through the API, seeing both custom order attributes, and custom order-item attributes returned.

Now that they’ve got their stuff up and running, I just got an email telling me that they do not see any of these custom attributes coming through. At first I thought maybe they were just blind, but through investigating further, I found that they are using .NET and therefore have chosen to use the V2 API.

I then experimented with the same sales_order.info method with the V2 and discovered that they are right. No custom order or custom order-item attributes are returned!

If I figure out why, I will update this post. If you know anything about this, please comment!

Update:

Actually, I found out that the V2 returns WAY less information than the standard soap API. Why is this!?

Update 2:

I believe I’ve found out the ‘why’ as to why there are so many fewer attributes, and why no custom attributes come through the V2 methods: According to a .NET developer I talked with, languages like .NET and Java need to know the number of properties that are going to be returned. For example, if .NET expects to get 5 attributes, but because of a custom attribute we added it gets 6, bad things happen. So, the V2 of the api will always only ever return a very specific set of attributes that does not change.

As for where in the code it is doing this limitation, I am not quite sure yet. When/if I figure it out, and how to modify it, I will make a post on that.

Posted in Magento | 3 Comments

Magento API: v2_soap Does Not Use Call()

If you are utilizing Magento’s API and have checked out the documentation, you’ll see that they have included a V2 of their soap API saying:

As of v1.3 you may also use http://yourmagentohost/api/v2_soap?wsdl=1 which has been added to improve compatbility with Java and .NET.

If you find the need to utilize the v2_soap API, take note that you will need to call your methods differently than the documentation explains.

For example, if you would like to use the sales_order.info method, using the standard soap API, you would do the following to get all the order info for order number 1000000142:

$client = new SoapClient('http://yourwebsite.com/api/soap/?wsdl');
$session = $client->login('username', 'password');
$result = $client->call($session, 'sales_order.info', '1000000142');

If you need to use the V2 API, you need to so it slightly different. Instead of using the call() method, you call the specific API method instead:

$client = new SoapClient('http://yourwebsite.com/api/soap/?wsdl');
$session = $client->login('username', 'password');
$result = $client->salesOrderInfo($session, '1000000142');

So, for V2, you must convert the method names in the Magento documentation to a camel-case method name.

Posted in Magento | 1 Comment

URL Rewrite Import/Export Module – Now on Github

I’m making another Magento extension available for free via Github. This module provides an easy interface to import/export Custom URL rewrites. As of the current version, please note that it does not import/export System rewrites.

https://github.com/Prattski/import-rewrites

If you are using 1.5.x+: Being that I developed this module on 1.4.x, the current package that you can download from Github will not work to install via the mage script. I would have to manually install it on 1.5 and then re-package it so that it’s a Magento Connect 2.0 package instead of a 1.0 package.

If you want to use this module, you’ll have to just unpackage it yourself. Inside, you’ll find 3 directories:

  • adminhtml – open it up, and copy the adminhtml/default/prattski/ directory into app/design/adminhtml/default/
  • modules – open it up and put Prattski_ImportRewrites.xml into app/etc/modules/
  • Prattski – put in app/code/community/
Posted in Magento | 9 Comments

Dependent Filters Module – Now on Github

Since I’m not going to bother with regular updates and support, why not make it free and accessible, right? I just put my Dependent Filters module up on Github for all of you to use.

This module adds on option on the attribute edit view that allows you to select dependencies from a list of existing filters. When dependencies have been set, the filter will not show up on the front end until one of the selected dependencies have been selected.

I hope you find it useful! https://github.com/Prattski/dependent-filters

Posted in Git, Magento | Leave a comment

It’s Been a While…

It is nearing 2 months since I last posted an article. I’m very overdue for a post. Why has it been so long? I’ll explain:

I have a full-time job, a family with 3 children, and I have been doing freelance Magento work on the side. All of this combined simply hasn’t been working for me. It’s too much. My family often gets the worst end of the deal when I’m busy with freelance. It was hard for them, stressful for me, and I needed to make a change.

I decided to stop most of my freelance work to focus on having more time with my family, so I pretty much went all out and stopped doing much of any work outside of my full-time job (including blogging). It has been very rewarding, and I’m glad I made that move. I know that I will never regret having spent a lot of time with my family, but I would definitely regret having not spent enough.

I still do almost nothing but Magento work, so I hope to start blogging more frequently with the things I’m doing/learning at work, so hopefully posts will pick up a bit more soon. I may even start opening up some open source Magento modules in Github soon as well (ones that I wanted to charge for, but I don’t want to deal with maintenance and support).

I hope you have found my blog useful, that’s what it’s here for. I hope to continue making it even more helpful.

Posted in Misc | 3 Comments

Netbeans: Display .gitignore File in Projects/Files

By default, Netbeans will ignore files such as CVS files, SVN files, dot files, etc. If you would like to have your .gitignore file displayed in your Projects or Files views, open up your Netbeans preferences, go to the the Miscellaneous tab at the top, then click on the Files tab. You’ll see a “Files Ignored by the IDE” section there. That field accepts a regular expression string that allows you to hide files. I have Netbeans NOT ignoring my .htaccess and my .gitignore files, but ignoring all other dot files. Here is what my pattern looks like:

^(CVS|SCCS|vssver.?\.scc|#.*#|%.*%|_svn)$|~$|^\.(?!(htaccess|gitignore)$).*$
Posted in Git, Misc | 1 Comment

ActiveCollab: Development/Module Drawbacks

The company I currently work with recently made the transition from Basecamp to ActiveCollab. Reasons? We wanted more flexibility, the source code, and ActiveCollab offers more features that were of interest to us.

There are some important tweaks that we would like to make to the system, having now used it for a little while. I started looking into module development, but was unable to find any evidence of being able to override any of the core/out-of-the-box modules. I recently posted this thread on ActiveCollab’s forum, and was really disappointed at the response.

Unlike Magento, which allows you to override pretty much anything, you cannot do overrides like that with ActiveCollab. It does allow you to hook into some events, but the flexibility for what you can actually change is very minimal. If you want to make changes, you have to actually change the existing modules. And, like you guessed, every time you want to do an upgrade, your changes will get wiped out.

You can of course write your own modules to add new functionality, which we will be exploring. But, if you want to modify existing functionality, be sure to document all the changes you make, as you may need to make those changes again and again with each upgrade.

Don’t get me wrong about ActiveCollab though. It is a nice system, and it is working well for the most part. There is just a very disappointing level of documentation for developers. As they said, ActiveCollab is a solution, not a platform/framework. We developers wish it wasn’t that way of course, but that is how they decided to do it.

Posted in ActiveCollab | 2 Comments