From 87b5351da2edea766e5ed3672bd0b0fed92f44f8 Mon Sep 17 00:00:00 2001 From: "Suherdy SYC. Yacob" Date: Mon, 29 Sep 2025 11:47:29 +0700 Subject: [PATCH] first commit --- __init__.py | 1 + __manifest__.py | 14 ++++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 209 bytes models/__init__.py | 5 ++ models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 370 bytes .../mrp_production.cpython-312.pyc | Bin 0 -> 2316 bytes .../product_template.cpython-312.pyc | Bin 0 -> 3564 bytes models/__pycache__/stock_lot.cpython-312.pyc | Bin 0 -> 1399 bytes models/__pycache__/stock_move.cpython-312.pyc | Bin 0 -> 3867 bytes .../stock_move_line.cpython-312.pyc | Bin 0 -> 1612 bytes models/mrp_production.py | 49 ++++++++++++ models/product_template.py | 63 ++++++++++++++++ models/stock_lot.py | 19 +++++ models/stock_move.py | 71 ++++++++++++++++++ models/stock_move_line.py | 24 ++++++ views/product_views.xml | 22 ++++++ 16 files changed, 268 insertions(+) create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/mrp_production.cpython-312.pyc create mode 100644 models/__pycache__/product_template.cpython-312.pyc create mode 100644 models/__pycache__/stock_lot.cpython-312.pyc create mode 100644 models/__pycache__/stock_move.cpython-312.pyc create mode 100644 models/__pycache__/stock_move_line.cpython-312.pyc create mode 100644 models/mrp_production.py create mode 100644 models/product_template.py create mode 100644 models/stock_lot.py create mode 100644 models/stock_move.py create mode 100644 models/stock_move_line.py create mode 100644 views/product_views.xml diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..10f11a8 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Product Lot Sequence Per Product', + 'version': '1.0', + 'depends': [ + 'stock', + 'mrp', + ], + 'data': [ + 'views/product_views.xml', + ], + 'installable': True, + 'auto_install': False, + 'application': False, +} \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5e3dcabd978f6ed747ff791fd55f88b035e7e37 GIT binary patch literal 209 zcmX@j%ge<81p2JEGDLv%V-N=hn4pZ$0zk%eh7^Vr#vF!R#wbQchDs()=9i2>VNJ$c zY`OUlIY~;;;eQRGO1& aSHuA{9%Oqli1C4$k&*EpgGdn@kOKfI!#AV= literal 0 HcmV?d00001 diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e66ea8a --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,5 @@ +from . import product_template +from . import stock_move +from . import stock_lot +from . import stock_move_line +from . import mrp_production \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7341e99d9d9e772b18b9ae2bb99efae956cf869 GIT binary patch literal 370 zcmYL_y-EZz6oqf*Z*_6hLeN6(rxEN#M0^4}n;{UENni)_XC@=eOW67hzJM>`8w`w% zm7TENR^H6+u(vq*&J8&?$yc7w0qZmGmv6ZK1T$5%h1CPXJ8R6r?}4&hWPK%nN1Z!VB!p8*i#-rp#+_`x_^XM{~5> zVzSm@|4=#0wkfsIk44sli!t3LLDC>ukQvGctWaV7#BZtX{L<*ka6t_>=K6+KEyk+_ z=SDYlx)=^-jd*E=t^~8fO|z)Nld_?V)w=eKUD7fK6rR~#|7{Z6U?)z9qnH$uE((bvgh*-W0TK!+L`bxvaty1*Ga+_uubExb zBwbgi;*d&}ng|ISsS-h|1SpkCZ|$k{RH`0oD@fjqsCsBG+yZGYoH}EB6F}tNIn4W= zH{YAL@7ce#waI|38*OyP@Br`^7o6p4G8+?&DFFl!%z!M6!>kY&AeV)Vn3duZ1Oj*u zAn_VN5{Bt|L?=jtu80j9ch9*L_Y*EC0(EC~gfj+(ssgF+Lj#j>LUfXB_%?Rf6gD~- zQvw*oAp&s$!MKQoYalMoiHbzqPLuo@qN766OzF9Kf0pEj@|$wf3BqG>bR5k_AKOd7 z0_hj6+=H!^1_3NfScJ?+XewSHMZpr%(hG_vTNF+9l8t3CkO(^27bQy~H%y;nb#5uT z%oaz7#FZwVF>wAf0E%u)O1C-IExLKuV|ff#ja=IDSgtF8z%QegD;?PKbU4f7M20-; zprhZSNa|ujI<%#6IOIZZmTlOPx9GHpJjjcD%ns7LMz5@G&dAK)Gc8_np_Xr3SDLR| z^jcnq-?BVK$TETwu%K~&8?0@0tbBWAA-|I+s~l_vv(Py8Kgk%rHe+R*3>($xekDC7 zzW${xVe=hX-bUE$aZZ}O4u=BH+c@-d`1-n95Cy*$&H`n}xqwolYmLgI)sb}+AZjR|lEAu~2{A{OEPRrsa{2OOY@5-dI zDRaIhMTVU4^TExqOh-~$W`6Wc25Sb68Q6@OGdMP#B8C||TQ|lg3)zd9#B>tVVhB%b zg^bA@8&DO2@Wvv%8JR@M5w4Cw&C0r>Nc=Ok~36b>D3c!b=;C{s?ngzLaB9CBMZXEDdF=%@lnaZndv$# z>SLSoK(K7|e^@^j{lp5s?1 zbfoJeqlsjJJ!F>Q)pJI|N%W;c<}p#5hD2i&C)nV0RBVtA<>zP@lW|^S&sMJszh1qf zWeV7sA9!i%4sGk`WoV7>S;?;g+uQn8;ETW`Zz;)zDr$9JPb(CEt>-tULfI=pk;3wCv{`#{fI z8$jxbF8ZFypmVp~y=QTZUB2GKP(AGf!LS{Qlt!0E%crZMeYMb`YUog9bYeC1{=%5u zv%B2+3`kv_%jnL`?U}ouR{P(s^&hSFAGLS%l#S*0?o8aCs04@YV5rhQe1GtP_~3(I z)SuPb__^x%xyr~#_Krw7R7REH8w}DI^fOEy5r%2t7-XOZIu}kpl|it}j&zr1mu8n+ zs*%B3afKhgDWp#d%r;n^h4^B;Gk z-bmDSn*Vob1m4emH!pkceWwBNT{6N9e9{TKfvC9+=EbFJYG7rbn0Kg I*!toB0R3koW&i*H literal 0 HcmV?d00001 diff --git a/models/__pycache__/product_template.cpython-312.pyc b/models/__pycache__/product_template.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd0e7948607f5c3801ae1ccb7946f776b19abffe GIT binary patch literal 3564 zcmai1T}&I<6~6Q1!RF5m_n0zC&!*o^kGs zN$eOcl_E6{NG0k68@1i+KCF_6NV|Qh`k2S9`cNC1dNeOxsZZM{2&vTOq3XFaV`EaY z9l_>JfY&y3F{q288}jC*{xO5PN5U*gzOzCW%r^gh{axHpN9a z2C?Wo!t7myIhk8>mj(C#ZgMsqab@{{>uWj*a3&|E6Q;13kds(5g_Mfrq!!y3LP&5X zd;bAq1IZ}DU=(376X94S^GlY^)dC2tz#QgpFxd4}*yQcKh+uIyALOJnT!-D*gX^C* zRCwKo@;2fo-2BvA@x=q2^-FCvPW9oIP1at=NA99chb7X8TkoPs)1h*H?5oHft&(fb zwguWv?`5LmOiaHjr_xDDmvg?fy)dLZE3X<7WJr^5W@IHM=Q^**Bq1e3@2a{upGjSp zi6&lgHX-_a3<7R4)31D*uEstXN~$`@Qi&YCD#zkVA|@rpbM$s5#H(>xyg-J^;v$h_ zD#4;2mvk|qi5X3Xi7MWZ6`6o(qJ2;Z&J-0#^`c6EADA7h!306mNkX|{3Rq1^3B}~& zax$H3naO}fDG}@r*)EuGL}O|yozdl}eQ-2QEg zuqLm`$wkv$t^|#qC5o@E;faNGR*$O+y_p3StLjL2A!a3*gt4k<3r;#l54f9_3FMen zZVNUrRrty(hSFK$1uePnTs~NrLzRoFWorkBXh9fO(O1X1ULeNvBj(<$5gacC&lZDc zOToEfaBeI3!!OCb+2@UcU5+yB0Y5qrc@HdDk^dLm2aSBKcw^>Uz_vvBUi&Caf=n6O zt9CKJ>D=%lcY`U@^hVF3U$QYafnw}6@Oy>LvrA=wu9DBQzhc)~c(lR+2hE4ifS1qn zwb}y7)LzeX%ZMD!bG3J=PkBCCv3+gy8@+F;GR`64+#1c`*9g0Q%VMFnR=ZXF##M=k zEAOhd0%nKUI|W+T?$)+(i5B2%AFju4Xh=>qRO?uKMC&33^%7%O*jp&jI8SJ|CG9Yn zY(gQ&fXMj(<|&R>(4Ig`M_<>$a4z67c|}UegeH(JSC@uLpfFrdQAkp-^~i%|uTV^Pg0IN&ikSy={L2gHlTp{Gmfw2Un^mT68c$A}C~ zf&fw=lLJ%^2!xh}>0OqR30y%T&E!D8*;+P2i(bz<$WJ;cf1@Jb+UKUpC|V!s0+RS( zs8O8X)GFQt5v>mLe-#-W-46#I416+O>=-F^go_>FKZKuM{c`-zlYg8n{5Z8erxYgC z!in^D$IW|QW8%VJyDxq*_L255_aOIp?7@eH?u!L?CyWLy@)nOyi`?5@@&>oOL8G;^ z)H+;j9WIQYFSedHLT8?ZOEW(!!cRzApMCBR?g*%F;8FO~@Z(Fx-icE0>0<9`qj&Jp z^rzFGgbE|43*s3=95s5rUFtbi>^Wuh_8G?qcN>Hmrfm-ieeLV-?lhy}srA{v_=CoU z_X__0$L`X=JH>%_w*2Sd@Zj+0;V0qGr=Cm|!apgb$m6N)L2cdr+&8e>OO5^a3l6T6X%j!a}a^(B4ud`&}xUfWc`gZcoTbw*D<| zztKNh>OWEJKVkU0O8#KcA1wJ#75%5GrsGBbc-3^Q=pWnipZJ38)$s#IXjb&?G@^l! zMeVl0@#GEAHXrbs?r4;vNi=HKN2AavGf7G}Mx!^uTW7}&ud_G+l1}KQn(j*gF{3Ix z6s>6SGjWO7;4lW;gr<~Phvl@aV9jcSGxK?`rgCI2a|@o)9{7hXNP5)FETGZ zLYGkBk3s3QcCR1b7`#77kMJEgnl8Sq<6DK*IS>|FRMiTcUjg2 zEt*5Z$sLP);r6?9W+zz1+FuAvY`Z3RS&u73Ij1bj_J;Z}9}rBQzD_Lvtw0f)h^Ffj z{kO>SlT5L75%s+kCYcb#B6&agPXweT{9m!1 Bb{hZy literal 0 HcmV?d00001 diff --git a/models/__pycache__/stock_lot.cpython-312.pyc b/models/__pycache__/stock_lot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d021123f95ced88cd1b37abd9a1d02c3ed04bc7 GIT binary patch literal 1399 zcmZ`(O=ufO6n?XRTFG&h#O#J_KTeOPxTldn!4UB>2!Cd+f20i-ibwhC&NH^%g1@LQb7o?Zj>&?=bW2dvD(R z=Dj!jWOz6SOxzq@_*@3~lYaKESdR zOLeCEzYw&YY6G-B`Whce_=ITv!x-v^Go}HF{0>8PQQkvoVu?uHCSokhQAU_!xvmR* zuoH!-qR`7qtORexN`2_SE|4e@%Ydeta=rZ;Zd$@^Se!f9Nk*`G6=PF@=ndhqrpEBi zd%&tF{8$_G3sh-Z4Tt7hflu0Ygk93fr?{N%G|yUtL+5(iY1$6+INKR=gR)R`j=t(2 z(*>^O8Cfb{b0R7`%|)tR3H%$OLo-ZcMNvRScpNoa9*ahEP3LBWuPkwVL|~guAXUS5 z%L%Av2aXjvhDMdp@vhSBqUD9ybHj)#9MX(nDl<|vi?QulVTf_aXXpf%m6w;vsak6( zTJW1Zbj>He|8b>ew;B0{MTp-FYoZ4Kxh^AsQ$ua=ENaPpG4m?7mVyzMqLZIbhq*kU z%BLdyfh{jq;JF5)lYfJrt)bPKy^-UamAjR#vrk8gkCY$v$NEnbPfOLk!sPd*?@Bwx zPrHSw-NJOYF#YqTUq0%d{-Rg7xRLp7?A@(vyOUGh$*G;s=6*fd8@sq6KN}g_D}L~> z`k>k?ezGyMHFobDOZ9%~VdX*P(fMxi)NXOMTb$jQGke7^Q~R5o#r`okI=Xga?bept z%a?ZaQlAlJvp{u>`7mHia~QL`wmpuIV0^u8d8tLmZgYWkISTmOq{oty;K8kNqwPg* zQV}|wHo3AVhr4SV^*%G@8SzN?OR)$zVGq#Au0 zw26I=f^l)=f}v7{cQ7Fl@CZ~r&;MCT#K$mj<#}G}(}SB-gOlu&6A0gC%X1ka^gB%K TYk6d>E`A%Y#s2_<$;p2K;?h)= literal 0 HcmV?d00001 diff --git a/models/__pycache__/stock_move.cpython-312.pyc b/models/__pycache__/stock_move.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a68854710410b78f788d2bbb2e74b0cc6f395a0 GIT binary patch literal 3867 zcmbW4U2GHC6@c$}#=rS-Ac2G=z@?BMvyOo+h{7Vah!9}4u%+&n#%nrEJmYwb?J;-8 z3311em5>M(73QG=s!9Z@5>)hoxAL$Lv`?sgu`%12E2JV7sp?ynu+ny)dhXa`8VB|A4EFETA|i^Orpj>I)(+*=3Chm%R};?CyGdC+k`B&Xzsi z1$2Rn_`)dQrrxlj#m=43W|X{7XU$L=+SHdNnw*OnqAJJgM_YoKvDg(>{sPsnxtqwa zhFNvhVWMBbyscV+Gf`4#Og9?jsNuFdtFK6U?D^)e%z+W=#v{Z3mju2k!9=3{*^$YC zA%y&7SW#qbBov%cE*PS&5LHUyxT>UNtYvW1Hh4CnWN_U+IF^ty(~2I%YP>O)kaR3z zT{)jqGBG7Y{V9zJtaZVn6SS@6jDU+e^;}jVv`5r3HF;vw{Dc{cx~COGG7Lif_KcCv zra%^9?h@dspew03<%yzGuPDY+lCF!Q&J2d}{I{p&{n6~4kpTJNl2J|8v|T@l#&U40 zv?R+~MvvNZ-_jMdvQ>%JoH=S4LS#-7%FdB?I3T|%Z2M4XwHvSt=%=9iW*)t9A%D-+ z>2l}xN@s9^ueSNGfB4G}e>=Nq+`Dx5($dI8d1Rt=Y_jy(*=L)j7n_G$$DeeRPDd-7 z&n%2TYaguo*Dm;0Kq!i}`dGt14}jM){{#P{WA<%P~u4LWA zT9-G2?*gX-5M(~t@SWzmhS$4IH*uP7SvWU}5ZNO;O^3=AJSGpLyy=l$2Vj#aBpWf% zlwo?NIXlp0x22)i^sW{Src3rPIkFd84%WIhWjd2>O<8u!blDO)bj=Wu=_G3m_SLL% z)dw-V5%d8qP2P^LFq-oP+N60fp%2%~NsXkXl$uwt3O8wVKw0|hK9OoeBa7vM8K;p3SkXrOTp#?!WnHO=EhOItx-cmEL` zgU^rEdyN>zqY&nD3IH)RhqWAOo`E3{2vKDv(0ZFsfFop9)s-NQOR3b96g$VXV|39C zkR%b1q$H#n6>JlSKjlI;rN-2{L7SrT-Wqt|Hq--^$ke8Yayx5W@WHppN|`xE0tb9j z>SBukF((5a$}5=}$^$t7Fa}Yx)KxPGb*M6RN!hHDkqPq|x>c;rh_&6i$n0VTxtLNj ziZ~;s^gtW&v$bBySPM}8w_;2y{V`O)m*r06@4lY6nkfFF+_`muUv{DX!G+Fh&&JoN z!y8-}ul5bz@!a+-_E-9XPy60~=veCASMJ?c>HT2g*z@jzTNmzs|2LOj_Ta_s%e-T7 zZPD|U3-xTOj_oZTyA>_>Y=60a!_DN4J2uMdfYS}HEgazHlwCGNNZj=LUFa~FKU&4?Fxwu~ ztpiU0Jfu!k({FalUJI-3fXt3C`VQHAtL!jn&B0&6H;V$k{68Di0k*No|1LaA zF++t6L)8JTGukGPs66hEKrZlms|w$b5%mIh_HRow6#v)QoN@5+pM5Cb>ek z2rK9<9FD>{L7WLRkC{!Vu>_vga*zehS-H^8;I82Np->Z1)ME>xYh@)a>GnMp$@dk<>u>v?Vz?SqwMO{-wn}v4)l6FLx-&i}T zld!Cd`m8i+%A%jev5hrbind19)fxn&d|u54*3>ZOj%gVKGCeA=#D;p~D$xzLo;qV% zE>p)XTT#!z>iScGWnPqre2#J=_1j0u7MgYHi%aJe(UL~CFon0VdkDCWiw z(bCr4<^DZO{RhhZ2OdQ#{l|*#YVW3{-cY$WR1}`~ZCRXrxb^AKo-g)3-d#E#DMh7c zA56jCrT$R4KU6&Y>d?gF+@JHMADyZk`tjd}!;cOv>h~_*z4+zG-qLWm_z6@1+wX;n zAHBdscim-tY z?HgU@VesX^wo=cwSCA#nmi@aQo_-`hKK>+E`svRrezD{c|NVL$8u}*KQMpnrE|@E_)7HiXfarE zZeDRXo!u)ua^Ypx`nq$YvuF7ra(S;zem=S4=yRUs*sTAJ)wEs#OYF&jlL{W9&GsAZUn8qy G?fwsYsP5+g literal 0 HcmV?d00001 diff --git a/models/__pycache__/stock_move_line.cpython-312.pyc b/models/__pycache__/stock_move_line.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a00a7119b631d1dc0fed382a697cb2ad7e9893bd GIT binary patch literal 1612 zcma)6O=ufe5PolWWh=5&rC4qqW8!BT*A{hUmmC^OkS|Hvg55)X2|}=}_dP``@9tCI zu3XE=kV6j2rAbaAP-uH>B0`;_lMu|o~BwV{ixGuEH#L($< zeF%6+1*2|cV}B!?)gs-hI?;qDxo`s;_l86VO!p07T11cn<)ol-TF@~Hpy*j6N^OVI z-rbUWV$<=(K{`M&=YgDaVZ$DxAQ;BSuloh;wn&3iMzv@G^sv7Wih6Azj4b;!fU|O1 z8CGLNH}pNE4XP#^lxw^16QZ;R^(og6wT1?mV=7aLkAMtVZhWa7>i=Wk>(R?TstC4Y z_Fo;Ll6)BvrC0Ypm?UJ6sB`197kN%J`sPl8 zd|WzpcY*;RB7au1+yHG?nJYrpzf%C0>tnBE*$v?XE`$loq6f`NN_r>AxLZ|{*X(dP zU{#^s9Y`zyaN*m6t&}aFyO7dOoynRy4Z`5=D&=sIJ%0_^>unEC`iTRZ~#xNOQQtMn@?qi)oh~mxF5nqhtiaEveT~ zm-eOdEY~>@yjR`G*n^4$2I#OsFN*wwVznNYrEkOF7A2SRy-P*A29@$G&ZQp|hbdwb z!$X@;33H$*_D8s`Y3FY(&*k!t}-GCIVpegSWOvM?E*z2reNB9>MP> zIZl!@sXOW0>EGk${>;p_w(e~`$t-{UX=ggup3eO?z21yJeLMH)(&ONVjr%J<$C~Nm z2{LoxXzu)1YxmbWbF1yS)n_sNY@%r#$4T<+osHWYovEcKQ%m1n_#A}X|wVq5qM_aWwS_XPGu{Bl2)yjlPS gKAXWT{El@>=^teNpNS03+`ZP?ytnzB0NeBY4_t$Q8UO$Q literal 0 HcmV?d00001 diff --git a/models/mrp_production.py b/models/mrp_production.py new file mode 100644 index 0000000..da4637a --- /dev/null +++ b/models/mrp_production.py @@ -0,0 +1,49 @@ +from odoo import models, _ +from odoo.exceptions import UserError + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + def _prepare_stock_lot_values(self): + self.ensure_one() + name = False + product = self.product_id + tmpl = product.product_tmpl_id + seq = getattr(tmpl, 'lot_sequence_id', False) + if seq: + tried = set() + # Try generating a unique candidate from the per-product sequence + for _i in range(10): + candidate = seq.next_by_id() + if not candidate: + break + if candidate in tried: + continue + tried.add(candidate) + exist_lot = self.env['stock.lot'].search([ + ('product_id', '=', product.id), + '|', ('company_id', '=', False), ('company_id', '=', self.company_id.id), + ('name', '=', candidate), + ], limit=1) + if not exist_lot: + name = candidate + break + + # Fallback to default behavior if no per-product candidate was found + if not name: + name = self.env['ir.sequence'].next_by_code('stock.lot.serial') + exist_lot = (not name) or self.env['stock.lot'].search([ + ('product_id', '=', product.id), + '|', ('company_id', '=', False), ('company_id', '=', self.company_id.id), + ('name', '=', name), + ], limit=1) + if exist_lot: + name = self.env['stock.lot']._get_next_serial(self.company_id, product) + if not name: + raise UserError(_("Please set the first Serial Number or a default sequence")) + + return { + 'product_id': product.id, + 'name': name, + } \ No newline at end of file diff --git a/models/product_template.py b/models/product_template.py new file mode 100644 index 0000000..c1187c7 --- /dev/null +++ b/models/product_template.py @@ -0,0 +1,63 @@ +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + lot_sequence_id = fields.Many2one( + 'ir.sequence', + string='Serial/Lot Numbers Sequence', + domain=[('code', '=', 'stock.lot.serial')], + help='Technical Field: The Ir.Sequence record that is used to generate serial/lot numbers for this product' + ) + serial_prefix_format = fields.Char( + 'Custom Lot/Serial', + compute='_compute_serial_prefix_format', + inverse='_inverse_serial_prefix_format', + help='Set a prefix to generate serial/lot numbers automatically when receiving or producing this product. ' + 'Use % codes like %(y)s for year, %(month)s for month, etc.' + ) + next_serial = fields.Char( + 'Next Number', + compute='_compute_next_serial', + help='The next serial/lot number to be generated for this product' + ) + + @api.depends('lot_sequence_id', 'lot_sequence_id.prefix') + def _compute_serial_prefix_format(self): + for template in self: + template.serial_prefix_format = template.lot_sequence_id.prefix or "" + + def _inverse_serial_prefix_format(self): + valid_sequences = self.env['ir.sequence'].search([('prefix', 'in', self.mapped('serial_prefix_format'))]) + sequences_by_prefix = {seq.prefix: seq for seq in valid_sequences} + for template in self: + if template.serial_prefix_format: + if template.serial_prefix_format in sequences_by_prefix: + template.lot_sequence_id = sequences_by_prefix[template.serial_prefix_format] + else: + # Create a new sequence with the given prefix + new_sequence = self.env['ir.sequence'].create({ + 'name': f'{template.name} Serial Sequence', + 'code': 'stock.lot.serial', + 'prefix': template.serial_prefix_format, + 'padding': 7, + 'company_id': False, # Global sequence to avoid cross-company conflicts + }) + template.lot_sequence_id = new_sequence + sequences_by_prefix[template.serial_prefix_format] = new_sequence + else: + # Reset to default if no prefix + template.lot_sequence_id = self.env.ref('stock.sequence_production_lots', raise_if_not_found=False) + + @api.depends('serial_prefix_format', 'lot_sequence_id') + def _compute_next_serial(self): + for template in self: + if template.lot_sequence_id: + template.next_serial = '{:0{}d}{}'.format( + template.lot_sequence_id.number_next_actual, + template.lot_sequence_id.padding, + template.lot_sequence_id.suffix or "" + ) + else: + template.next_serial = '00001' \ No newline at end of file diff --git a/models/stock_lot.py b/models/stock_lot.py new file mode 100644 index 0000000..cbbc509 --- /dev/null +++ b/models/stock_lot.py @@ -0,0 +1,19 @@ +from odoo import api, models + + +class StockLot(models.Model): + _inherit = 'stock.lot' + + @api.model_create_multi + def create(self, vals_list): + # For each lot being created without a name, assign the next serial from the product's template sequence + for vals in vals_list: + if not vals.get('name') and vals.get('product_id'): + product = self.env['product.product'].browse(vals['product_id']) + seq = getattr(product.product_tmpl_id, 'lot_sequence_id', False) + if seq: + vals['name'] = seq.next_by_id() + else: + # Fallback to global sequence if no product sequence + vals['name'] = self.env['ir.sequence'].next_by_code('stock.lot.serial') + return super().create(vals_list) \ No newline at end of file diff --git a/models/stock_move.py b/models/stock_move.py new file mode 100644 index 0000000..7707655 --- /dev/null +++ b/models/stock_move.py @@ -0,0 +1,71 @@ +from odoo import api, models + + +class StockMove(models.Model): + _inherit = 'stock.move' + + @api.onchange('product_id') + def _onchange_product_id(self): + """Seed the next_serial field on stock.move when product changes, if product has a sequence.""" + res = super()._onchange_product_id() + if self.product_id and getattr(self.product_id.product_tmpl_id, 'lot_sequence_id', False): + self.next_serial = getattr(self.product_id.product_tmpl_id, 'next_serial', False) + return res + + def _create_lot_ids_from_move_line_vals(self, vals_list, product_id, company_id=False): + """ + Normalize incoming lot names during 'Generate Serials/Lots' or 'Import Serials/Lots'. + - If user leaves '0' or empty as lot name, create lots without a name to let stock.lot.create() + generate names from the product's per-product sequence (handled by our stock.lot override). + - Otherwise, fallback to the standard behavior for explicit names. + """ + Lot = self.env['stock.lot'] + + # First handle entries that should be auto-generated (empty or '0') + remaining_vals = [] + for vals in vals_list: + lot_name = (vals.get('lot_name') or '').strip() + if not lot_name or lot_name == '0': + lot_vals = { + 'product_id': product_id, + } + if company_id: + lot_vals['company_id'] = company_id + # omit 'name' to trigger sequence in stock.lot.create() override + lot = Lot.create([lot_vals])[0] + vals['lot_id'] = lot.id + vals['lot_name'] = False + else: + remaining_vals.append(vals) + + # Delegate remaining with explicit names to the standard implementation + if remaining_vals: + return super()._create_lot_ids_from_move_line_vals(remaining_vals, product_id, company_id) + return None + + @api.model + def action_generate_lot_line_vals(self, context, mode, first_lot, count, lot_text): + """ + If the 'Generate Serials/Lots' action is invoked with an empty or '0' base, + generate names using the per-product sequence instead of stock.lot.generate_lot_names('0', n), + which would yield 0,1,2... + """ + if mode == 'generate': + product_id = context.get('default_product_id') + if product_id: + product = self.env['product.product'].browse(product_id) + tmpl = product.product_tmpl_id + if (not first_lot or first_lot == '0') and getattr(tmpl, 'lot_sequence_id', False): + seq = tmpl.lot_sequence_id + # Generate count names directly from the sequence + generated_names = [seq.next_by_id() for _ in range(count or 0)] + # Reuse parent implementation for the rest of the processing (locations, uom, etc.) + # by passing a non-zero base and then overriding the names in the returned list. + fake_first = 'SEQDUMMY-1' + vals_list = super().action_generate_lot_line_vals(context, mode, fake_first, count, lot_text) + # Overwrite the lot_name with sequence-based names; keep all other computed values (uom, putaway). + for vals, name in zip(vals_list, generated_names): + vals['lot_name'] = name + return vals_list + # Fallback to standard behavior + return super().action_generate_lot_line_vals(context, mode, first_lot, count, lot_text) \ No newline at end of file diff --git a/models/stock_move_line.py b/models/stock_move_line.py new file mode 100644 index 0000000..559b469 --- /dev/null +++ b/models/stock_move_line.py @@ -0,0 +1,24 @@ +from odoo import api, models + + +class StockMoveLine(models.Model): + _inherit = 'stock.move.line' + + def _prepare_new_lot_vals(self): + """ + Ensure that bogus base like '0' or empty lot_name does not create a lot literally named '0'. + If lot_name is empty or equals '0', let stock.lot.create() generate the name from + the product's per-product sequence (handled by our stock.lot override). + """ + self.ensure_one() + # Normalize lot_name + lot_name = (self.lot_name or '').strip() + normalized_name = lot_name if lot_name and lot_name != '0' else False + + vals = { + 'name': normalized_name, # False => triggers product sequence in stock.lot.create() + 'product_id': self.product_id.id, + } + if self.product_id.company_id and self.company_id in (self.product_id.company_id.all_child_ids | self.product_id.company_id): + vals['company_id'] = self.company_id.id + return vals \ No newline at end of file diff --git a/views/product_views.xml b/views/product_views.xml new file mode 100644 index 0000000..759f667 --- /dev/null +++ b/views/product_views.xml @@ -0,0 +1,22 @@ + + + + + product.template.form.inherit.stock.lot.sequence + product.template + + + + + + + + + \ No newline at end of file