Align cell content in excel using python - python

I am struggling to set the alignment for data in excel using python
My python function loads data from excel into a pandas dataframe, calculates some new columns, then adds these columns to the original sheet. This all works well, but I now want to tidy up the result.
I can set italics / bold etc using
sheet['E1:J24'].font.bold = True
sheet['E1:J24'].font.italic = True
But I cannot set the alignment properly. I have tried the following, and several other suggestions I found online, but none of them seems to work.
sheet['E1:J24'].alignment = Alignment(horizontal="center")
Any help would be appreciated.
Update to question,
With further on-line searching I came upon this line of code which successfully adjusts the alignment.
sheet.range(f'$E1:J24').api.HorizontalAlignment = -4152
I think the problem is that I connected to the worksheet using xlwings and then tried to use openpyxl to format it. Jupyter didn't give an error because I had imported 'Alignment' from openpyxl
Note, for alignments use setting as follows
center = -4108
right = -4152
Left = -4131
Not sure where the numbers come from

Use 'VerticalAlignment' and/or 'HorizontalAlignment'.
Import VAlign, HAlign from the Xlwings constants to use the name or just use the Excel code. I have copied these into the comments for your information.
import xlwings as xw
from xlwings.constants import VAlign, HAlign
### Xlwings constants
"""
VAlign Class
xlVAlignBottom = -4107
xlVAlignCenter = -4108
xlVAlignDistributed = -4117
xlVAlignJustify = -4130
xlVAlignTop = -4160
HAlign Class
xlHAlignCenter = -4108
xlHAlignCenterAcrossSelection = 7
xlHAlignDistributed = -4117
xlHAlignFill = 5
xlHAlignGeneral = 1
xlHAlignJustify = -4130
xlHAlignLeft = -4131
xlHAlignRight = -4152
"""
path = "foo.xlsx"
with xw.App() as app:
wb = xw.Book(path)
ws = wb.sheets[0]
# Align text vertically
ws.range(1, 1).api.VerticalAlignment = -4160
ws.range(1, 2).api.VerticalAlignment = VAlign.xlVAlignCenter
ws.range(1, 3).api.VerticalAlignment = VAlign.xlVAlignBottom
# Align text horizontally
ws.range(2, 1).api.HorizontalAlignment = HAlign.xlHAlignLeft
ws.range(2, 2).api.HorizontalAlignment = HAlign.xlHAlignCenter
ws.range(2, 3).api.HorizontalAlignment = -4152
wb.save(path)
wb.close()

Related

Excel alert and problem found when applying conditional formatting to an xlsx file with openpyxl

My goal is to add conditional formatting to a dataset, more specifically I want to make every row blue where the total profit is less than 70 thousand. When applying the below code on the dataset the Excel gives an alert on opening and the formatting is not applied to the file:
The dataset is as follows. 'A1 : M101'
I am using the Pycharm IDE, the latest version of openpyxl (3.0.10), other formatting rules work like number_format and I have reduced the code to the below where the issue shows up. Does anyone know why this issue shows up and how I can fix it or work around it using Python?
import openpyxl
from openpyxl.styles import PatternFill, colors
from openpyxl.styles.differential import DifferentialStyle
from openpyxl.formatting.rule import Rule
work_book = openpyxl.load_workbook('datasets/sales_record.xlsx')
sheet = work_book.active
blue_background = PatternFill(bgColor=colors.BLUE)
diff_style = DifferentialStyle(fill=blue_background)
rule = Rule(type='expression', dxf=diff_style)
rule.formula = ["$1M<70000"]
sheet.conditional_formatting.add(sheet.calculate_dimension(), rule)
work_book.save('workbooks/filename.xlsx')
I have also tried the below but I am unsure if the issue is with the color or some of the other formatting:
a_background = PatternFill(bgColor="FFC7CE", fill_type = "solid")
Your corrupted workbook is due to the formula
rule.formula = ["$1M<70000"]
It may just be a typo transposing the 1 and M however you don't want the Header row included anyway so this should be
rule.formula = ["$M2<70000"]
The cell range set would also offset the formatting. It should exclude the Headers as well, so the start of the range should be 'A2'. Therefore the conditional formatting range should cover 'A2:MXX' where XX is the last row.
The changes to your code shown below will then highlight the whole row from Col A to M where the Total_Profit is less than 70,000.
I would also suggest using a different highlight colour than BLUE since it's too dark to see the text.
import openpyxl
from openpyxl.styles import PatternFill, colors
from openpyxl.styles.differential import DifferentialStyle
from openpyxl.formatting.rule import Rule
work_book = openpyxl.load_workbook('sales_record.xlsx')
sheet = work_book.active
blue_background = PatternFill(bgColor=colors.BLUE)
diff_style = DifferentialStyle(fill=blue_background)
rule = Rule(type='expression', dxf=diff_style)
rule.formula = ["$M2<70000"]
# sheet.conditional_formatting.add(sheet.calculate_dimension(), rule)
cf_range = '$A2:$' + sheet.calculate_dimension().split(':')[1]
sheet.conditional_formatting.add(cf_range, rule)
work_book.save('filename.xlsx')

