HEX
Server: Microsoft-IIS/10.0
System: Windows NT ITPWINWEBSVR22 10.0 build 20348 (Windows Server 2022) AMD64
User: www.conferencesearch.co.uk (0)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: D:/bin/scripts/python3.4/expressmusic/expressmusic_stock_update.py
# (SS,27/08/15) Python script to update Express Music stock levels from separate MySQL databased called seanic
# Version of Python must be 3.4 or greater
# (SS,12/12/25) Added def is_stock_update_exclusion_date() which looks at "Stock Update Exclusion Dates" token in sitedetails
# This allows Express Music to modify the exclusion dates
# (SS,14/12/25) Added log_stock_update to log the update into a different token

import datetime
import mysql.connector
import sys
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import email.utils
  
      
def do_stock_update():   
    # get the source records
    LSQL = """SELECT Stock_Levels.System_Code, SUM(Stock_Level - Allocated) AS StockLevel, RRP
      FROM Stock_Levels
      INNER JOIN Product ON Product.System_Code = Stock_Levels.System_Code
      GROUP BY System_Code"""
    cursor_source.execute(LSQL)
    
    # empty the existing destination table
    cursor_destin.execute("TRUNCATE TABLE seanic_stock_levels")
    
    # copy the records from source to destination
    for row in cursor_source.fetchall(): 
      LSQL = "INSERT INTO seanic_stock_levels VALUES " + str(tuple(row));
      cursor_destin.execute(LSQL)
      
    # update the stocks levels in products table if different to seanic and product isn't disabled 
    LSQL = """UPDATE products p, seanic_stock_levels s 
      SET p.NumInStock = s.Stock_Level
      WHERE s.System_Code = p.ProductCode AND
      NOT p.ProductDisabled AND p.NumInStock <> s.Stock_Level"""
    cursor_destin.execute(LSQL)
    print_log("Product records updated: " + str(cursor_destin.rowcount))
    
    # count products where prices don't match seanic
    LSQL = """SELECT COUNT(*) FROM products p
      INNER JOIN seanic_stock_levels s ON s.System_Code = p.ProductCode
      WHERE NOT p.ProductDisabled AND p.StdPrice <> s.RRP AND COALESCE(p.SalePrice, 0) <> s.RRP"""
    cursor_destin.execute(LSQL)
    result = cursor_destin.fetchone()
    print_log("Product records with different prices: " + str(result[0]))

def set_alert(AAlert):
    global FAlert # required to modify a global 
    FAlert = AAlert
      
def get_alert():
    return FAlert
      
def print_log(AText):
    global FPrintLog # required to modify a global
    print(AText)
    FPrintLog = FPrintLog + AText + '\r\n'
      
def send_email_text(ABody, ASubject, AFrom, ATo, ABcc):
    # Create a text/plain message
    msg = MIMEText(ABody)

    # header    
    if get_alert():
        msg['Subject'] = 'ALERT!!! ' + ASubject
    else:
        msg['Subject'] = ASubject
    msg['From'] = AFrom
    msg['To'] = ATo
    msg['Date'] = email.utils.formatdate(localtime=True)
    if get_alert():       
        msg['X-Priority'] = '2'
            
    # send the messsage using localhost
    s = smtplib.SMTP('localhost')
    s.sendmail(AFrom, [ATo, ABcc], msg.as_string())
    s.quit()

# (SS,12/12/2025) created by Google Gemini 3 Pro (Thinking) using my instructions
import datetime

# ------------------------------------------------------------------
# FUNCTION 1: CHECK EXCLUSION DATES
# ------------------------------------------------------------------
def is_stock_update_exclusion_date():
    """
    Checks if today is a stock update exclusion date based on rules 
    stored in the 'Stock Update Exclusion Dates' token.
    Validates rules to prevent typos from breaking the logic.
    """
    global cursor_destin
    
    # Valid days set for validation
    valid_days = {
        "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"
    }
    
    try:
        # 1. Get the text from the database
        query = 'SELECT Text FROM sitedetails WHERE Type = "Tokens" AND Name = "Stock Update Exclusion Dates"'
        cursor_destin.execute(query)
        result = cursor_destin.fetchone()
        
        # If no token exists, assume no exclusions
        if not result or result[0] is None:
            print_log("Stock Update Exclusion: No token found. Proceeding with update.")
            return False
            
        token_text = result[0]
        
        # 2. Parse and Validate rules
        lines = token_text.splitlines()
        active_rules = []
        
        for line in lines:
            clean_line = line.strip()
            
            # Skip empty lines and comments
            if not clean_line or clean_line.startswith("#"):
                continue
            
            is_valid_rule = False
            
            # Case A: Starts with a digit -> Treat as Date (DD/MM/YYYY)
            if clean_line[0].isdigit():
                try:
                    datetime.datetime.strptime(clean_line, "%d/%m/%Y")
                    is_valid_rule = True
                except ValueError:
                    print_log(f"WARNING: Exclusion rule '{clean_line}' is not in DD/MM/YYYY format and will be ignored.")
            
            # Case B: Starts with a letter -> Treat as Day Name
            else:
                if clean_line.lower() in valid_days:
                    is_valid_rule = True
                else:
                    print_log(f"WARNING: Exclusion rule '{clean_line}' is not a valid day of the week.")

            if is_valid_rule:
                active_rules.append(clean_line)
        
        print_log(f"Stock Update Exclusion Rules Loaded: {', '.join(active_rules)}")

        # 3. Get current date information
        now = datetime.datetime.now()
        current_day_name = now.strftime("%A")        # e.g., "Monday"
        current_date_str = now.strftime("%d/%m/%Y")  # e.g., "25/12/2025"
        
        print_log(f"Stock Update Exclusion Check: Today is {current_day_name}, {current_date_str}")
        
        # 4. Check for matches
        for rule in active_rules:
            # Check Day Name (case-insensitive)
            if rule.lower() == current_day_name.lower():
                print_log(f"Stock Update Exclusion Match: Today is '{rule}'. Update Excluded.")
                return True
            
            # Check Specific Date
            if rule == current_date_str:
                print_log(f"Stock Update Exclusion Match: Today is '{rule}'. Update Excluded.")
                return True

        # No match found
        print_log("Stock Update Exclusion: No exclusion found. Update Allowed.")
        return False

    except Exception as e:
        print_log(f"Error checking exclusion dates: {e}")
        # Default to False so the update runs if the check crashes
        return False

