质量模块和库存扫码
This commit is contained in:
BIN
stock_barcode/static/description/icon.png
Normal file
BIN
stock_barcode/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
1
stock_barcode/static/description/icon.svg
Normal file
1
stock_barcode/static/description/icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="98.162%" x2="0%" y1="1.838%" y2="100%"><stop offset="0%" stop-color="#797DA5"/><stop offset="50.799%" stop-color="#6D7194"/><stop offset="100%" stop-color="#626584"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M32.008 69H4c-2 0-4-1-4-4V35.936l11.093-11.422L15.61 19 33 13h4l18 6 3.928 5.93-4.884 5.147-.077 12.337L32.008 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" d="M40 39h1v5h-2v-5h1zm-7 0h1v5h-2v-5h1zm-3 0h1v5h-2v-5h1zm4 16h-.38a.293.293 0 0 1-.145-.04l-18.24-10.322c-.142-.08-.235-.27-.235-.481V30.589c0-.168.059-.325.159-.423.1-.098.227-.123.343-.07 0 0 12.918 6.018 13.27 6.185.297.139.488-.212.488-.212l4.065-6.876C33.426 29.021 34 29 34 29h1s.574.021.675.193l4.065 6.876s.191.35.487.211c.353-.166 13.27-6.184 13.27-6.184.117-.054.245-.028.344.07.1.098.159.255.159.423v13.568c0 .21-.093.4-.235.48L35.525 54.96a.292.292 0 0 1-.145.04H34zM22 38v7h25v-7H22zm2 6v-5h1v5h-1zm3 0v-5h1v5h-1zm8 0v-5h1v5h-1zm2 0v-5h1v5h-1zm5 0v-5h1v5h-1zm2 0v-5h1v5h-1zm14.925-17.582a.518.518 0 0 1 .058.402.423.423 0 0 1-.24.292L41.708 33.97a.325.325 0 0 1-.121.024.359.359 0 0 1-.294-.166l-4.769-6.972-1.413.461-.021.005v.001a.32.32 0 0 1-.2 0l-1.414-.461-4.769 6.972a.36.36 0 0 1-.294.166.325.325 0 0 1-.121-.024l-17.035-6.858a.423.423 0 0 1-.24-.292.518.518 0 0 1 .058-.402l3.466-5.494a.384.384 0 0 1 .2-.163l20.146-6.747a.39.39 0 0 1 .226-.006l20.146 6.747c.08.027.15.084.2.163l3.466 5.494zM35.01 25.413l14.426-4.688L35.01 15.92l-14.447 4.81 14.447 4.682z" opacity=".3"/><path fill="#FFF" d="M40 37h1v5h-2v-5h1zm-7 0h1v5h-2v-5h1zm-3 0h1v5h-2v-5h1zm6 6h11v-7H36v-9h.9s.516.021.608.193l3.658 6.876s.172.35.438.211c.318-.166 11.944-6.184 11.944-6.184.104-.054.22-.028.31.07.089.098.142.255.142.423v13.568c0 .21-.083.4-.211.48L37.373 52.96a.243.243 0 0 1-.131.04H36V43zm-2-7H22v7h12v10h-.38a.293.293 0 0 1-.145-.04l-18.24-10.322c-.142-.08-.235-.27-.235-.481V28.589c0-.168.059-.325.159-.423.1-.098.227-.123.343-.07 0 0 12.918 6.018 13.27 6.185.297.139.488-.212.488-.212l4.065-6.876C33.426 27.021 34 27 34 27v9zm-10 6v-5h1v5h-1zm3 0v-5h1v5h-1zm8 0v-5h1v5h-1zm2 0v-5h1v5h-1zm5 0v-5h1v5h-1zm2 0v-5h1v5h-1zm14.925-17.582a.518.518 0 0 1 .058.402.423.423 0 0 1-.24.292L41.708 31.97a.325.325 0 0 1-.121.024.359.359 0 0 1-.294-.166l-4.769-6.972-1.413.461-.021.005v.001a.32.32 0 0 1-.2 0l-1.414-.461-4.769 6.972a.36.36 0 0 1-.294.166.325.325 0 0 1-.121-.024l-17.035-6.858a.423.423 0 0 1-.24-.292.518.518 0 0 1 .058-.402l3.466-5.494a.384.384 0 0 1 .2-.163l20.146-6.747a.39.39 0 0 1 .226-.006l20.146 6.747c.08.027.15.084.2.163l3.466 5.494zM35.01 23.413l14.426-4.688L35.01 13.92l-14.447 4.81 14.447 4.682z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
20
stock_barcode/static/img/barcode.svg
Normal file
20
stock_barcode/static/img/barcode.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="400" height="300" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<metadata id="metadata27">image/svg+xml</metadata>
|
||||
<g class="layer">
|
||||
<title>Barcode Icon</title>
|
||||
<g id="g20">
|
||||
<g id="g12" fill="#8f8f8f">
|
||||
<rect height="186" id="rect2" width="44" x="61" y="57"/>
|
||||
<rect height="186" id="rect4" width="44" x="236.5" y="57"/>
|
||||
<rect height="186" id="rect6" width="44" x="295" y="57"/>
|
||||
<rect height="186" id="rect8" width="24" x="129.5" y="57"/>
|
||||
<rect height="186" id="rect10" width="14" x="193" y="57"/>
|
||||
</g>
|
||||
<g id="g18" fill="#8f8f8f">
|
||||
<polygon id="polygon14" points="59.5,269.5 25.5,269.5 25.5,30.5 59.5,30.5 59.5,19.5 14.5,19.5 14.5,280.5 59.5,280.5"/>
|
||||
<polygon id="polygon16" points="339.4999694824219,19.5 339.4999694824219,30.5 374.4999694824219,30.5 374.4999694824219,269.5 339.4999694824219,269.5 339.4999694824219,280.5 385.4999694824219,280.5 385.4999694824219,19.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
stock_barcode/static/img/barcode_white.png
Normal file
BIN
stock_barcode/static/img/barcode_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
stock_barcode/static/img/barcodes_actions.pdf
Normal file
BIN
stock_barcode/static/img/barcodes_actions.pdf
Normal file
Binary file not shown.
BIN
stock_barcode/static/img/barcodes_demo.pdf
Normal file
BIN
stock_barcode/static/img/barcodes_demo.pdf
Normal file
Binary file not shown.
117
stock_barcode/static/img/make_barcodes.py
Normal file
117
stock_barcode/static/img/make_barcodes.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# Note to run this script (specifically for datamatrix generation) you will need the following installed:
|
||||
# - dmtx-utils (available through apt-get)
|
||||
# - pylibdmtx (available on pip3)
|
||||
# - reportlab version 3.5.52 or higher (available on pip3)
|
||||
|
||||
|
||||
from io import BytesIO
|
||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
||||
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode import dmtx
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4
|
||||
|
||||
|
||||
OTHER_DEMO_FILENAME = "barcodes_demo.pdf"
|
||||
FONT = "Helvetica"
|
||||
HEADER_FONT_SIZE = 16
|
||||
LABEL_FONT_SIZE = 14
|
||||
TITLE_FONT_SIZE = 11
|
||||
CODE_FONT_SIZE = 8
|
||||
FOOTER_FONT_SIZE = 8
|
||||
PAGE_SIZE = A4
|
||||
|
||||
|
||||
def create_page(barcodes, font_size_and_texts):
|
||||
packet = BytesIO()
|
||||
can = canvas.Canvas(packet, pagesize=PAGE_SIZE)
|
||||
for barcode in barcodes:
|
||||
d = Drawing(PAGE_SIZE[0], PAGE_SIZE[1])
|
||||
d.add(dmtx.DataMatrixWidget(barcode[2]))
|
||||
renderPDF.draw(d, can, barcode[0], barcode[1])
|
||||
|
||||
# add text
|
||||
for font_size, texts in font_size_and_texts:
|
||||
can.setFont(FONT, font_size)
|
||||
for text in texts:
|
||||
can.drawString(text[0], text[1], text[2])
|
||||
can.save()
|
||||
packet.seek(0)
|
||||
return PdfFileReader(packet)
|
||||
|
||||
|
||||
# same on all GS1 pages
|
||||
footer = [(45, 45, "Don't have any barcode scanner? Right click on your screen > Inspect > Console and type the following command:"),
|
||||
(45, 35, ' odoo.__DEBUG__.services["web.core"].bus.trigger("barcode_scanned", "setyourbarcodehere", $(".o_web_client")[0])'),
|
||||
(45, 25, 'and replace "setyourbarcodehere" by the barcode you would like to scan OR use our mobile app.'),
|
||||
(45, 15, 'For GS1 barcodes, remove all "("s and ")"s')]
|
||||
|
||||
# page 1
|
||||
# format is (x, y, "barcode")
|
||||
# barcodes to generate
|
||||
barcodes = [(65, 635, "WH-RECEIPTS"), (250, 623, "0106016478556677300000000510Lot0001"), (435, 623, "0106016478556677300000000510Lot0002"),
|
||||
(65, 490, "0106016478556677300000000510Lot0003"), (250, 500, "O-BTN.validate"),
|
||||
(65, 345, "WH-RECEIPTS"), (250, 345, "01060164785599993650000010"), (435, 345, "01060164785599823160000002"),
|
||||
(65, 215, "O-BTN.validate")]
|
||||
|
||||
header = [(45, 785, "GS1 Barcodes (set Barcode Nomenclature to Default GS1 Nomenclature)")]
|
||||
# text to describe barcode flow
|
||||
flow_labels = [(45, 752, "Receive products tracked by lot (activate Lots & Serial Numbers) "),
|
||||
(45, 460, "Receive products with different unit of measures (activate Units of Measure)")]
|
||||
|
||||
# text to describe (above) each barcode
|
||||
barcode_titles = [(55, 733, "YourCompany Receipts"), (215, 733, "Cable Management Box x5 Lot0001"), (400, 733, "Cable Management Box x5 Lot0002"),
|
||||
(45, 600, "Cable Management Box x5 Lot0003"), (280, 600, "Validate"),
|
||||
(55, 444, "YourCompany Receipts"), (230, 444, "Customized Cabinet (USA) 10 ft³"), (415, 444, "Customized Cabinet (Metric) 2 m³"),
|
||||
(93, 315, "Validate")]
|
||||
|
||||
# text of barcode code (below), but made to look pretty (not literal barcode)
|
||||
barcode_code = [(85, 631, "WH-RECEIPTS"), (220, 619, "(01)06016478556677(30)00000005(10)Lot0001"), (405, 619, "(01)06016478556677(30)00000005(10)Lot0002"),
|
||||
(45, 485, "(01)06016478556677(30)00000005(10)Lot0003"), (275, 495, "O-BTN.validate"),
|
||||
(85, 340, "WH-RECEIPTS"), (238, 340, "(01)06016478559999(3650)000010"), (425, 340, "(01)06016478559982(3160)000002"),
|
||||
(85, 212, "O-BTN.validate")]
|
||||
|
||||
|
||||
font_size_and_texts = [(HEADER_FONT_SIZE, header),
|
||||
(LABEL_FONT_SIZE, flow_labels),
|
||||
(TITLE_FONT_SIZE, barcode_titles),
|
||||
(CODE_FONT_SIZE, barcode_code),
|
||||
(FOOTER_FONT_SIZE, footer)]
|
||||
|
||||
page1 = create_page(barcodes, font_size_and_texts)
|
||||
|
||||
# page 2
|
||||
# format is (x, y, "barcode")
|
||||
# barcodes to generate
|
||||
barcodes = [(65, 635, "WH-RECEIPTS"), (250, 635, "0106016478556332305"), (435, 635, "0006016471234567890591PAL"),
|
||||
(65, 500, "O-BTN.validate")]
|
||||
|
||||
header = []
|
||||
# text to describe barcode flow
|
||||
flow_labels = [(45, 752, "Put in Pack (activate Packages)")]
|
||||
|
||||
# text to describe (above) each barcode
|
||||
barcode_titles = [(55, 733, "YourCompany Receipts"), (233, 733, "Individual Workplace x10"), (410, 733, "Put in Pack: Pallet with SSCC"),
|
||||
(93, 600, "Validate")]
|
||||
|
||||
# text of barcode code (below), but made to look pretty (not literal barcode)
|
||||
barcode_code = [(85, 631, "WH-RECEIPTS"), (245, 631, "(01)06016478556332(30)10"), (425, 631, "(00)060164712345678905(91)PAL"),
|
||||
(85, 495, "O-BTN.validate")]
|
||||
|
||||
font_size_and_texts = [(HEADER_FONT_SIZE, header),
|
||||
(LABEL_FONT_SIZE, flow_labels),
|
||||
(TITLE_FONT_SIZE, barcode_titles),
|
||||
(CODE_FONT_SIZE, barcode_code),
|
||||
(FOOTER_FONT_SIZE, footer)]
|
||||
|
||||
page2 = create_page(barcodes, font_size_and_texts)
|
||||
|
||||
|
||||
# merge with other demo barcodes
|
||||
merger = PdfFileMerger()
|
||||
merger.append(PdfFileReader(open(OTHER_DEMO_FILENAME, "rb")))
|
||||
merger.append(page1)
|
||||
merger.append(page2)
|
||||
merger.write(OTHER_DEMO_FILENAME)
|
||||
192
stock_barcode/static/img/make_barcodes.sh
Normal file
192
stock_barcode/static/img/make_barcodes.sh
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/bin/sh
|
||||
|
||||
barcode -t 2x7+40+40 -m 50x30 -p "210x297mm" -e code128b -n > barcodes_actions_barcode.ps << BARCODES
|
||||
O-CMD.MAIN-MENU
|
||||
O-CMD.DISCARD
|
||||
O-BTN.validate
|
||||
O-CMD.cancel
|
||||
O-BTN.print-op
|
||||
O-BTN.print-slip
|
||||
O-BTN.pack
|
||||
O-BTN.scrap
|
||||
O-BTN.record-components
|
||||
O-CMD.PREV
|
||||
O-CMD.NEXT
|
||||
O-CMD.PAGER-FIRST
|
||||
O-CMD.PAGER-LAST
|
||||
BARCODES
|
||||
|
||||
cat > barcodes_actions_header.ps << HEADER
|
||||
/showTitle { /Helvetica findfont 12 scalefont setfont moveto show } def
|
||||
(MAIN MENU) 89 768 showTitle
|
||||
(DISCARD) 348 768 showTitle
|
||||
(VALIDATE) 89 660 showTitle
|
||||
(CANCEL) 348 660 showTitle
|
||||
(PRINT PICKING OPERATION) 89 551 showTitle
|
||||
(PRINT DELIVERY SLIP) 348 551 showTitle
|
||||
(PUT IN PACK) 89 444 showTitle
|
||||
(SCRAP) 348 444 showTitle
|
||||
(RECORD COMPONENTS) 89 337 showTitle
|
||||
(PREVIOUS PAGE) 348 337 showTitle
|
||||
(NEXT PAGE) 89 230 showTitle
|
||||
(FIRST PAGE) 348 230 showTitle
|
||||
(LAST PAGE) 89 123 showTitle
|
||||
|
||||
HEADER
|
||||
|
||||
cat barcodes_actions_header.ps barcodes_actions_barcode.ps | ps2pdf - - > barcodes_actions.pdf
|
||||
rm barcodes_actions_header.ps barcodes_actions_barcode.ps
|
||||
|
||||
# pg 1 of demo barcodes due to ps headers being restricted to 1 page. Some blanks may exist due to flows having a rows with less than 3 barcodes.
|
||||
barcode -t 3x7+20+35 -m 25x30 -p "210x297mm" -e code128b -n > barcodes_demo_barcode_pg_1.ps << BARCODES
|
||||
WH-RECEIPTS
|
||||
601647855638
|
||||
O-BTN.validate
|
||||
WH/OUT/00005
|
||||
601647855644
|
||||
O-BTN.validate
|
||||
WH-RECEIPTS
|
||||
601647855640
|
||||
601647855631
|
||||
LOT-000002
|
||||
LOT-000003
|
||||
O-BTN.validate
|
||||
WH-STOCK
|
||||
601647855649
|
||||
2601892
|
||||
O-BTN.validate
|
||||
|
||||
|
||||
WH-RECEIPTS
|
||||
601647855650
|
||||
O-BTN.pack
|
||||
BARCODES
|
||||
|
||||
# blank lines included for easier visual matching to barcode spacing
|
||||
cat > barcodes_demo_header_pg_1.ps << HEADER
|
||||
/showLabel { /Helvetica findfont 14 scalefont setfont moveto show } def
|
||||
/showTitle { /Helvetica findfont 11 scalefont setfont moveto show } def
|
||||
/showCode { /Helvetica findfont 8 scalefont setfont moveto show } def
|
||||
/showFooter { /Helvetica findfont 8 scalefont setfont moveto show } def
|
||||
(Receive products in stock) 45 797 showLabel
|
||||
(YourCompany Receipts) 45 777 showTitle
|
||||
(WH-RECEIPTS) 85 718 showCode
|
||||
(Desk Stand with Screen) 230 777 showTitle
|
||||
(601647855638) 271 718 showCode
|
||||
(Validate) 415 777 showTitle
|
||||
(O-BTN.validate) 456 718 showCode
|
||||
(Deliver products to your customers) 45 687 showLabel
|
||||
(WH/OUT/00005) 45 667 showTitle
|
||||
(WH/OUT/00005) 85 608 showCode
|
||||
(Desk Combination) 230 667 showTitle
|
||||
(601647855644) 271 608 showCode
|
||||
(Validate) 415 667 showTitle
|
||||
(O-BTN.validate) 456 608 showCode
|
||||
(Receive products tracked by lot number (activate Lots & Serial Numbers)) 45 577 showLabel
|
||||
(YourCompany Receipts) 45 557 showTitle
|
||||
(WH-RECEIPTS) 85 498 showCode
|
||||
(Corner Desk Black) 230 557 showTitle
|
||||
(601647855640) 271 498 showCode
|
||||
(Cable Management Box) 415 557 showTitle
|
||||
(601647855631) 456 498 showCode
|
||||
(LOT-000002) 45 447 showTitle
|
||||
(LOT-000002) 85 388 showCode
|
||||
(LOT-000003) 230 447 showTitle
|
||||
(LOT-000003) 271 388 showCode
|
||||
(Validate) 415 447 showTitle
|
||||
(O-BTN.validate) 456 388 showCode
|
||||
(Internal transfer (activate Storage Locations)) 45 357 showLabel
|
||||
(WH/Stock) 45 337 showTitle
|
||||
(WH-STOCK) 85 278 showCode
|
||||
(Pedal Bin) 230 337 showTitle
|
||||
(601647855649) 271 278 showCode
|
||||
(WH/Stock/Shelf1) 415 337 showTitle
|
||||
(2601892) 456 278 showCode
|
||||
(Validate) 45 227 showTitle
|
||||
(O-BTN.validate) 85 168 showCode
|
||||
|
||||
|
||||
(Put in Pack (activate Packages)) 45 137 showLabel
|
||||
(YourCompany Receipts) 45 117 showTitle
|
||||
(WH-RECEIPTS) 85 58 showCode
|
||||
(Large Cabinet) 230 117 showTitle
|
||||
(601647855650) 271 58 showCode
|
||||
(Put in Pack) 415 117 showTitle
|
||||
(O-BTN.pack) 456 58 showCode
|
||||
|
||||
(Don't have any barcode scanner? Right click on your screen > Inspect > Console and type the following command:) 45 35 showFooter
|
||||
( odoo.__DEBUG__.services["web.core"].bus.trigger("barcode_scanned", "setyourbarcodehere", \$(".o_web_client")[0])) 45 25 showFooter
|
||||
(and replace "setyourbarcodehere" by the barcode you would like to scan OR use our mobile app.) 45 15 showFooter
|
||||
HEADER
|
||||
|
||||
# pg 2 of demo barcodes. Some blanks may exist due to flows having a rows with less than 3 barcodes.
|
||||
barcode -t 3x7+20+35 -m 25x30 -p "210x297mm" -e code128b -n > barcodes_demo_barcode_pg_2.ps << BARCODES
|
||||
O-BTN.validate
|
||||
|
||||
|
||||
BATCH/00002
|
||||
601647855637
|
||||
601647855651
|
||||
601647855635
|
||||
O-BTN.validate
|
||||
|
||||
BATCH/00001
|
||||
601647855652
|
||||
CLUSTER-PACK-1
|
||||
601647855653
|
||||
CLUSTER-PACK-1
|
||||
601647855651
|
||||
CLUSTER-PACK-2
|
||||
O-BTN.validate
|
||||
|
||||
BARCODES
|
||||
|
||||
cat > barcodes_demo_header_pg_2.ps << HEADER
|
||||
/showLabel { /Helvetica findfont 14 scalefont setfont moveto show } def
|
||||
/showTitle { /Helvetica findfont 11 scalefont setfont moveto show } def
|
||||
/showCode { /Helvetica findfont 8 scalefont setfont moveto show } def
|
||||
/showFooter { /Helvetica findfont 8 scalefont setfont moveto show } def
|
||||
(Validate) 45 777 showTitle
|
||||
(O-BTN.validate) 85 718 showCode
|
||||
|
||||
|
||||
(Batch picking (activate Batch Pickings)) 45 687 showLabel
|
||||
(BATCH/00002) 45 667 showTitle
|
||||
(BATCH/00002) 85 608 showCode
|
||||
(Large Meeting Table) 230 667 showTitle
|
||||
(601647855637) 271 608 showCode
|
||||
(Four Person Desk) 415 667 showTitle
|
||||
(601647855651) 456 608 showCode
|
||||
(Three-Seat Sofa) 45 557 showTitle
|
||||
(601647855635) 85 498 showCode
|
||||
(Validate) 230 557 showTitle
|
||||
(O-BTN.validate) 271 498 showCode
|
||||
|
||||
(Batch picking with cluster pickings (activate Batch Pickings and Packages)) 45 467 showLabel
|
||||
(BATCH/00001) 45 447 showTitle
|
||||
(BATCH/00001) 85 388 showCode
|
||||
(Cabinet with Doors) 230 447 showTitle
|
||||
(601647855652) 271 388 showCode
|
||||
(CLUSTER-PACK-1) 415 447 showTitle
|
||||
(CLUSTER-PACK-1) 456 388 showCode
|
||||
(Acoustic Bloc Screens) 45 337 showTitle
|
||||
(601647855653) 85 278 showCode
|
||||
(CLUSTER-PACK-1) 230 337 showTitle
|
||||
(CLUSTER-PACK-1) 271 278 showCode
|
||||
(Four Person Desk) 415 337 showTitle
|
||||
(601647855651) 456 278 showCode
|
||||
(CLUSTER-PACK-2) 45 227 showTitle
|
||||
(CLUSTER-PACK-2) 85 168 showCode
|
||||
(Validate) 230 227 showTitle
|
||||
(O-BTN.validate) 271 168 showCode
|
||||
|
||||
(Don't have any barcode scanner? Right click on your screen > Inspect > Console and type the following command:) 45 35 showFooter
|
||||
( odoo.__DEBUG__.services["web.core"].bus.trigger("barcode_scanned", "setyourbarcodehere", \$(".o_web_client")[0])) 45 25 showFooter
|
||||
(and replace "setyourbarcodehere" by the barcode you would like to scan OR use our mobile app.) 45 15 showFooter
|
||||
HEADER
|
||||
|
||||
cat barcodes_demo_header_pg_1.ps barcodes_demo_barcode_pg_1.ps barcodes_demo_header_pg_2.ps barcodes_demo_barcode_pg_2.ps | ps2pdf - - > barcodes_demo.pdf
|
||||
rm barcodes_demo_header_pg_1.ps barcodes_demo_barcode_pg_1.ps
|
||||
rm barcodes_demo_header_pg_2.ps barcodes_demo_barcode_pg_2.ps
|
||||
|
||||
python3 make_barcodes.py
|
||||
21
stock_barcode/static/src/components/grouped_line.js
Normal file
21
stock_barcode/static/src/components/grouped_line.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import LineComponent from "@stock_barcode/components/line";
|
||||
|
||||
export default class GroupedLineComponent extends LineComponent {
|
||||
|
||||
get isSelected() {
|
||||
return this.line.virtual_ids.indexOf(this.env.model.selectedLineVirtualId) !== -1;
|
||||
}
|
||||
|
||||
get opened() {
|
||||
return this.env.model.groupKey(this.line) === this.env.model.unfoldLineKey;
|
||||
}
|
||||
|
||||
toggleSublines(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.toggleSublines(this.line);
|
||||
}
|
||||
}
|
||||
GroupedLineComponent.components = { LineComponent };
|
||||
GroupedLineComponent.template = 'stock_barcode.GroupedLineComponent';
|
||||
28
stock_barcode/static/src/components/grouped_line.scss
Normal file
28
stock_barcode/static/src/components/grouped_line.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.o_barcode_client_action .o_barcode_lines {
|
||||
.o_barcode_line_summary {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
flex-basis: 100%;
|
||||
|
||||
&.o_unfolded {
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sublines .o_barcode_line {
|
||||
border-left-width: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
box-shadow: none;
|
||||
border-bottom: 0;
|
||||
|
||||
&.o_selected {
|
||||
box-shadow: inset 0px 0px 0px 3px $o-enterprise-primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
stock_barcode/static/src/components/grouped_line.xml
Normal file
27
stock_barcode/static/src/components/grouped_line.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="stock_barcode.GroupedLineComponent" owl="1">
|
||||
<div t-on-click="select"
|
||||
class="o_barcode_line list-group-item d-flex flex-row flex-wrap pt-0"
|
||||
t-att-data-barcode="line.product_id.barcode" t-attf-class="{{componentClasses}}">
|
||||
<div class="o_barcode_line_summary d-flex flex-grow-1 py-2 mt-2" t-att-class="opened ? 'o_unfolded': ''">
|
||||
<div class="o_barcode_line_details flex-grow-1">
|
||||
<t t-call="stock_barcode.LineSourceLocation"/>
|
||||
<t t-call="stock_barcode.LineTitle"/>
|
||||
<t t-call="stock_barcode.LineQuantity"/>
|
||||
<t t-call="stock_barcode.LineDestinationLocation"/>
|
||||
</div>
|
||||
<button t-on-click="toggleSublines" class="o_line_button o_toggle_sublines btn btn-primary ms-2 ms-sm-4">
|
||||
<i t-att-class="'fa fa-2x fa-caret-' + (opened ? 'up' : 'down')"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_sublines mb-2 flex-grow-1" t-if="opened">
|
||||
<t t-foreach="line.lines" t-as="subline" t-key="subline.virtual_id">
|
||||
<LineComponent line="subline" displayUOM="props.displayUOM" subline="true"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
125
stock_barcode/static/src/components/line.js
Normal file
125
stock_barcode/static/src/components/line.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { bus } from 'web.core';
|
||||
const { Component } = owl;
|
||||
|
||||
export default class LineComponent extends Component {
|
||||
get destinationLocationPath () {
|
||||
return this._getLocationPath(this.env.model._defaultDestLocation(), this.line.location_dest_id);
|
||||
}
|
||||
|
||||
get displayDestinationLocation() {
|
||||
return !this.props.subline && this.env.model.displayDestinationLocation;
|
||||
}
|
||||
|
||||
get displayResultPackage() {
|
||||
return this.env.model.displayResultPackage;
|
||||
}
|
||||
|
||||
get displaySourceLocation() {
|
||||
return !this.props.subline && this.env.model.displaySourceLocation;
|
||||
}
|
||||
|
||||
get highlightLocation() {
|
||||
return this.env.model.lastScanned.sourceLocation &&
|
||||
this.env.model.lastScanned.sourceLocation.id == this.line.location_id.id;
|
||||
}
|
||||
|
||||
get isComplete() {
|
||||
if (!this.qtyDemand || this.qtyDemand != this.qtyDone) {
|
||||
return false;
|
||||
} else if (this.isTracked && !this.lotName) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get isSelected() {
|
||||
return this.line.virtual_id === this.env.model.selectedLineVirtualId ||
|
||||
(this.line.package_id && this.line.package_id.id === this.env.model.lastScanned.packageId);
|
||||
}
|
||||
|
||||
get isTracked() {
|
||||
return this.line.product_id.tracking !== 'none';
|
||||
}
|
||||
|
||||
get lotName() {
|
||||
return (this.line.lot_id && this.line.lot_id.name) || this.line.lot_name || '';
|
||||
}
|
||||
|
||||
get nextExpected() {
|
||||
if (!this.isSelected) {
|
||||
return false;
|
||||
} else if (this.isTracked && !this.lotName) {
|
||||
return 'lot';
|
||||
} else if (this.qtyDemand && this.qtyDone < this.qtyDemand) {
|
||||
return 'quantity';
|
||||
}
|
||||
}
|
||||
|
||||
get qtyDemand() {
|
||||
return this.env.model.getQtyDemand(this.line);
|
||||
}
|
||||
|
||||
get qtyDone() {
|
||||
return this.env.model.getQtyDone(this.line);
|
||||
}
|
||||
|
||||
get quantityIsSet() {
|
||||
return this.line.inventory_quantity_set;
|
||||
}
|
||||
|
||||
get incrementQty() {
|
||||
return this.env.model.getIncrementQuantity(this.line);
|
||||
}
|
||||
|
||||
get line() {
|
||||
return this.props.line;
|
||||
}
|
||||
|
||||
get requireLotNumber() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get sourceLocationPath() {
|
||||
return this._getLocationPath(this.env.model._defaultLocation(), this.line.location_id);
|
||||
}
|
||||
|
||||
get componentClasses() {
|
||||
return [
|
||||
this.isComplete ? 'o_line_completed' : 'o_line_not_completed',
|
||||
this.env.model.lineIsFaulty(this) ? 'o_faulty' : '',
|
||||
this.isSelected ? 'o_selected o_highlight' : ''
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
_getLocationPath(rootLocation, currentLocation) {
|
||||
let locationName = currentLocation.display_name;
|
||||
if (this.env.model.shouldShortenLocationName) {
|
||||
if (rootLocation && rootLocation.id != currentLocation.id) {
|
||||
const name = rootLocation.display_name;
|
||||
locationName = locationName.replace(name, '...');
|
||||
}
|
||||
}
|
||||
return locationName.replace(new RegExp(currentLocation.name + '$'), '');
|
||||
}
|
||||
|
||||
edit() {
|
||||
bus.trigger('edit-line', { line: this.line });
|
||||
}
|
||||
|
||||
addQuantity(quantity, ev) {
|
||||
this.env.model.updateLineQty(this.line.virtual_id, quantity);
|
||||
}
|
||||
|
||||
select(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.selectLine(this.line);
|
||||
this.env.model.trigger('update');
|
||||
}
|
||||
|
||||
setOnHandQuantity(ev) {
|
||||
this.env.model.setOnHandQuantity(this.line);
|
||||
}
|
||||
}
|
||||
LineComponent.template = 'stock_barcode.LineComponent';
|
||||
128
stock_barcode/static/src/components/line.scss
Normal file
128
stock_barcode/static/src/components/line.scss
Normal file
@@ -0,0 +1,128 @@
|
||||
$o-barcode-completed-color: #befdcc;
|
||||
|
||||
.o_barcode_client_action .o_barcode_lines .o_barcode_line {
|
||||
flex: 0 0 auto;
|
||||
border-width: 1px 0;
|
||||
|
||||
&:first-child {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
box-shadow: 0 3px 10px $gray-300;
|
||||
margin-bottom: 60vh;
|
||||
}
|
||||
|
||||
&_details {
|
||||
.fa:first-child {
|
||||
opacity: 0.5;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_barcode_line_package {
|
||||
.o_barcode_line_details > * {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.o_barcode_line_details > .o_barcode_package_name {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
> span {
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.o_faulty {
|
||||
background-color: rgba(map-get($theme-colors, 'danger'), 0.25);
|
||||
|
||||
&.o_selected {
|
||||
box-shadow: inset 0px 0px 0px 3px map-get($theme-colors, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
&.o_line_completed {
|
||||
background: var(--barcode__line--completed, #befdcc);
|
||||
}
|
||||
|
||||
&.o_line_not_completed {
|
||||
background: var(--barcode__line--notCompleted, #fcf9f2);
|
||||
}
|
||||
|
||||
&.o_selected {
|
||||
box-shadow: inset 0px 0px 0px 3px $o-enterprise-primary-color;
|
||||
}
|
||||
|
||||
.o_barcode_scanner_qty {
|
||||
font-size: 1em;
|
||||
border-color: transparent; // Overwrite default badge color
|
||||
margin-left: -$badge-padding-x; // Compensate badge padding
|
||||
|
||||
&[class*="badge-"] {
|
||||
margin-left: 0; // If a style class is applied, reset compensation margin
|
||||
}
|
||||
|
||||
.qty-done, .inventory_quantity {
|
||||
min-width: 20px;
|
||||
&.o_flash {
|
||||
animation-name: highlighting-flash-primary;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_line_buttons {
|
||||
min-width: 132px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.o_line_button {
|
||||
min-width: 60px;
|
||||
height: 60px;
|
||||
padding: 0 8px;
|
||||
border-radius: 8px;
|
||||
line-height: 16px;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-transform: none;
|
||||
|
||||
&.o_shortcut_displayed {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
@include o-hover-opacity();
|
||||
}
|
||||
|
||||
&.o_set {
|
||||
border: 4px solid $o-brand-primary;
|
||||
color: $o-brand-primary;
|
||||
|
||||
&.o_difference {
|
||||
color: orange;
|
||||
border-color: orange;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background-color: gray;
|
||||
border-color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.o_next_expected {
|
||||
color: #00A09D;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
[name=source_location].o_highlight {
|
||||
background-color: $o-barcode-completed-color;
|
||||
& .fa { opacity: 1; }
|
||||
}
|
||||
}
|
||||
136
stock_barcode/static/src/components/line.xml
Normal file
136
stock_barcode/static/src/components/line.xml
Normal file
@@ -0,0 +1,136 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<!-- Line's sub-elements -->
|
||||
<t t-name="stock_barcode.LineTitle" owl="1">
|
||||
<t t-if="props.line.product_id.default_code or props.line.product_id.code">
|
||||
<div class="o_barcode_line_title">
|
||||
<i class="fa fa-fw fa-tags"/>
|
||||
<span t-if="props.line.product_id.default_code"
|
||||
class="o_barcode_product_ref h5 fw-bold"
|
||||
t-esc="props.line.product_id.default_code"/>
|
||||
<span t-if="props.line.product_id.code != props.line.product_id.default_code"
|
||||
class="o_barcode_partner_code ms-1 h5 text-muted"
|
||||
t-esc="props.line.product_id.code"/>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fa fa-fw"/>
|
||||
<span class="product-label" t-esc="props.line.product_id.display_name"/>
|
||||
</div>
|
||||
</t>
|
||||
<div t-else="" class="o_barcode_line_title pb-1">
|
||||
<i class="fa fa-fw fa-tags"/>
|
||||
<span class="product-label" t-esc="props.line.product_id.display_name"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="stock_barcode.LineQuantity" owl="1">
|
||||
<div name="quantity">
|
||||
<i class="fa fa-fw fa-cube" t-attf-class="{{nextExpected === 'quantity' ? 'o_next_expected' : ''}}"/>
|
||||
<span t-attf-class="o_barcode_scanner_qty font-monospace badge #{' '}">
|
||||
<span class="qty-done d-inline-block text-start"
|
||||
t-attf-class="
|
||||
{{nextExpected === 'quantity' && qtyDone ? 'o_flash' : ''}}
|
||||
{{isSelected && qtyDemand && qtyDone && qtyDone < qtyDemand ? 'fw-bolder' : ''}}"
|
||||
t-esc="env.model.IsNotSet(line) ? '?' : qtyDone"/>
|
||||
<span t-if="qtyDemand" t-esc="'/ ' + qtyDemand"/>
|
||||
</span>
|
||||
<span t-if="props.displayUOM" t-esc="line.product_uom_id.name"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="stock_barcode.LineOwner" owl="1">
|
||||
<div t-if="line.owner_id">
|
||||
<i class="fa fa-fw fa-user-o"/>
|
||||
<span class="o_line_owner" t-esc="line.owner_id.display_name"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="stock_barcode.LineSourceLocation" owl="1">
|
||||
<div name="source_location" t-if="displaySourceLocation" title="Source Location"
|
||||
t-attf-class="{{line.location_id.usage != 'internal' ? 'text-danger' : ''}} {{highlightLocation ? 'o_highlight' : ''}}">
|
||||
<i class="fa fa-fw fa-sign-out"/>
|
||||
<span class="o_line_source_location fst-italic text-muted">
|
||||
<t t-esc="sourceLocationPath"/>
|
||||
<span t-esc="line.location_id.name"
|
||||
t-attf-class="
|
||||
{{highlightLocation ? 'fw-bold' : ''}}
|
||||
{{line.location_id.usage != 'internal' ? 'text-danger' : 'text-black'}}"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="stock_barcode.LineDestinationLocation" owl="1">
|
||||
<div name="destination_location" t-if="displayDestinationLocation" title="Destination Location"
|
||||
t-att-class="line.location_dest_id.usage != 'internal' ? 'text-danger' : ''">
|
||||
<i class="fa fa-fw fa-sign-in"/>
|
||||
<span class="o_line_destination_location fst-italic text-muted">
|
||||
<t t-esc="destinationLocationPath"/>
|
||||
<span t-esc="line.location_dest_id.name"
|
||||
t-attf-class="
|
||||
{{env.model.lastScanned.destLocation && env.model.lastScanned.destLocation.id == line.location_dest_id.id ? 'fw-bold' : ''}}
|
||||
{{line.location_dest_id.usage != 'internal' ? 'text-danger' : 'text-black'}}"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Line's template -->
|
||||
<t t-name="stock_barcode.LineComponent" owl="1">
|
||||
<div t-on-click="select"
|
||||
class="o_barcode_line list-group-item d-flex flex-row flex-nowrap"
|
||||
t-att-data-virtual-id="line.virtual_id" t-attf-class="{{componentClasses}}"
|
||||
t-att-data-barcode="line.product_id.barcode">
|
||||
<div class="o_barcode_line_details flex-grow-1 flex-column flex-nowrap">
|
||||
<t t-call="stock_barcode.LineSourceLocation"/>
|
||||
<!-- Hides product's name if it's a subline, as it is already on the parent line. -->
|
||||
<t t-if="!props.subline" t-call="stock_barcode.LineTitle"/>
|
||||
<div t-if="isTracked and requireLotNumber" name="lot">
|
||||
<i class="fa fa-fw fa-barcode" t-attf-class="{{nextExpected === 'lot' ? 'o_next_expected' : ''}}"/>
|
||||
<span class="o_line_lot_name" t-esc="lotName"/>
|
||||
</div>
|
||||
<t t-call="stock_barcode.LineQuantity"/>
|
||||
<div t-if="line.package_id || line.result_package_id" name="package">
|
||||
<i class="fa fa-fw fa-archive"/>
|
||||
<span t-if="line.package_id" class="package" t-esc="line.package_id.name"/>
|
||||
<i t-if="displayResultPackage" class="fa fa-long-arrow-right mx-1"/>
|
||||
<span t-if="line.result_package_id" class="result-package" t-esc="line.result_package_id.name"/>
|
||||
<span t-if="line.result_package_id && line.result_package_id.package_type_id"
|
||||
class="fst-italic text-muted">
|
||||
(<t t-esc="line.result_package_id.package_type_id.name"/>)
|
||||
</span>
|
||||
</div>
|
||||
<t t-call="stock_barcode.LineOwner"/>
|
||||
<t t-call="stock_barcode.LineDestinationLocation"/>
|
||||
</div>
|
||||
<div class="o_line_buttons">
|
||||
<button t-on-click="edit" class="o_line_button o_edit btn"
|
||||
t-att-class="this.env.model.lineCanBeEdited(line) ? 'btn-secondary' : ''"
|
||||
t-att-disabled="!this.env.model.lineCanBeEdited(line)">
|
||||
<i class="fa fa-2x fa-pencil"/>
|
||||
</button>
|
||||
<button t-if="env.model.displaySetButton" t-on-click="setOnHandQuantity"
|
||||
class="o_line_button o_set btn ms-2 ms-sm-4"
|
||||
t-attf-class="{{quantityIsSet && qtyDone != qtyDemand ? 'o_difference' : ''}}">
|
||||
<i t-if="quantityIsSet" class="fa fa-2x"
|
||||
t-attf-class="{{qtyDone == qtyDemand ? 'fa-check' : 'fa-times'}}"/>
|
||||
</button>
|
||||
<span t-attf-class="{{env.model.incrementButtonsDisplayStyle}}">
|
||||
<button t-if="env.model.getDisplayDecrementBtn(line)" name="decrementButton" t-on-click="(ev) => this.addQuantity(-1, ev)"
|
||||
class="o_line_button o_remove_unit btn btn-primary ms-2 ms-sm-4"
|
||||
t-attf-disabled="{{qtyDone <= 0 || qtyDone == '?'}}">-1</button>
|
||||
<button t-if="env.model.getDisplayIncrementBtn(line)" name="incrementButton"
|
||||
t-on-click="(ev) => this.addQuantity(incrementQty, ev)" t-esc="'+' + incrementQty"
|
||||
t-att-disabled="!this.env.model.lineCanBeEdited(line)"
|
||||
class="o_line_button o_add_quantity btn btn-primary ms-2 ms-sm-4"/>
|
||||
</span>
|
||||
<button t-if="isSelected and env.model.getDisplayIncrementPackagingBtn(line)" name="incrementPackagingButton"
|
||||
t-on-click="(ev) => this.addQuantity(line.product_packaging_uom_qty, ev)"
|
||||
class="o_line_button w-100 btn btn-primary my-3 d-block">
|
||||
<div class="text-capitalize">
|
||||
+ <t t-esc="line.product_packaging_id.name"/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
391
stock_barcode/static/src/components/main.js
Normal file
391
stock_barcode/static/src/components/main.js
Normal file
@@ -0,0 +1,391 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ChatterContainer } from '@mail/components/chatter_container/chatter_container';
|
||||
|
||||
import BarcodePickingModel from '@stock_barcode/models/barcode_picking_model';
|
||||
import BarcodeQuantModel from '@stock_barcode/models/barcode_quant_model';
|
||||
import { bus } from 'web.core';
|
||||
import config from 'web.config';
|
||||
import GroupedLineComponent from '@stock_barcode/components/grouped_line';
|
||||
import LineComponent from '@stock_barcode/components/line';
|
||||
import PackageLineComponent from '@stock_barcode/components/package_line';
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import * as BarcodeScanner from '@web/webclient/barcode/barcode_scanner';
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { View } from "@web/views/view";
|
||||
|
||||
const { Component, onMounted, onPatched, onWillStart, onWillUnmount, useSubEnv } = owl;
|
||||
|
||||
/**
|
||||
* Main Component
|
||||
* Gather the line information.
|
||||
* Manage the scan and save process.
|
||||
*/
|
||||
|
||||
class MainComponent extends Component {
|
||||
//--------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
setup() {
|
||||
this.rpc = useService('rpc');
|
||||
this.orm = useService('orm');
|
||||
this.notification = useService('notification');
|
||||
this.props.model = this.props.action.res_model;
|
||||
this.props.id = this.props.action.context.active_id;
|
||||
const model = this._getModel(this.props);
|
||||
useSubEnv({model});
|
||||
this._scrollBehavior = 'smooth';
|
||||
this.isMobile = config.device.isMobile;
|
||||
|
||||
onWillStart(async () => {
|
||||
const barcodeData = await this.rpc(
|
||||
'/stock_barcode/get_barcode_data',
|
||||
{
|
||||
model: this.props.model,
|
||||
res_id: this.props.id || false,
|
||||
}
|
||||
);
|
||||
this.groups = barcodeData.groups;
|
||||
this.env.model.setData(barcodeData);
|
||||
this.env.model.on('process-action', this, this._onDoAction);
|
||||
this.env.model.on('notification', this, this._onNotification);
|
||||
this.env.model.on('refresh', this, this._onRefreshState);
|
||||
this.env.model.on('update', this, () => this.render(true));
|
||||
this.env.model.on('do-action', this, args => bus.trigger('do-action', args));
|
||||
this.env.model.on('history-back', this, () => this.env.config.historyBack());
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
bus.on('barcode_scanned', this, this._onBarcodeScanned);
|
||||
bus.on('edit-line', this, this._onEditLine);
|
||||
bus.on('exit', this, this.exit);
|
||||
bus.on('open-package', this, this._onOpenPackage);
|
||||
bus.on('refresh', this, this._onRefreshState);
|
||||
bus.on('warning', this, this._onWarning);
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
bus.off('edit-line', this, this._onEditLine);
|
||||
bus.off('exit', this, this.exit);
|
||||
bus.off('open-package', this, this._onOpenPackage);
|
||||
bus.off('refresh', this, this._onRefreshState);
|
||||
bus.off('warning', this, this._onWarning);
|
||||
});
|
||||
|
||||
onPatched(() => {
|
||||
this._scrollToSelectedLine();
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
get displayHeaderInfoAsColumn() {
|
||||
return this.env.model.isDone || this.env.model.isCancelled;
|
||||
}
|
||||
|
||||
get displayBarcodeApplication() {
|
||||
return this.env.model.view === 'barcodeLines';
|
||||
}
|
||||
|
||||
get displayBarcodeActions() {
|
||||
return this.env.model.view === 'actionsView';
|
||||
}
|
||||
|
||||
get displayBarcodeLines() {
|
||||
return this.displayBarcodeApplication && this.env.model.canBeProcessed;
|
||||
}
|
||||
|
||||
get displayInformation() {
|
||||
return this.env.model.view === 'infoFormView';
|
||||
}
|
||||
|
||||
get displayNote() {
|
||||
return !this._hideNote && this.env.model.record.note;
|
||||
}
|
||||
|
||||
get displayPackageContent() {
|
||||
return this.env.model.view === 'packagePage';
|
||||
}
|
||||
|
||||
get displayProductPage() {
|
||||
return this.env.model.view === 'productPage';
|
||||
}
|
||||
|
||||
get lineFormViewData() {
|
||||
const data = this.env.model.viewsWidgetData;
|
||||
data.context = data.additionalContext;
|
||||
data.resId = this._editedLineParams && this._editedLineParams.currentId;
|
||||
return data;
|
||||
}
|
||||
|
||||
get highlightValidateButton() {
|
||||
return this.env.model.highlightValidateButton;
|
||||
}
|
||||
|
||||
get info() {
|
||||
return this.env.model.barcodeInfo;
|
||||
}
|
||||
|
||||
get isTransfer() {
|
||||
return this.currentSourceLocation && this.currentDestinationLocation;
|
||||
}
|
||||
|
||||
get lines() {
|
||||
return this.env.model.groupedLines;
|
||||
}
|
||||
|
||||
get mobileScanner() {
|
||||
return BarcodeScanner.isBarcodeScannerSupported();
|
||||
}
|
||||
|
||||
get packageLines() {
|
||||
return this.env.model.packageLines;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_getModel(params) {
|
||||
const { rpc, orm, notification } = this;
|
||||
if (params.model === 'stock.picking') {
|
||||
return new BarcodePickingModel(params, { rpc, orm, notification });
|
||||
} else if (params.model === 'stock.quant') {
|
||||
return new BarcodeQuantModel(params, { rpc, orm, notification });
|
||||
} else {
|
||||
throw new Error('No JS model define');
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async cancel() {
|
||||
await this.env.model.save();
|
||||
const action = await this.orm.call(
|
||||
this.props.model,
|
||||
'action_cancel_from_barcode',
|
||||
[[this.props.id]]
|
||||
);
|
||||
const onClose = res => {
|
||||
if (res && res.cancelled) {
|
||||
this.env.model._cancelNotification();
|
||||
this.env.config.historyBack();
|
||||
}
|
||||
};
|
||||
bus.trigger('do-action', {
|
||||
action,
|
||||
options: {
|
||||
on_close: onClose.bind(this),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async openMobileScanner() {
|
||||
const barcode = await BarcodeScanner.scanBarcode();
|
||||
if (barcode) {
|
||||
this.env.model.processBarcode(barcode);
|
||||
if ('vibrate' in window.navigator) {
|
||||
window.navigator.vibrate(100);
|
||||
}
|
||||
} else {
|
||||
this.env.services.notification.notify({
|
||||
type: 'warning',
|
||||
message: this.env._t("Please, Scan again !"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async exit(ev) {
|
||||
if (this.displayBarcodeApplication) {
|
||||
await this.env.model.save();
|
||||
this.env.config.historyBack();
|
||||
} else {
|
||||
this.toggleBarcodeLines();
|
||||
}
|
||||
}
|
||||
|
||||
hideNote(ev) {
|
||||
this._hideNote = true;
|
||||
this.render();
|
||||
}
|
||||
|
||||
async openProductPage() {
|
||||
if (!this._editedLineParams) {
|
||||
await this.env.model.save();
|
||||
}
|
||||
this.env.model.displayProductPage();
|
||||
}
|
||||
|
||||
async print(action, method) {
|
||||
await this.env.model.save();
|
||||
const options = this.env.model._getPrintOptions();
|
||||
if (options.warning) {
|
||||
return this.env.model.notification.add(options.warning, { type: 'warning' });
|
||||
}
|
||||
if (!action && method) {
|
||||
action = await this.orm.call(
|
||||
this.props.model,
|
||||
method,
|
||||
[[this.props.id]]
|
||||
);
|
||||
}
|
||||
bus.trigger('do-action', { action, options });
|
||||
}
|
||||
|
||||
putInPack(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model._putInPack();
|
||||
}
|
||||
|
||||
saveFormView(lineRecord) {
|
||||
const lineId = (lineRecord && lineRecord.data.id) || (this._editedLineParams && this._editedLineParams.currentId);
|
||||
const recordId = (lineRecord.resModel === this.props.model) ? lineId : undefined
|
||||
this._onRefreshState({ recordId, lineId });
|
||||
}
|
||||
|
||||
toggleBarcodeActions(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.displayBarcodeActions();
|
||||
}
|
||||
|
||||
async toggleBarcodeLines(lineId) {
|
||||
this._editedLineParams = undefined;
|
||||
await this.env.model.displayBarcodeLines(lineId);
|
||||
}
|
||||
|
||||
async toggleInformation() {
|
||||
await this.env.model.save();
|
||||
this.env.model.displayInformation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `validate` on the model and then triggers up the action because OWL
|
||||
* components don't seem able to manage wizard without doing custom things.
|
||||
*
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
async validate(ev) {
|
||||
ev.stopPropagation();
|
||||
await this.env.model.validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler called when a barcode is scanned.
|
||||
*
|
||||
* @private
|
||||
* @param {string} barcode
|
||||
*/
|
||||
_onBarcodeScanned(barcode) {
|
||||
if (this.displayBarcodeApplication) {
|
||||
this.env.model.processBarcode(barcode);
|
||||
}
|
||||
}
|
||||
|
||||
_scrollToSelectedLine() {
|
||||
if (!this.displayBarcodeLines) {
|
||||
this._scrollBehavior = 'auto';
|
||||
return;
|
||||
}
|
||||
let selectedLine = document.querySelector('.o_sublines .o_barcode_line.o_highlight');
|
||||
const isSubline = Boolean(selectedLine);
|
||||
if (!selectedLine) {
|
||||
selectedLine = document.querySelector('.o_barcode_line.o_highlight');
|
||||
}
|
||||
if (!selectedLine) {
|
||||
const matchingLine = this.env.model.findLineForCurrentLocation();
|
||||
if (matchingLine) {
|
||||
selectedLine = document.querySelector(`.o_barcode_line[data-virtual-id="${matchingLine.virtual_id}"]`);
|
||||
}
|
||||
}
|
||||
if (selectedLine) {
|
||||
// If a line is selected, checks if this line is on the top of the
|
||||
// page, and if it's not, scrolls until the line is on top.
|
||||
const header = document.querySelector('.o_barcode_header');
|
||||
const lineRect = selectedLine.getBoundingClientRect();
|
||||
const navbar = document.querySelector('.o_main_navbar');
|
||||
const page = document.querySelector('.o_barcode_lines');
|
||||
// Computes the real header's height (the navbar is present if the page was refreshed).
|
||||
const headerHeight = navbar ? navbar.offsetHeight + header.offsetHeight : header.offsetHeight;
|
||||
if (lineRect.top < headerHeight || lineRect.bottom > (headerHeight + lineRect.height)) {
|
||||
let top = lineRect.top - headerHeight + page.scrollTop;
|
||||
if (isSubline) {
|
||||
const parentLine = selectedLine.closest('.o_barcode_lines > .o_barcode_line');
|
||||
const parentSummary = parentLine.querySelector('.o_barcode_line_summary');
|
||||
top -= parentSummary.getBoundingClientRect().height;
|
||||
}
|
||||
page.scroll({ left: 0, top, behavior: this._scrollBehavior });
|
||||
this._scrollBehavior = 'smooth';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async _onDoAction(ev) {
|
||||
bus.trigger('do-action', {
|
||||
action: ev,
|
||||
options: {
|
||||
on_close: this._onRefreshState.bind(this),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async _onEditLine(ev) {
|
||||
let { line } = ev;
|
||||
const virtualId = line.virtual_id;
|
||||
await this.env.model.save();
|
||||
// Updates the line id if it's missing, in order to open the line form view.
|
||||
if (!line.id && virtualId) {
|
||||
line = this.env.model.pageLines.find(l => Number(l.dummy_id) === virtualId);
|
||||
}
|
||||
this._editedLineParams = this.env.model.getEditedLineParams(line);
|
||||
await this.openProductPage();
|
||||
}
|
||||
|
||||
_onNotification(notifParams) {
|
||||
const { message } = notifParams;
|
||||
delete notifParams.message;
|
||||
this.env.services.notification.add(message, notifParams);
|
||||
}
|
||||
|
||||
_onOpenPackage(packageId) {
|
||||
this._inspectedPackageId = packageId;
|
||||
this.env.model.displayPackagePage();
|
||||
}
|
||||
|
||||
async _onRefreshState(paramsRefresh) {
|
||||
const { recordId, lineId } = paramsRefresh || {}
|
||||
const { route, params } = this.env.model.getActionRefresh(recordId);
|
||||
const result = await this.rpc(route, params);
|
||||
await this.env.model.refreshCache(result.data.records);
|
||||
await this.toggleBarcodeLines(lineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles triggered warnings. It can happen from an onchange for example.
|
||||
*
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
_onWarning(ev) {
|
||||
const { title, message } = ev.detail;
|
||||
this.env.services.dialog.add(ConfirmationDialog, { title, body: message });
|
||||
}
|
||||
}
|
||||
MainComponent.template = 'stock_barcode.MainComponent';
|
||||
MainComponent.components = {
|
||||
View,
|
||||
GroupedLineComponent,
|
||||
LineComponent,
|
||||
PackageLineComponent,
|
||||
ChatterContainer,
|
||||
};
|
||||
|
||||
registry.category("actions").add("stock_barcode_client_action", MainComponent);
|
||||
|
||||
export default MainComponent;
|
||||
270
stock_barcode/static/src/components/main.scss
Normal file
270
stock_barcode/static/src/components/main.scss
Normal file
@@ -0,0 +1,270 @@
|
||||
.o_barcode_client_action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: $o-view-background-color;
|
||||
overflow: auto;
|
||||
|
||||
.o_strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Top navbar
|
||||
// =====================================
|
||||
.o_barcode_header {
|
||||
flex: 0 0 46px;
|
||||
color: white;
|
||||
background-color: var(--barcode__header-bg, #{$o-brand-odoo});
|
||||
|
||||
.nav-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-link, .navbar-text {
|
||||
font-size: 16px;
|
||||
color: #FFFFFF;
|
||||
|
||||
&:hover {
|
||||
color: rgba($color: #FFFFFF, $alpha: 0.75)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Top Block
|
||||
// =====================================
|
||||
.o_barcode_message {
|
||||
box-shadow: inset 0 0 20px $gray-900;
|
||||
|
||||
.o_barcode_pic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 60%;
|
||||
max-width: 200px;
|
||||
.fa-exclamation-triangle {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
// =====================================
|
||||
.o_barcode_lines_header {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
background-color: var(--barcode__linesHeader-bg, #{$o-gray-800});
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Lines Block
|
||||
// =====================================
|
||||
.o_barcode_lines {
|
||||
clear: both;
|
||||
flex: auto;
|
||||
overflow: auto;
|
||||
color: $gray-800;
|
||||
margin-bottom: 60px;
|
||||
@media (orientation: portrait) {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
&.o_js_has_highlight .o_barcode_line.o_highlight {
|
||||
&.o_highlight_green {
|
||||
box-shadow: inset 0px 0px 0px 3px $o-brand-secondary;
|
||||
}
|
||||
|
||||
.product-label, .o_barcode_scanner_qty {
|
||||
color: $headings-color;
|
||||
}
|
||||
|
||||
.qty-done, .inventory_quantity {
|
||||
font-weight: bold;
|
||||
|
||||
&.o_js_qty_animate {
|
||||
animation: o_barcode_scanner_qty_update .2s alternate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Embedded views
|
||||
// =====================================
|
||||
.o_barcode_generic_view {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.o_view_controller, .o_view_controller .o_form_view.o_form_nosheet {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.o_field_one2many.o_field_widget .o_kanban_record {
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.o_form_view {
|
||||
&.o_xxs_form_view {
|
||||
.o_td_label > .o_form_label {
|
||||
color: $gray-900;
|
||||
font-weight: bold;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.o_field_widget {
|
||||
font-size: 1em;
|
||||
.btn.fa {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
.o_list_view {
|
||||
th, .o_field_widget {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_form_nosheet {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
.o_kanban_record {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Settings menu
|
||||
// =====================================
|
||||
.o_barcode_settings {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
|
||||
> button {
|
||||
flex: 1 0 auto;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Control buttons (validate, previous,
|
||||
// next, put in pack, ...)
|
||||
// =====================================
|
||||
.o_barcode_control {
|
||||
flex: 0 0 60px;
|
||||
margin: 0 -1px;
|
||||
width: 100%;
|
||||
> .btn {
|
||||
flex: 1;
|
||||
width: 50%;
|
||||
@media (orientation: portrait) {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
@media (orientation: landscape) {
|
||||
height: 60px;
|
||||
}
|
||||
border-width: 1px 0 0 0;
|
||||
border-style: solid;
|
||||
&.btn-secondary {
|
||||
color: $gray-800;
|
||||
border-color: $gray-400;
|
||||
}
|
||||
&.btn-primary {
|
||||
border-color: $primary;
|
||||
}
|
||||
&.btn-success {
|
||||
border-color: $success;
|
||||
}
|
||||
&[disabled] {
|
||||
opacity: 1;
|
||||
background-color: $gray-200;
|
||||
color: $btn-link-disabled-color;
|
||||
}
|
||||
+ .btn {
|
||||
border-left-width: 1px;
|
||||
border-left-color: $gray-400;
|
||||
}
|
||||
}
|
||||
.fa-angle-left, .fa-angle-right {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
&:first-of-type {
|
||||
box-shadow: 0 -3px 10px $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
// Line form
|
||||
// =====================================
|
||||
.o_barcode_line_form {
|
||||
margin-left: 24px;
|
||||
margin-bottom: 36px;
|
||||
font-size: 1.4em;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 700px;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
&.row-long {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a.o_field_widget {
|
||||
display: inline-block;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
// Avoids to make the UoM field as long as the quantity done field.
|
||||
.o_field_widget[name="product_uom_id"] input {
|
||||
@include media-breakpoint-up(sm) {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_qty_done_field_completed input {
|
||||
background: var(--barcode__input--completed, #f6fdf6);
|
||||
}
|
||||
|
||||
.o_qty_done_field_not_completed input {
|
||||
background: var(--barcode__input--notCompleted, #fcf9f2);
|
||||
}
|
||||
|
||||
& > div {
|
||||
.o_field_float {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.o_input {
|
||||
padding: 8px;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.o_required_modifier .o_input {
|
||||
border-bottom: 2px solid $border-color
|
||||
}
|
||||
|
||||
.o_dropdown_button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
i {
|
||||
min-width: 24px;
|
||||
max-width: 24px;
|
||||
color: $o-main-color-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
stock_barcode/static/src/components/main.xml
Normal file
127
stock_barcode/static/src/components/main.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="stock_barcode.MainComponent" class="o_content o_barcode_client_action" owl="1">
|
||||
<div class="o_barcode_header">
|
||||
<div class="navbar navbar-expand navbar-dark">
|
||||
<nav class="navbar-nav me-auto">
|
||||
<button t-on-click="exit" class="o_exit btn nav-link me-4">
|
||||
<i class="fa fa-chevron-left"/>
|
||||
</button>
|
||||
<span class="o_title navbar-text" t-esc="env.model.name"/>
|
||||
</nav>
|
||||
<nav class="navbar-nav">
|
||||
<t t-if="displayBarcodeApplication">
|
||||
<button t-if="env.model.formViewId" t-on-click="toggleInformation" class="o_show_information btn nav-link">
|
||||
<i class="fa fa-info-circle"/>
|
||||
</button>
|
||||
<button t-if="mobileScanner" class="o_stock_mobile_barcode btn nav-link" t-on-click="openMobileScanner">
|
||||
<i class="fa fa-barcode"/>
|
||||
</button>
|
||||
<button t-on-click="toggleBarcodeActions" class="o_barcode_actions btn nav-link">
|
||||
<i class="fa fa-cog"/>
|
||||
</button>
|
||||
</t>
|
||||
<button t-else="" t-on-click="() => this.toggleBarcodeLines()" class="o_close btn nav-link">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<t t-if="displayBarcodeApplication">
|
||||
<div t-if="displayNote" class="alert alert-warning text-start mb-0">
|
||||
<button type="button" class="close" title="Close" aria-label="Close" t-on-click="hideNote">×</button>
|
||||
<t t-esc="env.model.record.note"/>
|
||||
</div>
|
||||
<div class="o_barcode_lines_header row alert m-0 px-1 "
|
||||
t-attf-class="{{displayHeaderInfoAsColumn ? 'flex-column justify-content-center align-items-center' : 'justify-content-between'}}">
|
||||
<div t-if="info.warning" class="o_barcode_pic position-relative text-center mt-2 mb-1">
|
||||
<i class="fa fa-5x fa-exclamation-triangle"/>
|
||||
</div>
|
||||
<div name="barcode_messages" class="d-flex align-items-center justify-content-center w-100">
|
||||
<span t-if="info.icon" class="fa fa-3x me-3" t-attf-class="fa-{{info.icon}}"/>
|
||||
<span class="o_scan_message" t-attf-class="o_{{info.class}}">
|
||||
<span t-if="info.warning" name="warning" class="fa fa-exclamation-triangle me-1"/>
|
||||
<span t-out="info.message"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="displayBarcodeLines && (lines.length || packageLines.length)" class="o_barcode_lines"> <!-- Lines -->
|
||||
<t t-foreach="lines" t-as="line" t-key="line.virtual_id">
|
||||
<GroupedLineComponent t-if="line.lines" line="line" displayUOM="groups.group_uom"/>
|
||||
<LineComponent t-else="" line="line" displayUOM="groups.group_uom"/>
|
||||
</t>
|
||||
<t t-foreach="packageLines" t-as="line" t-key="line.virtual_id">
|
||||
<PackageLineComponent line="line" displayUOM="false"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="displayProductPage"> <!-- Barcode Line Edit Form View -->
|
||||
<View type="'form'" mode="'edit'"
|
||||
viewId="env.model.lineFormViewId"
|
||||
resModel="lineFormViewData.resModel"
|
||||
resId="lineFormViewData.resId"
|
||||
display="{ controlPanel: false }"
|
||||
context="lineFormViewData.context"
|
||||
onSave="(record) => this.saveFormView(record)"
|
||||
onDiscard="() => this.toggleBarcodeLines()"/>
|
||||
</div>
|
||||
<div t-if="displayPackageContent"> <!-- Quants (in package) Kanban View -->
|
||||
<View type="'kanban'"
|
||||
viewId="packageKanbanViewId"
|
||||
display="{ controlPanel: false }"
|
||||
resModel="'stock.quant'"
|
||||
domain="[['package_id', '=', _inspectedPackageId]]"/>
|
||||
</div>
|
||||
<div t-if="displayInformation"> <!-- Res Model Form View -->
|
||||
<View type="'form'" mode="'edit'"
|
||||
viewId="env.model.formViewId"
|
||||
display="{ controlPanel: false }"
|
||||
resModel="props.model"
|
||||
resId="props.id"
|
||||
onSave="() => this._onRefreshState({ lineId: this._editedLineParams && this._editedLineParams.currentId })"
|
||||
onDiscard="() => this.toggleBarcodeLines()"/>
|
||||
<ChatterContainer threadModel="props.model" threadId="props.id"/>
|
||||
</div>
|
||||
<div t-if="displayBarcodeActions" class="o_barcode_settings flex-column h100">
|
||||
<t t-foreach="env.model.printButtons" t-as="button" t-key="button.class">
|
||||
<button class="btn-lg btn btn-light text-uppercase"
|
||||
t-attf-class="{{button.class}}" t-esc="button.name"
|
||||
t-on-click="() => this.print(button.action, button.method)"/>
|
||||
</t>
|
||||
<button t-if="env.model.displayCancelButton"
|
||||
t-on-click="cancel"
|
||||
class="o_cancel_operation btn-lg btn btn-light text-uppercase">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div t-if="displayBarcodeLines" class="fixed-bottom"> <!-- Footer -->
|
||||
<div class="o_barcode_control o_action_buttons d-flex">
|
||||
<button class="o_add_line btn btn-secondary text-uppercase" t-on-click="openProductPage">
|
||||
<i class="fa fa-plus me-1"/> Add Product
|
||||
</button>
|
||||
<button t-if="env.model.displayPutInPackButton" t-on-click="putInPack"
|
||||
t-att-disabled="!env.model.canPutInPack"
|
||||
class="o_put_in_pack btn btn-secondary text-uppercase">
|
||||
<i class="fa fa-cube me-1"/> Put In Pack
|
||||
</button>
|
||||
<button t-if="env.model.displayValidateButton" t-on-click="validate"
|
||||
class="btn text-uppercase o_validate_page"
|
||||
t-att-disabled="!env.model.canBeValidate"
|
||||
t-attf-class="{{highlightValidateButton ? 'btn-success' : 'btn-secondary'}}">
|
||||
<i class="fa fa-check me-1"/> Validate
|
||||
</button>
|
||||
<button t-if="env.model.displayApplyButton" t-on-click="() => this.env.model.apply()"
|
||||
class="btn text-uppercase o_apply_page"
|
||||
t-att-disabled="env.model.applyOn === 0"
|
||||
t-attf-class="{{highlightValidateButton ? 'btn-success' : 'btn-secondary'}}">
|
||||
<i class="fa fa-check me-1"/> Apply
|
||||
<span t-attf-class="{{highlightValidateButton ? '' : 'text-muted'}}">
|
||||
(<t t-esc="env.model.applyOn"/>)
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
41
stock_barcode/static/src/components/package_line.js
Normal file
41
stock_barcode/static/src/components/package_line.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { bus } from 'web.core';
|
||||
import LineComponent from './line';
|
||||
|
||||
export default class PackageLineComponent extends LineComponent {
|
||||
get componentClasses() {
|
||||
return [
|
||||
this.qtyDone == 1 ? 'o_line_completed' : 'o_line_not_completed',
|
||||
this.isSelected ? 'o_selected o_highlight' : ''
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
get isSelected() {
|
||||
return this.line.package_id.id === this.env.model.lastScanned.packageId;
|
||||
}
|
||||
|
||||
get qtyDemand() {
|
||||
return this.props.line.reservedPackage ? 1 : false;
|
||||
}
|
||||
|
||||
get qtyDone() {
|
||||
const reservedQuantity = this.line.lines.reduce((r, l) => r + l.reserved_uom_qty, 0);
|
||||
const doneQuantity = this.line.lines.reduce((r, l) => r + l.qty_done, 0);
|
||||
if (reservedQuantity > 0) {
|
||||
return doneQuantity / reservedQuantity;
|
||||
}
|
||||
return doneQuantity >= 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
openPackage() {
|
||||
bus.trigger('open-package', this.line.package_id.id);
|
||||
}
|
||||
|
||||
select(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.selectPackageLine(this.line);
|
||||
this.env.model.trigger('update');
|
||||
}
|
||||
}
|
||||
PackageLineComponent.template = 'stock_barcode.PackageLineComponent';
|
||||
24
stock_barcode/static/src/components/package_line.xml
Normal file
24
stock_barcode/static/src/components/package_line.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="stock_barcode.PackageLineComponent" owl="1" t-on-click="select"
|
||||
class="o_barcode_line list-group-item d-flex flex-row flex-nowrap py-3"
|
||||
t-attf-class="{{componentClasses}}" t-att-data-package="line.package_id.name">
|
||||
<div class="o_barcode_line_details flex-grow-1 flex-column flex-nowrap">
|
||||
<t t-call="stock_barcode.LineSourceLocation"/>
|
||||
<div>
|
||||
<i class="fa fa-fw fa-archive"/>
|
||||
<t t-esc="line.package_id.name"/>
|
||||
<i class="fa fa-long-arrow-right mx-1"/>
|
||||
<t t-esc="line.result_package_id.name"/>
|
||||
</div>
|
||||
<t t-call="stock_barcode.LineQuantity"/>
|
||||
<t t-call="stock_barcode.LineOwner"/>
|
||||
<t t-call="stock_barcode.LineDestinationLocation"/>
|
||||
</div>
|
||||
<button t-on-click="openPackage" class="o_line_button o_package_content btn btn-secondary ms-2 ms-sm-4">
|
||||
<i class="fa fa-2x fa-dropbox"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,60 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { KanbanController } from '@web/views/kanban/kanban_controller';
|
||||
import { bus } from 'web.core';
|
||||
|
||||
const { onMounted, onWillUnmount } = owl;
|
||||
|
||||
export class StockBarcodeKanbanController extends KanbanController {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
onMounted(() => {
|
||||
bus.on('barcode_scanned', this, this._onBarcodeScannedHandler);
|
||||
document.activeElement.blur();
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
bus.off('barcode_scanned', this, this._onBarcodeScannedHandler);
|
||||
});
|
||||
}
|
||||
|
||||
openRecord(record) {
|
||||
this.actionService.doAction('stock_barcode.stock_barcode_picking_client_action', {
|
||||
additionalContext: { active_id: record.resId },
|
||||
});
|
||||
}
|
||||
|
||||
async createRecord() {
|
||||
const action = await this.model.orm.call(
|
||||
'stock.picking',
|
||||
'action_open_new_picking',
|
||||
[], { context: this.props.context }
|
||||
);
|
||||
if (action) {
|
||||
return this.actionService.doAction(action);
|
||||
}
|
||||
return super.createRecord(...arguments);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when the user scans a barcode.
|
||||
*
|
||||
* @param {String} barcode
|
||||
*/
|
||||
async _onBarcodeScannedHandler(barcode) {
|
||||
if (this.props.resModel != 'stock.picking') {
|
||||
return;
|
||||
}
|
||||
const kwargs = { barcode, context: this.props.context };
|
||||
const res = await this.model.orm.call(this.props.resModel, 'filter_on_barcode', [], kwargs);
|
||||
if (res.action) {
|
||||
this.actionService.doAction(res.action);
|
||||
} else if (res.warning) {
|
||||
const params = { title: res.warning.title, type: 'danger' };
|
||||
this.model.notificationService.add(res.warning.message, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { KanbanRenderer } from '@web/views/kanban/kanban_renderer';
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
|
||||
const { onWillStart } = owl;
|
||||
|
||||
export class StockBarcodeKanbanRenderer extends KanbanRenderer {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
const user = useService('user');
|
||||
this.display_protip = this.props.list.resModel === 'stock.picking';
|
||||
onWillStart(async () => {
|
||||
this.packageEnabled = await user.hasGroup('stock.group_tracking_lot');
|
||||
});
|
||||
}
|
||||
}
|
||||
StockBarcodeKanbanRenderer.template = 'stock_barcode.KanbanRenderer';
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="stock_barcode.KanbanRenderer" t-inherit="web.KanbanRenderer" owl="1">
|
||||
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
|
||||
<div t-if="display_protip" class="o_kanban_tip_filter text-center h5 w-100 mt-4 mb-2">
|
||||
<p t-if="packageEnabled">Scan a transfer, a product or a package to filter your records</p>
|
||||
<p t-else="">Scan a transfer or a product to filter your records</p>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
12
stock_barcode/static/src/kanban/stock_barcode_kanban_view.js
Normal file
12
stock_barcode/static/src/kanban/stock_barcode_kanban_view.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { kanbanView } from '@web/views/kanban/kanban_view';
|
||||
import { registry } from "@web/core/registry";
|
||||
import { StockBarcodeKanbanController } from './stock_barcode_kanban_controller';
|
||||
import { StockBarcodeKanbanRenderer } from './stock_barcode_kanban_renderer';
|
||||
|
||||
export const stockBarcodeKanbanView = Object.assign({}, kanbanView, {
|
||||
Controller: StockBarcodeKanbanController,
|
||||
Renderer: StockBarcodeKanbanRenderer,
|
||||
});
|
||||
registry.category("views").add("stock_barcode_list_kanban", stockBarcodeKanbanView);
|
||||
214
stock_barcode/static/src/lazy_barcode_cache.js
Normal file
214
stock_barcode/static/src/lazy_barcode_cache.js
Normal file
@@ -0,0 +1,214 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
export default class LazyBarcodeCache {
|
||||
constructor(cacheData, params) {
|
||||
this.rpc = params.rpc;
|
||||
this.dbIdCache = {}; // Cache by model + id
|
||||
this.dbBarcodeCache = {}; // Cache by model + barcode
|
||||
this.missingBarcode = new Set(); // Used as a cache by `_getMissingRecord`
|
||||
this.barcodeFieldByModel = {
|
||||
'stock.location': 'barcode',
|
||||
'product.product': 'barcode',
|
||||
'product.packaging': 'barcode',
|
||||
'stock.package.type': 'barcode',
|
||||
'stock.picking': 'name',
|
||||
'stock.quant.package': 'name',
|
||||
'stock.lot': 'name', // Also ref, should take in account multiple fields ?
|
||||
};
|
||||
this.gs1LengthsByModel = {
|
||||
'product.product': 14,
|
||||
'product.packaging': 14,
|
||||
'stock.location': 13,
|
||||
'stock.quant.package': 18,
|
||||
};
|
||||
// If there is only one active barcode nomenclature, set the cache to be compliant with it.
|
||||
if (cacheData['barcode.nomenclature'].length === 1) {
|
||||
this.nomenclature = cacheData['barcode.nomenclature'][0];
|
||||
}
|
||||
this.setCache(cacheData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds records to the barcode application's cache.
|
||||
*
|
||||
* @param {Object} cacheData each key is a model's name and contains an array of records.
|
||||
*/
|
||||
setCache(cacheData) {
|
||||
for (const model in cacheData) {
|
||||
const records = cacheData[model];
|
||||
// Adds the model's key in the cache's DB.
|
||||
if (!this.dbIdCache.hasOwnProperty(model)) {
|
||||
this.dbIdCache[model] = {};
|
||||
}
|
||||
if (!this.dbBarcodeCache.hasOwnProperty(model)) {
|
||||
this.dbBarcodeCache[model] = {};
|
||||
}
|
||||
// Adds the record in the cache.
|
||||
const barcodeField = this._getBarcodeField(model);
|
||||
for (const record of records) {
|
||||
this.dbIdCache[model][record.id] = record;
|
||||
if (barcodeField) {
|
||||
const barcode = record[barcodeField];
|
||||
if (!this.dbBarcodeCache[model][barcode]) {
|
||||
this.dbBarcodeCache[model][barcode] = [];
|
||||
}
|
||||
if (!this.dbBarcodeCache[model][barcode].includes(record.id)) {
|
||||
this.dbBarcodeCache[model][barcode].push(record.id);
|
||||
if (this.nomenclature && this.nomenclature.is_gs1_nomenclature && this.gs1LengthsByModel[model]) {
|
||||
this._setBarcodeInCacheForGS1(barcode, model, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get record from the cache, throw a error if we don't find in the cache
|
||||
* (the server should have return this information).
|
||||
*
|
||||
* @param {int} id id of the record
|
||||
* @param {string} model model_name of the record
|
||||
* @param {boolean} [copy=true] if true, returns a deep copy (to avoid to write the cache)
|
||||
* @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode)
|
||||
*/
|
||||
getRecord(model, id) {
|
||||
if (!this.dbIdCache.hasOwnProperty(model)) {
|
||||
throw new Error(`Model ${model} doesn't exist in the cache`);
|
||||
}
|
||||
if (!this.dbIdCache[model].hasOwnProperty(id)) {
|
||||
throw new Error(`Record ${model} with id=${id} doesn't exist in the cache, it should return by the server`);
|
||||
}
|
||||
const record = this.dbIdCache[model][id];
|
||||
return JSON.parse(JSON.stringify(record));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} barcode barcode to match with a record
|
||||
* @param {string} [model] model name of the record to match (if empty search on all models)
|
||||
* @param {boolean} [onlyInCache] search only in the cache
|
||||
* @param {Object} [filters]
|
||||
* @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode)
|
||||
*/
|
||||
async getRecordByBarcode(barcode, model = false, onlyInCache = false, filters = {}) {
|
||||
if (model) {
|
||||
if (!this.dbBarcodeCache.hasOwnProperty(model)) {
|
||||
throw new Error(`Model ${model} doesn't exist in the cache`);
|
||||
}
|
||||
if (!this.dbBarcodeCache[model].hasOwnProperty(barcode)) {
|
||||
if (onlyInCache) {
|
||||
return null;
|
||||
}
|
||||
await this._getMissingRecord(barcode, model);
|
||||
return await this.getRecordByBarcode(barcode, model, true);
|
||||
}
|
||||
const id = this.dbBarcodeCache[model][barcode][0];
|
||||
return this.getRecord(model, id);
|
||||
} else {
|
||||
const result = new Map();
|
||||
// Returns object {model: record} of possible record.
|
||||
const models = Object.keys(this.dbBarcodeCache);
|
||||
for (const model of models) {
|
||||
if (this.dbBarcodeCache[model].hasOwnProperty(barcode)) {
|
||||
const ids = this.dbBarcodeCache[model][barcode];
|
||||
for (const id of ids) {
|
||||
const record = this.dbIdCache[model][id];
|
||||
let pass = true;
|
||||
if (filters[model]) {
|
||||
const fields = Object.keys(filters[model]);
|
||||
for (const field of fields) {
|
||||
if (record[field] != filters[model][field]) {
|
||||
pass = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pass) {
|
||||
result.set(model, JSON.parse(JSON.stringify(record)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.size < 1) {
|
||||
if (onlyInCache) {
|
||||
return result;
|
||||
}
|
||||
await this._getMissingRecord(barcode, model, filters);
|
||||
return await this.getRecordByBarcode(barcode, model, true, filters);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
_getBarcodeField(model) {
|
||||
if (!this.barcodeFieldByModel.hasOwnProperty(model)) {
|
||||
return null;
|
||||
}
|
||||
return this.barcodeFieldByModel[model];
|
||||
}
|
||||
|
||||
async _getMissingRecord(barcode, model, filters) {
|
||||
const missCache = this.missingBarcode;
|
||||
const params = { barcode, model_name: model };
|
||||
// Check if we already try to fetch this missing record.
|
||||
if (missCache.has(barcode) || missCache.has(`${barcode}_${model}`)) {
|
||||
return false;
|
||||
}
|
||||
// Creates and passes a domain if some filters are provided.
|
||||
if (filters) {
|
||||
const domainsByModel = {};
|
||||
for (const filter of Object.entries(filters)) {
|
||||
const modelName = filter[0];
|
||||
const filtersByField = filter[1];
|
||||
domainsByModel[modelName] = [];
|
||||
for (const filterByField of Object.entries(filtersByField)) {
|
||||
domainsByModel[modelName].push([filterByField[0], '=', filterByField[1]]);
|
||||
}
|
||||
}
|
||||
params.domains_by_model = domainsByModel;
|
||||
}
|
||||
const result = await this.rpc('/stock_barcode/get_specific_barcode_data', params);
|
||||
this.setCache(result);
|
||||
// Set the missing cache if no filters (the barcode's result can vary if there is filter)
|
||||
if (!filters) {
|
||||
const keyCache = (model && `${barcode}_${model}`) || barcode;
|
||||
missCache.add(keyCache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets in the cache an entry for the given record with its formatted barcode as key.
|
||||
* The barcode will be formatted (if needed) at the length corresponding to its data part in a
|
||||
* GS1 barcode (e.g.: 14 digits for a product's barcode) by padding with 0 the original barcode.
|
||||
* That makes it easier to find when a GS1 barcode is scanned.
|
||||
* If the formatted barcode is similar to an another barcode for the same model, it will show a
|
||||
* warning in the console (as a clue to find where issue could come from, not to alert the user)
|
||||
*
|
||||
* @param {string} barcode
|
||||
* @param {string} model
|
||||
* @param {Object} record
|
||||
*/
|
||||
_setBarcodeInCacheForGS1(barcode, model, record) {
|
||||
const length = this.gs1LengthsByModel[model];
|
||||
if (!barcode || barcode.length >= length || isNaN(Number(barcode))) {
|
||||
// Barcode already has the good length, or is too long or isn't
|
||||
// fully numerical (and so, it doesn't make sense to adapt it).
|
||||
return;
|
||||
}
|
||||
const paddedBarcode = barcode.padStart(length, '0');
|
||||
// Avoids to override or mix records if there is already a key for this
|
||||
// barcode (which means there is a conflict somewhere).
|
||||
if (!this.dbBarcodeCache[model][paddedBarcode]) {
|
||||
this.dbBarcodeCache[model][paddedBarcode] = [record.id];
|
||||
} else if (!this.dbBarcodeCache[model][paddedBarcode].includes(record.id)) {
|
||||
const previousRecordId = this.dbBarcodeCache[model][paddedBarcode][0];
|
||||
const previousRecord = this.getRecord(model, previousRecordId);
|
||||
console.log(
|
||||
`Conflict for barcode %c${paddedBarcode}%c:`, 'font-weight: bold', '',
|
||||
`it could refer for both ${record.display_name} and ${previousRecord.display_name}.`,
|
||||
`\nThe last one will be used but consider to edit those products barcode to avoid error due to ambiguities.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
stock_barcode/static/src/main_menu.js
Normal file
75
stock_barcode/static/src/main_menu.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import * as BarcodeScanner from '@web/webclient/barcode/barcode_scanner';
|
||||
import { bus } from 'web.core';
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, onMounted, onWillUnmount, onWillStart, useState } = owl;
|
||||
|
||||
export class MainMenu extends Component {
|
||||
setup() {
|
||||
const displayDemoMessage = this.props.action.params.message_demo_barcodes;
|
||||
const user = useService('user');
|
||||
this.actionService = useService('action');
|
||||
this.dialogService = useService('dialog');
|
||||
this.home = useService("home_menu");
|
||||
this.notificationService = useService("notification");
|
||||
this.rpc = useService('rpc');
|
||||
this.state = useState({ displayDemoMessage });
|
||||
|
||||
this.mobileScanner = BarcodeScanner.isBarcodeScannerSupported();
|
||||
|
||||
onWillStart(async () => {
|
||||
this.locationsEnabled = await user.hasGroup('stock.group_stock_multi_locations');
|
||||
this.packagesEnabled = await user.hasGroup('stock.group_tracking_lot');
|
||||
});
|
||||
onMounted(() => {
|
||||
bus.on('barcode_scanned', this, this._onBarcodeScanned);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
});
|
||||
}
|
||||
|
||||
async openMobileScanner() {
|
||||
const barcode = await BarcodeScanner.scanBarcode();
|
||||
if (barcode){
|
||||
this._onBarcodeScanned(barcode);
|
||||
if ('vibrate' in window.navigator) {
|
||||
window.navigator.vibrate(100);
|
||||
}
|
||||
} else {
|
||||
this.notificationService.add(this.env._t("Please, Scan again !"), { type: 'warning' });
|
||||
}
|
||||
}
|
||||
|
||||
removeDemoMessage() {
|
||||
this.state.displayDemoMessage = false;
|
||||
const params = {
|
||||
title: this.env._t("Don't show this message again"),
|
||||
body: this.env._t("Do you want to permanently remove this message ?\
|
||||
It won't appear anymore, so make sure you don't need the barcodes sheet or you have a copy."),
|
||||
confirm: () => {
|
||||
this.rpc('/stock_barcode/rid_of_message_demo_barcodes');
|
||||
location.reload();
|
||||
},
|
||||
cancel: () => {},
|
||||
confirmLabel: this.env._t("Remove it"),
|
||||
cancelLabel: this.env._t("Leave it"),
|
||||
};
|
||||
this.dialogService.add(ConfirmationDialog, params);
|
||||
}
|
||||
|
||||
async _onBarcodeScanned(barcode) {
|
||||
const res = await this.rpc('/stock_barcode/scan_from_main_menu', { barcode });
|
||||
if (res.action) {
|
||||
return this.actionService.doAction(res.action);
|
||||
}
|
||||
this.notificationService.add(res.warning, { type: 'danger' });
|
||||
}
|
||||
}
|
||||
MainMenu.template = 'stock_barcode.MainMenu';
|
||||
|
||||
registry.category('actions').add('stock_barcode_main_menu', MainMenu);
|
||||
57
stock_barcode/static/src/main_menu.xml
Normal file
57
stock_barcode/static/src/main_menu.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="stock_barcode.MainMenu" class="o_stock_barcode_main_menu_container o_home_menu_background" owl="1">
|
||||
<div class="o_stock_barcode_main_menu position-relative bg-view">
|
||||
<a href="#" class="o_stock_barcode_menu d-block float-start" t-on-click="() => this.home.toggle(true)">
|
||||
<i class="fa fa-chevron-left"/>
|
||||
</a>
|
||||
<h1 class="mb-4">Barcode Scanning</h1>
|
||||
|
||||
<div t-if="state.displayDemoMessage" class="message_demo_barcodes alert alert-info alert-dismissible text-start" role="status">
|
||||
<button t-on-click="removeDemoMessage" type="button" class="btn-close" title="Close"/>
|
||||
We have created a few demo data with barcodes for you to explore the features. Print the
|
||||
<a href="/stock_barcode/static/img/barcodes_demo.pdf" target="_blank">stock barcodes sheet</a>
|
||||
to check out what this module can do! You can also print the barcode
|
||||
<a class="o_stock_inventory_commands_download" href="/stock_barcode/print_inventory_commands" target="_blank" aria-label="Download" title="Download">commands for Inventory</a>.
|
||||
</div>
|
||||
|
||||
<div class="o_stock_barcode_container position-relative d-inline-block mt-4 mb-5">
|
||||
<div t-if='mobileScanner' class="o_stock_mobile_barcode_container">
|
||||
<button class="btn btn-primary o_stock_mobile_barcode" t-on-click="openMobileScanner">
|
||||
<i class="fa fa-camera fa-2x o_mobile_barcode_camera"/> Tap to scan
|
||||
</button>
|
||||
<img src="/barcodes/static/img/barcode.png" alt="Barcode" class="img-fluid mb-1 mt-1"/>
|
||||
</div>
|
||||
<img t-else="" src="/barcodes/static/img/barcode.png" alt="Barcode" class="img-fluid mb-1 mt-1"/>
|
||||
<span class="o_stock_barcode_laser"/>
|
||||
</div>
|
||||
|
||||
<ul class="text-start mb-sm-5 ps-4">
|
||||
<li>Scan an <b>operation type</b> to create a new transfer.</li>
|
||||
<li t-if="locationsEnabled">Scan a <b>location</b> to create a new transfer from this location.</li>
|
||||
<li>Scan a <b>document</b> to open it.</li>
|
||||
<li>Scan a <b>product</b> to show its location and quantity.</li>
|
||||
<li t-if="packagesEnabled">Scan a <b>package</b> to know its content.</li>
|
||||
</ul>
|
||||
|
||||
<hr class="mb-4 d-none d-sm-block"/>
|
||||
|
||||
<div class="o_main_menu_buttons row">
|
||||
<div class="col">
|
||||
<button class="button_operations btn btn-block btn-primary mb-4 w-100"
|
||||
t-on-click="() => this.actionService.doAction('stock_barcode.stock_picking_type_action_kanban')">
|
||||
Operations
|
||||
</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button class="button_inventory btn btn-block btn-primary mb-4 w-100"
|
||||
t-on-click="() => this.actionService.doAction('stock_barcode.stock_barcode_inventory_client_action')">
|
||||
Inventory Adjustments
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
1429
stock_barcode/static/src/models/barcode_model.js
Normal file
1429
stock_barcode/static/src/models/barcode_model.js
Normal file
File diff suppressed because it is too large
Load Diff
1179
stock_barcode/static/src/models/barcode_picking_model.js
Normal file
1179
stock_barcode/static/src/models/barcode_picking_model.js
Normal file
File diff suppressed because it is too large
Load Diff
597
stock_barcode/static/src/models/barcode_quant_model.js
Normal file
597
stock_barcode/static/src/models/barcode_quant_model.js
Normal file
@@ -0,0 +1,597 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import BarcodeModel from '@stock_barcode/models/barcode_model';
|
||||
import {_t} from "web.core";
|
||||
import { sprintf } from '@web/core/utils/strings';
|
||||
|
||||
export default class BarcodeQuantModel extends BarcodeModel {
|
||||
constructor(params) {
|
||||
super(...arguments);
|
||||
this.lineModel = params.model;
|
||||
this.validateMessage = _t("The inventory adjustment has been validated");
|
||||
this.validateMethod = 'action_validate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates only the quants of the current inventory page and don't close it.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async apply() {
|
||||
await this.save();
|
||||
const linesToApply = this.pageLines.filter(line => line.inventory_quantity_set);
|
||||
if (linesToApply.length === 0) {
|
||||
const message = _t("There is nothing to apply in this page.");
|
||||
return this.notification.add(message, { type: 'warning' });
|
||||
}
|
||||
const action = await this.orm.call('stock.quant', 'action_validate',
|
||||
[linesToApply.map(quant => quant.id)]
|
||||
);
|
||||
const notifyAndGoAhead = res => {
|
||||
if (res && res.special) { // Do nothing if come from a discarded wizard.
|
||||
return this.trigger('refresh');
|
||||
}
|
||||
this.notification.add(_t("The inventory adjustment has been validated"), { type: 'success' });
|
||||
this.trigger('history-back');
|
||||
};
|
||||
if (action && action.res_model) {
|
||||
const options = { on_close: notifyAndGoAhead };
|
||||
return this.trigger('do-action', { action, options });
|
||||
}
|
||||
notifyAndGoAhead();
|
||||
}
|
||||
|
||||
get applyOn() {
|
||||
return this.pageLines.filter(line => line.inventory_quantity_set).length;
|
||||
}
|
||||
|
||||
get barcodeInfo() {
|
||||
// Takes the parent line if the current line is part of a group.
|
||||
let line = this._getParentLine(this.selectedLine) || this.selectedLine;
|
||||
if (!line && this.lastScanned.packageId) {
|
||||
const lines = this._moveEntirePackage() ? this.packageLines : this.pageLines;
|
||||
line = lines.find(l => l.package_id && l.package_id.id === this.lastScanned.packageId);
|
||||
}
|
||||
|
||||
if (line) { // Message depends of the selected line's state.
|
||||
const { tracking } = line.product_id;
|
||||
const trackingNumber = (line.lot_id && line.lot_id.name) || line.lot_name;
|
||||
if (this._lineIsNotComplete(line)) {
|
||||
if (tracking === 'none') {
|
||||
this.messageType = 'scan_product';
|
||||
} else {
|
||||
this.messageType = tracking === 'lot' ? 'scan_lot' : 'scan_serial';
|
||||
}
|
||||
} else if (tracking !== 'none' && !trackingNumber) {
|
||||
// Line's quantity is fulfilled but still waiting a tracking number.
|
||||
this.messageType = tracking === 'lot' ? 'scan_lot' : 'scan_serial';
|
||||
} else { // Line's quantity is fulfilled.
|
||||
this.messageType = this.groups.group_stock_multi_locations && line.location_id.id === this.location.id ?
|
||||
"scan_product_or_src" :
|
||||
"scan_product";
|
||||
}
|
||||
} else { // Message depends if multilocation is enabled.
|
||||
this.messageType = this.groups.group_stock_multi_locations && !this.lastScanned.sourceLocation ?
|
||||
'scan_src' :
|
||||
'scan_product';
|
||||
}
|
||||
|
||||
const barcodeInformations = { class: this.messageType, warning: false, icon: 'barcode' };
|
||||
switch (this.messageType) {
|
||||
case 'scan_product':
|
||||
barcodeInformations.message = this.groups.group_stock_multi_locations ?
|
||||
sprintf(_t("Scan a product in %s or scan another location"), this.location.display_name) :
|
||||
_t("Scan a product");
|
||||
break;
|
||||
case 'scan_src':
|
||||
barcodeInformations.message = _t("Scan a location");
|
||||
barcodeInformations.icon = 'sign-out';
|
||||
break;
|
||||
case 'scan_product_or_src':
|
||||
barcodeInformations.message = sprintf(
|
||||
_t("Scan more products in %s or scan another location"),
|
||||
this.location.display_name);
|
||||
break;
|
||||
case 'scan_product_or_dest':
|
||||
barcodeInformations.message = _t("Scan more products, or scan the destination location");
|
||||
barcodeInformations.icon = 'sign-in';
|
||||
break;
|
||||
case 'scan_lot':
|
||||
barcodeInformations.message = sprintf(
|
||||
_t("Scan lot numbers for product %s to change their quantity"),
|
||||
line.product_id.display_name
|
||||
);
|
||||
break;
|
||||
case 'scan_serial':
|
||||
barcodeInformations.message = sprintf(
|
||||
_t("Scan serial numbers for product %s to change their quantity"),
|
||||
line.product_id.display_name
|
||||
);
|
||||
break;
|
||||
}
|
||||
return barcodeInformations;
|
||||
}
|
||||
|
||||
get displayByUnitButton () {
|
||||
return true;
|
||||
}
|
||||
|
||||
get displaySetButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.userId = data.data.user_id;
|
||||
super.setData(...arguments);
|
||||
const companies = data.data.records['res.company'];
|
||||
this.companyIds = companies.map(company => company.id);
|
||||
this.lineFormViewId = data.data.line_view_id;
|
||||
}
|
||||
|
||||
get displayApplyButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getDisplayIncrementBtn(line) {
|
||||
return line.product_id.tracking !== 'serial' && this.selectedLine &&
|
||||
line.virtual_id === this.selectedLine.virtual_id;
|
||||
}
|
||||
|
||||
getDisplayDecrementBtn(line) {
|
||||
return this.getDisplayIncrementBtn(line);
|
||||
}
|
||||
|
||||
getQtyDone(line) {
|
||||
return line.inventory_quantity;
|
||||
}
|
||||
|
||||
getQtyDemand(line) {
|
||||
return line.quantity;
|
||||
}
|
||||
|
||||
getActionRefresh(newId) {
|
||||
const action = super.getActionRefresh(newId);
|
||||
action.params.res_id = this.currentState.lines.map(l => l.id);
|
||||
if (newId) {
|
||||
action.params.res_id.push(newId);
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
get highlightValidateButton() {
|
||||
return this.applyOn > 0 && this.applyOn === this.pageLines.length;
|
||||
}
|
||||
|
||||
get incrementButtonsDisplayStyle() {
|
||||
return "d-block my-3";
|
||||
}
|
||||
|
||||
IsNotSet(line) {
|
||||
return !line.inventory_quantity_set;
|
||||
}
|
||||
|
||||
lineIsFaulty(line) {
|
||||
return line.inventory_quantity_set && line.inventory_quantity !== line.quantity;
|
||||
}
|
||||
|
||||
get printButtons() {
|
||||
return [{
|
||||
name: _t("Print Inventory"),
|
||||
class: 'o_print_inventory',
|
||||
action: 'stock.action_report_inventory',
|
||||
}];
|
||||
}
|
||||
|
||||
get recordIds() {
|
||||
return this.currentState.lines.map(l => l.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the line as set and set its inventory quantity if it was unset, or
|
||||
* unset it if the line was already set.
|
||||
*
|
||||
* @param {Object} line
|
||||
*/
|
||||
setOnHandQuantity(line) {
|
||||
if (line.product_id.tracking === 'serial') { // Special case for product tracked by SN.
|
||||
const quantity = !(line.lot_name || line.lot_id) && line.quantity || 1;
|
||||
if (line.inventory_quantity_set) {
|
||||
line.inventory_quantity = line.inventory_quantity ? 0 : quantity;
|
||||
line.inventory_quantity_set = line.inventory_quantity != quantity;
|
||||
} else {
|
||||
line.inventory_quantity = quantity;
|
||||
line.inventory_quantity_set = true;
|
||||
}
|
||||
this._markLineAsDirty(line);
|
||||
} else {
|
||||
if (line.inventory_quantity_set) {
|
||||
line.inventory_quantity = 0;
|
||||
line.inventory_quantity_set = false;
|
||||
this._markLineAsDirty(line);
|
||||
} else {
|
||||
const inventory_quantity = line.quantity - line.inventory_quantity;
|
||||
this.updateLine(line, { inventory_quantity });
|
||||
line.inventory_quantity_set = true;
|
||||
}
|
||||
}
|
||||
this.trigger('update');
|
||||
}
|
||||
|
||||
updateLineQty(virtualId, qty = 1) {
|
||||
this.actionMutex.exec(() => {
|
||||
const line = this.pageLines.find(l => l.virtual_id === virtualId);
|
||||
this.updateLine(line, {inventory_quantity: qty});
|
||||
this.trigger('update');
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Private
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
_getCommands() {
|
||||
return Object.assign(super._getCommands(), {
|
||||
'O-BTN.apply': this.apply.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
_getNewLineDefaultContext() {
|
||||
return {
|
||||
default_company_id: this.companyIds[0],
|
||||
default_location_id: this._defaultLocation().id,
|
||||
default_inventory_quantity: 1,
|
||||
default_user_id: this.userId,
|
||||
inventory_mode: true,
|
||||
};
|
||||
}
|
||||
|
||||
_createCommandVals(line) {
|
||||
const values = {
|
||||
dummy_id: line.virtual_id,
|
||||
inventory_date: line.inventory_date,
|
||||
inventory_quantity: line.inventory_quantity,
|
||||
inventory_quantity_set: line.inventory_quantity_set,
|
||||
location_id: line.location_id,
|
||||
lot_id: line.lot_id,
|
||||
lot_name: line.lot_name,
|
||||
package_id: line.package_id,
|
||||
product_id: line.product_id,
|
||||
owner_id: line.owner_id,
|
||||
user_id: this.userId,
|
||||
};
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
values[key] = this._fieldToValue(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
async _createNewLine(params) {
|
||||
// When creating a new line, we need to know if a quant already exists
|
||||
// for this line, and in this case, update the new line fields.
|
||||
const product = params.fieldsParams.product_id;
|
||||
if (product.detailed_type != 'product') {
|
||||
const productName = (product.default_code ? `[${product.default_code}] ` : '') + product.display_name;
|
||||
const message = sprintf(
|
||||
_t("%s can't be inventoried. Only storable products can be inventoried."), productName);
|
||||
this.notification.add(message, { type: 'warning' });
|
||||
return false;
|
||||
}
|
||||
const domain = [
|
||||
['location_id', '=', this.location.id],
|
||||
['product_id', '=', product.id],
|
||||
];
|
||||
if (product.tracking !== 'none') {
|
||||
if (params.fieldsParams.lot_name) { // Search for a quant with the exact same lot.
|
||||
domain.push(['lot_id.name', '=', params.fieldsParams.lot_name]);
|
||||
} else { // Search for a quant with no lot.
|
||||
domain.push(['lot_id', '=', false]);
|
||||
}
|
||||
}
|
||||
if (params.fieldsParams.package_id) {
|
||||
domain.push(['package_id', '=', params.fieldsParams.package_id]);
|
||||
}
|
||||
const quant = await this.orm.searchRead(
|
||||
'stock.quant',
|
||||
domain,
|
||||
['id', 'inventory_date', 'inventory_quantity', 'inventory_quantity_set', 'quantity', 'user_id'],
|
||||
{ limit: 1 }
|
||||
);
|
||||
if (quant.length) {
|
||||
Object.assign(params.fieldsParams, quant[0], { inventory_quantity: 1 });
|
||||
}
|
||||
const newLine = await super._createNewLine(params);
|
||||
if (quant.length) {
|
||||
// If the quant already exits, we add it into the `initialState` to
|
||||
// avoid comparison issue with the `currentState` when the save occurs.
|
||||
this.initialState.lines.push(Object.assign({}, newLine, quant[0]));
|
||||
}
|
||||
return newLine;
|
||||
}
|
||||
|
||||
_convertDataToFieldsParams(args) {
|
||||
const params = {
|
||||
inventory_quantity: args.quantity,
|
||||
lot_id: args.lot,
|
||||
lot_name: args.lotName,
|
||||
owner_id: args.owner,
|
||||
package_id: args.package || args.resultPackage,
|
||||
product_id: args.product,
|
||||
product_uom_id: args.product && args.product.uom_id,
|
||||
};
|
||||
return params;
|
||||
}
|
||||
|
||||
_getNewLineDefaultValues(fieldsParams) {
|
||||
const defaultValues = super._getNewLineDefaultValues(...arguments);
|
||||
return Object.assign(defaultValues, {
|
||||
inventory_date: new Date().toISOString().slice(0, 10),
|
||||
inventory_quantity: 0,
|
||||
inventory_quantity_set: true,
|
||||
quantity: (fieldsParams && fieldsParams.quantity) || 0,
|
||||
user_id: this.userId,
|
||||
});
|
||||
}
|
||||
|
||||
_getFieldToWrite() {
|
||||
return [
|
||||
'inventory_date',
|
||||
'inventory_quantity',
|
||||
'inventory_quantity_set',
|
||||
'user_id',
|
||||
'location_id',
|
||||
'lot_name',
|
||||
'lot_id',
|
||||
'package_id',
|
||||
'owner_id',
|
||||
];
|
||||
}
|
||||
|
||||
_getSaveCommand() {
|
||||
const commands = this._getSaveLineCommand();
|
||||
if (commands.length) {
|
||||
return {
|
||||
route: '/stock_barcode/save_barcode_data',
|
||||
params: {
|
||||
model: this.params.model,
|
||||
res_id: false,
|
||||
write_field: false,
|
||||
write_vals: commands,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
_groupSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone) {
|
||||
return Object.assign(super._groupSublines(...arguments), {
|
||||
inventory_quantity: qtyDone,
|
||||
quantity: qtyDemand,
|
||||
});
|
||||
}
|
||||
|
||||
_lineIsNotComplete(line) {
|
||||
return line.inventory_quantity === 0;
|
||||
}
|
||||
|
||||
async _processPackage(barcodeData) {
|
||||
const { packageType, packageName } = barcodeData;
|
||||
let recPackage = barcodeData.package;
|
||||
this.lastScanned.packageId = false;
|
||||
if (!recPackage && !packageType && !packageName) {
|
||||
return; // No Package data to process.
|
||||
}
|
||||
// Scan a new package and/or a package type -> Create a new package with those parameters.
|
||||
const currentLine = this.selectedLine || this.lastScannedLine;
|
||||
if (currentLine.package_id && packageType &&
|
||||
!recPackage && ! packageName &&
|
||||
currentLine.package_id.id !== packageType) {
|
||||
// Changes the package type for the scanned one.
|
||||
await this.orm.write('stock.quant.package', [currentLine.package_id.id], {
|
||||
package_type_id: packageType.id,
|
||||
});
|
||||
const message = sprintf(
|
||||
_t("Package type %s was correctly applied to the package %s"),
|
||||
packageType.name, currentLine.package_id.name
|
||||
);
|
||||
barcodeData.stopped = true;
|
||||
return this.notification.add(message, { type: 'success' });
|
||||
}
|
||||
if (!recPackage) {
|
||||
if (currentLine && !currentLine.package_id) {
|
||||
const valueList = {};
|
||||
if (packageName) {
|
||||
valueList.name = packageName;
|
||||
}
|
||||
if (packageType) {
|
||||
valueList.package_type_id = packageType.id;
|
||||
}
|
||||
const newPackageData = await this.orm.call(
|
||||
'stock.quant.package',
|
||||
'action_create_from_barcode',
|
||||
[valueList]
|
||||
);
|
||||
this.cache.setCache(newPackageData);
|
||||
recPackage = newPackageData['stock.quant.package'][0];
|
||||
}
|
||||
}
|
||||
if (!recPackage && packageName) {
|
||||
const currentLine = this.selectedLine || this.lastScannedLine;
|
||||
if (currentLine && !currentLine.package_id) {
|
||||
const newPackageData = await this.orm.call(
|
||||
'stock.quant.package',
|
||||
'action_create_from_barcode',
|
||||
[{ name: packageName }]
|
||||
);
|
||||
this.cache.setCache(newPackageData);
|
||||
recPackage = newPackageData['stock.quant.package'][0];
|
||||
}
|
||||
}
|
||||
if (!recPackage || (
|
||||
recPackage.location_id && recPackage.location_id != this.location.id
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
// TODO: can check if quants already in cache to avoid to make a RPC if
|
||||
// there is all in it (or make the RPC only on missing quants).
|
||||
const res = await this.orm.call(
|
||||
'stock.quant',
|
||||
'get_stock_barcode_data_records',
|
||||
[recPackage.quant_ids]
|
||||
);
|
||||
const quants = res.records['stock.quant'];
|
||||
if (!quants.length) { // Empty package => Assigns it to the last scanned line.
|
||||
const currentLine = this.selectedLine || this.lastScannedLine;
|
||||
if (currentLine && !currentLine.package_id && !currentLine.result_package_id) {
|
||||
const fieldsParams = this._convertDataToFieldsParams({
|
||||
resultPackage: recPackage,
|
||||
});
|
||||
await this.updateLine(currentLine, fieldsParams);
|
||||
barcodeData.stopped = true;
|
||||
this.selectedLineVirtualId = false;
|
||||
this.lastScanned.packageId = recPackage.id;
|
||||
this.trigger('update');
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.cache.setCache(res.records);
|
||||
|
||||
// Checks if the package is already scanned.
|
||||
let alreadyExisting = 0;
|
||||
for (const line of this.pageLines) {
|
||||
if (line.package_id && line.package_id.id === recPackage.id &&
|
||||
this.getQtyDone(line) > 0) {
|
||||
alreadyExisting++;
|
||||
}
|
||||
}
|
||||
if (alreadyExisting === quants.length) {
|
||||
barcodeData.error = _t("This package is already scanned.");
|
||||
return;
|
||||
}
|
||||
// For each quants, creates or increments a barcode line.
|
||||
for (const quant of quants) {
|
||||
const product = this.cache.getRecord('product.product', quant.product_id);
|
||||
const searchLineParams = Object.assign({}, barcodeData, { product });
|
||||
const currentLine = this._findLine(searchLineParams);
|
||||
if (currentLine) { // Updates an existing line.
|
||||
const fieldsParams = this._convertDataToFieldsParams({
|
||||
quantity: quant.quantity,
|
||||
lotName: barcodeData.lotName,
|
||||
lot: barcodeData.lot,
|
||||
package: recPackage,
|
||||
owner: barcodeData.owner,
|
||||
});
|
||||
await this.updateLine(currentLine, fieldsParams);
|
||||
} else { // Creates a new line.
|
||||
const fieldsParams = this._convertDataToFieldsParams({
|
||||
product,
|
||||
quantity: quant.quantity,
|
||||
lot: quant.lot_id,
|
||||
package: quant.package_id,
|
||||
resultPackage: quant.package_id,
|
||||
owner: quant.owner_id,
|
||||
});
|
||||
const newLine = await this._createNewLine({ fieldsParams });
|
||||
newLine.inventory_quantity = quant.quantity;
|
||||
}
|
||||
}
|
||||
barcodeData.stopped = true;
|
||||
this.selectedLineVirtualId = false;
|
||||
this.lastScanned.packageId = recPackage.id;
|
||||
this.trigger('update');
|
||||
}
|
||||
|
||||
_updateLineQty(line, args) {
|
||||
if (args.quantity) { // Set stock quantity.
|
||||
line.quantity = args.quantity;
|
||||
}
|
||||
if (args.inventory_quantity) { // Increments inventory quantity.
|
||||
if (args.uom) {
|
||||
// An UoM was passed alongside the quantity, needs to check it's
|
||||
// compatible with the product's UoM.
|
||||
const productUOM = this.cache.getRecord('uom.uom', line.product_id.uom_id);
|
||||
if (args.uom.category_id !== productUOM.category_id) {
|
||||
// Not the same UoM's category -> Can't be converted.
|
||||
const message = sprintf(
|
||||
_t("Scanned quantity uses %s as Unit of Measure, but this UoM is not compatible with the product's one (%s)."),
|
||||
args.uom.name, productUOM.name
|
||||
);
|
||||
return this.notification.add(message, { title: _t("Wrong Unit of Measure"), type: 'warning' });
|
||||
} else if (args.uom.id !== productUOM.id) {
|
||||
// Compatible but not the same UoM => Need a conversion.
|
||||
args.inventory_quantity = (args.inventory_quantity / args.uom.factor) * productUOM.factor;
|
||||
}
|
||||
}
|
||||
line.inventory_quantity += args.inventory_quantity;
|
||||
line.inventory_quantity_set = true;
|
||||
if (line.product_id.tracking === 'serial' && (line.lot_name || line.lot_id)) {
|
||||
line.inventory_quantity = Math.max(0, Math.min(1, line.inventory_quantity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _updateLotName(line, lotName) {
|
||||
if (line.lot_name === lotName) {
|
||||
// No need to update the line's tracking number if it's already set.
|
||||
return Promise.resolve();
|
||||
}
|
||||
line.lot_name = lotName;
|
||||
// Checks if a quant exists for this line and updates the line in this case.
|
||||
const domain = [
|
||||
['location_id', '=', line.location_id.id],
|
||||
['product_id', '=', line.product_id.id],
|
||||
['lot_id.name', '=', lotName],
|
||||
['owner_id', '=', line.owner_id && line.owner_id.id],
|
||||
['package_id', '=', line.package_id && line.package_id.id],
|
||||
];
|
||||
const existingQuant = await this.orm.searchRead(
|
||||
'stock.quant',
|
||||
domain,
|
||||
['id', 'quantity'],
|
||||
{ limit: 1, load: false }
|
||||
);
|
||||
if (existingQuant.length) {
|
||||
Object.assign(line, existingQuant[0]);
|
||||
if (line.lot_id) {
|
||||
line.lot_id = await this.cache.getRecordByBarcode(lotName, 'stock.lot');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_createLinesState() {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const lines = [];
|
||||
for (const id of Object.keys(this.cache.dbIdCache['stock.quant']).map(id => Number(id))) {
|
||||
const quant = this.cache.getRecord('stock.quant', id);
|
||||
if (quant.user_id !== this.userId || quant.inventory_date > today) {
|
||||
// Doesn't take quants who must be counted by another user or in the future.
|
||||
continue;
|
||||
}
|
||||
// Checks if this line is already in the quant state to get back
|
||||
// its `virtual_id` (and so, avoid to set a new `virtual_id`).
|
||||
const prevLine = this.currentState && this.currentState.lines.find(l => l.id === id);
|
||||
const previousVirtualId = prevLine && prevLine.virtual_id;
|
||||
quant.virtual_id = quant.dummy_id || previousVirtualId || this._uniqueVirtualId;
|
||||
quant.product_id = this.cache.getRecord('product.product', quant.product_id);
|
||||
quant.location_id = this.cache.getRecord('stock.location', quant.location_id);
|
||||
quant.lot_id = quant.lot_id && this.cache.getRecord('stock.lot', quant.lot_id);
|
||||
quant.package_id = quant.package_id && this.cache.getRecord('stock.quant.package', quant.package_id);
|
||||
quant.owner_id = quant.owner_id && this.cache.getRecord('res.partner', quant.owner_id);
|
||||
lines.push(Object.assign({}, quant));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
_getName() {
|
||||
return _t("Inventory Adjustment");
|
||||
}
|
||||
|
||||
_getPrintOptions() {
|
||||
const options = super._getPrintOptions();
|
||||
const quantsToPrint = this.pageLines.filter(quant => quant.inventory_quantity_set);
|
||||
if (quantsToPrint.length === 0) {
|
||||
return { warning: _t("There is nothing to print in this page.") };
|
||||
}
|
||||
options.additional_context = { active_ids: quantsToPrint.map(quant => quant.id) };
|
||||
return options;
|
||||
}
|
||||
}
|
||||
15
stock_barcode/static/src/scss/stock_barcode.dark.scss
Normal file
15
stock_barcode/static/src/scss/stock_barcode.dark.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
// = Stock Barcode
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_barcode_client_action {
|
||||
--barcode__header-bg: #{$o-gray-100};
|
||||
|
||||
--barcode__linesHeader-bg: #{$o-gray-300};
|
||||
|
||||
--barcode__line--completed: #1f5a2d;
|
||||
--barcode__line--notCompleted: #303030;
|
||||
|
||||
--barcode__input--completed: #262c26;
|
||||
--barcode__input--notCompleted: #303030;
|
||||
}
|
||||
138
stock_barcode/static/src/scss/stock_barcode.scss
Normal file
138
stock_barcode/static/src/scss/stock_barcode.scss
Normal file
@@ -0,0 +1,138 @@
|
||||
.o_stock_barcode_main_menu_container {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.o_stock_barcode_main_menu {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
font-size: 1.2em;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.o_stock_mobile_barcode_container{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
.o_stock_mobile_barcode{
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
position: absolute;
|
||||
opacity: 0.75;
|
||||
font-size: 12px;
|
||||
.o_mobile_barcode_camera{
|
||||
margin: 5px;
|
||||
font-size: 2.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_stock_barcode_menu {
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.message_demo_barcodes {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
>ul {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.o_stock_barcode_container {
|
||||
span.o_stock_barcode_laser {
|
||||
@include o-position-absolute(50%, -15px, auto, -15px);
|
||||
height: 5px;
|
||||
background: rgba(red, 0.6);
|
||||
box-shadow: 0 1px 10px 1px rgba(red, 0.8);
|
||||
animation: o_barcode_scanner_intro 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) 0.4s;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 200px;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: 140px;
|
||||
height: 94px;
|
||||
}
|
||||
}
|
||||
.o_stock_mobile_barcode {
|
||||
height: 94px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex: 0 0 auto;
|
||||
width: 550px;
|
||||
border-radius: 4px;
|
||||
margin-top: -24px;
|
||||
|
||||
.o_stock_barcode_menu {
|
||||
line-height: 2.6;
|
||||
}
|
||||
|
||||
button.btn-primary {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.row .col .btn {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.row .col {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Defines animation for highlighting flash.
|
||||
$highlighting-colors: (
|
||||
"primary": theme-color("primary"),
|
||||
"white": white
|
||||
);
|
||||
|
||||
@each $c-name, $c-value in $highlighting-colors {
|
||||
@keyframes highlighting-flash-#{$c-name} {
|
||||
0% {
|
||||
background-color: #{$c-value};
|
||||
}
|
||||
20% {
|
||||
background-color: transparent;
|
||||
}
|
||||
21% {
|
||||
background-color: #{$c-value};
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes o_barcode_scanner_intro {
|
||||
25% {
|
||||
top: 75%;
|
||||
}
|
||||
50% {
|
||||
top: 0;
|
||||
}
|
||||
75% {
|
||||
top: 100%;
|
||||
}
|
||||
100% {
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
111
stock_barcode/static/src/widgets/digipad.js
Normal file
111
stock_barcode/static/src/widgets/digipad.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, onWillStart } = owl;
|
||||
|
||||
export class Digipad extends Component {
|
||||
setup() {
|
||||
this.orm = useService('orm');
|
||||
const user = useService('user');
|
||||
this.buttons = [7, 8, 9, 4, 5, 6, 1, 2, 3, '.', '0', 'erase'].map((value, index) => {
|
||||
return { index, value };
|
||||
});
|
||||
this.value = String(this.props.record.data[this.props.quantityField]);
|
||||
onWillStart(async () => {
|
||||
this.displayUOM = await user.hasGroup('uom.group_uom');
|
||||
await this._fetchPackagingButtons();
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Copies the input value if digipad value is not set yet, or overrides it if there is a
|
||||
* difference between the two values (in case user has manualy edited the input value).
|
||||
* @private
|
||||
*/
|
||||
_checkInputValue() {
|
||||
const input = document.querySelector(`div[name="${this.props.quantityField}"] input`);
|
||||
const inputValue = input.value;
|
||||
if (Number(this.value) != Number(inputValue)) {
|
||||
console.warn(`-- Change widget value: ${this.value} -> ${inputValue}`);
|
||||
this.value = inputValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the field value by the interval amount (1 by default).
|
||||
* @private
|
||||
* @param {integer} [interval=1]
|
||||
*/
|
||||
async _increment(interval=1) {
|
||||
this._checkInputValue();
|
||||
const numberValue = Number(this.value || 0);
|
||||
this.value = String(numberValue + interval);
|
||||
await this._notifyChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies changes on the field to mark the record as dirty.
|
||||
* @private
|
||||
*/
|
||||
async _notifyChanges() {
|
||||
const changes = { [this.props.quantityField]: Number(this.value) };
|
||||
await this.props.record.update(changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for the product's packaging buttons.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async _fetchPackagingButtons() {
|
||||
const record = this.props.record.data;
|
||||
const demandQty = record.reserved_uom_qty;
|
||||
const domain = [['product_id', '=', record.product_id[0]]];
|
||||
if (demandQty) { // Doesn't fetch packaging with a too high quantity.
|
||||
domain.push(['qty', '<=', demandQty]);
|
||||
}
|
||||
this.packageButtons = await this.orm.searchRead(
|
||||
'product.packaging',
|
||||
domain,
|
||||
['name', 'product_uom_id', 'qty'],
|
||||
{ limit: 3 },
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handles the click on one of the digipad's button and updates the value..
|
||||
* @private
|
||||
* @param {String} button
|
||||
*/
|
||||
_onCLickButton(button) {
|
||||
this._checkInputValue();
|
||||
if (button === 'erase') {
|
||||
this.value = this.value.substr(0, this.value.length - 1);
|
||||
} else {
|
||||
if (button === '.' && this.value.indexOf('.') != -1) {
|
||||
// Avoids to add a decimal separator multiple time.
|
||||
return;
|
||||
}
|
||||
this.value += button;
|
||||
}
|
||||
this._notifyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
Digipad.template = 'stock_barcode.DigipadTemplate';
|
||||
Digipad.extractProps = ({ attrs }) => {
|
||||
return {
|
||||
quantityField: attrs.quantity_field,
|
||||
};
|
||||
};
|
||||
registry.category('view_widgets').add('digipad', Digipad);
|
||||
46
stock_barcode/static/src/widgets/digipad.scss
Normal file
46
stock_barcode/static/src/widgets/digipad.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
.o_digipad_widget {
|
||||
display: flex;
|
||||
|
||||
.btn {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 2em;
|
||||
|
||||
.o_web_client.o_touch_device & {
|
||||
border-radius: 4px;
|
||||
font-size: 1em;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_digipad_digit_buttons, .o_digipad_special_buttons {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.o_digipad_digit_buttons {
|
||||
width: 75%;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.o_digipad_special_buttons {
|
||||
width: 25%;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
.o_packaging_button {
|
||||
font-size: 1em;
|
||||
overflow: hidden;
|
||||
|
||||
div[name=packaging_name] {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 0.66em;
|
||||
}
|
||||
}
|
||||
36
stock_barcode/static/src/widgets/digipad.xml
Normal file
36
stock_barcode/static/src/widgets/digipad.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="stock_barcode.DigipadTemplate" class="o_digipad_widget" owl="1">
|
||||
<t t-set="commonCssClasses">o_digipad_button btn d-flex justify-content-center align-items-center border w-100 py-2</t>
|
||||
<!-- Digits, erase and dot buttons -->
|
||||
<div class="o_digipad_digit_buttons me-2">
|
||||
<div t-foreach="buttons" t-as="button" t-key="button.index" t-att-data-button="button.value"
|
||||
class="btn-primary" t-att-class="commonCssClasses"
|
||||
t-on-click="() => this._onCLickButton(button.value)">
|
||||
<div t-if="button.value == 'erase'" class="fa fa-lg fa-long-arrow-left"/>
|
||||
<div t-else="" t-out="button.value"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_digipad_special_buttons">
|
||||
<!-- +1 / -1 buttons -->
|
||||
<div class="btn-secondary o_increase" t-att-class="commonCssClasses"
|
||||
t-on-click="() => this._increment()"
|
||||
t-att-data-button="increase">+1</div>
|
||||
<div class="btn-secondary o_decrease" t-att-class="commonCssClasses"
|
||||
t-on-click="() => this._increment(-1)"
|
||||
t-att-data-button="decrease">-1</div>
|
||||
<!-- Product packagings buttons -->
|
||||
<div t-foreach="packageButtons" t-as="button" t-key="button.id" t-att-data-qty="button.qty"
|
||||
t-on-click="() => this._increment(button.qty)"
|
||||
class="o_packaging_button btn btn-secondary border w-100 py-2">
|
||||
<div class="text-capitalize">
|
||||
<span t-out="'+' + button.qty"/>
|
||||
<span t-if="displayUOM" class="small-text ms-1" t-out="button.product_uom_id[1]"/>
|
||||
</div>
|
||||
<div name="packaging_name" class="small-text" t-out="button.name"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
34
stock_barcode/static/src/widgets/set_reserved_qty_button.js
Normal file
34
stock_barcode/static/src/widgets/set_reserved_qty_button.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, onWillStart } = owl;
|
||||
|
||||
export class SetReservedQuantityButton extends Component {
|
||||
setup() {
|
||||
const user = useService('user');
|
||||
onWillStart(async () => {
|
||||
this.displayUOM = await user.hasGroup('uom.group_uom');
|
||||
});
|
||||
}
|
||||
|
||||
get uom() {
|
||||
const [id, name] = this.props.record.data.product_uom_id || [];
|
||||
return { id, name };
|
||||
}
|
||||
|
||||
_setQuantity (ev) {
|
||||
ev.stopPropagation();
|
||||
this.props.record.update({ [this.props.fieldToSet]: this.props.value });
|
||||
}
|
||||
}
|
||||
|
||||
SetReservedQuantityButton.extractProps = ({ attrs }) => {
|
||||
if (attrs.field_to_set) {
|
||||
return { fieldToSet: attrs.field_to_set };
|
||||
}
|
||||
};
|
||||
|
||||
SetReservedQuantityButton.template = 'stock_barcode.SetReservedQuantityButtonTemplate';
|
||||
registry.category('fields').add('set_reserved_qty_button', SetReservedQuantityButton);
|
||||
@@ -0,0 +1,5 @@
|
||||
.o_web_client.o_touch_device .o_button_qty_done, .o_button_qty_done {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 1em;
|
||||
}
|
||||
14
stock_barcode/static/src/widgets/set_reserved_qty_button.xml
Normal file
14
stock_barcode/static/src/widgets/set_reserved_qty_button.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<button t-name="stock_barcode.SetReservedQuantityButtonTemplate" owl="1"
|
||||
class="o_button_qty_done d-flex btn"
|
||||
t-attf-class="{{props.value ? 'btn-primary' : 'btn-secondary'}}"
|
||||
t-on-click="_setQuantity" t-att-disabled="!props.value">
|
||||
<t t-if="props.value">
|
||||
/ <span name="product_uom_qty" class="ms-1" t-out="props.value"/>
|
||||
</t>
|
||||
<span t-if="displayUOM" name="product_uom_id" class="text-capitalize ms-1" t-out="uom.name"/>
|
||||
</button>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,52 @@
|
||||
odoo.define('stock_barcode.RunningTourActionHelper', function(require) {
|
||||
"use strict";
|
||||
|
||||
var RunningTourActionHelper = require('web_tour.RunningTourActionHelper');
|
||||
|
||||
RunningTourActionHelper.include({
|
||||
_scan: function (element, barcode) {
|
||||
odoo.__DEBUG__.services['web.core'].bus.trigger('barcode_scanned', barcode, element);
|
||||
},
|
||||
scan: function(barcode, element) {
|
||||
this._scan(this._get_action_values(element), barcode);
|
||||
},
|
||||
});
|
||||
|
||||
const StepUtils = require('web_tour.TourStepUtils');
|
||||
StepUtils.include({
|
||||
closeModal() {
|
||||
return {
|
||||
trigger: '.btn.btn-primary',
|
||||
in_modal: true,
|
||||
};
|
||||
},
|
||||
confirmAddingUnreservedProduct() {
|
||||
return {
|
||||
trigger: '.btn-primary',
|
||||
extra_trigger: '.modal-title:contains("Add extra product?")',
|
||||
in_modal: true,
|
||||
};
|
||||
},
|
||||
validateBarcodeForm() {
|
||||
return [{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan O-BTN.validate'
|
||||
}, {
|
||||
trigger: '.o_notification.border-success',
|
||||
}];
|
||||
},
|
||||
discardBarcodeForm() {
|
||||
return [{
|
||||
content: "discard barcode form",
|
||||
trigger: '.o_discard',
|
||||
auto: true,
|
||||
}, {
|
||||
content: "wait to be back on the barcode lines",
|
||||
trigger: '.o_add_line',
|
||||
auto: true,
|
||||
run() {},
|
||||
}];
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
290
stock_barcode/static/tests/tours/tour_helper_stock_barcode.js
Normal file
290
stock_barcode/static/tests/tours/tour_helper_stock_barcode.js
Normal file
@@ -0,0 +1,290 @@
|
||||
odoo.define('stock_barcode.tourHelper', function (require) {
|
||||
'use strict';
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
function fail (errorMessage) {
|
||||
tour._consume_tour(tour.running_tour, errorMessage);
|
||||
}
|
||||
|
||||
function getLine (description) {
|
||||
var $res;
|
||||
$('.o_barcode_lines > .o_barcode_line').each(function () {
|
||||
var $line = $(this);
|
||||
const barcode = $line[0].dataset.barcode.trim();
|
||||
if (description.barcode === barcode) {
|
||||
if ($res) {
|
||||
$res = $res.add($line);
|
||||
} else {
|
||||
$res = $line;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (! $res) {
|
||||
fail('cannot get the line with the barcode ' + description.barcode);
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
function getSubline(selector) {
|
||||
const $subline = $('.o_sublines .o_barcode_line' + selector);
|
||||
if ($subline.length === 0) {
|
||||
fail(`No subline was found for the selector "${selector}"`);
|
||||
} else if($subline.length > 1) {
|
||||
fail(`Multiple sublines were found for the selector "${selector}"`);
|
||||
}
|
||||
return $subline;
|
||||
}
|
||||
|
||||
function triggerKeydown(eventKey, shiftkey=false) {
|
||||
document.querySelector('.o_barcode_client_action')
|
||||
.dispatchEvent(new window.KeyboardEvent('keydown', { bubbles: true, key: eventKey, shiftKey: shiftkey}));
|
||||
}
|
||||
|
||||
function assert (current, expected, info) {
|
||||
if (current !== expected) {
|
||||
fail(info + ': "' + current + '" instead of "' + expected + '".');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a button on the given line is visible.
|
||||
*
|
||||
* @param {jQuerryElement} $line the line where we test the button visibility.
|
||||
* @param {string} buttonName could be 'add_quantity' or 'remove_unit'.
|
||||
* @param {boolean} [isVisible=true]
|
||||
*/
|
||||
function assertButtonIsVisible($line, buttonName, isVisible=true) {
|
||||
const $button = $line.find(`.o_${buttonName}`);
|
||||
assert($button.length, isVisible ? 1 : 0,
|
||||
isVisible ? `Buttons should be in the DOM` : "Button shouldn't be in the DOM");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a button on the given line is invisible.
|
||||
*
|
||||
* @param {jQuerryElement} $line the line where we test the button visibility.
|
||||
* @param {string} buttonName could be 'add_quantity' or 'remove_unit'.
|
||||
*/
|
||||
function assertButtonIsNotVisible ($line, buttonName) {
|
||||
assertButtonIsVisible($line, buttonName, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if both "Add unit" and "Add reserved remaining quantity" buttons are
|
||||
* displayed or not on the given line.
|
||||
*
|
||||
* @param {integer} lineIndex
|
||||
* @param {boolean} isVisible
|
||||
*/
|
||||
function assertLineButtonsAreVisible(lineIndex, isVisible, cssSelector='.o_line_button') {
|
||||
const $buttonAddQty = $(`.o_barcode_line:eq(${lineIndex}) ${cssSelector}`);
|
||||
const message = `Buttons must be ${(isVisible ? 'visible' : 'hidden')}`;
|
||||
assert($buttonAddQty.length > 0, isVisible, message);
|
||||
}
|
||||
|
||||
function assertValidateVisible (expected) {
|
||||
const validateButton = document.querySelector('.o_validate_page,.o_apply_page');
|
||||
assert(Boolean(validateButton), expected, 'Validate visible');
|
||||
}
|
||||
|
||||
function assertValidateEnabled (expected) {
|
||||
const validateButton = document.querySelector('.o_validate_page,.o_apply_page') || false;
|
||||
assert(validateButton && !validateButton.hasAttribute('disabled'), expected, 'Validate enabled');
|
||||
}
|
||||
|
||||
function assertValidateIsHighlighted (expected) {
|
||||
const validateButton = document.querySelector('.o_validate_page,.o_apply_page') || false;
|
||||
const isHighlighted = validateButton && validateButton.classList.contains('btn-success');
|
||||
assert(isHighlighted, expected, 'Validate button is highlighted');
|
||||
}
|
||||
|
||||
function assertLinesCount(expected) {
|
||||
const current = document.querySelectorAll('.o_barcode_lines > .o_barcode_line').length;
|
||||
assert(current, expected, `Should have ${expected} line(s)`);
|
||||
}
|
||||
|
||||
function assertScanMessage (expected) {
|
||||
const instruction = document.querySelector(`.o_scan_message`);
|
||||
const cssClass = instruction.classList[1];
|
||||
assert(cssClass, `o_${expected}`, "Not the right message displayed");
|
||||
}
|
||||
|
||||
function assertSublinesCount(expected) {
|
||||
const current = document.querySelectorAll('.o_sublines > .o_barcode_line').length;
|
||||
assert(current, expected, `Should have ${expected} subline(s), found ${current}`);
|
||||
}
|
||||
|
||||
function assertLineDestinationIsNotVisible(line) {
|
||||
const destinationElement = line.querySelector('.o_line_destination_location');
|
||||
if (destinationElement) {
|
||||
const product = line.querySelector('.product-label').innerText;
|
||||
fail(`The destination for line of the product ${product} should not be visible, "${destinationElement.innerText}" instead`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given line is going in the given location. Implies the destination is visible.
|
||||
* @param {Element} line
|
||||
* @param {string} location
|
||||
*/
|
||||
function assertLineDestinationLocation(line, location) {
|
||||
const destinationElement = line.querySelector('.o_line_destination_location');
|
||||
const product = line.querySelector('.product-label').innerText;
|
||||
if (!destinationElement) {
|
||||
fail(`The destination (${location}) for line of the product ${product} is not visible`);
|
||||
}
|
||||
assert(
|
||||
destinationElement.innerText, location,
|
||||
`The destination for line of product ${product} isn't in the right location`);
|
||||
}
|
||||
|
||||
function assertLineIsHighlighted ($line, expected) {
|
||||
assert($line.hasClass('o_highlight'), expected, 'line should be highlighted');
|
||||
}
|
||||
|
||||
function assertLineQty($line, expectedQuantity) {
|
||||
const lineNode = $line[0];
|
||||
if (!lineNode) {
|
||||
fail("Can't check the quantity: no line was given.");
|
||||
} else if (!lineNode.classList.contains('o_barcode_line')) {
|
||||
fail("Can't check the quantity: given element isn't a barcode line.");
|
||||
}
|
||||
const lineQuantity = lineNode.querySelector('.qty-done,.inventory_quantity').innerText;
|
||||
expectedQuantity = String(expectedQuantity);
|
||||
assert(lineQuantity, expectedQuantity, `Line's quantity is wrong`);
|
||||
}
|
||||
|
||||
function assertLineLocations(line, source=false, destination=false) {
|
||||
if (source) {
|
||||
assertLineSourceLocation(line, source);
|
||||
} else {
|
||||
assertLineSourceIsNotVisible(line);
|
||||
}
|
||||
if (destination) {
|
||||
assertLineDestinationLocation(line, destination);
|
||||
} else {
|
||||
assertLineDestinationIsNotVisible(line);
|
||||
}
|
||||
}
|
||||
|
||||
function assertLineProduct(line, productName) {
|
||||
const lineProduct = line.querySelector('.product-label').innerText;
|
||||
assert(lineProduct, productName, "No the expected product");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the done quantity on the reserved quantity is what is expected.
|
||||
*
|
||||
* @param {integer} lineIndex
|
||||
* @param {string} textQty quantity on the line, formatted as "n / N"
|
||||
*/
|
||||
function assertLineQuantityOnReservedQty (lineIndex, textQty) {
|
||||
const $line = $('.o_barcode_line').eq(lineIndex);
|
||||
const qty = $line.find('.qty-done').text();
|
||||
const reserved = $line.find('.qty-done').next().text();
|
||||
const qtyText = reserved ? qty + ' ' + reserved : qty;
|
||||
assert(qtyText, textQty, 'Something wrong with the quantities');
|
||||
}
|
||||
|
||||
function assertLineSourceIsNotVisible(line) {
|
||||
const sourceElement = line.querySelector('.o_line_source_location');
|
||||
if (sourceElement) {
|
||||
const product = line.querySelector('.product-label').innerText;
|
||||
fail(`The location for line of the product ${product} should not be visible, "${sourceElement.innerText}" instead`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given line is in the given location. Implies the location is visible.
|
||||
* @param {Element} line
|
||||
* @param {string} location
|
||||
*/
|
||||
function assertLineSourceLocation(line, location) {
|
||||
const sourceElement = line.querySelector('.o_line_source_location');
|
||||
const product = line.querySelector('.product-label').innerText;
|
||||
if (!sourceElement) {
|
||||
fail(`The source (${location}) for line of the product ${product} is not visible`);
|
||||
}
|
||||
assert(
|
||||
sourceElement.innerText, location,
|
||||
`The source for line of product ${product} isn't in the right location`);
|
||||
}
|
||||
|
||||
function assertFormLocationSrc(expected) {
|
||||
var $location = $('.o_field_widget[name="location_id"] input');
|
||||
assert($location.val(), expected, 'Wrong source location');
|
||||
}
|
||||
|
||||
function assertFormLocationDest(expected) {
|
||||
var $location = $('.o_field_widget[name="location_dest_id"] input');
|
||||
assert($location.val(), expected, 'Wrong destination location');
|
||||
}
|
||||
|
||||
function assertFormQuantity(expected) {
|
||||
const quantityField = document.querySelector(
|
||||
'.o_field_widget[name="inventory_quantity"] input, .o_field_widget[name="qty_done"] input');
|
||||
assert(quantityField.value, expected, 'Wrong quantity');
|
||||
}
|
||||
|
||||
function assertErrorMessage(expected) {
|
||||
var $errorMessage = $('.o_notification_content').eq(-1);
|
||||
assert($errorMessage[0].innerText, expected, 'wrong or absent error message');
|
||||
}
|
||||
|
||||
function assertKanbanRecordsCount(expected) {
|
||||
const kanbanRecords = document.querySelectorAll(
|
||||
'.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)');
|
||||
assert(kanbanRecords.length, expected, 'Wrong number of cards');
|
||||
}
|
||||
|
||||
function pressShift() {
|
||||
document.querySelector('.o_barcode_client_action').dispatchEvent(
|
||||
new window.KeyboardEvent(
|
||||
'keydown', { bubbles: true, key: 'Shift' },
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function releaseShift() {
|
||||
document.querySelector('.o_barcode_client_action').dispatchEvent(
|
||||
new window.KeyboardEvent(
|
||||
'keyup', { bubbles: true, key: 'Shift' },
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
assert: assert,
|
||||
assertButtonIsVisible: assertButtonIsVisible,
|
||||
assertButtonIsNotVisible: assertButtonIsNotVisible,
|
||||
assertLineButtonsAreVisible: assertLineButtonsAreVisible,
|
||||
assertLineDestinationIsNotVisible,
|
||||
assertLineDestinationLocation,
|
||||
assertLineLocations,
|
||||
assertLineSourceIsNotVisible,
|
||||
assertLineSourceLocation,
|
||||
assertErrorMessage: assertErrorMessage,
|
||||
assertFormLocationDest: assertFormLocationDest,
|
||||
assertFormLocationSrc: assertFormLocationSrc,
|
||||
assertFormQuantity,
|
||||
assertLinesCount: assertLinesCount,
|
||||
assertLineIsHighlighted: assertLineIsHighlighted,
|
||||
assertLineProduct,
|
||||
assertLineQty: assertLineQty,
|
||||
assertLineQuantityOnReservedQty: assertLineQuantityOnReservedQty,
|
||||
assertKanbanRecordsCount,
|
||||
assertScanMessage: assertScanMessage,
|
||||
assertSublinesCount,
|
||||
assertValidateEnabled: assertValidateEnabled,
|
||||
assertValidateIsHighlighted: assertValidateIsHighlighted,
|
||||
assertValidateVisible: assertValidateVisible,
|
||||
fail: fail,
|
||||
getLine: getLine,
|
||||
getSubline,
|
||||
pressShift: pressShift,
|
||||
releaseShift: releaseShift,
|
||||
triggerKeydown: triggerKeydown,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,651 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import helper from 'stock_barcode.tourHelper';
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('test_inventory_adjustment', {test: true}, [
|
||||
|
||||
{
|
||||
trigger: '.button_inventory',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_scan_message.o_scan_product',
|
||||
run: function () {
|
||||
helper.assertScanMessage('scan_product');
|
||||
helper.assertValidateVisible(true);
|
||||
helper.assertValidateIsHighlighted(false);
|
||||
helper.assertValidateEnabled(false);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan product1',
|
||||
},
|
||||
{
|
||||
trigger: '.o_barcode_line',
|
||||
run: function () {
|
||||
// Checks the product code and name are on separate lines.
|
||||
const $line = helper.getLine({barcode: 'product1'});
|
||||
helper.assert($line.find('.o_barcode_line_details > .o_barcode_line_title > .o_barcode_product_ref').length, 1);
|
||||
helper.assert($line.find('.o_barcode_line_details .product-label').length, 1);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan product1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_edit',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_field_widget[name="inventory_quantity"]',
|
||||
run: function () {
|
||||
helper.assertFormQuantity('2');
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_save',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line',
|
||||
run: function () {
|
||||
// Checks the product code and name are on separate lines.
|
||||
const $line = helper.getLine({barcode: 'product1'});
|
||||
helper.assert($line.find('.o_barcode_line_details > .o_barcode_line_title > .o_barcode_product_ref').length, 1);
|
||||
helper.assert($line.find('.o_barcode_line_details .product-label').length, 1);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_add_line',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: ".o_field_widget[name=product_id] input",
|
||||
run: 'text product2',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: ".ui-menu-item > a:contains('product2')",
|
||||
},
|
||||
|
||||
{
|
||||
trigger: ".o_field_widget[name=inventory_quantity] input",
|
||||
run: 'text 2',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_save',
|
||||
},
|
||||
|
||||
{
|
||||
extra_trigger: '.o_scan_message.o_scan_product',
|
||||
trigger: '.o_barcode_line',
|
||||
run: 'scan O-BTN.validate',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_stock_barcode_main_menu',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_notification.border-success',
|
||||
run: function () {
|
||||
helper.assertErrorMessage('The inventory adjustment has been validated');
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
tour.register('test_inventory_adjustment_multi_location', {test: true}, [
|
||||
|
||||
{
|
||||
trigger: '.button_inventory',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan LOC-01-00-00'
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_scan_message:contains("WH/Stock")',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan product1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan product1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan product2',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan LOC-01-01-00'
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_scan_message:contains("WH/Stock/Section 1")',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan product2',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan LOC-01-02-00'
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_scan_message:contains("WH/Stock/Section 2")',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan product1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan O-BTN.validate',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_stock_barcode_main_menu',
|
||||
run: function () {
|
||||
helper.assertErrorMessage('The inventory adjustment has been validated');
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
tour.register('test_inventory_adjustment_tracked_product', {test: true}, [
|
||||
|
||||
{
|
||||
trigger: '.button_inventory',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan productlot1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("productlot1")',
|
||||
run: 'scan lot1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan lot1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line.o_selected .qty-done:contains(2)',
|
||||
run: 'scan productserial1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("productserial1")',
|
||||
run: 'scan serial1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan serial1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_notification.border-danger',
|
||||
run: function () {
|
||||
// Check that other lines is correct
|
||||
let $line = helper.getLine({barcode: 'productserial1'});
|
||||
helper.assertLineQty($line, "1");
|
||||
helper.assert($line.find('.o_line_lot_name').text().trim(), 'serial1');
|
||||
$line = helper.getLine({barcode: 'productlot1'});
|
||||
helper.assertLineQty($line, "2");
|
||||
helper.assert($line.find('.o_line_lot_name').text().trim(), 'lot1');
|
||||
helper.assertErrorMessage('The scanned serial number is already used.');
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan serial2',
|
||||
},
|
||||
{ trigger: '.o_barcode_line.o_selected .btn.o_toggle_sublines .fa-caret-down' },
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("serial2")',
|
||||
run: 'scan productlot1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("productlot1")',
|
||||
run: 'scan lot1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line .qty-done:contains(3)',
|
||||
run: 'scan productserial1',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("productserial1")',
|
||||
run: 'scan serial3',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: ':contains("productserial1") .o_sublines .o_barcode_line:contains("serial3")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(2);
|
||||
helper.assertSublinesCount(3);
|
||||
},
|
||||
},
|
||||
|
||||
// Edit a line to trigger a save.
|
||||
{
|
||||
trigger: '.o_add_line',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_field_widget[name="product_id"]',
|
||||
},
|
||||
{
|
||||
trigger: '.o_discard',
|
||||
},
|
||||
|
||||
// Scan tracked by lots product, then scan new lots.
|
||||
{
|
||||
trigger: '.o_sublines .o_barcode_line:nth-child(3)',
|
||||
run: function () {
|
||||
helper.assertLinesCount(2);
|
||||
helper.assertSublinesCount(3);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan productlot1',
|
||||
},
|
||||
{
|
||||
trigger: '.o_barcode_line.o_selected:contains("productlot1")',
|
||||
run: 'scan lot2',
|
||||
},
|
||||
{ trigger: '.o_barcode_line.o_selected .btn.o_toggle_sublines .fa-caret-down' },
|
||||
{
|
||||
trigger: '.o_barcode_line .o_barcode_line:contains("lot2")',
|
||||
run: 'scan lot3',
|
||||
},
|
||||
|
||||
// Must have 6 lines in two groups: lot1, lot2, lot3 and serial1, serial2, serial3.
|
||||
// Grouped lines for `productlot1` should be unfolded.
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("productlot1") .o_sublines>.o_barcode_line.o_selected:contains("lot3")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(2);
|
||||
helper.assertSublinesCount(3);
|
||||
}
|
||||
},
|
||||
...tour.stepUtils.validateBarcodeForm(),
|
||||
|
||||
{
|
||||
trigger: '.o_stock_barcode_main_menu',
|
||||
run: function () {
|
||||
helper.assertErrorMessage('The inventory adjustment has been validated');
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
tour.register('test_inventory_nomenclature', {test: true}, [
|
||||
|
||||
{
|
||||
trigger: '.button_inventory',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: function() {
|
||||
helper.assertScanMessage('scan_product');
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan 2145631123457', // 12.345 kg
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.product-label:contains("product_weight")'
|
||||
},
|
||||
...tour.stepUtils.validateBarcodeForm(),
|
||||
{
|
||||
trigger: '.o_stock_barcode_main_menu',
|
||||
run: function () {
|
||||
helper.assertErrorMessage('The inventory adjustment has been validated');
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
tour.register('test_inventory_package', {test: true}, [
|
||||
|
||||
{
|
||||
trigger: '.button_inventory',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan PACK001',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("product2") .o_edit',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '[name="inventory_quantity"] input',
|
||||
run: 'text 21'
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_save',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_apply_page',
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_notification.border-success',
|
||||
run: function () {
|
||||
helper.assertErrorMessage('The inventory adjustment has been validated');
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
trigger: '.o_stock_barcode_main_menu',
|
||||
},
|
||||
]);
|
||||
|
||||
tour.register('test_inventory_owner_scan_package', {test: true}, [
|
||||
{
|
||||
trigger: '.button_inventory',
|
||||
},
|
||||
{
|
||||
trigger: '.o_barcode_client_action',
|
||||
run: 'scan P00001',
|
||||
},
|
||||
{
|
||||
trigger: '.o_barcode_client_action:contains("P00001")',
|
||||
},
|
||||
{
|
||||
trigger: '.o_barcode_client_action:contains("Azure Interior")',
|
||||
},
|
||||
...tour.stepUtils.validateBarcodeForm(),
|
||||
]);
|
||||
|
||||
tour.register('test_inventory_using_buttons', {test: true}, [
|
||||
{ trigger: '.button_inventory' },
|
||||
|
||||
// Scans product 1: must have 1 quantity and buttons +1/-1 must be visible.
|
||||
{ trigger: '.o_barcode_client_action', run: 'scan product1' },
|
||||
{
|
||||
trigger: '.o_barcode_client_action .o_barcode_line',
|
||||
run: function () {
|
||||
helper.assertLinesCount(1);
|
||||
const $line = helper.getLine({barcode: 'product1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '1');
|
||||
helper.assertButtonIsVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsVisible($line, 'remove_unit');
|
||||
}
|
||||
},
|
||||
// Clicks on -1 button: must have 0 quantity, -1 still visible but disabled.
|
||||
{ trigger: '.o_remove_unit' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("0")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(1);
|
||||
const $line = helper.getLine({barcode: 'product1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '0');
|
||||
helper.assertButtonIsVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsVisible($line, 'remove_unit');
|
||||
const decrementButton = document.querySelector('.o_line_button.o_remove_unit');
|
||||
helper.assert(decrementButton.hasAttribute('disabled'), true);
|
||||
}
|
||||
},
|
||||
// Clicks on +1 button: must have 1 quantity, -1 must be enabled now.
|
||||
{ trigger: '.o_add_quantity' },
|
||||
{
|
||||
trigger: '.o_barcode_line .qty-done:contains("1")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(1);
|
||||
const $line = helper.getLine({barcode: 'product1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '1');
|
||||
helper.assertButtonIsVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsVisible($line, 'remove_unit');
|
||||
const decrementButton = document.querySelector('.o_line_button.o_remove_unit');
|
||||
helper.assert(decrementButton.hasAttribute('disabled'), false);
|
||||
}
|
||||
},
|
||||
|
||||
// Scans productserial1: must have 0 quantity, buttons must be hidden (a
|
||||
// line for a product tracked by SN doesn't have -1/+1 buttons).
|
||||
{ trigger: '.o_barcode_client_action', run: 'scan productserial1' },
|
||||
{
|
||||
trigger: '.o_barcode_client_action .o_barcode_line:nth-child(2)',
|
||||
run: function () {
|
||||
helper.assertLinesCount(2);
|
||||
const $line = helper.getLine({barcode: 'productserial1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '0');
|
||||
helper.assertButtonIsNotVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsNotVisible($line, 'remove_unit');
|
||||
const setButton = document.querySelector('.o_selected .o_line_button.o_set > .fa-check');
|
||||
helper.assert(Boolean(setButton), true);
|
||||
}
|
||||
},
|
||||
// Scans a serial number: must have 1 quantity, check button must display a "X".
|
||||
{ trigger: '.o_barcode_client_action', run: 'scan BNG-118' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("BNG-118")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(2);
|
||||
const $line = helper.getLine({barcode: 'productserial1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '1');
|
||||
helper.assertButtonIsNotVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsNotVisible($line, 'remove_unit');
|
||||
const setButton = document.querySelector('.o_selected .o_line_button.o_set.o_difference');
|
||||
helper.assert(Boolean(setButton), true);
|
||||
}
|
||||
},
|
||||
// Clicks on set button: must set the inventory quantity equals to the quantity .
|
||||
{ trigger: '.o_barcode_line:contains("productserial1") .o_line_button.o_set' },
|
||||
{
|
||||
trigger: '.o_barcode_line.o_selected .fa-check',
|
||||
run: function () {
|
||||
helper.assertLinesCount(2);
|
||||
const $line = helper.getLine({barcode: 'productserial1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '0');
|
||||
helper.assertButtonIsNotVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsNotVisible($line, 'remove_unit');
|
||||
const goodQuantitySetButton = document.querySelector('.o_selected .o_line_button.o_set > .fa-check');
|
||||
helper.assert(Boolean(goodQuantitySetButton), true);
|
||||
const differenceSetButton = document.querySelector('.o_selected .o_line_button.o_set.o_difference');
|
||||
helper.assert(Boolean(differenceSetButton), false);
|
||||
}
|
||||
},
|
||||
// Clicks again on set button: must unset the quantity.
|
||||
{ trigger: '.o_barcode_line:contains("productserial1") .o_line_button.o_set' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("productserial1"):contains("?")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(2);
|
||||
const $line = helper.getLine({barcode: 'productserial1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '?');
|
||||
helper.assertButtonIsNotVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsNotVisible($line, 'remove_unit');
|
||||
const goodQuantitySetButton = document.querySelector('.o_selected .o_line_button.o_set > .fa-check');
|
||||
helper.assert(Boolean(goodQuantitySetButton), false);
|
||||
const differenceSetButton = document.querySelector('.o_selected .o_line_button.o_set.o_difference');
|
||||
helper.assert(Boolean(differenceSetButton), false);
|
||||
const emptySetButton = document.querySelector('.o_selected .o_line_button.o_set');
|
||||
helper.assert(Boolean(emptySetButton), true);
|
||||
}
|
||||
},
|
||||
|
||||
// Scans productlot1: must have 0 quantity, buttons should be visible.
|
||||
{ trigger: '.o_barcode_client_action', run: 'scan productlot1' },
|
||||
{
|
||||
trigger: '.o_barcode_client_action .o_barcode_line:nth-child(3)',
|
||||
run: function () {
|
||||
helper.assertLinesCount(3);
|
||||
const $line = helper.getLine({barcode: 'productlot1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '0');
|
||||
helper.assertButtonIsVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsVisible($line, 'remove_unit');
|
||||
const decrementButton = document.querySelector('.o_line_button.o_remove_unit');
|
||||
helper.assert(decrementButton.hasAttribute('disabled'), true);
|
||||
}
|
||||
},
|
||||
// Scans a lot number: must have 1 quantity, buttons should still be visible.
|
||||
{ trigger: '.o_barcode_client_action', run: 'scan toto-42' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("toto-42")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(3);
|
||||
const $line = helper.getLine({barcode: 'productlot1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '1');
|
||||
helper.assertButtonIsVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsVisible($line, 'remove_unit');
|
||||
const decrementButton = document.querySelector('.o_line_button.o_remove_unit');
|
||||
helper.assert(decrementButton.hasAttribute('disabled'), false);
|
||||
}
|
||||
},
|
||||
// Clicks on -1 button: must have 0 quantity, button -1 must be disabled again.
|
||||
{ trigger: '.o_barcode_line:contains("productlot1") .o_remove_unit' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("productlot1") .qty-done:contains("0")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(3);
|
||||
const $line = helper.getLine({barcode: 'productlot1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '0');
|
||||
helper.assertButtonIsVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsVisible($line, 'remove_unit');
|
||||
const decrementButton = document.querySelector('.o_line_button.o_remove_unit');
|
||||
helper.assert(decrementButton.hasAttribute('disabled'), true);
|
||||
}
|
||||
},
|
||||
// Clicks on +1 button: must have 1 quantity, buttons must be visible.
|
||||
{ trigger: '.o_barcode_line:contains("productlot1") .o_add_quantity' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("productlot1") .qty-done:contains(1)',
|
||||
run: function () {
|
||||
helper.assertLinesCount(3);
|
||||
const $line = helper.getLine({barcode: 'productlot1'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQty($line, '1');
|
||||
helper.assertButtonIsVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsVisible($line, 'remove_unit');
|
||||
const decrementButton = document.querySelector('.o_line_button.o_remove_unit');
|
||||
helper.assert(decrementButton.hasAttribute('disabled'), false);
|
||||
}
|
||||
},
|
||||
|
||||
// Scans product2 => Should retrieve the quantity on hand and display 1/10.
|
||||
{ trigger: '.o_barcode_client_action', run: 'scan product2' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("product2")',
|
||||
run: function () {
|
||||
helper.assertLinesCount(4);
|
||||
const $line = helper.getLine({barcode: 'product2'});
|
||||
helper.assertLineIsHighlighted($line, true);
|
||||
helper.assertLineQuantityOnReservedQty(3, '1 / 10');
|
||||
helper.assertButtonIsVisible($line, 'add_quantity');
|
||||
helper.assertButtonIsVisible($line, 'remove_unit');
|
||||
const setButton = document.querySelector('.o_selected .o_line_button.o_set.o_difference');
|
||||
helper.assert(Boolean(setButton), true);
|
||||
}
|
||||
},
|
||||
// Clicks multiple time on the set quantity button and checks the save is rightly done.
|
||||
{ trigger: '.o_selected .o_line_button.o_set.o_difference' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("product2"):contains("?")',
|
||||
run: function () {
|
||||
const line = document.querySelector('.o_barcode_line[data-barcode=product2]');
|
||||
const qty = line.querySelector('.o_barcode_scanner_qty').textContent;
|
||||
helper.assert(qty, '?/ 10');
|
||||
}
|
||||
},
|
||||
// Goes to the quant form view to trigger a save then go back.
|
||||
{ trigger: '.o_selected .o_line_button.o_edit' },
|
||||
{ trigger: '.o_discard' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("product2"):contains("?")',
|
||||
run: function () {
|
||||
const line = document.querySelector('.o_barcode_line[data-barcode=product2]');
|
||||
const qty = line.querySelector('.o_barcode_scanner_qty').textContent;
|
||||
helper.assert(qty, '?/ 10');
|
||||
}
|
||||
},
|
||||
|
||||
// Clicks again, should pass from "? / 10" to "10 / 10"
|
||||
{ trigger: '.o_barcode_line:contains("product2") .o_line_button.o_set' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("product2") .qty-done:contains("10")',
|
||||
run: function () {
|
||||
const line = document.querySelector('.o_barcode_line[data-barcode=product2]');
|
||||
const qty = line.querySelector('.o_barcode_scanner_qty').textContent;
|
||||
helper.assert(qty, '10/ 10');
|
||||
}
|
||||
},
|
||||
// Goes to the quant form view to trigger a save then go back.
|
||||
{ trigger: '.o_barcode_line:contains("product2") .o_line_button.o_edit' },
|
||||
{ trigger: '.o_discard' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("product2") .qty-done:contains("10")',
|
||||
run: function () {
|
||||
const line = document.querySelector('.o_barcode_line[data-barcode=product2]');
|
||||
const qty = line.querySelector('.o_barcode_scanner_qty').textContent;
|
||||
helper.assert(qty, '10/ 10');
|
||||
}
|
||||
},
|
||||
|
||||
// Clicks again, should pass from "10 / 10" to "? / 10"
|
||||
{ trigger: '.o_barcode_line:contains("product2") .o_line_button.o_set .fa-check' },
|
||||
{
|
||||
trigger: '.o_barcode_line:contains("product2"):contains("?")',
|
||||
run: function () {
|
||||
const line = document.querySelector('.o_barcode_line[data-barcode=product2]');
|
||||
const qty = line.querySelector('.o_barcode_scanner_qty').textContent;
|
||||
helper.assert(qty, '?/ 10');
|
||||
}
|
||||
},
|
||||
|
||||
// Validates the inventory.
|
||||
{ trigger: '.o_apply_page' },
|
||||
{ trigger: '.o_notification.border-success' }
|
||||
]);
|
||||
3051
stock_barcode/static/tests/tours/tour_test_barcode_flows_picking.js
Normal file
3051
stock_barcode/static/tests/tours/tour_test_barcode_flows_picking.js
Normal file
File diff suppressed because it is too large
Load Diff
1211
stock_barcode/static/tests/tours/tour_test_barcode_gs1.js
Normal file
1211
stock_barcode/static/tests/tours/tour_test_barcode_gs1.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
odoo.define('stock_mobile_barcode.stock_picking_barcode_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { mock } = require('web.test_utils');
|
||||
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
|
||||
const BarcodeScanner = require('@web/webclient/barcode/barcode_scanner');
|
||||
const { destroy, getFixture } = require("@web/../tests/helpers/utils");
|
||||
|
||||
QUnit.module('stock_mobile_barcode', {}, function () {
|
||||
|
||||
QUnit.module('Barcode', {
|
||||
beforeEach: function () {
|
||||
var self = this;
|
||||
|
||||
this.clientData = {
|
||||
action: {
|
||||
tag: 'stock_barcode_client_action',
|
||||
type: 'ir.actions.client',
|
||||
res_model: "stock.picking",
|
||||
context: {},
|
||||
},
|
||||
currentState: {
|
||||
actions: {},
|
||||
data: {
|
||||
records: {
|
||||
'barcode.nomenclature': [{
|
||||
id: 1,
|
||||
rule_ids: [],
|
||||
}],
|
||||
'stock.location': [],
|
||||
'stock.move.line': [],
|
||||
'stock.picking': [],
|
||||
},
|
||||
nomenclature_id: 1,
|
||||
},
|
||||
groups: {},
|
||||
},
|
||||
};
|
||||
this.mockRPC = function (route, args) {
|
||||
if (route === '/stock_barcode/get_barcode_data') {
|
||||
return Promise.resolve(self.clientData.currentState);
|
||||
} else if (route === '/stock_barcode/static/img/barcode.svg') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('scan barcode button in mobile device', async function (assert) {
|
||||
assert.expect(1);
|
||||
const pickingRecord = {
|
||||
id: 2,
|
||||
state: 'done',
|
||||
move_line_ids: [],
|
||||
};
|
||||
this.clientData.action.context.active_id = pickingRecord.id;
|
||||
this.clientData.currentState.data.records['stock.picking'].push(pickingRecord);
|
||||
this.clientData.currentState.groups.group_stock_multi_locations = false;
|
||||
|
||||
mock.patch(BarcodeScanner, {
|
||||
isBarcodeScannerSupported: () => true,
|
||||
scanBarcode: async () => {},
|
||||
});
|
||||
|
||||
const target = getFixture();
|
||||
|
||||
const webClient = await createWebClient({
|
||||
mockRPC: this.mockRPC,
|
||||
});
|
||||
await doAction(webClient, this.clientData.action);
|
||||
assert.containsOnce(target, '.o_stock_mobile_barcode');
|
||||
destroy(webClient);
|
||||
mock.unpatch(BarcodeScanner);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
odoo.define('stock_barcode.stock_picking_barcode_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
|
||||
const { getFixture } = require("@web/../tests/helpers/utils");
|
||||
|
||||
QUnit.module('stock_barcode', {}, function () {
|
||||
|
||||
QUnit.module('Barcode', {
|
||||
beforeEach: function () {
|
||||
var self = this;
|
||||
|
||||
this.clientData = {
|
||||
action: {
|
||||
tag: 'stock_barcode_client_action',
|
||||
type: 'ir.actions.client',
|
||||
res_model: "stock.picking",
|
||||
context: {},
|
||||
},
|
||||
currentState: {
|
||||
actions: {},
|
||||
data: {
|
||||
records: {
|
||||
'barcode.nomenclature': [{
|
||||
id: 1,
|
||||
rule_ids: [],
|
||||
}],
|
||||
'stock.location': [],
|
||||
'stock.move.line': [],
|
||||
'stock.picking': [],
|
||||
},
|
||||
nomenclature_id: 1,
|
||||
},
|
||||
groups: {},
|
||||
},
|
||||
};
|
||||
this.mockRPC = function (route, args) {
|
||||
if (route === '/stock_barcode/get_barcode_data') {
|
||||
return Promise.resolve(self.clientData.currentState);
|
||||
} else if (route === '/stock_barcode/static/img/barcode.svg') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('exclamation-triangle when picking is done', async function (assert) {
|
||||
assert.expect(1);
|
||||
const pickingRecord = {
|
||||
id: 2,
|
||||
state: 'done',
|
||||
move_line_ids: [],
|
||||
};
|
||||
this.clientData.action.context.active_id = pickingRecord.id;
|
||||
this.clientData.currentState.data.records['stock.picking'].push(pickingRecord);
|
||||
const target = getFixture();
|
||||
const webClient = await createWebClient({
|
||||
mockRPC: this.mockRPC,
|
||||
});
|
||||
await doAction(webClient, this.clientData.action);
|
||||
assert.containsOnce(target, '.fa-5x.fa-exclamation-triangle:not(.d-none)', "Should have warning icon");
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user