Approch to merge a template with header and Items with Data for each entry

I'm trying to learn Python and find a solution for my business.
I'm working on SAP and i need to merge data to fill a template.
Doing the merge based on Excel VBA, it's working but to fill a file with 10 K entries it's take a very long time.
My template is avaiable here
https://docs.google.com/spreadsheets/d/1FXc-4zUYx0fjGRvPf0FgMjeTm9nXVfSt/edit?usp=sharing&ouid=113964169462465283497&rtpof=true&sd=true
And a sample of data is here
https://drive.google.com/file/d/105FP8ti0xKbXCFeA2o5HU7d2l3Qi-JqJ/view?usp=sharing
So I need to merge for each record from my data file into the Excel template where we have an header and 2 lines (it's a FI posting so I need to fill the debit and credit.
In VBA, I have proceed like that:
Fix the cell:
Copy data from the template with function activecell.offset(x,y) ...
From my Data file fill the different record based on technical name.
Now I'm trying the same in Python.
Using Pandas or openpyxyl I can open the file but I can't see how can I continue or proceed to find a way to merge header data (must be copy for eache posting I have to book) and data.
from tkinter import *
import pandas as pd
import datetime
from openpyxl import load_workbook
import numpy as np
def sap_line_item(ligne):
ledger = ligne
print(ligne)
return
# Constante
c_dir = '/Users/sapfinance/PycharmProjects/SAP'
C_FILE_SEP = ';'
root = Tk()
root.withdraw()
# folder_selected = filedialog.askdirectory(initialdir=c_dir)
fiori_selected = filedialog.askopenfile(initialdir=c_dir)
data_selected = filedialog.askopenfile(initialdir=c_dir)
# read data
pd.options.display.float_format = '{:,.2f}'.format
fichier_cible = str(data_selected.name)
target_filename = fichier_cible + '_' + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + '.xlsx'
# target = pd.ExcelWriter(target_filename, engine='xlsxwriter')
df_full_data = pd.read_csv(data_selected.name, sep=C_FILE_SEP, encoding='unicode_escape', dtype='unicode')
nb_ligne_data = int(len(df_full_data))
print(nb_ligne_data)
#df_fiori = pd.read_excel(fiori_selected.name)
print(fiori_selected.name)
df_fiori = load_workbook(fiori_selected.name)
df_fiori_data = df_fiori.active
Any help to give some tick to approach and find a solution will be appreciate.
Have a great day
Philippe

Dynamically update openpyxl generated charts when sorting columns

I'm using openpyxl 2.4.8 to generate some excel files. I fill in some data into some columns and insert a chart that plots that data. If I do so manually in excel the charts dynamically update if I use the data-sort methods to remove datapoints. However, the openpyxl generated chart is static and ignore any such sorting.
When looking at the xml of a chart generate by excel and of the one generated by openpyxl I see a lot of differences (fx. all tags are prefaced by 'c:' in excel), but nothing that looks like a setting which would automatically update the content. I cannot find a setting in excel that would turn this on or off either.
The code I use to generate an excel file with a chart is here:
import numpy as np
from openpyxl import *
from random import random
from openpyxl.utils.cell import get_column_letter
from openpyxl.chart import (
LineChart,
BarChart,
ScatterChart,
Reference,
Series,
)
from openpyxl.drawing.text import CharacterProperties
wb = Workbook()
ws = wb.create_sheet()
ws.title = 'interactiveChart'
num = 9
ws.cell(column=1, row=2, value='X')
ws.cell(column=2, row=2, value='Y')
for i in range(num+1):
ws.cell(column=1, row=3+i, value=random()*100)
ws.cell(column=2, row=3+i, value='=A{0}*3+4+ABS(5/(11-A{0}))+ABS(10/(35- A{0}))+ABS(30/(67-A{0}))'.format(3+i))
textSize = 10
modeChart = ScatterChart()
modeChart.title = 'Resonance'
modeChart.title.tx.rich.p[0].r.rPr = CharacterProperties(sz=textSize*100, b=True)
modeChart.style = 48
modeChart.x_axis.title = "X"
modeChart.x_axis.title.tx.rich.p[0].r.rPr = CharacterProperties(sz=textSize*100, b=True)
modeChart.y_axis.title = "Y"
modeChart.y_axis.title.tx.rich.p[0].r.rPr = CharacterProperties(sz=textSize*100, b=True)
modeChart.legend = None
xvalues = Reference(ws, min_col=1, min_row=2, max_row=num+3)
yvalues = Reference(ws, min_col=2, min_row=2, max_row=num+3)
series = Series(yvalues, xvalues, title_from_data=False, title='Resonace')
modeChart.series.append(series)
s1 = modeChart.series[0]
s1.marker.symbol = "diamond"
s1.marker.graphicalProperties.solidFill = "6495ED"
s1.marker.graphicalProperties.line.solidFill = "6495ED"
s1.graphicalProperties.line.noFill = True
modeChart.x_axis.tickLblPos = "low"
modeChart.y_axis.tickLblPos = "low"
modeChart.width = 12
modeChart.height = 7
ws.add_chart(modeChart, "F6")
ws.auto_filter.ref = 'A2:B{}'.format(num+3)
ws = wb.get_sheet_by_name("Sheet")
wb.remove_sheet(ws)
wb.save('aTest.xlsx')
I can't find a reference to this behavior so I'm not certain what I should be looking for either.
This cannot be done directly in openpyxl.
openpyxl is a file format library and not a replacement for Excel. Filtering and sorting needs to be done by an application such as Excel, openpyxl just maintains the relevant metadata.
I found the solution. There is a tag in the xml of Excel charts called: <plotVisOnly/>. This tag was added in openpyxl 2.5 (see the bitbucket issue: Setting property plotVisOnly for charts).
Thanks to a friendly openpyxl contributor for helping me solve this.
In order to make this work with my anaconda installation I first removed openpyxl:
pip uninstall openpyxl
Then i downloaded the latest openpyxl version (>2.5) from python.org package index, unzipped it and ran:
python setup.py install
This installed the >2.5 version of openpyxl. I double checked this in python by running:
import openpyxl
openpyxl.__version__
Hope this helps someone!

Load existing Excel file, change the zoom level for all sheets, and remove gridlines

I'm using win32com to load an existing Excel doc. I'm currently able to loop through the sheets and remove text wrapping and autofit columns.
import win32com.client
excel = win32com.client.Dispatch('Excel.Application')
excel.Visible = False # I want to keep it this way
path = "C:\\Users\\username\\Documents\\DDA"
wb_new = excel.Workbooks.Open(path + '\\new_file.xlsx')
# wb_new is a file with three tabs, with one value in cell A1 each...
# ...if you want to recreate it.
active_sheets = wb_new.Sheets.Count
for i in range(0, active_sheets):
ws = wb_new.Worksheets(i + 1)
ws.Columns.WrapText = False
ws.Columns.AutoFit()
Next, I'd like to adjust the zoom level and remove gridlines for all sheets. I haven't found a solution that accomplishes this without making excel.Visible = True. I'm open to using a package from python-excel.org but I haven't found anything.
I came across the openpyxl.worksheet.views subpackage which contains the SheetView class, but it doesn't seem to be useful for existing documents.
import win32com.client
excel = win32com.client.Dispatch('Excel.Application')
excel.Visible = False # I want to keep it this way
path = "C:\\Users\\username\\Documents\\DDA"
wb_new = excel.Workbooks.Open(path + '\\new_file.xlsx')
# wb_new is a file with three tabs, with one value in cell A1 each...
# ...if you want to recreate it.
active_sheets = wb_new.Sheets.Count
for i in range(0, active_sheets):
ws = wb_new.Worksheets(i + 1)
ws.Columns.WrapText = False
ws.Columns.AutoFit()
ws.Activate() # answer starts here
excel.ActiveWindow.Zoom = 80
excel.ActiveWindow.DisplayGridlines = False

Setting styles in Openpyxl

I need advice on setting styles in Openpyxl.
I see that the NumberFormat of a cell can be set, but I also require setting of font colors and attributes (bold etc). There is a style.py class but it seems I can't set the style attribute of a cell, and I don't really want to start tinkering with the openpyxl source code.
Has anyone found a solution to this?
As of openpyxl version 1.5.7, I have successfully applied the following worksheet style options...
from openpyxl.reader.excel import load_workbook
from openpyxl.workbook import Workbook
from openpyxl.styles import Color, Fill
from openpyxl.cell import Cell
# Load the workbook...
book = load_workbook('foo.xlsx')
# define ws here, in this case I pick the first worksheet in the workbook...
# NOTE: openpyxl has other ways to select a specific worksheet (i.e. by name
# via book.get_sheet_by_name('someWorksheetName'))
ws = book.worksheets[0]
## ws is a openpypxl worksheet object
_cell = ws.cell('C1')
# Font properties
_cell.style.font.color.index = Color.GREEN
_cell.style.font.name = 'Arial'
_cell.style.font.size = 8
_cell.style.font.bold = True
_cell.style.alignment.wrap_text = True
# Cell background color
_cell.style.fill.fill_type = Fill.FILL_SOLID
_cell.style.fill.start_color.index = Color.DARKRED
# You should only modify column dimensions after you have written a cell in
# the column. Perfect world: write column dimensions once per column
#
ws.column_dimensions["C"].width = 60.0
FYI, you can find the names of the colors in openpyxl/style.py... I sometimes I patch in extra colors from the X11 color names
class Color(HashableObject):
"""Named colors for use in styles."""
BLACK = 'FF000000'
WHITE = 'FFFFFFFF'
RED = 'FFFF0000'
DARKRED = 'FF800000'
BLUE = 'FF0000FF'
DARKBLUE = 'FF000080'
GREEN = 'FF00FF00'
DARKGREEN = 'FF008000'
YELLOW = 'FFFFFF00'
DARKYELLOW = 'FF808000'
For openpyxl version 2.4.1 and above use below code to set font color:
from openpyxl.styles import Font
from openpyxl.styles.colors import Color
ws1['A1'].font = Font(color = "FF0000")
hex codes for various colors can be found at:
http://dmcritchie.mvps.org/excel/colors.htm
As of openpyxl 2.0, styles are immutable.
If you have a cell, you can (e.g.) set bold text by:
cell.style = cell.style.copy(font=cell.style.font.copy(bold=True))
Yes, this is annoying.
As of openpyxl 2.0, setting cell styles is done by creating new style objects and by assigning them to properties of a cell.
There are several style objects: Font, PatternFill, Border, and Alignment. See the doc.
To change a style property of a cell, first you either have to copy the existing style object from the cell and change the value of the property or you have to create a new style object with the desired settings. Then, assign the new style object to the cell.
Example of setting the font to bold and italic of cell A1:
from openpyxl import Workbook
from openpyxl.styles import Font
# Create workbook
wb = Workbook()
# Select active sheet
ws = wb.active()
# Select cell A1
cell = ws['A1']
# Make the text of the cell bold and italic
cell.font = cell.font.copy(bold=True, italic=True)
This seems like a feature that has changed a few times. I am using openpyxl 2.5.0, and I was able to set the strike-through option this way:
new_font = copy(cell.font)
new_font.strike = True
cell.font = new_font
It seems like earlier versions (1.9 to 2.4?) had a copy method on the font that is now deprecated and raises a warning:
cell.font = cell.font.copy(strike=True)
Versions up to 1.8 had mutable fonts, so you could just do this:
cell.font.strike=True
That now raises an error.
New 2021 Updated Way of Changing FONT in OpenPyXl:
sheet.cell.font = Font(size=23, underline='single', color='FFBB00', bold=True, italic=True)
Full Code:
import openpyxl # Connect the library
from openpyxl import Workbook
from openpyxl.styles import PatternFill # Connect cell styles
from openpyxl.workbook import Workbook
from openpyxl.styles import Font, Fill # Connect styles for text
from openpyxl.styles import colors # Connect colors for text and cells
wb = openpyxl.Workbook() # Create book
work_sheet = wb.create_sheet(title='Testsheet') # Created a sheet with a name and made it active
work_sheet['A1'] = 'Test text'
work_sheet_a1 = work_sheet['A5'] # Created a variable that contains cell A1 with the existing text
work_sheet_a1.font = Font(size=23, underline='single', color='FFBB00', bold=True,
italic=True) # We apply the following parameters to the text: size - 23, underline, color = FFBB00 (text color is specified in RGB), bold, oblique. If we do not need a bold font, we use the construction: bold = False. We act similarly if we do not need an oblique font: italic = False.
# Important:
# if necessary, the possibility of using standard colors is included in the styles, but the code in this case will look different:
work_sheet_a1.font = Font(size=23, underline='single', color=colors.RED, bold=True,
italic=True) # what color = colors.RED — color prescribed in styles
work_sheet_a1.fill = PatternFill(fill_type='solid', start_color='ff8327',
end_color='ff8327') # This code allows you to do design color cells
Like openpyxl doc said:
This is an open source project, maintained by volunteers in their spare time. This may well mean that particular features or functions that you would like are missing.
I checked openpyxl source code, found that:
Till openpyxl 1.8.x, styles are mutable. Their attribute can be assigned directly like this:
from openpyxl.workbook import Workbook
from openpyxl.style import Color
wb = Workbook()
ws = wb.active
ws['A1'].style.font.color.index = Color.RED
However from of openpyxl 1.9, styles are immutable.
Styles are shared between objects and once they have been assigned they cannot be changed. This stops unwanted side-effects such as changing the style for lots of cells when instead of only one.
To create a new style object, you can assign it directly, or copy one from an existing cell's style with new attributes, answer to the question as an example(forgive my Chinese English):
from openpyxl.styles import colors
from openpyxl.styles import Font, Color
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
a1 = ws['A1']
d4 = ws['D4']
# create a new style with required attributes
ft_red = Font(color=colors.RED)
a1.font = ft_red
# you can also do it with function copy
ft_red_bold = ft_red.copy(bold=True)
# you can copy from a cell's style with required attributes
ft_red_sigle_underline = a1.font.copy(underline="single")
d4.font = ft_red_bold
# apply style to column E
col_e = ws.column_dimensions['E']
col_e.font = ft_red_sigle_underline
A cell' style contains these attributes: font, fill, border, alignment, protection and number_format. Check openpyxl.styles.
They are similar and should be created as an object, except number_format, its value is string type.
Some pre-defined number formats are available, number formats can also be defined in string type. Check openpyxl.styles.numbers.
from openpyxl.styles import numbers
# use pre-defined values
ws.cell['T49'].number_format = numbers.FORMAT_GENERAL
ws.cell(row=2, column=4).number_format = numbers.FORMAT_DATE_XLSX15
# use strings
ws.cell['T57'].number_format = 'General'
ws.cell(row=3, column=5).number_format = 'd-mmm-yy'
ws.cell['E5'].number_format = '0.00'
ws.cell['E50'].number_format = '0.00%'
ws.cell['E100'].number_format = '_ * #,##0_ ;_ * -#,##0_ ;_ * "-"??_ ;_ #_ '
As of openpyxl-1.7.0 you can do this too:
cell.style.fill.start_color.index = "FF124191"
I've got a couple of helper functions which set a style on a given cell - things like headers, footers etc.
You can define a common style then you can apply the same to any cell or range.
Define Style:
Apply on a cell.
This worked for me (font colour + bold font):
from openpyxl.styles import colors
from openpyxl.styles import Font, Color
from openpyxl import Workbook
wb = Workbook()
ws = wb['SheetName']
ws.cell(row_number,column_number).font = Font(color = "0000FF",bold = True)

Categories