Merge branch 'master' into finley/110-extra-ccs
This commit is contained in:
commit
ff729adf55
122
CLAUDE.md
Normal file
122
CLAUDE.md
Normal file
|
|
@ -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/`
|
||||
50
Dockerfile.go
Normal file
50
Dockerfile.go
Normal file
|
|
@ -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"]
|
||||
46
README.md
46
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ foreach ($purchaseOrders as $purchaseOrder):
|
|||
<td>
|
||||
<?php if($purchaseOrder['PurchaseOrder']['document_id'] > 0) { ?>
|
||||
<a href="/documents/view/<?=$purchaseOrder['PurchaseOrder']['document_id']; ?>">View</a>
|
||||
<?php } ?>
|
||||
<a href="/documents/revise/<?=$purchaseOrder['PurchaseOrder']['document_id']; ?>">Revise</a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
|
|
|||
858
app/webroot/js/document_add_edit_20240421.js
Normal file
858
app/webroot/js/document_add_edit_20240421.js
Normal file
|
|
@ -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 <ul> <li>[REMOVE BUTTON] NAME [HIDDEN INPUT]</li> </ul>
|
||||
* Copypasta'd from add_edit_shipment.js.
|
||||
*/
|
||||
function addToList(modelName, id, value, ULelement) {
|
||||
var thisLI = $('<li></li>');
|
||||
var thisButton = $('<button>X</button>');
|
||||
thisButton.addClass('removeFromList');
|
||||
thisButton.button();
|
||||
|
||||
var thisHiddenInput = $('<input type="hidden">');
|
||||
|
||||
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 = "<li><a href=\"#\" class=\"search_product\" data-product-id=\""+id+"\">"+data[id]+"</a></li>";
|
||||
$("#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 + '<br><b>Item Code:</b> ' + data.item_code;
|
||||
}
|
||||
|
||||
if(data.item_description) {
|
||||
descText = descText + '<br><b>Item Description:</b> ' + data.item_description + '<br>';
|
||||
|
||||
}
|
||||
|
||||
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('<li>DELIVERY IS ESTIMATED AT '+deliveryTime+ ' '+deliveryTF+ ' FROM RECEIPT OF YOUR TECHNICALLY AND COMMERCIALLY CLEAR ORDER');
|
||||
commList.append('<li>PAYMENT TERMS: '+paymentTerms+'</li>');
|
||||
commList.append('<li>QUOTATION IS VALID <u>FOR '+daysValid+' DAYS</li>');
|
||||
commList.append('<li>ALL PRICES ARE <u>'+deliveryPoint+'</u></li>');
|
||||
commList.append('<li>EXCHANGE RATE: '+exchangeRate+'</u></li>');
|
||||
commList.append('<li>CUSTOMS DUTY INCLUDED AT: '+customsDuty+'</li>');
|
||||
commList.append('<li>GST 10% EXTRA</li>');
|
||||
commList.append('<li>WHEN PAYMENTS ARE MADE INTO OUR BANK ACCOUNT, BANK CHARGES ARE YOUR RESPONSIBILITY</li>');
|
||||
$('#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('<input type="hidden" name="'+hiddenName+'" value="'+pageCount+'">');
|
||||
$('.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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
13
cmc-django/.gitignore
vendored
13
cmc-django/.gitignore
vendored
|
|
@ -1,13 +0,0 @@
|
|||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
lib/
|
||||
bin/
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
lib/
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
|
@ -1 +0,0 @@
|
|||
3.9
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
Loading…
Reference in a new issue