diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..618cb48e
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,122 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Application Overview
+
+CMC Sales is a B2B sales management system for CMC Technologies. The codebase consists of:
+
+- **Legacy CakePHP 1.2.5 application** (2008-era) - Primary business logic
+- **Modern Go API** (in `/go-app/`) - New development using sqlc and Gorilla Mux
+
+**Note**: Documentation also references a Django application that is not present in the current codebase.
+
+## Development Commands
+
+### Docker Development
+```bash
+# Start all services (both CakePHP and Go applications)
+docker compose up
+
+# Start specific services
+docker compose up cmc-php # CakePHP app only
+docker compose up cmc-go # Go app only
+docker compose up db # Database only
+
+# Restore database from backup (get backups via rsync first)
+rsync -avz --progress cmc@sales.cmctechnologies.com.au:~/backups .
+gunzip < backups/backup_*.sql.gz | mariadb -h 127.0.0.1 -u cmc -p cmc
+
+# Access development sites (add to /etc/hosts: 127.0.0.1 cmclocal)
+# http://cmclocal (CakePHP legacy app via nginx)
+# http://localhost:8080 (Go modern app direct access)
+```
+
+### Testing
+```bash
+# CakePHP has minimal test coverage
+# Test files located in /app/tests/
+# Uses SimpleTest framework
+```
+
+### Go Application Development
+```bash
+# Navigate to Go app directory
+cd go-app
+
+# Configure private module access (first time only)
+go env -w GOPRIVATE=code.springupsoftware.com
+
+# Install dependencies and sqlc
+make install
+
+# Generate sqlc code from SQL queries
+make sqlc
+
+# Run the Go server locally
+make run
+
+# Run tests
+make test
+
+# Build binary
+make build
+```
+
+## Architecture
+
+### CakePHP Application (Legacy)
+- **Framework**: CakePHP 1.2.5 (MVC pattern)
+- **PHP Version**: PHP 5 (Ubuntu Lucid 10.04 container)
+- **Location**: `/app/`
+- **Key Directories**:
+ - `app/models/` - ActiveRecord models
+ - `app/controllers/` - Request handlers
+ - `app/views/` - Templates (.ctp files)
+ - `app/config/` - Configuration including database.php
+ - `app/vendors/` - Third-party libraries (TCPDF for PDFs, PHPExcel)
+ - `app/webroot/` - Public assets and file uploads
+
+### Go Application (Modern)
+- **Framework**: Gorilla Mux (HTTP router)
+- **Database**: sqlc for type-safe SQL queries
+- **Location**: `/go-app/`
+- **Structure**:
+ - `cmd/server/` - Main application entry point
+ - `internal/cmc/handlers/` - HTTP request handlers
+ - `internal/cmc/db/` - Generated sqlc code
+ - `sql/queries/` - SQL query definitions
+ - `sql/schema/` - Database schema
+- **API Base Path**: `/api/v1`
+
+### Database
+- **Engine**: MariaDB
+- **Host**: `db` (Docker service) or `127.0.0.1` locally
+- **Name**: `cmc`
+- **User**: `cmc`
+- **Password**: `xVRQI&cA?7AU=hqJ!%au` (hardcoded in `app/config/database.php`)
+
+## Key Business Entities
+
+- **Customers** - Client companies with ABN validation
+- **Purchase Orders** - Orders with suppliers (revision tracking enabled)
+- **Quotes** - Customer price quotations
+- **Invoices** - Billing documents
+- **Products** - Catalog with categories and options
+- **Shipments** - Logistics and freight management
+- **Jobs** - Project tracking
+- **Documents** - PDF generation via TCPDF
+- **Emails** - Correspondence tracking
+
+## File Storage
+
+- **PDF files**: `/app/webroot/pdf/` (mounted volume)
+- **Attachments**: `/app/webroot/attachments_files/` (mounted volume)
+
+## Critical Considerations
+
+- **Security**: Running on extremely outdated Ubuntu Lucid (10.04) with PHP 5
+- **Database Changes**: Exercise caution - the database may be shared with other applications
+- **CakePHP Conventions**: Uses 2008-era patterns and practices
+- **Authentication**: Basic auth configured via `userpasswd` file
+- **PDF Generation**: Uses TCPDF library in `/app/vendors/tcpdf/`
\ No newline at end of file
diff --git a/Dockerfile.go b/Dockerfile.go
new file mode 100644
index 00000000..2e59e2ec
--- /dev/null
+++ b/Dockerfile.go
@@ -0,0 +1,50 @@
+# Build stage
+FROM golang:1.23-alpine AS builder
+
+# Install build dependencies
+RUN apk add --no-cache git
+
+# Set working directory
+WORKDIR /app
+
+# Copy go mod files
+COPY go-app/go.mod go-app/go.sum ./
+
+# Download dependencies
+RUN go mod download
+
+# Copy source code
+COPY go-app/ .
+
+# Install sqlc (compatible with Go 1.23+)
+RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
+
+# Generate sqlc code
+RUN sqlc generate
+
+# Build the application
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server cmd/server/main.go
+
+# Runtime stage
+FROM alpine:latest
+
+# Install runtime dependencies
+RUN apk --no-cache add ca-certificates
+
+WORKDIR /root/
+
+# Copy the binary from builder
+COPY --from=builder /app/server .
+
+# Copy templates and static files
+COPY go-app/templates ./templates
+COPY go-app/static ./static
+
+# Copy .env file if needed
+COPY go-app/.env.example .env
+
+# Expose port
+EXPOSE 8080
+
+# Run the application
+CMD ["./server"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 463b5ce9..b9c72315 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,50 @@
# cmc-sales
-## Development Docker compose instructions
+## Development Setup
+
+CMC Sales now runs both legacy CakePHP and modern Go applications side by side.
+
+### Quick Start
``` shell
git clone git@code.springupsoftware.com:cmc/cmc-sales.git
cd cmc-sales
+
+# Easy way - use the setup script
+./start-development.sh
+
+# Manual way
rsync -avz --progress cmc@sales.cmctechnologies.com.au:~/backups .
-docker compose up
-# DB password is in config/database.php because..reasons.
-# TODO move it to an environment var and rotate it..
+docker compose up --build
gunzip < backups/backup_*.sql.gz | mariadb -h 127.0.0.1 -u cmc -p cmc
-
-# edit your dev machine /etc/hosts and add
-127.0.0.1 cmclocal
-
-# open up cmclocal in your browser.. hopefully it'll work.
```
+### Access Applications
+
+**Add to /etc/hosts:**
+```
+127.0.0.1 cmclocal
+```
+
+**Application URLs:**
+- **CakePHP (Legacy)**: http://cmclocal - Original CakePHP 1.2.5 application
+- **Go (Modern)**: http://localhost:8080 - New Go application with HTMX frontend
+- **Database**: localhost:3306 (user: `cmc`, password: see `app/config/database.php`)
+
+### Architecture
+
+- **cmc-php**: Legacy CakePHP application (nginx proxied)
+- **cmc-go**: Modern Go application (direct access on port 8080)
+- **db**: Shared MariaDB database
+- **nginx**: Reverse proxy for CakePHP app
+
+Both applications share the same database, allowing for gradual migration.
+
+### Requirements
+
+- **Go Application**: Requires Go 1.23+ (for latest sqlc)
+ - Alternative: Use `Dockerfile.go.legacy` with Go 1.21 and sqlc v1.26.0
+
## Install a new server
diff --git a/app/config/database.php b/app/config/database.php
index 4325e127..56058f83 100644
--- a/app/config/database.php
+++ b/app/config/database.php
@@ -9,7 +9,7 @@ class DATABASE_CONFIG {
var $default = array(
'driver' => 'mysql',
'persistent' => false,
- 'host' => 'db',
+ 'host' => '172.17.0.1',
'login' => 'cmc',
'password' => 'xVRQI&cA?7AU=hqJ!%au',
'database' => 'cmc',
diff --git a/app/controllers/documents_controller.php b/app/controllers/documents_controller.php
index e8f4df97..6aaa42c2 100755
--- a/app/controllers/documents_controller.php
+++ b/app/controllers/documents_controller.php
@@ -444,6 +444,19 @@ ENDINSTRUCTIONS;
}
+ function getBaseTitle($titleWithRevision) {
+ // The pattern matches:
+ // - : the literal hyphen
+ // Rev : the literal string "Rev" (case-sensitive)
+ // \d+ : one or more digits (representing the revision number)
+ // $ : asserts that this pattern must be at the end of the string
+ //
+ // If "Rev" could be "rev", "REV", etc., you can make it case-insensitive
+ // by adding the 'i' flag: '/-Rev\d+$/i'
+ $baseTitle = preg_replace('/-Rev\d+$/', '', $titleWithRevision);
+ return $baseTitle;
+ }
+
/**
* Revise a Document.
@@ -464,7 +477,48 @@ ENDINSTRUCTIONS;
$this->Document->create();
if(!empty($document['Invoice']['id'])) {
- echo "WE HAVE AN INVOICE";
+ // Invoice revision
+ $newDoc = array();
+
+ // 1. Copy Document fields and update revision
+ $newDoc['Document'] = $this->unset_keys($document['Document'], array('id', 'created', 'modified'));
+ $newDoc['Document']['revision'] = $document['Document']['revision'] + 1;
+ // user_id for the new revision will be the current user
+ $currentUser = $this->GetCurrentUser(); // Assumes GetCurrentUser() is available
+ $newDoc['Document']['user_id'] = $currentUser['User']['id'];
+ // CakePHP will set created/modified timestamps automatically for the new record
+
+ // 2. Copy Invoice fields
+ $newDoc['Invoice'] = $this->unset_keys($document['Invoice'], array('id', 'document_id', 'created', 'modified'));
+ $newDoc['Invoice']['issue_date'] = date('Y-m-d');
+ $newDoc['Invoice']['due_date'] = date("Y-m-d", strtotime("+30 days"));
+
+ // Modify title for uniqueness, appending revision number
+ $orginalTitleParts = explode('-', $document['Invoice']['title']);
+ $newDoc['Invoice']['title'] = $orginalTitleParts[0] . '-REV' . $newDoc['Document']['revision'];
+
+ // 3. Copy DocPage records (if any)
+ if (!empty($document['DocPage'])) {
+ $newDoc['DocPage'] = $document['DocPage'];
+ foreach ($newDoc['DocPage'] as $index => $page) {
+ $newDoc['DocPage'][$index]['id'] = null; // New record
+ $newDoc['DocPage'][$index]['document_id'] = null; // Will be set by saveAll
+ }
+ }
+
+ // 4. Copy Job associations (if applicable - check your data model)
+ if (!empty($document['Job'])) {
+ $job_ids = array();
+ foreach ($document['Job'] as $job) {
+ $job_ids[] = $job['id'];
+ }
+ // This structure is typically used by saveAll for HABTM relationships
+ $newDoc['Job']['Job'] = $job_ids;
+ }
+
+ // Store info for flash message
+ // $this->set('revision_number_for_flash', $newDoc['Document']['revision']);
+ // $this->set('document_type_for_flash', 'Invoice');
}
else if (!empty($document['Quote']['id'])) {
@@ -498,12 +552,56 @@ ENDINSTRUCTIONS;
}
else if (!empty($document['PurchaseOrder']['id'])) {
- echo "WE ARE REVISING A PO";
+ // Purchase Order revision
+ $newDoc = array();
+
+ // 1. Copy Document fields and update revision
+ $newDoc['Document'] = $this->unset_keys($document['Document'], array('id', 'created', 'modified'));
+ $newDoc['Document']['revision'] = $document['Document']['revision'] + 1;
+ $newDoc['Document']['type'] = 'purchaseOrder'; // Ensure type is set
+
+ // user_id for the new revision will be the current user
+ $currentUser = $this->GetCurrentUser();
+ $newDoc['Document']['user_id'] = $currentUser['User']['id'];
+
+ // 2. Copy PurchaseOrder fields
+ $newDoc['PurchaseOrder'] = $this->unset_keys($document['PurchaseOrder'], array('id', 'document_id', 'created', 'modified'));
+ $newDoc['PurchaseOrder']['issue_date'] = date('Y-m-d');
+
+ // Modify title for uniqueness, appending revision number
+ $baseTitle = $this->getBaseTitle($document['PurchaseOrder']['title']);
+ $newDoc['PurchaseOrder']['title'] = $baseTitle . '-Rev' . $newDoc['Document']['revision'];
+
+ // 3. Copy DocPage records (if any)
+ if (!empty($document['DocPage'])) {
+ $newDoc['DocPage'] = array();
+ foreach ($document['DocPage'] as $page) {
+ $newPage = $this->unset_keys($page, array('id', 'document_id', 'created', 'modified'));
+ $newDoc['DocPage'][] = $newPage;
+ }
+ }
+
+ // 4. Handle Job associations (HABTM)
+ // First, we need to fetch the jobs associated with the original purchase order
+ $originalPO = $this->Document->PurchaseOrder->find('first', array(
+ 'conditions' => array('PurchaseOrder.id' => $document['PurchaseOrder']['id']),
+ 'contain' => array('Job')
+ ));
+
+ if (!empty($originalPO['Job'])) {
+ $job_ids = array();
+ foreach ($originalPO['Job'] as $job) {
+ $job_ids[] = $job['id'];
+ }
+ // Store job IDs to be processed after the main save
+ $newDoc['_job_ids'] = $job_ids;
+ }
+ print_r($newDoc);
}
else if (!empty($document['OrderAcknowledgement']['id'])) {
- echo "WE ARE REVISING An ORDER ACK";
+ //TODO Order Acknowledgement revision
}
@@ -517,13 +615,45 @@ ENDINSTRUCTIONS;
$this->set('newDoc', $newDoc);
if ($this->Document->saveAll($newDoc)) {
- $newid = $this->Document->id;
- $this->Session->setFlash(__("Revision {$number_of_revisions} created", true));
- $this->redirect(array('action'=>'view',$newid));
- } else {
- $this->Session->setFlash(__('The Document could not be saved. Please, try again.', true));
+ $newid = $this->Document->id;
+
+ // Handle Purchase Order Job associations
+ if (!empty($document['PurchaseOrder']['id']) && !empty($newDoc['_job_ids'])) {
+ // Get the new PurchaseOrder ID
+ $newPurchaseOrder = $this->Document->PurchaseOrder->find('first', array(
+ 'conditions' => array('PurchaseOrder.document_id' => $newid),
+ 'fields' => array('PurchaseOrder.id')
+ ));
+
+ if (!empty($newPurchaseOrder['PurchaseOrder']['id'])) {
+ $po_id = $newPurchaseOrder['PurchaseOrder']['id'];
+
+ // Method 1: Using CakePHP's HABTM save (recommended)
+ $this->Document->PurchaseOrder->id = $po_id;
+
+ // Method 2: If Method 1 doesn't work, use the SQL approach as a fallback
+ foreach($newDoc['_job_ids'] as $job_id) {
+ $query = "INSERT INTO `jobs_purchase_orders` (`job_id`, `purchase_order_id`) VALUES ('{$job_id}', '{$po_id}');";
+ $this->Document->query($query);
+ }
+ }
+ }
+
+ // Updated flash message logic
+ $revisionNumber = $newDoc['Document']['revision'];
+ $docTypeFullName = 'Unknown Document';
+ if (isset($newDoc['Document']['type'])) {
+ $docTypeFullName = $this->Document->getDocFullName($newDoc['Document']['type']);
+ } else if (isset($document['Document']['type'])) {
+ $docTypeFullName = $this->Document->getDocFullName($document['Document']['type']);
}
+ $this->Session->setFlash(__("Revision {$revisionNumber} of {$docTypeFullName} created", true));
+ $this->redirect(array('action'=>'view',$newid));
+ } else {
+ $this->Session->setFlash(__('The Document could not be saved. Please, try again.', true));
+ }
+
}
diff --git a/app/views/purchase_orders/index.ctp b/app/views/purchase_orders/index.ctp
index 0d8221e3..21121c92 100755
--- a/app/views/purchase_orders/index.ctp
+++ b/app/views/purchase_orders/index.ctp
@@ -65,7 +65,8 @@ foreach ($purchaseOrders as $purchaseOrder):
0) { ?>
View
-
+ Revise
+
diff --git a/app/webroot/js/document_add_edit_20240421.js b/app/webroot/js/document_add_edit_20240421.js
new file mode 100644
index 00000000..d6403479
--- /dev/null
+++ b/app/webroot/js/document_add_edit_20240421.js
@@ -0,0 +1,858 @@
+/**
+ * Could (should) tidy this up to move the Quote/Invoice etc Specific
+ * functionality into seperate files.
+ *
+ *
+ * Karl - 20/5/2011
+ */
+
+
+$(function() {
+
+
+ /**
+ * A more generic way of handling the HABTM [REMOVE BUTTON] NAME [HIDDEN INPUT]
+ * Copypasta'd from add_edit_shipment.js.
+ */
+ function addToList(modelName, id, value, ULelement) {
+ var thisLI = $(' ');
+ var thisButton = $('X ');
+ thisButton.addClass('removeFromList');
+ thisButton.button();
+
+ var thisHiddenInput = $(' ');
+
+ var modelString = '['+modelName+']';
+
+ thisHiddenInput.attr('name', 'data[PurchaseOrder]'+modelString+'[]');
+
+ thisHiddenInput.attr('value', id);
+
+ thisLI.attr('id', modelName+'ID_'+id);
+ thisLI.html(value);
+ thisLI.prepend(thisButton);
+ thisLI.append(thisHiddenInput);
+ ULelement.append(thisLI);
+
+
+
+ }
+
+
+ //Remove X button clicked.
+ $('.removeFromList').live('click', function() {
+ $(this).parent().remove();
+ });
+
+
+
+
+ // var config defined in global.js
+ loadLineItems();
+ var docID = $('#documentID').html();
+
+
+
+ $("#lineItemDetails").hide();
+
+ $( "#addLineItemModal" ).dialog({
+ autoOpen: false,
+ height: 900,
+ width: 600,
+ modal: true,
+ buttons: {
+ "Add Line Item": function() {
+
+ $('#LineItemDescription').ckeditor(function() {
+ this.updateElement();
+ });
+
+
+ var thisLineItemInputs = $('#LineItemAddForm').find('input,select,textarea');
+
+ $.post('/line_items/ajax_add', thisLineItemInputs, function(data) {
+
+ if(data == 'SUCCESS') {
+ loadLineItems();
+ $( "#addLineItemModal" ).dialog('close');
+ }
+ else {
+ alert("Line Item could not be saved")
+ $('#LineItemDescription').ckeditor(config);
+ }
+
+ });
+
+ },
+ Cancel: function() {
+ $( this ).dialog( "close" );
+ }
+ },
+ close: function() {
+ loadLineItems();
+ }
+
+ });
+
+ $( "#editLineItemModal" ).dialog({
+ autoOpen: false,
+ height: 900,
+ width: 600,
+ modal: true,
+ buttons: {
+ "Edit Line Item": function() {
+ $('#LineItemDescription').ckeditor(function() {
+ this.updateElement();
+ });
+
+ var thisLineItemInputs = $('#LineItemEditForm').find('input,select,textarea');
+
+ $.post('/line_items/ajax_edit', thisLineItemInputs, function(data) {
+ if(data == 'SUCCESS') {
+ $( "#editLineItemModal" ).dialog('close');
+ }
+ else {
+ alert("Line Item could not be saved")
+ $('#LineItemDescription').ckeditor(config);
+ }
+ });
+
+ },
+ Cancel: function() {
+ $( "#editLineItemModal" ).dialog('close');
+
+ }
+ },
+ close: function() {
+ loadLineItems();
+
+ }
+ });
+
+
+ $( "#QuoteDetails" ).dialog({
+ autoOpen: false,
+ height: 900,
+ width: 600,
+ modal: true,
+ buttons: {
+ "Edit Quote Details": function() {
+
+
+ $('#QuoteCommercialComments').ckeditor(function() {
+ this.updateElement();
+ this.destroy();
+ });
+
+
+ var quoteInputs = $('#QuoteEditForm').find('input,select,textarea');
+
+ $.post('/quotes/ajax_edit', quoteInputs, function(data) {
+ $( "#QuoteDetails" ).dialog('close');
+ });
+
+ },
+ Cancel: function() {
+ $( this ).dialog( "close" );
+
+ }
+ },
+ close: function() {
+ loadLineItems();
+ }
+ });
+
+
+
+ $( "#addJobConfirmation" ).dialog({
+ autoOpen: false,
+ height: 400,
+ width: 400,
+ modal: true,
+ buttons: {
+ "Create Order Acknowledgement": function() {
+
+ var documentID = $("#documentID").html();
+
+ //window.location.href = "/documents/convert_to_oa/"+documentID;
+ //var newOAform = $('#DocumentConvertToOaForm').find('input');
+ var newOAform = $('#DocumentConvertToOaForm');
+ newOAform.submit();
+
+
+
+ /*$.post('/documents/convert_to_oa', newOAform, function(data) {
+ if(data =='SUCCESS') {
+ $("#flashMessage").html("Invoice Saved Successfully");
+ }
+ else {
+ $("#flashMessage").html("Unable to Save Invoice");
+ }
+
+ $("#flashMessage").show();
+
+
+ loadLineItems();
+ });
+ */
+
+
+ },
+ Cancel: function() {
+ $( this ).dialog( "close" );
+
+ }
+ },
+ close: function() {
+ }
+ });
+
+
+ $("#pageContentFactory").hide();
+
+
+ //Add a new Page Element.
+ $("#addPage").button().click(function(event) {
+ event.preventDefault();
+
+ newPage(false);
+ return false;
+ });
+
+
+ //Open the LineItem dialog
+ $(".addLineItem").button().click(function() {
+
+
+
+ $('#LineItemDescription').ckeditor(function() {
+ this.destroy();
+ });
+
+
+ $("#editLineItemModal").empty();
+
+
+
+ var nextItemNo = $(".lineItem").length;
+ nextItemNo++;
+
+ $.get('/line_items/add/'+docID, function(data) {
+ $("#addLineItemModal").html(data);
+
+
+ $("#LineItemItemNumber").val(nextItemNo); //Auto fill in the next Item No
+
+
+ $("#productDetails").hide();
+
+ $('#LineItemDescription').ckeditor(config);
+
+ showHideTextPrices();
+
+ $( "#addLineItemModal" ).dialog('open');
+
+ });
+
+ return false;
+ });
+
+
+ $(".editLineItem").live('click', function() {
+
+
+ $('#LineItemDescription').ckeditor(function() {
+ this.destroy();
+ });
+
+ $("#addLineItemModal").empty();
+
+ var thisLineItemID = $(this).parent('td').attr('id');
+ $.get('/line_items/edit/'+thisLineItemID, function(data) {
+ $("#editLineItemModal").html(data);
+ $("#productDetails").hide();
+ $('#LineItemDescription').ckeditor(config);
+ showHideTextPrices();
+ $( "#editLineItemModal" ).dialog('open');
+
+ });
+ });
+
+
+ var products = {};
+ $("#principleSelect").live('change',function() {
+
+
+ var principleID = getSelectedID('#principleSelect');
+
+ $("#productDetails").hide();
+
+ $.get('/documents/getProducts/'+principleID, function(data) {
+ $('#productsDiv').html(data);
+
+ var resp = $(data).filter(".products_json");
+ products = jQuery.parseJSON(resp.html());
+
+ });
+ });
+
+
+ //Search for a Product
+ $("#productSearch").live('change', function() {
+ productSearch();
+ });
+
+ $("#productSearchButton").live('click', function() {
+ productSearch();
+ });
+
+ function productSearch() {
+ var searchVal = $("#productSearch").val();
+ searchVal = searchVal.toLowerCase();
+ var param = "term="+searchVal;
+
+ $.getJSON("/products/autocomplete", param, function(data) {
+
+ $("#productList").empty();
+
+ for(var id in data) {
+ var link = ""+data[id]+" ";
+ $("#productList").append(link);
+ }
+ });
+ }
+
+ //Click on a search product name.
+ $(".search_product").live('click', function() {
+ var productID = $(this).data('product-id');
+ getProductDetails(productID);
+ return false;
+ });
+
+ $("#productSelect").live('change',function() {
+
+ var productID = getSelectedID('#productSelect');
+
+ getProductDetails(productID);
+
+ });
+
+ function getProductDetails(productID) {
+ $.get('/documents/getProductDetails/'+productID, function(data) {
+
+ $("#lineItemDetails").show();
+ $("#LineItemProductId").val(data.id);
+ $("#LineItemTitle").val(data.title);
+
+ var descText = '';
+ if(data.item_code) {
+ descText = descText + 'Item Code: ' + data.item_code;
+ }
+
+ if(data.item_description) {
+ descText = descText + 'Item Description: ' + data.item_description + ' ';
+
+ }
+
+ descText = descText + data.description;
+
+ $("#LineItemDescription").val(descText);
+
+
+
+
+ }, "json");
+ }
+
+ //Autocomplete product title for adding lineItem
+
+
+
+ $( "#productAutocomplete" ).live('focus', function() {
+ $(this).autocomplete({
+ source: "/products/autocomplete",
+ minLength: 2,
+ select: function( event, ui ) {
+ console.log(ui);
+ },
+ appendTo: '#searchProducts'
+ });
+ });
+
+ $("#productAutocomplete").insertAfter();
+
+ // Initialize the editor.
+ // Callback function can be passed and executed after full instance creation.
+ $('.page').ckeditor(config);
+
+
+ $("#LineItemHasTextPrices").live('change', function() {
+ showHideTextPrices();
+ });
+
+
+ //Remove this Page
+ $(".removePage").live('click',function() {
+
+ $('.page').ckeditor(function() {
+ this.destroy();
+ });
+
+ $(this).parents(".docPage").remove();
+
+ $('.page').ckeditor(config);
+
+ });
+
+
+ $("#savePages").click(function() {
+ savePages();
+
+ });
+
+ var timeoutID = window.setTimeout(savePages, 30000);
+
+
+ $(".quickpricing").live('change', function() {
+ calculateQuickPrices();
+ });
+
+
+ $('.removeLineItem').live('click', function() {
+ var thisLineItemID = $(this).parent('td').attr('id');
+ $.post('/line_items/ajax_delete/'+thisLineItemID, function(data) {
+ loadLineItems();
+ });
+ });
+
+
+ $("#editQuoteDetails").click(function() {
+ var quoteID = $("#quoteID").html();
+
+ $('#QuoteCommercialComments').ckeditor(function() {
+ this.destroy();
+ });
+
+ $.get('/quotes/edit/'+quoteID, function(data) {
+ $("#QuoteDetails").html(data);
+ $('#QuoteCommercialComments').ckeditor(config);
+ $("#QuoteDetails").dialog('open');
+ });
+ });
+
+
+
+ // Fairly quick and easy way to make this select box editable. Good enough.
+ $("#QuotePaymentTermsOptions").live('change', function(data) {
+ selectedOption = $("#QuotePaymentTermsOptions").val();
+ $("#QuotePaymentTerms").val(selectedOption);
+ console.log(selectedOption);
+ });
+
+
+ $("#generateFirstPage").click(function() {
+
+ if($(".firstPage").length == 0) {
+ newPage(true);
+ }
+ else {
+ var confirmed = confirm("This will overwrite any changes you have made to the first page");
+ }
+
+ if(confirmed) {
+ $.get('/documents/generateFirstPage/'+docID, function(data) {
+ $(".firstPage").val(data);
+ savePages();
+ });
+ }
+
+ });
+
+ $("#pdfDocButton").click(function() {
+ window.location = $(this).data('url');
+ });
+
+ $("#emailDocButton").click(function() {
+ var confirmed = confirm("This will email this Document and all Attachments to the Contact. Are you sure you want to do this?");
+
+ if(confirmed) {
+ window.location = $(this).data('url');
+ }
+ });
+
+
+ //Invoice View
+ $('#shippingDetails').ckeditor(config);
+
+ $("#DocumentBillTo").ckeditor(config);
+ $("#DocumentShipTo").ckeditor(config);
+
+ $("#saveInvoiceButton").click(function() {
+ saveDocument('Invoice');
+ });
+ //OA View.
+ $("#saveOAButton").click(function() {
+ saveDocument('Order Acknowledgement');
+ //if the job has changed, the create invoice button wont work properly.
+ //window.location.reload(true);
+ $('.job-title').html($("#OrderAcknowledgementJobId :selected").text());
+ });
+
+
+ //This is fucked beyond all words.
+ $("#PurchaseOrderDeliverTo").ckeditor(config);
+ $("#PurchaseOrderOrderedFrom").ckeditor(config);
+ $("#PurchaseOrderShippingInstructions").ckeditor(config);
+ $("#PurchaseOrderDescription").ckeditor(config);
+
+ //PackingList View. Damn you past Karl.
+ $("#savePackingListButton").click(function() {
+ saveDocument('Packing List');
+ //if the job has changed, the create invoice button wont work properly.
+ //window.location.reload(true);
+ $('.job-title').html($("#PackingListJobId :selected").text());
+ });
+
+
+ //PurchaseOrder View. Damn you past Karl.
+ $("#savePurchaseOrderButton").click(function() {
+ saveDocument('Purchase Order');
+ });
+
+ // Changing PO principal, fill in the default fields.
+ $("#PurchaseOrderPrincipleId").live('change', function(d) {
+ var principleID = getSelectedID('#PurchaseOrderPrincipleId');
+
+ $.getJSON('/principles/defaults/'+principleID, function(data) {
+ console.log(data.Principle.po_ordered_from);
+ $('#PurchaseOrderOrderedFrom').val(data.Principle.po_ordered_from);
+ updateTextFields(); //Update the CKEditor instances.
+ });
+ });
+
+ // Changing Freight Forwarder text
+ $("#freightForwarderSelect").live('change', function(d) {
+ $("#PurchaseOrderFreightForwarderText").val(getSelectedText("#freightForwarderSelect"));
+ saveDocument('Purchase Order');
+ });
+
+
+ $("#createOA").click(function() {
+ $("#addJobConfirmation").dialog('open');
+ });
+
+ // Issue #56 - try to stop doubleclicks on button-links.
+ $(".button-link").one('click', function() {
+ location.href = $(this).data('href');
+ });
+
+ //Choosing an address
+ $(".billing_address").click(function() {
+ var address = $(this).next().html();
+ setAddress(address, '#DocumentBillTo', 'Bill To');
+ });
+ $(".shipping_address").click(function() {
+ var address = $(this).next().html();
+ setAddress(address, '#DocumentShipTo', 'Ship To');
+ });
+
+
+
+ //Fuck it. Copypaste. Autocompletion of jobs on PO document view
+ $( "#job_autocomplete" ).autocomplete({
+ source: "/jobs/autocomplete",
+ minLength: 2,
+ select: function( event, ui ) {
+
+ if($('#JobID_'+ui.item.id).length == 0) { //This Job is not already in the List.
+ addToList('Job', ui.item.id, ui.item.value, $('#jobsList'));
+
+ var jobs_val = $("#PurchaseOrderJobsText").val();
+
+ $("#PurchaseOrderJobsText").val(jobs_val +', '+ ui.item.value)
+
+ console.log(ui.item.id);
+ console.log(ui.item.value);
+ //POST the job ID to a method on the documents controller to add the LineItems
+ $.post('/documents/add_job_items_to_po/'+ui.item.id+'/'+$("#DocumentId").val(), function(data) {
+
+ loadLineItems();
+ saveDocument('Purchase Order');
+ });
+
+
+ }
+ }
+ });
+
+
+
+});
+
+$('#generateShippingInstructions').live('click', function(event) {
+ event.preventDefault();
+ saveDocument('Purchase Order');
+ $.post('/documents/generateShippingInstructions/'+$("#DocumentId").val(), function(data) {
+ $('#PurchaseOrderShippingInstructions').val(data);
+ updateTextFields(); //Update the CKEditor instances.
+ //$("#PurchaseOrderShippingInstructions").ckeditor(config);
+ });
+
+});
+
+$('.generateCommercialComments').live('click', function(event) {
+ event.preventDefault();
+
+
+
+ var deliveryTime = $("#QuoteDeliveryTime").val();
+ var deliveryTF = $("#QuoteDeliveryTimeFrame").val();
+ var paymentTerms = $("#QuotePaymentTerms").val();
+ var daysValid = $("#QuoteDaysValid").val();
+ var deliveryPoint = $("#QuoteDeliveryPoint").val();
+ var exchangeRate = $("#QuoteExchangeRate").val();
+ var customsDuty = $("#QuoteCustomsDuty").val();
+
+
+ deliveryTime = deliveryTime.toUpperCase();
+ paymentTerms = paymentTerms.toUpperCase();
+ deliveryPoint = deliveryPoint.toUpperCase();
+
+ var commComments = $("#commCommentsInitialString").clone();
+
+ var commList = commComments.find('ol');
+
+ commList.append('DELIVERY IS ESTIMATED AT '+deliveryTime+ ' '+deliveryTF+ ' FROM RECEIPT OF YOUR TECHNICALLY AND COMMERCIALLY CLEAR ORDER');
+ commList.append(' PAYMENT TERMS: '+paymentTerms+' ');
+ commList.append('QUOTATION IS VALID FOR '+daysValid+' DAYS ');
+ commList.append('ALL PRICES ARE '+deliveryPoint+' ');
+ commList.append('EXCHANGE RATE: '+exchangeRate+' ');
+ commList.append('CUSTOMS DUTY INCLUDED AT: '+customsDuty+' ');
+ commList.append('GST 10% EXTRA ');
+ commList.append('WHEN PAYMENTS ARE MADE INTO OUR BANK ACCOUNT, BANK CHARGES ARE YOUR RESPONSIBILITY ');
+ $('#QuoteCommercialComments').val(commComments.html());
+
+
+});
+
+
+//Save the current document
+//@param string documentName for message
+function saveDocument(documentName) {
+ updateTextFields(); //Update the CKEditor instances.
+ var fields = $('#DocumentEditForm').find('input,select,textarea');
+
+ $.post('/documents/ajax_edit', fields, function(data) {
+ if(data =='SUCCESS') {
+ $("#flashMessage").html(documentName+" saved Successfully");
+ }
+ else {
+ $("#flashMessage").html("Unable to save "+documentName);
+ }
+ $("#flashMessage").show();
+ loadLineItems();
+ });
+}
+
+
+//Set the address given the addressFieldID and a label FieldName
+function setAddress(address, addressFieldID, fieldName) {
+ console.log(address);
+ if($(addressFieldID).val() == '') {
+ $(addressFieldID).val(address);
+ }
+ else {
+ var c = confirm("Set " + fieldName+ " to this address?");
+ if(c) {
+ $(addressFieldID).val(address);
+ }
+ }
+}
+
+function showHideTextPrices() {
+ if( $('#LineItemHasTextPrices').val() == 1) {
+ $("#noCosting").hide();
+ $("#noCosting").find('input').val('');
+ $("#textPrices").show();
+ }
+ else {
+ $("#noCosting").show();
+ $("#textPrices").hide();
+ }
+}
+
+
+//I am a much better programmer now.
+//FUCK YEAH!!
+function updateTextFields() {
+ var fields = [
+ '#shippingDetails',
+ "#DocumentBillTo",
+ "#DocumentShipTo",
+ "#PurchaseOrderDeliverTo",
+ "#PurchaseOrderOrderedFrom",
+ "#PurchaseOrderShippingInstructions",
+ "#PurchaseOrderDescription"
+ ];
+
+ for (i in fields) {
+ $(fields[i]).ckeditor(function() {
+ this.updateElement();
+ });
+ }
+}
+
+
+function newPage(firstPage) {
+
+
+ $('.page').ckeditor(function() {
+ this.destroy();
+ });
+
+
+
+ var newPage = $('#pageContentFactory').clone();
+ newPage.removeAttr('id');
+ newPage.show();
+
+
+
+ var pageCount = $('.docPage').length;
+
+ //alert(pageCount);
+
+ pageCount++;
+ var model = 'DocPage';
+ var field = 'content';
+ var ID = getCakeID(model,pageCount, field);
+ var name = getCakeName(model, pageCount, field);
+
+ newPage.find('label').attr('for', ID);
+
+ newPage.find('textarea').attr('id', ID).attr('name', name);
+ newPage.addClass('docPage');
+
+
+ if(firstPage == true) {
+ newPage.find('textarea').addClass('firstPage');
+ }
+
+
+ var hiddenName = getCakeName(model, pageCount, 'page_number');
+
+ newPage.append(' ');
+ $('.pages').append(newPage);
+
+ $('.page').ckeditor(config);
+}
+
+function savePages() {
+ var docPages = $('#DocumentEditForm').find('input,select,textarea');
+
+ $('.page').ckeditor(function() {
+ this.updateElement();
+ });
+
+ $.post('/documents/ajax_edit', docPages, function(data) {
+ $("#flashMessage").html("Document saved");
+ $("#flashMessage").show();
+ loadLineItems();
+ });
+}
+
+
+function loadLineItems() {
+ var documentID = $("#documentID").html();
+
+ /*$.get('/line_items/getTable/'+documentID, function(data) {
+ $("#lineItems").html(data);
+ });*/
+
+ $.ajax({
+ url: '/line_items/getTable/'+documentID,
+ cache: false,
+ success: function(data) {
+ $("#lineItems").html(data);
+ }
+ });
+
+}
+
+
+function calculateQuickPrices() {
+
+ var quantity = $('#LineItemQuantity').val();
+
+ var gross_unit_price = $("#LineItemGrossUnitPrice").val();
+ var net_unit_price = $("#LineItemNetUnitPrice").val();
+ var discount_percent = $("#LineItemDiscountPercent").val();
+ var discount_amount_unit = $("#LineItemDiscountAmountUnit").val();
+ var discount_amount_total = $("#LineItemDiscountAmountTotal").val();
+ var gross_price = $("#LineItemGrossPrice").val();
+ var net_price = $("#LineItemNetPrice").val();
+
+ gross_price = quantity * gross_unit_price;
+ $("#LineItemGrossPrice").val(gross_price);
+
+ discount_amount_unit = (discount_percent/100) * gross_unit_price;
+ discount_amount_unit = discount_amount_unit.toFixed(2);
+
+ discount_amount_total = (discount_percent/100) * gross_price;
+ discount_amount_total = discount_amount_total.toFixed(2);
+ $("#LineItemDiscountAmountTotal").val(discount_amount_total);
+
+ net_price = gross_price - discount_amount_total;
+ $("#LineItemNetPrice").val(net_price);
+
+
+ $("#LineItemDiscountAmountUnit").val(discount_amount_unit);
+ net_unit_price = gross_unit_price - discount_amount_unit;
+ $("#LineItemNetUnitPrice").val(net_unit_price);
+
+
+
+}
+
+
+function calcNetPrice() {
+ var discountPercent = $("#discountPercent").val();
+ var unitPrice = $('#unitPrice').val();
+
+
+ var quantity = $('#LineItemQuantity').val();
+
+
+ var grossSellPrice = quantity * unitPrice;
+
+ //Calculate the Sale Discount amount.
+ var UnitDiscountAmount = (discountPercent/100) * unitPrice;
+
+ var TotalDiscountAmount = (discountPercent/100) * grossSellPrice;
+ UnitDiscountAmount = UnitDiscountAmount.toFixed(2);
+ TotalDiscountAmount = TotalDiscountAmount.toFixed(2);
+
+ $('#total_discountAmount').val(TotalDiscountAmount);
+ $('#discountAmountEach').val(UnitDiscountAmount);
+ $('#net_price_each').val(unitPrice - UnitDiscountAmount);
+
+ $('#grossPrice').val(grossSellPrice);
+
+ var netPrice = grossSellPrice - TotalDiscountAmount;
+ $('#netPrice').val(netPrice);
+
+}
+
+
+function checkNaN(value) {
+ if( isNaN(value) == true) {
+ return 0;
+ }
+ else {
+ return value;
+ }
+
+
+
+
+}
diff --git a/cmc-django/.gitignore b/cmc-django/.gitignore
deleted file mode 100644
index 21e56100..00000000
--- a/cmc-django/.gitignore
+++ /dev/null
@@ -1,13 +0,0 @@
-# Python-generated files
-__pycache__/
-*.py[oc]
-build/
-dist/
-wheels/
-*.egg-info
-
-lib/
-bin/
-
-# Virtual environments
-.venv
diff --git a/cmc-django/.gitignore~ b/cmc-django/.gitignore~
deleted file mode 100644
index c310698b..00000000
--- a/cmc-django/.gitignore~
+++ /dev/null
@@ -1,12 +0,0 @@
-# Python-generated files
-__pycache__/
-*.py[oc]
-build/
-dist/
-wheels/
-*.egg-info
-
-lib/
-
-# Virtual environments
-.venv
diff --git a/cmc-django/.python-version b/cmc-django/.python-version
deleted file mode 100644
index bd28b9c5..00000000
--- a/cmc-django/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.9
diff --git a/cmc-django/Dockerfile b/cmc-django/Dockerfile
deleted file mode 100644
index 831bd931..00000000
--- a/cmc-django/Dockerfile
+++ /dev/null
@@ -1,78 +0,0 @@
-# Stage 1: Base Image and System Dependencies
-# ------------------------------------------
-# Use an official Python slim image. These are multi-arch and will pull the arm64 variant on Apple Silicon.
-# Choose the Python version matching your project requirement (e.g., 3.11, 3.12).
-FROM python:3.11-slim AS base
-COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
-
-# Set environment variables to prevent interactive prompts during package installations
-ENV PYTHONDONTWRITEBYTECODE 1
-ENV PYTHONUNBUFFERED 1
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Install essential system dependencies
-# - build-essential: Required for compiling some Python packages with C extensions.
-# - pkg-config: Helper tool for finding library compilation flags.
-# - default-libmysqlclient-dev: Required for mysqlclient (MySQL/MariaDB adapter).
-# (Replaced libpq-dev from previous version).
-# - curl: Needed to download the uv installer script.
-# Clean up apt cache afterwards to keep the image size down.
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
- build-essential \
- pkg-config \
- default-libmysqlclient-dev \
- curl \
- clang \
- && apt-get clean \
- && rm -rf /var/lib/apt/lists/*
-
-# Stage 2: Install uv
-# -------------------
-# Install uv using the recommended script.
-# RUN curl -LsSf https://astral.sh/uv/install.sh | sh
-# Add uv to the PATH
-# ENV PATH="/root/.cargo/bin:$PATH"
-
-# Stage 3: Application Setup
-# --------------------------
-# Set the working directory inside the container
-WORKDIR /app
-
-# Copy dependency file(s)
-# Ensure requirements.txt includes Django and all other necessary packages.
-# COPY requirements.txt requirements.txt
-# If you use pyproject.toml for dependencies primarily, copy it instead or as well:
-COPY pyproject.toml pyproject.toml
-
-# Install Python dependencies using uv from requirements.txt
-# --system: Install packages into the system Python environment within the container (common for Docker)
-# --no-cache: Avoid caching downloads/builds within this layer to keep it smaller
-# RUN uv pip install --system --no-cache-dir -r requirements.txt
-
-# NOTE: Removed the 'uv pip install .' command below as it caused the
-# "Multiple top-level packages discovered" error. We typically only need
-# to install dependencies from requirements.txt for running a Django app in Docker.
-# Ensure your requirements.txt is complete.
-# -----------------------------------------------------------------------------
-# Or if using pyproject.toml (ensure it defines dependencies correctly):
-
-ENV UV_LINK_MODE=copy
-RUN uv pip install --system --no-cache-dir .
-# -----------------------------------------------------------------------------
-
-
-# Copy the rest of the application code into the container
-COPY . .
-
-# Collect static files
-RUN uv run python cmcsales/manage.py collectstatic --noinput
-
-# Expose the port the Django app runs on (adjust if you use a different port)
-EXPOSE 8000
-
-
-# Default command to run the application
-# This is often overridden by docker-compose.yml, but it's good practice to have a default.
-# Replace 'myproject' with your actual Django project directory name if different.
-CMD ["uv", "run", "python", "cmcsales/manage.py", "runserver", "0.0.0.0:8000"]
\ No newline at end of file
diff --git a/cmc-django/README.md b/cmc-django/README.md
deleted file mode 100644
index eeca43f4..00000000
--- a/cmc-django/README.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# CMC Django Project
-
-This is a Django-based web application designed to replace the old CakePHP 1.2 version of cmc-sales.
-
-## Features
-
-- [Feature 1]
-- [Feature 2]
-- [Feature 3]
-
-## Requirements
-
-- Python 3.x
-- Django 4.x
-- [Other dependencies]
-
-## Installation
-
-1. Clone the repository:
- ```bash
- git clone https://github.com/yourusername/cmc-django.git
- cd cmc-django
- ```
-
-2. Create and activate a virtual environment:
- `
Internal server error - SpringUp Software Pty Ltd
500
Internal server error
Forgejo version: 7.0.1+gitea-1.22.0