# ------------------------------------------------------------------
# FUNCTION 2: LOG TO DATABASE TOKEN
# ------------------------------------------------------------------
def log_stock_update(log_message, max_lines=1000):
    """
    Prepends a timestamped message to the 'Stock Update Log'.
    Uses Windows-style line breaks (\r\n) for compatibility with Delphi apps.
    """
    global cursor_destin
    token_name = "Stock Update Log"
    
    try:
        # 1. Fetch existing log
        select_query = f'SELECT Text FROM sitedetails WHERE Type = "Tokens" AND Name = "{token_name}"'
        cursor_destin.execute(select_query)
        result = cursor_destin.fetchone()
        
        current_text = ""
        if result and result[0] is not None:
            current_text = result[0]
            
        # 2. Prepare new entry
        now = datetime.datetime.now()
        timestamp = now.strftime("%d/%m/%Y (%a) %H:%M:%S")
        new_entry = f"{timestamp} - {log_message}"
        
        # 3. Prepend and Trim (Using \r\n for Windows/Delphi)
        if current_text:
            # splitlines() handles \r, \n, or \r\n automatically
            lines = current_text.splitlines()
            
            # Keep newest (max_lines - 1)
            lines = lines[:max_lines-1]
            
            # Re-join everything with \r\n to fix formatting for the whole log
            updated_text = new_entry + "\r\n" + "\r\n".join(lines)
        else:
            updated_text = new_entry
            
        # 4. Update Database
        safe_updated_text = updated_text.replace('"', '""')
        
        update_query = f'UPDATE sitedetails SET Text = "{safe_updated_text}" WHERE Type = "Tokens" AND Name = "{token_name}"'
        cursor_destin.execute(update_query)
        
        print_log(f"Logged to '{token_name}': {new_entry}")

    except Exception as e:
        print_log(f"Error logging to token '{token_name}': {e}")

 
FAlert = False  # global used to set alert
FPrintLog = ""  # global used to hold log that is also the email body 
        
start = datetime.datetime.now()
print_log('Started: ' + start.strftime('%Y-%m-%d %H:%M:%S'))
print_log('')

try:

  # source connection
  cnx_source = None
  cursor_source = None
  
  # destination connection
  cnx_destin = None
  cursor_destin = None

  print("1")
  
  # open destination connection
  # replaced localhost with 127.0.0.1
  cnx_destin = mysql.connector.connect(user='expressmusic', password='bsharp',
                                host='127.0.0.1',
                                database='expressmusic')
  print("1.5")
  cursor_destin = cnx_destin.cursor()
 

  print("2")
  
  # (SS,12/12/25) added following to check for dates to exclude and only run if today is not excluded
  if is_stock_update_exclusion_date():
    log_stock_update("Update SKIPPED (Exclusion Rule Match)")
  else:  
    try:
      
      # open source connection seanic database
      cnx_source = mysql.connector.connect(user='elvis_presley', password='Su5p1ci0u5',
                                    host='data.seanicretail.co.uk',
                                    database='Express_Music')                              
      cursor_source = cnx_source.cursor()  
      
      print("4")

      do_stock_update();
      log_stock_update("Update COMPLETED successfully")

    except Exception as e:
      log_stock_update(f"Update FAILED: {e}")
    
except:
  set_alert(True)
  print_log("Error:")
  print_log(str(sys.exc_info()))
  #raise

finally:
  # close connections if they were opened
  if cursor_destin:
    cursor_destin.close()
  if cnx_destin:
    cnx_destin.commit()
    cnx_destin.close()
  if cursor_source:
    cursor_source.close()
  if cnx_source:
    cnx_source.close()

finish = datetime.datetime.now()
time_taken = finish - start
print_log('')
print_log('Finished: ' + finish.strftime('%Y-%m-%d %H:%M:%S'))
print_log('Time taken: %0.3f seconds' % (time_taken.seconds + time_taken.microseconds / 1E6))


# send the text as an email, from address stock-update@expressmusicstore.co.uk doesn't actually exist as valid mailbox
send_email_text(FPrintLog, 'Express Music Stock Level Update Result', 'stock-update@expressmusicstore.co.uk', 'surinder@itpartnership.com', '')