Feature: Added sale voucher to check sale import.

Chore: Frontend moved from Moment to date-fns and from dd-MMM-yyyy to ISO Date format.
This commit is contained in:
tanshu committed 2026-09-15 11:29:27 +00:00
1 parent 7656c7e877
commit fc3cd00662
102 files changed
+2121 -2103

No files matched your search

+2
View File
@@ -45,6 +45,7 @@ from .routers import (
rebase,
recipe,
role,
sale,
sales_import,
tag,
temporal_product,
@@ -165,6 +166,7 @@ app.include_router(credit_salary.router, prefix="/api/credit-salary", tags=["vou
app.include_router(voucher.router, prefix="/api", tags=["vouchers"])
app.include_router(transaction_import.router, prefix="/api/transaction-import", tags=["vouchers"])
app.include_router(sales_import.router, prefix="/api/sales-import", tags=["vouchers"])
app.include_router(sale.router, prefix="/api/sale", tags=["vouchers"])
app.include_router(lock_information.router, prefix="/api/lock-information", tags=["settings"])
app.include_router(maintenance.router, prefix="/api/maintenance", tags=["settings"])
+1 -1
View File
@@ -86,7 +86,7 @@ def effective_date(d: str | None = None) -> date:
datetime.now(UTC).replace(tzinfo=None) + timedelta(minutes=330) # Adjust for IST (Indian Standard Time)
).date()
if d is None
else datetime.strptime(d, "%d-%b-%Y").date()
else date.fromisoformat(d)
)
+3 -4
View File
@@ -1,6 +1,6 @@
import uuid
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
@@ -142,10 +142,9 @@ async def show_balance(
id_: uuid.UUID,
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
d: str | None = None,
d: date | None = None,
) -> AccountBalance:
date_ = None if d is None or d == "" else datetime.strptime(d, "%d-%b-%Y")
return AccountBalance(date_=balance(id_, date_, db), total=balance(id_, None, db))
return AccountBalance(date_=balance(id_, d, db), total=balance(id_, None, db))
@router.get("/{id_}", response_model=schemas.Account)
+5 -7
View File
@@ -1,5 +1,4 @@
import datetime
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Security
@@ -24,18 +23,17 @@ router = APIRouter()
@router.get("", response_model=list[schemas.Batch])
def batch_term(
q: str | None, d: str, user: Annotated[UserToken, Security(get_user)], db: SessionDep
q: str | None, d: date, user: Annotated[UserToken, Security(get_user)], db: SessionDep
) -> list[schemas.Batch]:
date_ = datetime.datetime.strptime(d, "%d-%b-%Y")
list_: list[schemas.Batch] = []
query = (
select(Batch)
.join(Batch.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(SkuVersion, onclause=_sv_onclause(date_))
.where(Batch.quantity_remaining > 0, Batch.name <= date_)
.join(ProductVersion, onclause=_pv_onclause(d))
.join(SkuVersion, onclause=_sv_onclause(d))
.where(Batch.quantity_remaining > 0, Batch.name <= d)
.options(
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.product).contains_eager(Product.versions),
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.versions),
+4 -5
View File
@@ -1,5 +1,5 @@
from calendar import monthrange
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
@@ -28,11 +28,10 @@ router = APIRouter()
def credit_salary(
user: Annotated[UserToken, Security(get_user, scopes=["attendance"])],
db: SessionDep,
month: str = Body(..., embed=True),
month: date = Body(..., embed=True),
) -> Message:
month_ = datetime.strptime(month, "%d-%b-%Y").date()
start_date = get_first_day(month_)
finish_date = get_last_day(month_)
start_date = get_first_day(month)
finish_date = get_last_day(month)
voucher = Voucher(
date_=finish_date,
narration="Auto Generated Salary Entry",
@@ -82,7 +82,7 @@ def employee_attendance(
prints, hours_worked, full_day = get_prints(employee.id, item, db)
list_.append(
schemas.EmployeeAttendanceItem(
date_=item.strftime("%d-%b-%Y"), # type: ignore
date_=item,
attendance_type=AttendanceTypeSchema(id_=at_type),
prints=prints,
hours_worked=hours_worked,
@@ -1,7 +1,7 @@
import csv
import io
from datetime import date, datetime
from datetime import date
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
@@ -19,19 +19,14 @@ router = APIRouter()
@router.get("", response_class=StreamingResponse)
def get_report(
s: str,
f: str,
s: date,
f: date,
# user: Annotated[UserToken, Security(get_user, scopes=["attendance"])] ## removed as jwt headers are a pain in the ass
db: SessionDep,
) -> StreamingResponse:
try:
output = io.StringIO()
attendance_record(
datetime.strptime(s, "%d-%b-%Y"),
datetime.strptime(f, "%d-%b-%Y"),
output,
db,
)
attendance_record(s, f, output, db)
headers = {"Content-Disposition": "attachment; filename = fingerprint-record.csv"}
output.seek(0)
return StreamingResponse(output, media_type="text/csv", headers=headers)
+6 -6
View File
@@ -45,11 +45,11 @@ async def save(
)
db.add(item)
add_items(item, data.items, db)
report_date(request, data.date_.strftime("%d-%b-%Y"))
report_date(request, data.date_)
if data.valid_from is not None:
report_start_date(request, data.valid_from.strftime("%d-%b-%Y"))
report_start_date(request, data.valid_from)
if data.valid_till is not None:
report_finish_date(request, data.valid_till.strftime("%d-%b-%Y"))
report_finish_date(request, data.valid_till)
db.commit()
return rate_contract_info(item, db)
@@ -77,11 +77,11 @@ async def update_route(
item.narration = data.narration
item.user_id = user.id_
update_items(item, data.items, db)
report_date(request, data.date_.strftime("%d-%b-%Y"))
report_date(request, data.date_)
if data.valid_from is not None:
report_start_date(request, data.valid_from.strftime("%d-%b-%Y"))
report_start_date(request, data.valid_from)
if data.valid_till is not None:
report_finish_date(request, data.valid_till.strftime("%d-%b-%Y"))
report_finish_date(request, data.valid_till)
db.commit()
return rate_contract_info(item, db)
+4 -4
View File
@@ -732,10 +732,10 @@ def _parse_import_date(value: str) -> date:
return datetime.strptime(value.strip(), fmt).date()
except ValueError:
continue
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid date; use DD-MMM-YYYY or YYYY-MM-DD",
)
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid date; use DD-MMM-YYYY or YYYY-MM-DD",
)
def _import_options(date_: date, create: str, sale_prices: bool, master_data: bool, tags: bool) -> ImportOptions:
+28 -22
View File
@@ -1,4 +1,4 @@
from datetime import date, datetime, timedelta
from datetime import date, timedelta
from fastapi import Request
@@ -6,40 +6,43 @@ from fastapi import Request
def report_start_date(request: Request, s: str | date | None = None) -> date:
session = request.session
if isinstance(s, date):
session["start"] = s.strftime("%d-%b-%Y")
session["start"] = s.isoformat()
return s
if s:
session["start"] = s
return datetime.strptime(s, "%d-%b-%Y").date()
d = date.fromisoformat(s)
session["start"] = d.isoformat()
return d
if "start" not in session:
session["start"] = get_first_day(date.today()).strftime("%d-%b-%Y")
return datetime.strptime(session["start"], "%d-%b-%Y").date()
session["start"] = get_first_day(date.today()).isoformat()
return date.fromisoformat(session["start"])
def report_finish_date(request: Request, f: str | date | None = None) -> date:
session = request.session
if isinstance(f, date):
session["finish"] = f.strftime("%d-%b-%Y")
session["finish"] = f.isoformat()
return f
if f:
session["finish"] = f
return datetime.strptime(f, "%d-%b-%Y").date()
d = date.fromisoformat(f)
session["finish"] = d.isoformat()
return d
if "finish" not in session:
session["finish"] = get_last_day(date.today()).strftime("%d-%b-%Y")
return datetime.strptime(session["finish"], "%d-%b-%Y").date()
session["finish"] = get_last_day(date.today()).isoformat()
return date.fromisoformat(session["finish"])
def report_date(request: Request, d: str | date | None = None) -> date:
session = request.session
if isinstance(d, date):
session["date"] = d.strftime("%d-%b-%Y")
session["date"] = d.isoformat()
return d
if d:
session["date"] = d
return datetime.strptime(d, "%d-%b-%Y").date()
parsed_date = date.fromisoformat(d)
session["date"] = parsed_date.isoformat()
return parsed_date
if "date" not in session:
session["date"] = get_last_day(date.today()).strftime("%d-%b-%Y")
return datetime.strptime(session["date"], "%d-%b-%Y").date()
session["date"] = get_last_day(date.today()).isoformat()
return date.fromisoformat(session["date"])
def get_first_day(dt: date, d_years: int = 0, d_months: int = 0) -> date:
@@ -56,20 +59,23 @@ def get_last_day(dt: date) -> date:
def op_report_start_date(request: Request, s: str | None = None) -> date | None:
session = request.session
if s:
session["start"] = s
return datetime.strptime(s, "%d-%b-%Y").date()
d = date.fromisoformat(s)
session["start"] = d.isoformat()
return d
return None
def op_report_finish_date(request: Request, f: str | None = None) -> date | None:
session = request.session
if f:
session["finish"] = f
return datetime.strptime(f, "%d-%b-%Y").date()
d = date.fromisoformat(f)
session["finish"] = d.isoformat()
return d
return None
def path_date(request: Request, date_: str) -> date:
session = request.session
session["date"] = date_
return datetime.strptime(date_, "%d-%b-%Y").date()
d = date.fromisoformat(date_)
session["date"] = d.isoformat()
return d
@@ -102,11 +102,11 @@ def build_report(
)
for voucher, inventory, journal, stockKeepingUnit in query:
# We are tracking at purchase cost centre, but the journal is of the cost centre so we reverse the debit direction.
journal_debit = journal.debit * -1
if voucher.voucher_type == VoucherType.SALE:
print(journal_debit, " pre")
# Else sale will be seen as a debit since
journal_debit = -1 * journal_debit
print(journal_debit)
name = (
journal.cost_centre.name
if voucher.voucher_type in [VoucherType.ISSUE, VoucherType.CLOSING_STOCK, VoucherType.SALE]
@@ -130,9 +130,7 @@ def build_report(
url=[
"/",
voucher.voucher_type.name.replace("_", "-").lower(),
str(voucher.id)
if voucher.voucher_type != VoucherType.CLOSING_STOCK
else voucher.date_.strftime("%d-%b-%Y"),
str(voucher.id) if voucher.voucher_type != VoucherType.CLOSING_STOCK else voucher.date_.isoformat(),
],
type_=voucher.voucher_type.name.replace("_", " ").title(),
narration=voucher.narration,
@@ -35,7 +35,7 @@ def report_data(
user: Annotated[UserToken, Security(get_user, scopes=["trial-balance"])],
db: SessionDep,
) -> schemas.TrialBalance:
report_finish_date(request, date_.strftime("%d-%b-%Y"))
report_finish_date(request, date_)
return schemas.TrialBalance(
date_=date_,
body=build_report(date_, db),
+83
View File
@@ -0,0 +1,83 @@
import uuid
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import select
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionDep
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from ..schemas import sale as schemas
from ..schemas import voucher as output
from ..schemas.user import UserToken
from .reports import report_date
from .voucher import voucher_info
router = APIRouter()
def compute_total_amount(vouchers: list[output.Voucher]) -> Decimal:
total = Decimal(0)
for v in vouchers:
for inv in v.inventories:
if inv.amount is not None:
total += inv.amount
return total
@router.get("/{id_}", response_model=schemas.SaleDayResponse)
def get_sale_day_by_id(
id_: uuid.UUID,
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
) -> schemas.SaleDayResponse:
voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one_or_none()
if voucher is None or voucher.voucher_type != VoucherType.SALE:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Sale voucher not found")
vouchers = (
db.execute(
select(Voucher)
.where(Voucher.date_ == voucher.date_, Voucher.voucher_type == VoucherType.SALE)
.order_by(Voucher.creation_date)
)
.scalars()
.unique()
.all()
)
voucher_infos = [voucher_info(v, db) for v in vouchers]
return schemas.SaleDayResponse(
date_=voucher.date_,
selected_voucher_id=id_,
total_amount=compute_total_amount(voucher_infos),
vouchers=voucher_infos,
)
@router.get("", response_model=schemas.SaleDayResponse)
def get_sale_day_by_date(
d: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
) -> schemas.SaleDayResponse:
vouchers = (
db.execute(
select(Voucher)
.where(Voucher.date_ == d, Voucher.voucher_type == VoucherType.SALE)
.order_by(Voucher.creation_date)
)
.scalars()
.unique()
.all()
)
voucher_infos = [voucher_info(v, db) for v in vouchers]
return schemas.SaleDayResponse(
date_=d,
selected_voucher_id=None,
total_amount=compute_total_amount(voucher_infos),
vouchers=voucher_infos,
)
+2 -19
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import to_camel
from .attendance_type import AttendanceType
@@ -31,14 +25,3 @@ class Attendance(BaseModel):
date_: date | None = None
body: list[AttendanceItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+3 -20
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -21,18 +15,7 @@ class BalanceSheetItem(BaseModel):
class BalanceSheet(BaseModel):
date_: date | str
date_: date
body: list[BalanceSheetItem]
footer: BalanceSheetItem | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+2 -30
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -21,17 +15,6 @@ class BatchIntegrityItem(BaseModel):
price: Daf
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
class BatchIntegrity(BaseModel):
id_: uuid.UUID
@@ -42,14 +25,3 @@ class BatchIntegrity(BaseModel):
actual: Daf
details: list[BatchIntegrityItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+2 -19
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from ..models.voucher_type import VoucherType
from . import to_camel
@@ -21,14 +15,3 @@ class BlankVoucherInfo(BaseModel):
source: CostCentreLink | None = None
destination: CostCentreLink | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+4 -32
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -27,30 +21,8 @@ class CashFlowBody(BaseModel):
class CashFlow(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
body: CashFlowBody
footer: list[CashFlowItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+1 -31
View File
@@ -2,13 +2,7 @@ import uuid
from datetime import datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import to_camel
@@ -25,32 +19,8 @@ class Client(ClientIn):
creation_date: datetime
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("creation_date", mode="before")
@classmethod
def parse_creation_date(cls, value: datetime | str) -> datetime | None:
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("creation_date")
def serialize_creation_date(self, value: datetime, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
class ClientList(Client):
last_user: str
last_date: datetime | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("last_date", mode="before")
@classmethod
def parse_last_date(cls, value: datetime | str | None) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_date")
def serialize_last_date(self, value: datetime | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
+1 -44
View File
@@ -3,13 +3,7 @@ import uuid
from datetime import date, datetime
from decimal import Decimal
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict, field_validator
from . import Daf, to_camel
from .cost_centre import CostCentreLink
@@ -59,40 +53,3 @@ class ClosingStock(BaseModel):
posted: bool
poster: UserLink | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("creation_date", mode="before")
@classmethod
def parse_creation_date(cls, value: None | datetime | str) -> datetime | None:
if value is None:
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("creation_date")
def serialize_creation_date(self, value: datetime | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
@field_validator("last_edit_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: None | datetime | str) -> datetime | None:
if value is None:
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
+5 -44
View File
@@ -1,21 +1,15 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
class DaybookItem(BaseModel):
id_: uuid.UUID | None = None
date_: date | str
date_: date
url: list[str]
type_: str
narration: str
@@ -26,42 +20,9 @@ class DaybookItem(BaseModel):
posted: bool
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
class Daybook(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
body: list[DaybookItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+1 -28
View File
@@ -1,14 +1,11 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
Field,
FieldSerializationInfo,
field_serializer,
field_validator,
model_validator,
)
@@ -25,30 +22,6 @@ class EmployeeIn(AccountBase):
leaving_date: date | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("joining_date", mode="before")
@classmethod
def parse_joining_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("joining_date")
def serialize_joining_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("leaving_date", mode="before")
@classmethod
def parse_leaving_date(cls, value: None | date | str) -> date | None:
if isinstance(value, date):
return value
if value is None:
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("leaving_date")
def serialize_leaving_date(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@model_validator(mode="after")
def leaving_date_more_than_joining_date(self) -> EmployeeIn:
if self.is_active:
+2 -45
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import to_camel
from .account import AccountLink
@@ -21,17 +15,6 @@ class EmployeeAttendanceItem(BaseModel):
full_day: bool | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
class EmployeeAttendance(BaseModel):
start_date: date | None = None
@@ -39,29 +22,3 @@ class EmployeeAttendance(BaseModel):
employee: AccountLink | None = None
body: list[EmployeeAttendanceItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
+1 -40
View File
@@ -2,13 +2,7 @@ import uuid
from datetime import date, datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
from .user_link import UserLink
@@ -29,39 +23,6 @@ class Entries(BaseModel):
user: UserLink
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("creation_date", mode="before")
@classmethod
def parse_creation_date(cls, value: datetime | str) -> datetime:
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("creation_date")
def serialize_creation_date(self, value: datetime, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
@field_validator("last_edit_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: datetime | str) -> datetime:
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
class Report(BaseModel):
counts: int
+2 -19
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import to_camel
@@ -18,14 +12,3 @@ class Fingerprint(BaseModel):
employee_id: uuid.UUID
date_: datetime
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: datetime | str) -> datetime:
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
+13 -41
View File
@@ -1,10 +1,10 @@
import json
import uuid
from datetime import date, datetime
from typing import Self
from fastapi import Form
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
from . import Daf, to_camel
from .account import AccountLink
@@ -20,17 +20,10 @@ class JournalIn(VoucherIn):
journals: list[Journal]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@classmethod
def load_form(cls, data: str = Form(...)) -> BaseModel:
def load_form(cls, data: str = Form(...)) -> Self:
json_data = json.loads(data)
return cls.parse_obj(json_data)
return cls.model_validate(json_data)
class PurchaseIn(VoucherIn):
@@ -38,17 +31,10 @@ class PurchaseIn(VoucherIn):
inventories: list[Inventory]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@classmethod
def load_form(cls, data: str = Form(...)) -> BaseModel:
def load_form(cls, data: str = Form(...)) -> Self:
json_data = json.loads(data)
return cls.parse_obj(json_data)
return cls.model_validate(json_data)
class IssueIn(VoucherIn):
@@ -58,49 +44,35 @@ class IssueIn(VoucherIn):
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@model_validator(mode="after")
def source_destination_unique(self) -> IssueIn:
def source_destination_unique(self) -> Self:
if self.source.id_ == self.destination.id_:
raise ValueError("Source and destination cannot be the same")
return self
@classmethod
def load_form(cls, data: str = Form(...)) -> BaseModel:
def load_form(cls, data: str = Form(...)) -> Self:
json_data = json.loads(data)
return cls.parse_obj(json_data)
return cls.model_validate(json_data)
class EmployeeBenefitIn(VoucherIn):
employee_benefits: list[EmployeeBenefit]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@classmethod
def load_form(cls, data: str = Form(...)) -> BaseModel:
def load_form(cls, data: str = Form(...)) -> Self:
json_data = json.loads(data)
return cls.parse_obj(json_data)
return cls.model_validate(json_data)
class IncentiveIn(VoucherIn):
incentives: list[Incentive]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@classmethod
def load_form(cls, data: str = Form(...)) -> BaseModel:
def load_form(cls, data: str = Form(...)) -> Self:
json_data = json.loads(data)
return cls.parse_obj(json_data)
return cls.model_validate(json_data)
class IncentiveEmployee(BaseModel):
+5 -44
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
from .account import AccountLink
@@ -16,7 +10,7 @@ from .account import AccountLink
class LedgerItem(BaseModel):
id_: uuid.UUID | None = None
date_: date | str
date_: date
name: str
url: list[str]
type_: str
@@ -27,43 +21,10 @@ class LedgerItem(BaseModel):
tags: list[str]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
class Ledger(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
account: AccountLink | None = None
body: list[LedgerItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@@ -2,13 +2,7 @@ import uuid
from datetime import date, datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
from .product import ProductLink
@@ -26,32 +20,8 @@ class MozimoDailyRegisterItem(BaseModel):
last_edit_date: datetime | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@field_validator("last_edit_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: None | datetime | str) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
class MozimoDailyRegister(BaseModel):
date_: date | str
date_: date
body: list[MozimoDailyRegisterItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@@ -2,13 +2,7 @@ import uuid
from datetime import date, datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
from .product import ProductLink
@@ -26,30 +20,6 @@ class MozimoProductRegisterItem(BaseModel):
last_edit_date: datetime | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("last_edit_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: None | datetime | str) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
class MozimoProductRegister(BaseModel):
start_date: date
@@ -57,25 +27,3 @@ class MozimoProductRegister(BaseModel):
product: ProductLink | None = None
body: list[MozimoProductRegisterItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
+4 -32
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -20,29 +14,7 @@ class NetTransactionsItem(BaseModel):
class NetTransactions(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
body: list[NetTransactionsItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -19,14 +13,3 @@ class NonContractPurchase(BaseModel):
contract_price: Daf
purchase_price: Daf
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+2 -31
View File
@@ -1,15 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
Field,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
@@ -19,28 +12,6 @@ class PeriodIn(BaseModel):
valid_till: date
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("valid_from", mode="before")
@classmethod
def parse_valid_from(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@classmethod
def parse_valid_till(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
class Period(PeriodIn):
id_: uuid.UUID
+4 -43
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
from .product import ProductLink
@@ -32,43 +26,10 @@ class ProductLedgerItem(BaseModel):
posted: bool
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
class ProductLedger(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
product: ProductLink | None = None
body: list[ProductLedgerItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+2 -30
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -26,25 +20,3 @@ class ProfitLoss(BaseModel):
body: list[ProfitLossItem]
footer: ProfitLossItem
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+4 -40
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -21,41 +15,11 @@ class PurchaseEntriesItem(BaseModel):
tax: Daf
discount: Daf
amount: Daf
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class PurchaseEntries(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
body: list[PurchaseEntriesItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+4 -32
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -21,30 +15,8 @@ class PurchasesItem(BaseModel):
class Purchases(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
body: list[PurchasesItem]
footer: PurchasesItem
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+3 -75
View File
@@ -2,13 +2,7 @@ import uuid
from datetime import date, datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import to_camel
from .account import AccountLink
@@ -18,61 +12,17 @@ from .user_link import UserLink
class RateContractBlank(BaseModel):
date_: date
valid_from: date | None
valid_till: date | None
valid_from: date | None = None
valid_till: date | None = None
narration: str
items: list[RateContractItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("valid_from", mode="before")
@classmethod
def parse_valid_from(cls, value: date | str | None) -> date | None:
if value is None:
return value
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@classmethod
def parse_valid_till(cls, value: date | str | None) -> date | None:
if value is None:
return value
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
class RateContractIn(RateContractBlank):
vendor: AccountLink
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
class RateContract(RateContractIn):
id_: uuid.UUID
@@ -80,25 +30,3 @@ class RateContract(RateContractIn):
last_edit_date: datetime
user: UserLink
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("creation_date", mode="before")
@classmethod
def parse_creation_date(cls, value: datetime | str) -> datetime | None:
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("creation_date")
def serialize_creation_date(self, value: datetime, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
@field_validator("last_edit_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: datetime | str) -> datetime | None:
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
+4 -32
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -32,30 +26,8 @@ class RawMaterialCostItem(BaseModel):
class RawMaterialCost(BaseModel):
id_: uuid.UUID | None = None
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
body: list[RawMaterialCostItem]
footer: RawMaterialCostItem | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+4 -19
View File
@@ -1,6 +1,8 @@
import json
import uuid
from datetime import date, datetime
from datetime import date
from typing import Self
from fastapi import Form
from pydantic import (
@@ -8,9 +10,6 @@ from pydantic import (
BaseModel,
ConfigDict,
Field,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from . import Daf, to_camel
@@ -44,23 +43,9 @@ class RecipeIn(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@classmethod
def load_form(cls, data: str = Form(...)) -> BaseModel:
import json
def load_form(cls, data: str = Form(...)) -> Self:
json_data = json.loads(data)
# pydantic v2
return cls.model_validate(json_data)
+4 -56
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
from .account import AccountLink
@@ -28,56 +22,10 @@ class ReconcileItem(BaseModel):
reconcile_date: date | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("reconcile_date", mode="before")
@classmethod
def parse_reconcile_date(cls, value: None | date | str) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("reconcile_date")
def serialize_reconcile_date(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
class Reconcile(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
account: AccountLink | None = None
body: list[ReconcileItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+18
View File
@@ -0,0 +1,18 @@
import uuid
from datetime import date
from decimal import Decimal
from pydantic import BaseModel, ConfigDict
from . import to_camel
from .voucher import Voucher
class SaleDayResponse(BaseModel):
date_: date
selected_voucher_id: uuid.UUID | None = None
total_amount: Decimal
vouchers: list[Voucher]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+2 -47
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import to_camel
@@ -18,19 +12,6 @@ class LockDate(BaseModel):
date_: date | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: None | date | str) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
class VoucherTypesSelected(BaseModel):
id_: int
@@ -59,32 +40,6 @@ class LockInformation(BaseModel):
index: int
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@field_validator("valid_from", mode="before")
@classmethod
def parse_valid_from(cls, value: date | str | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@classmethod
def parse_valid_till(cls, value: date | str | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
class Maintenance(BaseModel):
enabled: bool
+4 -32
View File
@@ -1,14 +1,8 @@
import uuid
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -26,29 +20,7 @@ class StockMovementItem(BaseModel):
class StockMovement(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
body: list[StockMovementItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+2 -54
View File
@@ -1,10 +1,10 @@
import uuid
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, Field
from . import Daf, to_camel
from .product_group import ProductGroupLink
@@ -43,32 +43,6 @@ class Product(BaseModel):
valid_till: date | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("valid_from", mode="before")
@classmethod
def parse_valid_from(cls, value: str | date | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@classmethod
def parse_valid_till(cls, value: str | date | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
class StockKeepingUnit(BaseModel):
id_: uuid.UUID
@@ -83,32 +57,6 @@ class StockKeepingUnit(BaseModel):
valid_till: date | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("valid_from", mode="before")
@classmethod
def parse_valid_from(cls, value: str | date | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@classmethod
def parse_valid_till(cls, value: str | date | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
class TemporalProduct(BaseModel):
products: list[Product]
+3 -20
View File
@@ -1,12 +1,6 @@
from datetime import date, datetime
from datetime import date
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
@@ -20,17 +14,6 @@ class TrialBalanceItem(BaseModel):
class TrialBalance(BaseModel):
date_: date | str
date_: date
body: list[TrialBalanceItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+1 -21
View File
@@ -2,14 +2,7 @@ import uuid
from datetime import datetime
from pydantic import (
BaseModel,
ConfigDict,
Field,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
from .role import RoleItem
@@ -37,19 +30,6 @@ class UserList(BaseModel):
last_date: datetime | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("last_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: datetime | str | None) -> datetime | None:
if value is None:
return value
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_date")
def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
class UserToken(BaseModel):
id_: uuid.UUID
+5 -59
View File
@@ -2,15 +2,10 @@ import json
import uuid
from datetime import date, datetime
from typing import Self
from fastapi import Form
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from pydantic import BaseModel, ConfigDict
from . import Daf, to_camel
from .account import AccountLink
@@ -34,22 +29,12 @@ class VoucherIn(BaseModel):
type_: str
files: list[ImageUpload]
tags: list[Tag]
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@classmethod
def load_form(cls, data: str = Form(...)) -> BaseModel:
def load_form(cls, data: str = Form(...)) -> Self:
json_data = json.loads(data)
return cls.parse_obj(json_data)
return cls.model_validate(json_data)
class Voucher(VoucherIn):
@@ -71,42 +56,3 @@ class Voucher(VoucherIn):
employee_benefits: list[EmployeeBenefit]
files: list[ImageUpload]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("reconcile_date", mode="before")
@classmethod
def parse_reconcile_date(cls, value: None | date | str) -> date | None:
if value is None or value == "":
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("reconcile_date")
def serialize_reconcile_date(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("creation_date", mode="before")
@classmethod
def parse_creation_date(cls, value: None | datetime | str) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("creation_date")
def serialize_creation_date(self, value: datetime | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
@field_validator("last_edit_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: None | datetime | str) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
+2 -4
View File
@@ -14,21 +14,19 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^22.1.2",
"@angular/cdk": "^22.1.2",
"@angular/common": "^22.1.2",
"@angular/compiler": "^22.1.2",
"@angular/core": "^22.1.2",
"@angular/forms": "^22.1.2",
"@angular/material": "^22.1.2",
"@angular/material-moment-adapter": "^22.1.2",
"@angular/material-date-fns-adapter": "^22.1.2",
"@angular/platform-browser": "^22.1.2",
"@angular/platform-browser-dynamic": "^22.1.2",
"@angular/router": "^22.1.2",
"date-fns": "^4.4.0",
"html2canvas": "^1.4.1",
"jspdf": "^4.2.0",
"mathjs": "^15.1.1",
"moment": "^2.30.1",
"rxjs": "~7.8.0",
"tslib": "^2.6.0"
},
+5 -4
View File
@@ -6,11 +6,12 @@ import {
LOCALE_ID,
provideZonelessChangeDetection,
} from '@angular/core';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { provideDateFnsAdapter } from '@angular/material-date-fns-adapter';
import { MAT_DATE_LOCALE } from '@angular/material/core';
import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
import { BrowserModule } from '@angular/platform-browser';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { enIN } from 'date-fns/locale';
import { dateFormat } from './app.environment';
import { routes } from './app.routes';
@@ -24,6 +25,7 @@ export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(BrowserModule),
{ provide: LOCALE_ID, useValue: 'en-IN' },
{ provide: MAT_DATE_LOCALE, useValue: enIN },
AuthService,
provideHttpClient(
withInterceptors([
@@ -34,8 +36,7 @@ export const appConfig: ApplicationConfig = {
]),
),
provideRouter(routes, withComponentInputBinding()),
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{ provide: MAT_DATE_FORMATS, useValue: dateFormat },
provideDateFnsAdapter(dateFormat),
{ provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 3000 } },
provideZonelessChangeDetection(),
],
+5 -5
View File
@@ -6,12 +6,12 @@ export const environment = {
export const dateFormat = {
parse: {
dateInput: 'DD-MMM-YYYY',
dateInput: ['dd-MMM-yyyy', 'dd/MM/yyyy', 'dd-MM-yyyy', 'yyyy-MM-dd'],
},
display: {
dateInput: 'DD-MMM-YYYY',
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'DD-MMM-YYYY',
monthYearA11yLabel: 'MMM YYYY',
dateInput: 'dd-MMM-yyyy',
monthYearLabel: 'MMM yyyy',
dateA11yLabel: 'dd-MMM-yyyy',
monthYearA11yLabel: 'MMMM yyyy',
},
};
+4
View File
@@ -153,6 +153,10 @@ export const routes: Routes = [
path: 'sales-import',
loadChildren: () => import('./sales-import/sales-import.routes').then((mod) => mod.routes),
},
{
path: 'sale',
loadChildren: () => import('./sale/sale.routes').then((mod) => mod.routes),
},
{
path: 'settings',
loadChildren: () => import('./settings/settings.routes').then((mod) => mod.routes),
@@ -12,9 +12,9 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { Attendance } from './attendance';
@@ -66,7 +66,7 @@ export class AttendanceComponent {
model = linkedSignal({
source: this.info,
computation: (infoVal): AttendanceFormData => ({
date: moment(infoVal.date, 'DD-MMM-YYYY').toDate(),
date: parseDate(infoVal.date),
attendances: infoVal.body.map((x) => ({
attendanceType: x.attendanceType.id,
})),
@@ -98,7 +98,7 @@ export class AttendanceComponent {
}
show() {
const date = moment(this.model().date).format('DD-MMM-YYYY');
const date = formatIsoDate(this.model().date);
if (this.date() === date) {
this.infoResource.reload();
} else {
@@ -120,7 +120,7 @@ export class AttendanceComponent {
getAttendance(): Attendance {
const formModel = this.model();
const currentInfo = this.info();
currentInfo.date = moment(formModel.date).format('DD-MMM-YYYY');
currentInfo.date = formatIsoDate(formModel.date);
const array = formModel.attendances;
if (array) {
currentInfo.body.forEach((item, index) => {
@@ -12,8 +12,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ClearPipe } from '../shared/clear.pipe';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
@@ -58,7 +58,7 @@ export class BalanceSheetComponent {
formModel = linkedSignal<import('./balance-sheet').BalanceSheet, BalanceSheetFormData>({
source: this.info,
computation: (info: BalanceSheet): BalanceSheetFormData => ({
date: info.date ? moment(info.date, 'DD-MMM-YYYY').toDate() : new Date(),
date: parseDate(info.date),
}),
});
@@ -121,7 +121,7 @@ export class BalanceSheetComponent {
getDate(): string {
const formModel = this.formModel();
return moment(formModel.date).format('DD-MMM-YYYY');
return formatIsoDate(formModel.date);
}
handlePageEvent(e: PageEvent) {
@@ -4,9 +4,9 @@
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
<input matInput [matDatepicker]="startDatePicker" [formField]="form.startDate" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startDatePicker></mat-datepicker>
<input matInput [matDatepicker]="startDate" [formField]="form.startDate" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
<mat-datepicker #startDate></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Finish Date</mat-label>
@@ -12,8 +12,8 @@ import { MatInputModule } from '@angular/material/input';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ClearPipe } from '../shared/clear.pipe';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
@@ -78,11 +78,11 @@ export class CashFlowComponent {
dataSource = computed(() => this.sortedList());
model = linkedSignal({
model = linkedSignal<import('./cash-flow').CashFlow, CashFlowFormData>({
source: this.info,
computation: (info: CashFlow): CashFlowFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
}),
});
@@ -121,8 +121,8 @@ export class CashFlowComponent {
const formModel = this.model();
return new CashFlow({
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
});
}
@@ -14,10 +14,10 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { CostCentre } from '../core/cost-centre';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { User } from '../core/user';
import { CostCentreService } from '../cost-centre/cost-centre.service';
import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component';
@@ -85,7 +85,7 @@ export class ClosingStockComponent {
model = linkedSignal({
source: computed(() => ({ info: this.info(), costCentres: this.costCentres() })),
computation: ({ info, costCentres }): ClosingStockFormData => ({
date: moment(info.date, 'DD-MMM-YYYY').toDate(),
date: parseDate(info.date),
costCentre: costCentres.find((c) => c.id === info.costCentre?.id) || null,
stocks: info.items.map((x) => ({
physical: x.physical,
@@ -176,7 +176,7 @@ export class ClosingStockComponent {
getClosingStock(): ClosingStock {
const formModel = this.model();
const currentInfo = this.info();
currentInfo.date = moment(formModel.date).format('DD-MMM-YYYY');
currentInfo.date = formatIsoDate(formModel.date);
const array = formModel.stocks;
currentInfo.items.forEach((item, index) => {
item.physical = array[index].physical ?? 0;
@@ -189,7 +189,7 @@ export class ClosingStockComponent {
const formModel = this.model();
return new ClosingStock({
date: moment(formModel.date).format('DD-MMM-YYYY'),
date: formatIsoDate(formModel.date),
costCentre: formModel.costCentre ?? new CostCentre(),
});
}
+54
View File
@@ -0,0 +1,54 @@
import { endOfMonth, format, getDaysInMonth, isValid, parse, parseISO, startOfMonth } from 'date-fns';
export const ISO_DATE_FORMAT = 'yyyy-MM-dd';
export const DISPLAY_DATE_FORMAT = 'dd-MMM-yyyy';
export const DISPLAY_DATETIME_FORMAT = 'dd-MMM-yyyy HH:mm';
export function isValidDate(value: unknown): value is Date {
return value instanceof Date && isValid(value);
}
export function formatIsoDate(date: Date | null | undefined): string {
if (!date || !isValid(date)) return '';
return format(date, ISO_DATE_FORMAT);
}
export function formatDisplayDate(date: Date | null | undefined): string {
if (!date || !isValid(date)) return '';
return format(date, DISPLAY_DATE_FORMAT);
}
export function formatDisplayDateTime(date: Date | null | undefined): string {
if (!date || !isValid(date)) return '';
return format(date, DISPLAY_DATETIME_FORMAT);
}
export function parseDate(value: string | Date | null | undefined): Date;
export function parseDate(value: string | Date | null | undefined, defaultValue: null): Date | null;
export function parseDate(value: string | Date | null | undefined, defaultValue: Date): Date;
export function parseDate(
value: string | Date | null | undefined,
defaultValue: Date | null = new Date(),
): Date | null {
if (!value) return defaultValue;
if (value instanceof Date) return value;
const trimmed = value.trim();
if (!trimmed) return defaultValue;
// ISO 8601 strings (YYYY-MM-DD or full ISO datetime)
if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) {
return parseISO(trimmed);
}
// Common UI format: dd-MMM-yyyy (e.g. 15-Sep-2026)
const parsedCustom = parse(trimmed, 'dd-MMM-yyyy', new Date());
if (isValid(parsedCustom)) {
return parsedCustom;
}
// Fallback to native date parsing
const fallback = new Date(trimmed);
return fallback;
}
export { endOfMonth, getDaysInMonth, startOfMonth };
@@ -13,8 +13,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { Daybook } from './daybook';
@@ -98,8 +98,8 @@ export class DaybookComponent {
model = linkedSignal({
source: this.info,
computation: (info: Daybook): DaybookFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
}),
});
@@ -143,8 +143,8 @@ export class DaybookComponent {
const formModel = this.model();
return {
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
body: [],
};
}
@@ -12,9 +12,9 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { AttendanceTypeService } from '../attendance/attendance-type.service';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { Employee } from '../employee/employee';
import { EmployeeService } from '../employee/employee.service';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
@@ -69,9 +69,9 @@ export class EmployeeAttendanceComponent {
model = linkedSignal({
source: this.info,
computation: (info): EmployeeAttendanceFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
computation: (info: EmployeeAttendance): EmployeeAttendanceFormData => ({
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
employee: info.employee ?? null,
attendances: info.body.map((x) => ({
attendanceType: x.attendanceType.id,
@@ -121,8 +121,8 @@ export class EmployeeAttendanceComponent {
show() {
const formValue = this.model();
const currentInfo = this.info();
currentInfo.startDate = moment(formValue.startDate).format('DD-MMM-YYYY');
currentInfo.finishDate = moment(formValue.finishDate).format('DD-MMM-YYYY');
currentInfo.startDate = formatIsoDate(formValue.startDate);
currentInfo.finishDate = formatIsoDate(formValue.finishDate);
currentInfo.employee =
typeof formValue.employee !== 'string' && formValue.employee ? formValue.employee : undefined;
if (currentInfo.employee) {
@@ -157,8 +157,8 @@ export class EmployeeAttendanceComponent {
getEmployeeAttendance(): EmployeeAttendance {
const formValue = this.model();
const currentInfo = this.info();
currentInfo.startDate = moment(formValue.startDate).format('DD-MMM-YYYY');
currentInfo.finishDate = moment(formValue.finishDate).format('DD-MMM-YYYY');
currentInfo.startDate = formatIsoDate(formValue.startDate);
currentInfo.finishDate = formatIsoDate(formValue.finishDate);
currentInfo.employee =
typeof formValue.employee !== 'string' && formValue.employee ? formValue.employee : undefined;
const array = formValue.attendances;
@@ -12,9 +12,9 @@ import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { formatIsoDate, getDaysInMonth, parseDate } from '../core/date-utils';
import { EmployeeBenefit } from '../core/employee-benefit';
import { User } from '../core/user';
import { Voucher } from '../core/voucher';
@@ -80,7 +80,7 @@ export class EmployeeBenefitsComponent {
formModel = linkedSignal<Voucher, EmployeeBenefitsFormData>({
source: this.item,
computation: (v): EmployeeBenefitsFormData => ({
date: v.date ? moment(v.date, 'DD-MMM-YYYY').toDate() : new Date(),
date: parseDate(v.date),
addRow: {
employee: null as Employee | string | null,
grossSalary: '',
@@ -168,7 +168,7 @@ export class EmployeeBenefitsComponent {
}
const grossSalary = Number(formValue?.grossSalary ?? 0);
const daysWorked = Number(formValue?.daysWorked ?? 0);
const daysInMonth = moment(this.formModel().date).daysInMonth();
const daysInMonth = getDaysInMonth(this.formModel().date);
const esi = EmployeeBenefitsComponent.getEsi(grossSalary, daysWorked, daysInMonth);
const pf = EmployeeBenefitsComponent.getPf(grossSalary, daysWorked, daysInMonth);
@@ -245,7 +245,7 @@ export class EmployeeBenefitsComponent {
const m = this.formModel();
const base = this.item();
const v = new Voucher({ ...base });
v.date = moment(m.date).format('DD-MMM-YYYY');
v.date = formatIsoDate(m.date);
v.employeeBenefits = m.benefits.map(
(b) =>
new EmployeeBenefit({
@@ -6,8 +6,8 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import moment from 'moment';
import { endOfMonth, formatIsoDate } from '../core/date-utils';
import { EmployeeFunctionsService } from './employee-functions.service';
@Component({
@@ -28,15 +28,15 @@ export class EmployeeFunctionsComponent {
private snackBar = inject(MatSnackBar);
private ser = inject(EmployeeFunctionsService);
creditSalaryModel = signal({
date: moment().date(moment().daysInMonth()),
creditSalaryModel = signal<{ date: Date }>({
date: endOfMonth(new Date()),
});
creditSalaryForm = createForm(this.creditSalaryModel);
attendanceRecordModel = signal({
startDate: moment(),
finishDate: moment(),
attendanceRecordModel = signal<{ startDate: Date; finishDate: Date }>({
startDate: new Date(),
finishDate: new Date(),
});
attendanceRecordForm = createForm(this.attendanceRecordModel);
@@ -44,28 +44,24 @@ export class EmployeeFunctionsComponent {
fingerprintFile: File | null = null;
constructor() {
const startDate = moment().date(1);
const finishDate = moment().date(startDate.daysInMonth());
this.creditSalaryModel.set({ date: finishDate });
this.creditSalaryModel.set({ date: endOfMonth(new Date()) });
}
chosenYearHandler(normalizedYear: moment.Moment) {
const ctrlValue = moment(this.creditSalaryModel().date);
ctrlValue.year(normalizedYear.year());
ctrlValue.date(ctrlValue.daysInMonth());
this.creditSalaryModel.update((m) => ({ ...m, date: ctrlValue }));
chosenYearHandler(normalizedYear: Date) {
const current = this.creditSalaryModel().date;
const updated = new Date(normalizedYear.getFullYear(), current.getMonth(), 1);
this.creditSalaryModel.update((m) => ({ ...m, date: endOfMonth(updated) }));
}
chosenMonthHandler(normlizedMonth: moment.Moment, datepicker: MatDatepicker<moment.Moment>) {
const ctrlValue = moment(this.creditSalaryModel().date);
ctrlValue.month(normlizedMonth.month());
ctrlValue.date(ctrlValue.daysInMonth());
this.creditSalaryModel.update((m) => ({ ...m, date: ctrlValue }));
chosenMonthHandler(normalizedMonth: Date, datepicker: MatDatepicker<Date>) {
const current = this.creditSalaryModel().date;
const updated = new Date(current.getFullYear(), normalizedMonth.getMonth(), 1);
this.creditSalaryModel.update((m) => ({ ...m, date: endOfMonth(updated) }));
datepicker.close();
}
creditSalary() {
const date = (this.creditSalaryModel().date ?? moment()).format('DD-MMM-YYYY');
const date = formatIsoDate(this.creditSalaryModel().date);
if (!date) {
this.snackBar.open('Please choose a valid date.', 'Danger');
return;
@@ -81,20 +77,20 @@ export class EmployeeFunctionsComponent {
}
attendanceRecordUrl() {
const startDate = (this.attendanceRecordModel().startDate ?? moment()).format('DD-MMM-YYYY');
const finishDate = (this.attendanceRecordModel().finishDate ?? moment()).format('DD-MMM-YYYY');
const startDate = formatIsoDate(this.attendanceRecordModel().startDate ?? new Date());
const finishDate = formatIsoDate(this.attendanceRecordModel().finishDate ?? new Date());
if (!startDate || !finishDate) {
// this.snackBar.open('Please choose a start and finish date.', 'Danger');
this.snackBar.open('Please choose a start and finish date.', 'Danger');
return '';
}
return `/attendance-report?s=${startDate}&f=${finishDate}`;
}
fingerPrintUrl() {
const startDate = (this.attendanceRecordModel().startDate ?? moment()).format('DD-MMM-YYYY');
const finishDate = (this.attendanceRecordModel().finishDate ?? moment()).format('DD-MMM-YYYY');
const startDate = formatIsoDate(this.attendanceRecordModel().startDate);
const finishDate = formatIsoDate(this.attendanceRecordModel().finishDate);
if (!startDate || !finishDate) {
// this.snackBar.open('Please choose a start and finish date.', 'Danger');
this.snackBar.open('Please choose a start and finish date.', 'Danger');
return '';
}
return `/fingerprint-report?s=${startDate}&f=${finishDate}`;
@@ -11,8 +11,8 @@ import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../../core/date-utils';
import { CostCentreService } from '../../cost-centre/cost-centre.service';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
@@ -70,8 +70,8 @@ export class EmployeeDetailComponent {
points: itemVal.points || 0,
isActive: itemVal.isActive ?? true,
costCentre: itemVal.costCentre?.id ?? '',
joiningDate: itemVal.joiningDate ? moment(itemVal.joiningDate, 'DD-MMM-YYYY').toDate() : new Date(),
leavingDate: itemVal.leavingDate ? moment(itemVal.leavingDate, 'DD-MMM-YYYY').toDate() : null,
joiningDate: parseDate(itemVal.joiningDate),
leavingDate: parseDate(itemVal.leavingDate, null),
}),
});
@@ -147,9 +147,8 @@ export class EmployeeDetailComponent {
item.points = Number(formValue.points);
item.isActive = formValue.isActive;
item.costCentre.id = formValue.costCentre ?? '';
item.joiningDate = moment(formValue.joiningDate).format('DD-MMM-YYYY');
item.leavingDate =
item.isActive || !formValue.leavingDate ? null : moment(formValue.leavingDate).format('DD-MMM-YYYY');
item.joiningDate = formatIsoDate(formValue.joiningDate);
item.leavingDate = item.isActive || !formValue.leavingDate ? null : formatIsoDate(formValue.leavingDate);
return item;
}
}
@@ -4,15 +4,9 @@
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Create / Last Edit Date From</mat-label>
<input
matInput
#startDateElement
[matDatepicker]="startDatePicker"
[formField]="form.startDate"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startDatePicker></mat-datepicker>
<input matInput [matDatepicker]="startDate" [formField]="form.startDate" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
<mat-datepicker #startDate></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Create / Last Edit Date Till</mat-label>
@@ -19,8 +19,8 @@ import { MatRadioChange, MatRadioModule } from '@angular/material/radio';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { LocalTimePipe } from '../shared/local-time.pipe';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
@@ -103,8 +103,8 @@ export class EntriesComponent {
issue: this.issue(),
}),
computation: (src): EntriesFormData => ({
startDate: src.startDate ? moment(src.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: src.finishDate ? moment(src.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(src.startDate),
finishDate: parseDate(src.finishDate),
posted: src.posted,
issue: src.issue,
}),
@@ -127,8 +127,8 @@ export class EntriesComponent {
refresh() {
const formModel = this.formModel();
const startDate = moment(formModel.startDate).format('DD-MMM-YYYY');
const finishDate = moment(formModel.finishDate).format('DD-MMM-YYYY');
const startDate = formatIsoDate(formModel.startDate);
const finishDate = formatIsoDate(formModel.finishDate);
this.router.navigate([], {
relativeTo: this.route,
queryParams: { s: startDate, f: finishDate },
@@ -10,11 +10,11 @@ import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { Account } from '../core/account';
import { AccountBalance } from '../core/account-balance';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { Incentive } from '../core/incentive';
import { User } from '../core/user';
import { Voucher } from '../core/voucher';
@@ -23,7 +23,7 @@ import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.
import { LocalTimePipe } from '../shared/local-time.pipe';
export interface IncentiveFormData {
date: moment.Moment;
date: Date;
incentives: Incentive[];
}
@@ -62,7 +62,7 @@ export class IncentiveComponent {
model = linkedSignal({
source: this.voucher,
computation: (v): IncentiveFormData => ({
date: v.date ? moment(v.date, 'DD-MMM-YYYY') : moment(new Date()),
date: parseDate(v.date),
incentives: v.incentives.map((x) => new Incentive({ ...x })),
}),
});
@@ -71,7 +71,7 @@ export class IncentiveComponent {
constructor() {
effect(async () => {
const date = this.form.date().value().format('DD-MMM-YYYY');
const date = formatIsoDate(this.form.date().value());
if (date !== this.d() && !this.id()) {
this.router.navigate(['/incentive'], { queryParams: { d: date }, replaceUrl: true });
}
@@ -167,7 +167,7 @@ export class IncentiveComponent {
getVoucher(): Voucher {
const formModel = this.model();
const v = this.voucher();
v.date = moment(formModel.date).format('DD-MMM-YYYY');
v.date = formatIsoDate(formModel.date);
v.incentives = formModel.incentives;
return v;
}
+6 -6
View File
@@ -15,12 +15,12 @@ import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import { round } from 'mathjs';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { Batch } from '../core/batch';
import { BatchService } from '../core/batch.service';
import { CostCentre } from '../core/cost-centre';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { Inventory } from '../core/inventory';
import { User } from '../core/user';
import { Voucher } from '../core/voucher';
@@ -118,7 +118,7 @@ export class IssueComponent {
2,
);
return {
date: voucher.date ? moment(voucher.date, 'DD-MMM-YYYY').toDate() : new Date(),
date: parseDate(voucher.date),
source: (voucher.source as CostCentre)?.id ?? '',
destination: (voucher.destination as CostCentre)?.id ?? '',
amount,
@@ -150,7 +150,7 @@ export class IssueComponent {
displayedColumns = ['product', 'batch', 'quantity', 'rate', 'amount', 'action'];
gridColumns = ['source', 'destination', 'gridAmount', 'load'];
balanceDate = computed(() => moment(this.form.date().value()).format('DD-MMM-YYYY'));
balanceDate = computed(() => formatIsoDate(this.form.date().value()));
batchSearch = computed(() => {
const v = this.form.addRow.batch().value();
@@ -200,7 +200,7 @@ export class IssueComponent {
if (!d) return;
const url = new URL(window.location.href);
url.searchParams.set('d', moment(d).format('DD-MMM-YYYY'));
url.searchParams.set('d', formatIsoDate(d));
window.history.replaceState({}, '', url.toString());
});
}
@@ -291,7 +291,7 @@ export class IssueComponent {
amount: row.amount,
batch: row.batch,
}),
date: moment(this.formModel().date).format('DD-MMM-YYYY'),
date: formatIsoDate(this.formModel().date),
},
});
@@ -337,7 +337,7 @@ export class IssueComponent {
const m = this.formModel();
const base = this.item();
const v = new Voucher({ ...base });
v.date = moment(m.date).format('DD-MMM-YYYY');
v.date = formatIsoDate(m.date);
v.source = new CostCentre({ id: m.source });
v.destination = new CostCentre({ id: m.destination });
v.narration = m.narration ?? '';
@@ -17,13 +17,13 @@ import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import { round } from 'mathjs';
import moment from 'moment';
import { zip } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { Account } from '../core/account';
import { AccountService } from '../core/account.service';
import { CostCentre } from '../core/cost-centre';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { DbFile } from '../core/db-file';
import { Journal } from '../core/journal';
import { User } from '../core/user';
@@ -135,7 +135,7 @@ export class JournalComponent {
event.preventDefault();
this.router.navigate(['payment'], {
queryParams: {
d: moment(this.formModel().date).format('DD-MMM-YYYY'),
d: formatIsoDate(this.formModel().date),
},
queryParamsHandling: 'merge',
});
@@ -145,7 +145,7 @@ export class JournalComponent {
event.preventDefault();
this.router.navigate(['receipt'], {
queryParams: {
d: moment(this.formModel().date).format('DD-MMM-YYYY'),
d: formatIsoDate(this.formModel().date),
},
queryParamsHandling: 'merge',
});
@@ -175,7 +175,7 @@ export class JournalComponent {
source: this.item,
computation: (voucher): JournalFormModel => ({
isStarred: voucher.isStarred,
date: moment(voucher.date, 'DD-MMM-YYYY').toDate(),
date: parseDate(voucher.date),
addRow: {
debit: 1,
account: null as Account | string | null,
@@ -218,7 +218,7 @@ export class JournalComponent {
tagsResource = this.tagSer.autocomplete(this.tagQuery);
tags = computed(() => this.tagsResource.value() ?? []);
balanceDate = computed(() => moment(this.form.date().value()).format('DD-MMM-YYYY'));
balanceDate = computed(() => formatIsoDate(this.form.date().value()));
accountId = computed((): string | null => {
const account = this.form.addRow.account().value();
return typeof account === 'string' ? null : (account?.id ?? null);
@@ -237,7 +237,7 @@ export class JournalComponent {
dateChanged(newDate: Date) {
this.router.navigate([], {
queryParams: { d: moment(newDate).format('DD-MMM-YYYY') },
queryParams: { d: formatIsoDate(newDate) },
replaceUrl: true,
queryParamsHandling: 'merge',
});
@@ -292,7 +292,7 @@ export class JournalComponent {
width: '750px',
data: {
journal: new Journal({ ...row }),
date: moment(this.formModel().date).format('DD-MMM-YYYY'),
date: formatIsoDate(this.formModel().date),
},
});
@@ -328,7 +328,7 @@ export class JournalComponent {
const m = this.formModel();
const base = this.item();
const v = new Voucher({ ...base });
v.date = moment(m.date).format('DD-MMM-YYYY');
v.date = formatIsoDate(m.date);
v.narration = m.narration ?? '';
v.journals = m.journals.map(
(j) => new Journal({ id: j.id, debit: j.debit, amount: j.amount, account: j.account, costCentre: j.costCentre }),
+5 -5
View File
@@ -17,10 +17,10 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { Account } from '../core/account';
import { AccountService } from '../core/account.service';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { AccountingPipe } from '../shared/accounting.pipe';
import { ClearPipe } from '../shared/clear.pipe';
import { ToCsvService } from '../shared/to-csv.service';
@@ -148,8 +148,8 @@ export class LedgerComponent {
model = linkedSignal<import('./ledger').Ledger, LedgerFormData>({
source: this.info,
computation: (info: Ledger): LedgerFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
account: info.account ?? null,
tags: this.tags(),
}),
@@ -283,8 +283,8 @@ export class LedgerComponent {
return new Ledger({
account,
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
});
}
@@ -13,9 +13,9 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { LocalTimePipe } from '../shared/local-time.pipe';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
@@ -78,7 +78,7 @@ export class MozimoDailyRegisterComponent {
model = linkedSignal({
source: this.info,
computation: (v): MozimoDailyRegisterFormData => ({
date: moment(v.date, 'DD-MMM-YYYY').toDate(),
date: parseDate(v.date),
items: v.body.map((x) => ({
received: x.received,
sale: x.sale,
@@ -110,7 +110,7 @@ export class MozimoDailyRegisterComponent {
}
show() {
const date = moment(this.model().date).format('DD-MMM-YYYY');
const date = formatIsoDate(this.model().date);
if (this.date() === date) {
this.infoResource.reload();
} else {
@@ -132,7 +132,7 @@ export class MozimoDailyRegisterComponent {
getMozimoDailyRegister(): MozimoDailyRegister {
const formModel = this.model();
const info = this.info();
info.date = moment(formModel.date).format('DD-MMM-YYYY');
info.date = formatIsoDate(formModel.date);
const array = formModel.items;
if (array) {
@@ -13,9 +13,9 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { Product } from '../core/product';
import { ProductService } from '../product/product.service';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
@@ -84,8 +84,8 @@ export class MozimoProductRegisterComponent {
model = linkedSignal({
source: this.info,
computation: (info): MozimoProductRegisterFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
product: info.product ?? '',
items: info.body.map((x) => ({
received: x.received,
@@ -167,8 +167,8 @@ export class MozimoProductRegisterComponent {
getMozimoProductRegister(): MozimoProductRegister {
const formModel = this.model();
const info = this.info();
info.startDate = moment(formModel.startDate).format('DD-MMM-YYYY');
info.finishDate = moment(formModel.finishDate).format('DD-MMM-YYYY');
info.startDate = formatIsoDate(formModel.startDate);
info.finishDate = formatIsoDate(formModel.finishDate);
const array = formModel.items;
if (array) {
@@ -190,8 +190,8 @@ export class MozimoProductRegisterComponent {
return new MozimoProductRegister({
product: formModel.product as Product,
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
});
}
@@ -4,9 +4,9 @@
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
<input matInput [matDatepicker]="startDatePicker" [formField]="form.startDate" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startDatePicker></mat-datepicker>
<input matInput [matDatepicker]="startDate" [formField]="form.startDate" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
<mat-datepicker #startDate></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Finish Date</mat-label>
@@ -13,8 +13,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { AccountingPipe } from '../shared/accounting.pipe';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
@@ -92,8 +92,8 @@ export class NetTransactionsComponent {
model = linkedSignal<import('./net-transactions').NetTransactions, NetTransactionsFormData>({
source: this.info,
computation: (info: NetTransactions): NetTransactionsFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
}),
});
@@ -133,8 +133,8 @@ export class NetTransactionsComponent {
const formModel = this.model();
return new NetTransactions({
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
});
}
@@ -17,13 +17,13 @@ import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import { round } from 'mathjs';
import moment from 'moment';
import { zip } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { Account } from '../core/account';
import { AccountService } from '../core/account.service';
import { CostCentre } from '../core/cost-centre';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { DbFile } from '../core/db-file';
import { Journal } from '../core/journal';
import { User } from '../core/user';
@@ -133,7 +133,7 @@ export class PaymentComponent {
);
return {
isStarred: voucher.isStarred,
date: voucher.date ? moment(voucher.date, 'DD-MMM-YYYY').toDate() : new Date(),
date: parseDate(voucher.date),
paymentAccount: pj?.account?.id ?? this.a() ?? '',
paymentAmount,
addRow: {
@@ -180,7 +180,7 @@ export class PaymentComponent {
tagsResource = this.tagSer.autocomplete(this.tagQuery);
tags = computed(() => this.tagsResource.value() ?? []);
balanceDate = computed(() => moment(this.form.date().value()).format('DD-MMM-YYYY'));
balanceDate = computed(() => formatIsoDate(this.form.date().value()));
accountId = computed((): string | null => {
const account = this.form.addRow.account().value();
return typeof account === 'string' ? null : (account?.id ?? null);
@@ -206,7 +206,7 @@ export class PaymentComponent {
this.router.navigate(['receipt'], {
queryParams: {
a: this.formModel().paymentAccount,
d: moment(this.formModel().date).format('DD-MMM-YYYY'),
d: formatIsoDate(this.formModel().date),
},
queryParamsHandling: 'merge',
});
@@ -216,7 +216,7 @@ export class PaymentComponent {
event.preventDefault();
this.router.navigate(['journal'], {
queryParams: {
d: moment(this.formModel().date).format('DD-MMM-YYYY'),
d: formatIsoDate(this.formModel().date),
},
queryParamsHandling: 'merge',
});
@@ -246,7 +246,7 @@ export class PaymentComponent {
if (!d) return;
const url = new URL(window.location.href);
url.searchParams.set('d', moment(d).format('DD-MMM-YYYY'));
url.searchParams.set('d', formatIsoDate(d));
window.history.replaceState({}, '', url.toString());
});
@@ -319,7 +319,7 @@ export class PaymentComponent {
account: row.account,
costCentre: row.costCentre,
}),
date: moment(this.formModel().date).format('DD-MMM-YYYY'),
date: formatIsoDate(this.formModel().date),
},
});
@@ -358,7 +358,7 @@ export class PaymentComponent {
const m = this.formModel();
const base = this.item();
const v = new Voucher({ ...base });
v.date = moment(m.date).format('DD-MMM-YYYY');
v.date = formatIsoDate(m.date);
v.narration = m.narration ?? '';
const existingPj = base.journals.find((x) => x.debit === -1);
@@ -11,8 +11,8 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../../core/date-utils';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
@@ -48,8 +48,8 @@ export class PeriodDetailComponent {
model = linkedSignal({
source: this.item,
computation: (item): PeriodFormData => ({
validFrom: item.validFrom ? moment(item.validFrom, 'DD-MMM-YYYY').toDate() : new Date(),
validTill: item.validTill ? moment(item.validTill, 'DD-MMM-YYYY').toDate() : new Date(),
validFrom: parseDate(item.validFrom),
validTill: parseDate(item.validTill),
}),
});
@@ -100,8 +100,8 @@ export class PeriodDetailComponent {
getItem(): Period {
const formValue = this.model();
const item = this.item();
item.validFrom = moment(formValue.validFrom).format('DD-MMM-YYYY');
item.validTill = moment(formValue.validTill).format('DD-MMM-YYYY');
item.validFrom = formatIsoDate(formValue.validFrom);
item.validTill = formatIsoDate(formValue.validTill);
return item;
}
}
@@ -15,6 +15,7 @@
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
<mat-datepicker #startDate></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Finish Date</mat-label>
<input matInput [matDatepicker]="finishDate" [formField]="form.finishDate" autocomplete="off" />
@@ -39,9 +40,6 @@
[displayWith]="displayFn"
(optionSelected)="selected($event)"
>
@for (product of products(); track product.id) {
<mat-option [value]="product">{{ product.name }}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
@@ -11,9 +11,9 @@ import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { Router, RouterModule } from '@angular/router';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { Product } from '../core/product';
import { ProductSku } from '../core/product-sku';
import { ProductService } from '../product/product.service';
@@ -23,6 +23,7 @@ export interface ProductLedgerFormData {
finishDate: Date;
product: Product | string | null;
}
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { ProductLedger } from './product-ledger';
@@ -57,7 +58,6 @@ import { ProductLedgerService } from './product-ledger.service';
],
})
export class ProductLedgerComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private toCsv = inject(ToCsvService);
private ser = inject(ProductLedgerService);
@@ -152,8 +152,8 @@ export class ProductLedgerComponent {
model = linkedSignal({
source: this.info,
computation: (info: ProductLedger): ProductLedgerFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
product: info.product ?? null,
}),
});
@@ -234,8 +234,8 @@ export class ProductLedgerComponent {
const product = prodVal && typeof prodVal !== 'string' ? prodVal : undefined;
return new ProductLedger({
product,
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
});
}
@@ -9,8 +9,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { ProfitLoss } from './profit-loss';
@@ -58,8 +58,8 @@ export class ProfitLossComponent {
formModel = linkedSignal({
source: this.info,
computation: (info: ProfitLoss): ProfitLossFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
}),
});
@@ -133,8 +133,8 @@ export class ProfitLossComponent {
const formModel = this.formModel();
return new ProfitLoss({
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
footer: this.info().footer,
body: this.info().body,
});
@@ -9,8 +9,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { PurchaseEntries } from './purchase-entries';
@@ -103,8 +103,8 @@ export class PurchaseEntriesComponent {
formModel = linkedSignal({
source: () => this.info(),
computation: (info: PurchaseEntries): PurchaseEntriesFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
}),
});
@@ -137,8 +137,8 @@ export class PurchaseEntriesComponent {
const formModel = this.formModel();
return {
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
body: [],
};
}
@@ -16,7 +16,6 @@ import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import { round } from 'mathjs';
import moment from 'moment';
import { zip } from 'rxjs';
import { AuthService } from '../auth/auth.service';
@@ -24,6 +23,7 @@ import { Account } from '../core/account';
import { AccountService } from '../core/account.service';
import { Batch } from '../core/batch';
import { BatchService } from '../core/batch.service';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { DbFile } from '../core/db-file';
import { Inventory } from '../core/inventory';
import { User } from '../core/user';
@@ -134,7 +134,7 @@ export class PurchaseReturnComponent {
);
return {
isStarred: voucher.isStarred,
date: voucher.date ? moment(voucher.date, 'DD-MMM-YYYY').toDate() : new Date(),
date: parseDate(voucher.date),
account: voucher.vendor ?? (this.a() ? new Account({ id: this.a()! }) : null),
amount,
addRow: {
@@ -184,7 +184,7 @@ export class PurchaseReturnComponent {
tagsResource = this.tagSer.autocomplete(this.tagQuery);
tags = computed(() => this.tagsResource.value() ?? []);
balanceDate = computed(() => moment(this.form.date().value()).format('DD-MMM-YYYY'));
balanceDate = computed(() => formatIsoDate(this.form.date().value()));
accountId = computed((): string | null => {
const account = this.form.account().value();
return typeof account === 'string' ? null : (account?.id ?? null);
@@ -240,7 +240,7 @@ export class PurchaseReturnComponent {
if (!d) return;
const url = new URL(window.location.href);
url.searchParams.set('d', moment(d).format('DD-MMM-YYYY'));
url.searchParams.set('d', formatIsoDate(d));
window.history.replaceState({}, '', url.toString());
});
@@ -326,7 +326,7 @@ export class PurchaseReturnComponent {
amount: row.amount,
batch: row.batch,
}),
date: moment(this.formModel().date).format('DD-MMM-YYYY'),
date: formatIsoDate(this.formModel().date),
},
});
@@ -372,7 +372,7 @@ export class PurchaseReturnComponent {
const m = this.formModel();
const base = this.item();
const v = new Voucher({ ...base });
v.date = moment(m.date).format('DD-MMM-YYYY');
v.date = formatIsoDate(m.date);
const account = m.account;
v.vendor = !account || typeof account === 'string' ? undefined : account;
v.narration = m.narration ?? '';
@@ -16,13 +16,13 @@ import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import { round } from 'mathjs';
import moment from 'moment';
import { zip } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { Account } from '../core/account';
import { AccountService } from '../core/account.service';
import { Batch } from '../core/batch';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { DbFile } from '../core/db-file';
import { Inventory } from '../core/inventory';
import { Product } from '../core/product';
@@ -139,7 +139,7 @@ export class PurchaseComponent {
);
return {
isStarred: voucher.isStarred,
date: voucher.date ? moment(voucher.date, 'DD-MMM-YYYY').toDate() : new Date(),
date: parseDate(voucher.date),
account: voucher.vendor ?? (this.a() ? new Account({ id: this.a()! }) : null),
amount,
addRow: {
@@ -197,7 +197,7 @@ export class PurchaseComponent {
return typeof v === 'string' ? v : null;
});
balanceDate = computed(() => moment(this.form.date().value()).format('DD-MMM-YYYY'));
balanceDate = computed(() => formatIsoDate(this.form.date().value()));
accountId = computed((): string | null => {
const account = this.form.account().value();
return typeof account === 'string' ? null : (account?.id ?? null);
@@ -254,7 +254,7 @@ export class PurchaseComponent {
if (!d) return;
const url = new URL(window.location.href);
url.searchParams.set('d', moment(d).format('DD-MMM-YYYY'));
url.searchParams.set('d', formatIsoDate(d));
window.history.replaceState({}, '', url.toString());
});
@@ -409,7 +409,7 @@ export class PurchaseComponent {
const m = this.formModel();
const base = this.item();
const v = new Voucher({ ...base });
v.date = moment(m.date).format('DD-MMM-YYYY');
v.date = formatIsoDate(m.date);
const account = m.account;
v.vendor = !account || typeof account === 'string' ? undefined : account;
v.narration = m.narration ?? '';
@@ -9,8 +9,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { Purchases } from './purchases';
@@ -97,8 +97,8 @@ export class PurchasesComponent {
formModel = linkedSignal({
source: () => this.info(),
computation: (info: Purchases): PurchasesFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
}),
});
@@ -131,8 +131,8 @@ export class PurchasesComponent {
const formModel = this.formModel();
return {
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
body: [],
footer: new PurchasesItem(),
};
@@ -13,10 +13,10 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { Account } from '../../core/account';
import { AccountService } from '../../core/account.service';
import { formatIsoDate, parseDate } from '../../core/date-utils';
import { Product } from '../../core/product';
import { ProductService } from '../../product/product.service';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
@@ -88,9 +88,9 @@ export class RateContractDetailComponent {
formModel = linkedSignal({
source: this.item,
computation: (itemVal): RateContractDetailFormData => ({
date: itemVal.date ? moment(itemVal.date, 'DD-MMM-YYYY').toDate() : new Date(),
validFrom: itemVal.validFrom ? moment(itemVal.validFrom, 'DD-MMM-YYYY').toDate() : new Date(),
validTill: itemVal.validTill ? moment(itemVal.validTill, 'DD-MMM-YYYY').toDate() : new Date(),
date: parseDate(itemVal.date),
validFrom: parseDate(itemVal.validFrom),
validTill: parseDate(itemVal.validTill),
account: itemVal.vendor?.name ?? '',
addRow: {
product: '',
@@ -240,9 +240,9 @@ export class RateContractDetailComponent {
getItem(): RateContract {
const formModel = this.formModel();
const item = this.item();
item.date = moment(formModel.date).format('DD-MMM-YYYY');
item.validFrom = moment(formModel.validFrom).format('DD-MMM-YYYY');
item.validTill = moment(formModel.validTill).format('DD-MMM-YYYY');
item.date = formatIsoDate(formModel.date);
item.validFrom = formatIsoDate(formModel.validFrom);
item.validTill = formatIsoDate(formModel.validTill);
item.vendor = this.account ?? new Account();
item.narration = formModel.narration ?? '';
item.items = formModel.items.map(
@@ -10,8 +10,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { ToCsvService } from '../shared/to-csv.service';
@@ -107,9 +107,9 @@ export class RawMaterialCostComponent {
formModel = linkedSignal({
source: () => this.info(),
computation: (info): RawMaterialCostFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
computation: (info: RawMaterialCost): RawMaterialCostFormData => ({
startDate: parseDate(info.startDate),
finishDate: parseDate(info.finishDate),
}),
});
@@ -147,8 +147,8 @@ export class RawMaterialCostComponent {
const formModel = this.formModel();
return new RawMaterialCost({
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
});
}
@@ -17,13 +17,13 @@ import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import { round } from 'mathjs';
import moment from 'moment';
import { zip } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { Account } from '../core/account';
import { AccountService } from '../core/account.service';
import { CostCentre } from '../core/cost-centre';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { DbFile } from '../core/db-file';
import { Journal } from '../core/journal';
import { User } from '../core/user';
@@ -135,7 +135,7 @@ export class ReceiptComponent {
);
return {
isStarred: voucher.isStarred,
date: voucher.date ? moment(voucher.date, 'DD-MMM-YYYY').toDate() : new Date(),
date: parseDate(voucher.date),
receiptAccount: rj?.account?.id ?? this.a() ?? '',
receiptAmount,
addRow: {
@@ -182,7 +182,7 @@ export class ReceiptComponent {
tagsResource = this.tagSer.autocomplete(this.tagQuery);
tags = computed(() => this.tagsResource.value() ?? []);
balanceDate = computed(() => moment(this.form.date().value()).format('DD-MMM-YYYY'));
balanceDate = computed(() => formatIsoDate(this.form.date().value()));
accountId = computed((): string | null => {
const account = this.form.addRow.account().value();
return typeof account === 'string' ? null : (account?.id ?? null);
@@ -209,7 +209,7 @@ export class ReceiptComponent {
this.router.navigate(['payment'], {
queryParams: {
a: this.formModel().receiptAccount,
d: moment(this.formModel().date).format('DD-MMM-YYYY'),
d: formatIsoDate(this.formModel().date),
},
queryParamsHandling: 'merge',
});
@@ -223,7 +223,7 @@ export class ReceiptComponent {
event.preventDefault();
this.router.navigate(['journal'], {
queryParams: {
d: moment(this.formModel().date).format('DD-MMM-YYYY'),
d: formatIsoDate(this.formModel().date),
},
queryParamsHandling: 'merge',
});
@@ -253,7 +253,7 @@ export class ReceiptComponent {
if (!d) return;
const url = new URL(window.location.href);
url.searchParams.set('d', moment(d).format('DD-MMM-YYYY'));
url.searchParams.set('d', formatIsoDate(d));
window.history.replaceState({}, '', url.toString());
});
@@ -326,7 +326,7 @@ export class ReceiptComponent {
account: row.account,
costCentre: row.costCentre,
}),
date: moment(this.formModel().date).format('DD-MMM-YYYY'),
date: formatIsoDate(this.formModel().date),
},
});
@@ -365,7 +365,7 @@ export class ReceiptComponent {
const m = this.formModel();
const base = this.item();
const v = new Voucher({ ...base });
v.date = moment(m.date).format('DD-MMM-YYYY');
v.date = formatIsoDate(m.date);
v.narration = m.narration ?? '';
const existingRj = base.journals.find((x) => x.debit === 1);
@@ -23,8 +23,8 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../../core/date-utils';
import { Product } from '../../core/product';
import { ProductSku } from '../../core/product-sku';
import { ProductService } from '../../product/product.service';
@@ -63,7 +63,7 @@ export interface IngredientFormData {
export interface RecipeDetailFormData {
basics: {
date: string;
date: Date;
source: string;
recipeYield: string;
product: ProductSku | string | null;
@@ -136,7 +136,7 @@ export class RecipeDetailComponent {
source: this.item,
computation: (itemVal): RecipeDetailFormData => ({
basics: {
date: itemVal.date,
date: parseDate(itemVal.date),
source: itemVal.source ?? '',
recipeYield: `${itemVal.recipeYield ?? ''}`,
product: itemVal.sku ?? null,
@@ -488,7 +488,7 @@ export class RecipeDetailComponent {
const base = this.item();
const item = new Recipe({ ...base });
item.date = moment(basics.date).format('DD-MMM-YYYY');
item.date = formatIsoDate(basics.date);
item.source = basics.source ?? '';
item.recipeYield = this.math.parseAmount(basics.recipeYield ?? '1', 2);
item.garnishing = notes.garnishing ?? '';
@@ -1,13 +1,4 @@
import {
Component,
computed,
effect,
inject,
input,
linkedSignal,
untracked,
afterNextRender,
} from '@angular/core';
import { Component, computed, effect, inject, input, linkedSignal, untracked, afterNextRender } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -143,11 +134,7 @@ export class RoleDetailComponent {
const newPermission = isImplied ? true : currentDirect;
const newDisabled = isImplied;
if (
p.direct !== currentDirect ||
p.permission !== newPermission ||
p.disabled !== newDisabled
) {
if (p.direct !== currentDirect || p.permission !== newPermission || p.disabled !== newDisabled) {
changed = true;
return {
...p,
+306
View File
@@ -0,0 +1,306 @@
.right {
display: flex;
justify-content: flex-end;
}
.center {
display: flex;
justify-content: center;
}
.font-semibold {
font-weight: 600;
}
.text-accent {
color: var(--mat-sys-primary, #0284c7);
}
.header-row {
display: flex;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
gap: 12px;
}
.title-with-back {
display: flex;
align-items: center;
gap: 8px;
}
.back-button {
margin-right: 4px;
}
.title-group {
display: flex;
align-items: center;
gap: 10px;
}
.title-group h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.readonly-badge {
display: inline-flex;
align-items: center;
font-size: 0.75rem;
font-weight: 500;
padding: 3px 8px;
border-radius: 6px;
background: var(--mat-sys-surface-variant, #f1f5f9);
color: var(--mat-sys-on-surface-variant, #64748b);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.day-total-badge {
display: inline-flex;
align-items: baseline;
gap: 8px;
padding: 6px 14px;
border-radius: 8px;
background: var(--mat-sys-secondary-container, #e0f2fe);
color: var(--mat-sys-on-secondary-container, #0369a1);
border: 1px solid rgba(2, 132, 199, 0.2);
}
.day-total-label {
font-size: 0.85rem;
font-weight: 500;
}
.day-total-value {
font-size: 1.15rem;
font-weight: 700;
}
.day-total-sub {
font-size: 0.75rem;
opacity: 0.8;
}
.date-bar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.date-field {
max-width: 220px;
}
/* Tabs */
.cost-centre-tabs {
margin-top: 8px;
}
.tab-label-container {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
}
.tab-icon {
font-size: 18px;
width: 18px;
height: 18px;
vertical-align: middle;
}
.tab-cc-name {
font-weight: 500;
}
.tab-cc-badge {
font-size: 0.75rem;
font-weight: 600;
padding: 2px 7px;
border-radius: 12px;
background: rgba(2, 132, 199, 0.1);
color: var(--mat-sys-primary, #0284c7);
}
/* Voucher Details */
.voucher-detail-section {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0;
}
.voucher-info-card {
display: flex;
flex-wrap: wrap;
gap: 24px;
padding: 14px 18px;
border-radius: 10px;
background: var(--mat-sys-surface-container-low, #f8fafc);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
}
.info-item {
display: flex;
flex-direction: column;
gap: 3px;
}
.info-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--mat-sys-on-surface-variant, #64748b);
font-weight: 500;
}
.info-value {
font-size: 1rem;
}
.status-pill {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
width: fit-content;
}
.status-pill.posted {
background: #dcfce7;
color: #166534;
}
/* Tables */
.full-width {
width: 100%;
}
.items-table,
.journals-table,
.summary-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
}
.product-name {
font-weight: 500;
}
/* Narration */
.narration-box {
display: flex;
gap: 8px;
padding: 12px 16px;
border-radius: 8px;
background: var(--mat-sys-surface-container-lowest, #ffffff);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
font-size: 0.9rem;
}
.narration-label {
font-weight: 600;
color: var(--mat-sys-on-surface-variant, #64748b);
white-space: nowrap;
}
.narration-text {
color: var(--mat-sys-on-surface, #1e293b);
}
/* Journals Section */
.journals-section {
margin-top: 8px;
}
.section-title {
margin: 0 0 8px 0;
font-size: 1rem;
font-weight: 600;
color: var(--mat-sys-on-surface, #1e293b);
}
/* Metadata Footer */
.metadata-footer {
display: flex;
justify-content: flex-end;
padding: 12px 0 6px;
border-top: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
margin-top: 8px;
}
.metadata-text {
font-size: 0.85rem;
color: var(--mat-sys-on-surface-variant, #64748b);
}
/* Day Summary Tab */
.summary-header-card {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 16px 20px;
border-radius: 10px;
background: var(--mat-sys-surface-container-low, #f8fafc);
border: 1px solid var(--mat-sys-outline-variant, #e2e8f0);
margin-bottom: 8px;
}
.summary-metric {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 140px;
}
.metric-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--mat-sys-on-surface-variant, #64748b);
font-weight: 500;
}
.metric-value {
font-size: 1.25rem;
}
/* Empty State */
.empty-state-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 16px;
text-align: center;
color: var(--mat-sys-on-surface-variant, #64748b);
}
.empty-icon {
font-size: 48px;
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.empty-state-container h3 {
margin: 0 0 6px 0;
font-size: 1.2rem;
font-weight: 600;
color: var(--mat-sys-on-surface, #1e293b);
}
.empty-state-container p {
margin: 0;
font-size: 0.95rem;
}
+299
View File
@@ -0,0 +1,299 @@
<header class="row-container space-between header-row">
<div class="title-with-back">
<button mat-icon-button (click)="goBack()" aria-label="Go back" class="back-button">
<mat-icon>arrow_back</mat-icon>
</button>
<div class="title-group">
<h2>Sale Voucher</h2>
<span class="readonly-badge">Read-only</span>
</div>
</div>
@if (dayTotalAmount() > 0) {
<div class="day-total-badge" aria-label="Day total sales summary">
<span class="day-total-label">Day Total:</span>
<span class="day-total-value">{{ dayTotalAmount() | currency: 'INR' }}</span>
<span class="day-total-sub">
({{ costCentres().length }} cost centre{{ costCentres().length === 1 ? '' : 's' }})
</span>
</div>
}
</header>
<div class="row-container date-bar">
<mat-form-field class="date-field">
<mat-label>Business Date</mat-label>
<input
matInput
[matDatepicker]="datePicker"
[value]="dayData()?.date"
(dateChange)="onDateChange($event)"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="datePicker"></mat-datepicker-toggle>
<mat-datepicker #datePicker></mat-datepicker>
</mat-form-field>
</div>
@if (dayResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
} @else if (dayResource.error(); as err) {
<app-error-state [message]="'Failed to load sale vouchers.'" (retryAction)="retry()" />
} @else if (costCentres().length === 0) {
<div class="empty-state-container" role="status">
<mat-icon class="empty-icon">receipt_long</mat-icon>
<h3>No Sale Vouchers</h3>
<p>There are no sale vouchers recorded for the day.</p>
</div>
} @else {
<mat-tab-group
[selectedIndex]="activeCostCentreIndex()"
(selectedIndexChange)="activeCostCentreIndex.set($event)"
class="cost-centre-tabs"
animationDuration="150ms"
>
@for (tab of costCentres(); track trackByTabId($index, tab)) {
<mat-tab>
<ng-template mat-tab-label>
<div class="tab-label-container">
<span class="tab-cc-name">{{ tab.costCentreName }}</span>
<span class="tab-cc-badge">{{ tab.amount | currency: 'INR' }}</span>
</div>
</ng-template>
<section class="voucher-detail-section" [attr.aria-label]="'Sale voucher for ' + tab.costCentreName">
<!-- Voucher Info Banner -->
<div class="voucher-info-card">
<div class="info-item">
<span class="info-label">Cost Centre</span>
<span class="info-value font-semibold">{{ tab.costCentreName }}</span>
</div>
<div class="info-item">
<span class="info-label">Date</span>
<span class="info-value">{{ tab.voucher.date }}</span>
</div>
<div class="info-item">
<span class="info-label">Amount</span>
<span class="info-value text-accent font-semibold">{{ tab.amount | currency: 'INR' }}</span>
</div>
<div class="info-item">
<span class="info-label">Status</span>
<span class="status-pill posted">Posted</span>
</div>
</div>
<!-- Items Table (mirrors PurchaseComponent table) -->
<mat-table
[dataSource]="sortedInventories()"
matSort
(matSortChange)="sortData($event)"
class="full-width items-table"
aria-label="Sale line items"
>
<!-- Product Column -->
<ng-container matColumnDef="product">
<mat-header-cell *matHeaderCellDef mat-sort-header>Product</mat-header-cell>
<mat-cell *matCellDef="let row">
<span class="product-name">{{ row.batch?.sku?.name || row.batch?.name }}</span>
</mat-cell>
<mat-footer-cell *matFooterCellDef class="font-semibold">Total</mat-footer-cell>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Quantity</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">
{{ row.quantity | number: '1.2-2' }}
</mat-cell>
<mat-footer-cell *matFooterCellDef class="right font-semibold">
{{ activeVoucherQuantity() | number: '1.2-2' }}
</mat-footer-cell>
</ng-container>
<!-- Rate Column -->
<ng-container matColumnDef="rate">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Rate</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">
{{ row.rate | currency: 'INR' }}
</mat-cell>
<mat-footer-cell *matFooterCellDef class="right"></mat-footer-cell>
</ng-container>
<!-- Tax Column -->
<ng-container matColumnDef="tax">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Tax</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">
{{ row.tax | percent: '1.0-2' }}
</mat-cell>
<mat-footer-cell *matFooterCellDef class="right"></mat-footer-cell>
</ng-container>
<!-- Discount Column -->
<ng-container matColumnDef="discount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Discount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">
{{ row.discount | percent: '1.0-2' }}
</mat-cell>
<mat-footer-cell *matFooterCellDef class="right"></mat-footer-cell>
</ng-container>
<!-- Amount Column -->
<ng-container matColumnDef="amount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right font-semibold">
{{ row.amount | currency: 'INR' }}
</mat-cell>
<mat-footer-cell *matFooterCellDef class="right font-semibold text-accent">
{{ tab.amount | currency: 'INR' }}
</mat-footer-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="inventoryColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: inventoryColumns"></mat-row>
<mat-footer-row *matFooterRowDef="inventoryColumns"></mat-footer-row>
</mat-table>
<!-- Narration -->
@if (tab.voucher.narration) {
<div class="narration-box">
<span class="narration-label">Narration:</span>
<span class="narration-text">{{ tab.voucher.narration }}</span>
</div>
}
<!-- Accounting / Journals Section -->
<div class="journals-section">
<h4 class="section-title">Accounting Entries</h4>
<mat-table
[dataSource]="tab.voucher.journals"
class="full-width journals-table"
aria-label="Accounting journals"
>
<ng-container matColumnDef="account">
<mat-header-cell *matHeaderCellDef>Account</mat-header-cell>
<mat-cell *matCellDef="let j">{{ j.account?.name }}</mat-cell>
<mat-footer-cell *matFooterCellDef class="font-semibold">Total</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="costCentre">
<mat-header-cell *matHeaderCellDef>Cost Centre</mat-header-cell>
<mat-cell *matCellDef="let j">{{ j.costCentre?.name }}</mat-cell>
<mat-footer-cell *matFooterCellDef></mat-footer-cell>
</ng-container>
<ng-container matColumnDef="debit">
<mat-header-cell *matHeaderCellDef class="right">Debit</mat-header-cell>
<mat-cell *matCellDef="let j" class="right">
{{ j.debit === 1 ? (j.amount | currency: 'INR') : '' }}
</mat-cell>
<mat-footer-cell *matFooterCellDef class="right font-semibold">
{{ tab.amount | currency: 'INR' }}
</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="credit">
<mat-header-cell *matHeaderCellDef class="right">Credit</mat-header-cell>
<mat-cell *matCellDef="let j" class="right">
{{ j.debit === -1 ? (j.amount | currency: 'INR') : '' }}
</mat-cell>
<mat-footer-cell *matFooterCellDef class="right font-semibold">
{{ tab.amount | currency: 'INR' }}
</mat-footer-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="journalColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: journalColumns"></mat-row>
<mat-footer-row *matFooterRowDef="journalColumns"></mat-footer-row>
</mat-table>
</div>
<!-- Metadata Footer (matches PurchaseComponent) -->
<footer class="metadata-footer">
<span class="metadata-text">
Created on <strong>{{ tab.voucher.creationDate | localTime }}</strong> and Last Edited on
<strong>{{ tab.voucher.lastEditDate | localTime }}</strong>
@if (tab.voucher.user?.name) {
by <strong>{{ tab.voucher.user.name }}</strong
>.
}
{{ tab.voucher.posted && tab.voucher.poster ? 'Posted by ' + tab.voucher.poster : '' }}
</span>
</footer>
</section>
</mat-tab>
}
<!-- All Cost Centres Overview Tab -->
<mat-tab>
<ng-template mat-tab-label>
<div class="tab-label-container">
<mat-icon class="tab-icon">analytics</mat-icon>
<span class="tab-cc-name">Day Summary</span>
</div>
</ng-template>
<section class="voucher-detail-section" aria-label="Day summary for all cost centres">
<div class="summary-header-card">
<div class="summary-metric">
<span class="metric-label">Business Date</span>
<span class="metric-value">{{ dayData()?.date }}</span>
</div>
<div class="summary-metric">
<span class="metric-label">Total Cost Centres</span>
<span class="metric-value">{{ costCentres().length }}</span>
</div>
<div class="summary-metric">
<span class="metric-label">Total Items Sold</span>
<span class="metric-value">{{ dayTotalItems() | number: '1.0-0' }}</span>
</div>
<div class="summary-metric">
<span class="metric-label">Grand Total Sales</span>
<span class="metric-value text-accent font-semibold">{{ dayTotalAmount() | currency: 'INR' }}</span>
</div>
</div>
<mat-table [dataSource]="costCentres()" class="full-width summary-table" aria-label="Cost centres summary">
<!-- Cost Centre Column -->
<ng-container matColumnDef="costCentre">
<mat-header-cell *matHeaderCellDef>Cost Centre</mat-header-cell>
<mat-cell *matCellDef="let row" class="font-semibold">{{ row.costCentreName }}</mat-cell>
<mat-footer-cell *matFooterCellDef class="font-semibold">Grand Total</mat-footer-cell>
</ng-container>
<!-- Items Count Column -->
<ng-container matColumnDef="itemCount">
<mat-header-cell *matHeaderCellDef class="right">Line Items</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.itemCount | number: '1.0-0' }}</mat-cell>
<mat-footer-cell *matFooterCellDef class="right font-semibold">
{{ dayTotalItems() | number: '1.0-0' }}
</mat-footer-cell>
</ng-container>
<!-- Amount Column -->
<ng-container matColumnDef="amount">
<mat-header-cell *matHeaderCellDef class="right">Total Sale</mat-header-cell>
<mat-cell *matCellDef="let row" class="right font-semibold">
{{ row.amount | currency: 'INR' }}
</mat-cell>
<mat-footer-cell *matFooterCellDef class="right font-semibold text-accent">
{{ dayTotalAmount() | currency: 'INR' }}
</mat-footer-cell>
</ng-container>
<!-- Action Column -->
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef class="center">Action</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" class="center">
<button mat-button color="primary" (click)="selectCostCentreTab(i)">View Voucher</button>
</mat-cell>
<mat-footer-cell *matFooterCellDef class="center"></mat-footer-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="summaryColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: summaryColumns"></mat-row>
<mat-footer-row *matFooterRowDef="summaryColumns"></mat-footer-row>
</mat-table>
</section>
</mat-tab>
</mat-tab-group>
}
+196
View File
@@ -0,0 +1,196 @@
import { CurrencyPipe, DecimalPipe, Location, PercentPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, inject, input, linkedSignal, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerInputEvent, MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { formatIsoDate } from '../core/date-utils';
import { Inventory } from '../core/inventory';
import { Journal } from '../core/journal';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { LocalTimePipe } from '../shared/local-time.pipe';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { CostCentreTab } from './sale';
import { SaleService } from './sale.service';
@Component({
selector: 'app-sale',
templateUrl: './sale.component.html',
styleUrls: ['./sale.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
DecimalPipe,
PercentPipe,
CurrencyPipe,
LocalTimePipe,
MatButtonModule,
MatIconModule,
MatTableModule,
MatSortModule,
MatTabsModule,
MatFormFieldModule,
MatInputModule,
MatDatepickerModule,
SkeletonLoaderComponent,
ErrorStateComponent,
],
})
export class SaleComponent {
private saleSer = inject(SaleService);
private location = inject(Location);
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
dateParam = signal<string | null>(null);
dayResource = this.saleSer.getDay(this.id, this.dateParam);
dayData = computed(() => this.dayResource.value() ?? null);
vouchers = computed(() => this.dayData()?.vouchers ?? []);
costCentres = computed<CostCentreTab[]>(() => {
return this.vouchers().map((v) => {
const amount = v.inventories?.reduce((acc, inv) => acc + (inv.amount || 0), 0) ?? 0;
let ccName = v.source?.name || v.journals?.find((j) => j.debit === -1)?.costCentre?.name || '';
if (!ccName && v.narration) {
const match = v.narration.match(/\(([^)]+)\)/);
if (match) {
ccName = match[1];
}
}
if (!ccName) {
ccName = 'Cost Centre';
}
return {
id: v.id ?? '',
costCentreId: v.source?.id ?? '',
costCentreName: ccName,
amount,
itemCount: v.inventories?.length ?? 0,
voucher: v,
};
});
});
activeCostCentreIndex = linkedSignal<CostCentreTab[], number>({
source: this.costCentres,
computation: (tabs) => {
const selectedId = this.dayData()?.selectedVoucherId ?? this.id();
if (selectedId) {
const idx = tabs.findIndex((t) => t.id === selectedId);
if (idx !== -1) return idx;
}
return 0;
},
});
activeTab = computed<CostCentreTab | null>(() => {
const tabs = this.costCentres();
const idx = this.activeCostCentreIndex();
return idx >= 0 && idx < tabs.length ? tabs[idx] : null;
});
activeVoucher = computed(() => this.activeTab()?.voucher ?? null);
dayTotalAmount = computed(() => {
return this.costCentres().reduce((acc, c) => acc + c.amount, 0);
});
dayTotalItems = computed(() => {
return this.costCentres().reduce((acc, c) => acc + c.itemCount, 0);
});
activeVoucherQuantity = computed(() => {
const v = this.activeVoucher();
return v?.inventories?.reduce((acc, inv) => acc + (inv.quantity || 0), 0) ?? 0;
});
// Sorting
sortActive = signal('product');
sortDirection = signal<'asc' | 'desc' | ''>('asc');
sortedInventories = computed<Inventory[]>(() => {
const items = [...(this.activeVoucher()?.inventories ?? [])];
const active = this.sortActive();
const direction = this.sortDirection();
if (!active || !direction) return items;
return items.sort((a, b) => {
let valA: string | number;
let valB: string | number;
switch (active) {
case 'product':
valA = (a.batch?.sku?.name ?? '').toLowerCase();
valB = (b.batch?.sku?.name ?? '').toLowerCase();
break;
case 'quantity':
valA = a.quantity ?? 0;
valB = b.quantity ?? 0;
break;
case 'rate':
valA = a.rate ?? 0;
valB = b.rate ?? 0;
break;
case 'tax':
valA = a.tax ?? 0;
valB = b.tax ?? 0;
break;
case 'discount':
valA = a.discount ?? 0;
valB = b.discount ?? 0;
break;
case 'amount':
valA = a.amount ?? 0;
valB = b.amount ?? 0;
break;
default:
return 0;
}
const cmp = valA < valB ? -1 : valA > valB ? 1 : 0;
return direction === 'asc' ? cmp : -cmp;
});
});
inventoryColumns = ['product', 'quantity', 'rate', 'tax', 'discount', 'amount'];
journalColumns = ['account', 'costCentre', 'debit', 'credit'];
summaryColumns = ['costCentre', 'itemCount', 'amount', 'action'];
sortData(sort: Sort): void {
this.sortActive.set(sort.active);
this.sortDirection.set(sort.direction);
}
onDateChange(event: MatDatepickerInputEvent<Date>): void {
if (event.value) {
this.dateParam.set(formatIsoDate(event.value));
}
}
selectCostCentreTab(index: number): void {
this.activeCostCentreIndex.set(index);
}
goBack(): void {
this.location.back();
}
retry(): void {
this.dayResource.reload();
}
trackByTabId(_index: number, item: CostCentreTab): string {
return item.id;
}
trackByInventoryId(index: number, item: Inventory): string {
return item.id ?? `inv-${index}`;
}
trackByJournalId(index: number, item: Journal): string {
return item.id ?? `j-${index}`;
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service';
import { SaleComponent } from './sale.component';
export const routes: Routes = [
{
path: '',
component: SaleComponent,
canActivate: [authGuard],
},
{
path: ':id',
component: SaleComponent,
canActivate: [authGuard],
},
];
+22
View File
@@ -0,0 +1,22 @@
import { httpResource } from '@angular/common/http';
import { Injectable, ResourceRef, Signal } from '@angular/core';
import { SaleDayResponse } from './sale';
const url = '/api/sale';
@Injectable({
providedIn: 'root',
})
export class SaleService {
getDay(id: Signal<string | null>, date: Signal<string | null>): ResourceRef<SaleDayResponse | undefined> {
return httpResource<SaleDayResponse>(() => {
const i = id();
if (i) {
return `${url}/${i}`;
}
const d = date();
return d ? `${url}?d=${d}` : `${url}`;
});
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Voucher } from '../core/voucher';
export interface SaleDayResponse {
date: string;
selectedVoucherId: string | null;
totalAmount: number;
vouchers: Voucher[];
}
export interface CostCentreTab {
id: string;
costCentreId: string;
costCentreName: string;
amount: number;
itemCount: number;
voucher: Voucher;
}
@@ -68,6 +68,29 @@
background-color: rgba(0, 0, 0, 0.04);
}
tr.selected td {
border-bottom-color: transparent;
}
tr.detail-row {
height: 0;
}
tr.detail-row.collapsed {
display: none;
}
.detail-cell {
padding: 12px 16px 16px !important;
background-color: rgba(0, 0, 0, 0.02);
}
.vouchers-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.visually-hidden {
position: absolute;
width: 1px;
@@ -62,7 +62,7 @@
</div>
</section>
<table mat-table [dataSource]="days()" class="full-width">
<table mat-table [dataSource]="days()" multiTemplateDataRows class="full-width">
<ng-container matColumnDef="expand">
<th mat-header-cell *matHeaderCellDef scope="col"><span class="visually-hidden">Expand</span></th>
<td mat-cell *matCellDef="let day">
@@ -95,59 +95,69 @@
<th mat-header-cell *matHeaderCellDef scope="col">Amount</th>
<td mat-cell *matCellDef="let day" class="number">{{ day.amount | number: '1.2-2' }}</td>
</ng-container>
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let day" [attr.colspan]="displayedColumns.length" class="detail-cell">
@if (expanded() === day) {
<section class="vouchers-section" aria-label="Vouchers for selected day">
@for (voucher of day.vouchers; track trackVoucher($index, voucher)) {
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>{{ voucher.saleCategoryName }} → {{ voucher.costCentre.name }}</mat-panel-title>
<mat-panel-description>
<span [class]="actionClass(voucher.action)">{{ voucher.action }}</span
>&nbsp;
<span class="number">{{ voucher.amount | number: '1.2-2' }}</span>
</mat-panel-description>
</mat-expansion-panel-header>
<p>{{ voucher.narration }}</p>
<table mat-table [dataSource]="voucher.lines" class="full-width">
<ng-container matColumnDef="productName">
<th mat-header-cell *matHeaderCellDef scope="col">Product</th>
<td mat-cell *matCellDef="let line">{{ line.name }} ({{ line.units }})</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef scope="col">Quantity</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.quantity }}</td>
</ng-container>
<ng-container matColumnDef="rate">
<th mat-header-cell *matHeaderCellDef scope="col">Rate</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.rate | number: '1.2-2' }}</td>
</ng-container>
<ng-container matColumnDef="taxRate">
<th mat-header-cell *matHeaderCellDef scope="col">Tax</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.taxRate }}</td>
</ng-container>
<ng-container matColumnDef="discount">
<th mat-header-cell *matHeaderCellDef scope="col">Discount</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.discount }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef scope="col">Amount</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.amount | number: '1.2-2' }}</td>
</ng-container>
<ng-container matColumnDef="happyHour">
<th mat-header-cell *matHeaderCellDef scope="col">Happy Hour</th>
<td mat-cell *matCellDef="let line">{{ line.isHappyHour ? 'Yes' : '' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="lineColumns"></tr>
<tr mat-row *matRowDef="let row; columns: lineColumns"></tr>
</table>
</mat-expansion-panel>
}
</section>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [class.selected]="expanded() === row"></tr>
<tr
mat-row
*matRowDef="let row; columns: detailColumns"
class="detail-row"
[class.collapsed]="expanded() !== row"
></tr>
</table>
@if (expanded(); as day) {
<section aria-label="Vouchers for selected day">
@for (voucher of day.vouchers; track trackVoucher($index, voucher)) {
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>{{ voucher.saleCategoryName }} → {{ voucher.costCentre.name }}</mat-panel-title>
<mat-panel-description>
<span [class]="actionClass(voucher.action)">{{ voucher.action }}</span>
<span class="number">{{ voucher.amount | number: '1.2-2' }}</span>
</mat-panel-description>
</mat-expansion-panel-header>
<p>{{ voucher.narration }}</p>
<table mat-table [dataSource]="voucher.lines" class="full-width">
<ng-container matColumnDef="productName">
<th mat-header-cell *matHeaderCellDef scope="col">Product</th>
<td mat-cell *matCellDef="let line">{{ line.name }} ({{ line.units }})</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef scope="col">Quantity</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.quantity }}</td>
</ng-container>
<ng-container matColumnDef="rate">
<th mat-header-cell *matHeaderCellDef scope="col">Rate</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.rate | number: '1.2-2' }}</td>
</ng-container>
<ng-container matColumnDef="taxRate">
<th mat-header-cell *matHeaderCellDef scope="col">Tax</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.taxRate }}</td>
</ng-container>
<ng-container matColumnDef="discount">
<th mat-header-cell *matHeaderCellDef scope="col">Discount</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.discount }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef scope="col">Amount</th>
<td mat-cell *matCellDef="let line" class="number">{{ line.amount | number: '1.2-2' }}</td>
</ng-container>
<ng-container matColumnDef="happyHour">
<th mat-header-cell *matHeaderCellDef scope="col">Happy Hour</th>
<td mat-cell *matCellDef="let line">{{ line.isHappyHour ? 'Yes' : '' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="lineColumns"></tr>
<tr mat-row *matRowDef="let row; columns: lineColumns"></tr>
</table>
</mat-expansion-panel>
}
</section>
}
<div class="row-container">
<button
mat-flat-button
@@ -13,8 +13,8 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import moment from 'moment';
import { formatIsoDate, startOfMonth } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { DayPlan, ImportRequest, MappingEntry, PreviewResponse, SaleLine, VoucherPlan } from './sales-import';
@@ -55,8 +55,8 @@ export class SalesImportComponent {
private snackBar = inject(MatSnackBar);
model = signal<SalesImportFormData>({
startDate: moment().startOf('month').toDate(),
finishDate: moment().toDate(),
startDate: startOfMonth(new Date()),
finishDate: new Date(),
});
form = createForm(this.model);
@@ -86,6 +86,7 @@ export class SalesImportComponent {
expanded = signal<DayPlan | null>(null);
displayedColumns = ['expand', 'businessDate', 'action', 'voucherCount', 'amount'];
detailColumns = ['expandedDetail'];
lineColumns = ['productName', 'quantity', 'rate', 'taxRate', 'discount', 'amount', 'happyHour'];
constructor() {
@@ -102,8 +103,8 @@ export class SalesImportComponent {
show(): void {
const model = this.model();
this.previewRequest.set({
startDate: moment(model.startDate).format('YYYY-MM-DD'),
finishDate: moment(model.finishDate).format('YYYY-MM-DD'),
startDate: formatIsoDate(model.startDate),
finishDate: formatIsoDate(model.finishDate),
});
}
@@ -130,8 +131,8 @@ export class SalesImportComponent {
import(): void {
const model = this.model();
const request: ImportRequest = {
startDate: moment(model.startDate).format('YYYY-MM-DD'),
finishDate: moment(model.finishDate).format('YYYY-MM-DD'),
startDate: formatIsoDate(model.startDate),
finishDate: formatIsoDate(model.finishDate),
};
this.importing.set(true);
this.ser.execute(request).subscribe({
+12 -11
View File
@@ -12,12 +12,12 @@ import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { Router } from '@angular/router';
import moment from 'moment';
import { AccountTypeService } from '../account/account-type.service';
import { environment } from '../app.environment';
import { AuthService } from '../auth/auth.service';
import { AccountType } from '../core/account-type';
import { endOfMonth, formatDisplayDate, formatIsoDate } from '../core/date-utils';
import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component';
import { LockInfo } from './lock-info';
import { SettingsService } from './settings.service';
@@ -87,11 +87,11 @@ export class SettingsComponent {
index: 0,
lockOlder: false,
olderRolling: false,
olderDate: moment().date(moment().date(1).daysInMonth()).toDate(),
olderDate: endOfMonth(new Date()),
olderDays: 0,
lockNewer: false,
newerRolling: false,
newerDate: moment().date(moment().date(1).daysInMonth()).toDate(),
newerDate: endOfMonth(new Date()),
newerDays: 0,
accountTypes: s.accTypes.map(() => ({ accountType: false })),
voucherTypes: s.vchTypes.map(() => ({ voucherType: false })),
@@ -107,8 +107,8 @@ export class SettingsComponent {
lockNewer = computed(() => this.lockInfoModel().lockNewer);
newerRolling = computed(() => this.lockInfoModel().newerRolling);
rebaseDataModel = signal({
date: moment(),
rebaseDataModel = signal<{ date: Date }>({
date: new Date(),
});
rebaseDataForm = createForm(this.rebaseDataModel);
@@ -133,13 +133,13 @@ export class SettingsComponent {
if (this.lockOlder() && this.olderRolling()) {
item.start.days = model.olderDays ?? 0;
} else if (this.lockOlder() && !this.olderRolling()) {
item.start.date = moment(model.olderDate ?? new Date()).format('DD-MMM-YYYY');
item.start.date = formatIsoDate(model.olderDate ?? new Date());
}
if (this.lockNewer() && this.newerRolling()) {
item.finish.days = model.newerDays ?? 0;
}
if (this.lockNewer() && !this.newerRolling()) {
item.finish.date = moment(model.newerDate ?? new Date()).format('DD-MMM-YYYY');
item.finish.date = formatIsoDate(model.newerDate ?? new Date());
}
this.accountTypes().forEach((at: AccountType, index: number) => {
@@ -156,12 +156,12 @@ export class SettingsComponent {
const validFrom = model.validFrom;
if (validFrom) {
item.validFrom = moment(validFrom).format('DD-MMM-YYYY');
item.validFrom = formatIsoDate(validFrom);
}
const validTill = model.validTill;
if (validTill) {
item.validTill = moment(validTill).format('DD-MMM-YYYY');
item.validTill = formatIsoDate(validTill);
}
item.index = model.index ?? 0;
@@ -183,12 +183,13 @@ export class SettingsComponent {
}
confirmRebase(): void {
const rebaseDate = (this.rebaseDataModel().date ?? moment()).format('DD-MMM-YYYY');
const rebaseDate = formatIsoDate(this.rebaseDataModel().date ?? new Date());
const rebaseDisplay = formatDisplayDate(this.rebaseDataModel().date ?? new Date());
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '250px',
data: {
title: 'Rebase the database?',
content: `Are you sure you want to rebase the database as on ${rebaseDate}? This action is destructive and cannot be undone.`,
content: `Are you sure you want to rebase the database as on ${rebaseDisplay}? This action is destructive and cannot be undone.`,
},
});
+22 -7
View File
@@ -1,17 +1,32 @@
import { Pipe, PipeTransform } from '@angular/core';
import moment from 'moment';
import { format, isValid, parse, parseISO } from 'date-fns';
@Pipe({
name: 'localTime',
standalone: true,
})
export class LocalTimePipe implements PipeTransform {
transform(value: string): string {
if (value === undefined) {
transform(value: string | Date | null | undefined): string {
if (!value) {
return '';
}
return moment(value, 'DD-MMM-YYYY HH:mm')
.subtract(new Date().getTimezoneOffset(), 'minutes')
.format('DD-MMM-YYYY HH:mm');
let date: Date;
if (value instanceof Date) {
date = value;
} else {
const trimmed = value.trim();
if (!trimmed) {
return '';
}
if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) {
const isoString = trimmed.includes('Z') || /[+-]\d{2}:\d{2}$/.test(trimmed) ? trimmed : `${trimmed}Z`;
date = parseISO(isoString);
} else {
date = parse(trimmed, 'dd-MMM-yyyy HH:mm', new Date());
}
}
if (!isValid(date)) {
return '';
}
return format(date, 'dd-MMM-yyyy HH:mm');
}
}
@@ -9,8 +9,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import moment from 'moment';
import { formatIsoDate, parseDate } from '../core/date-utils';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { StockMovement } from './stock-movement';
@@ -96,8 +96,8 @@ export class StockMovementComponent {
formModel = linkedSignal({
source: this.info,
computation: (v): StockMovementFormData => ({
startDate: v.startDate ? moment(v.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: v.finishDate ? moment(v.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
startDate: parseDate(v.startDate),
finishDate: parseDate(v.finishDate),
}),
});
@@ -130,8 +130,8 @@ export class StockMovementComponent {
const formModel = this.formModel();
return {
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
startDate: formatIsoDate(formModel.startDate),
finishDate: formatIsoDate(formModel.finishDate),
body: [],
};
}
@@ -44,8 +44,8 @@
<mat-form-field class="flex-auto">
<mat-label>Product Group</mat-label>
<mat-select [formField]="form.productForm.productGroup">
@for (pg of productGroups(); track pg) {
<mat-option [value]="pg.id">
@for (pg of productGroups(); track pg.id) {
<mat-option [value]="pg">
{{ pg.name }}
</mat-option>
}
@@ -61,7 +61,7 @@
</div>
<!-- nutritional info shows based on currently selected group in the form -->
@if (selectedProductGroup()?.nutritional ?? false) {
@if (form.productForm.productGroup().value().nutritional ?? false) {
<h3>Nutritional Information</h3>
<div class="nutrition-grid">
<mat-form-field class="flex-auto">
@@ -103,7 +103,7 @@
</div>
}
@if (selectedProductGroup()?.iceCream ?? false) {
@if (form.productForm.productGroup().value().iceCream ?? false) {
<h3>Ice Cream Information</h3>
<div class="row-container">
<mat-form-field class="flex-auto">
@@ -173,7 +173,7 @@
<ng-container matColumnDef="productGroup">
<mat-header-cell *matHeaderCellDef>Product Group</mat-header-cell>
<mat-cell *matCellDef="let p">{{ getPGName(p.productGroup) }}</mat-cell>
<mat-cell *matCellDef="let p">{{ p.productGroup.name }}</mat-cell>
</ng-container>
<ng-container matColumnDef="validFrom">
@@ -13,8 +13,8 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { Router } from '@angular/router';
import moment from 'moment';
import { formatDisplayDate, formatIsoDate, parseDate } from '../../core/date-utils';
import { ProductGroup } from '../../core/product-group';
import { ProductGroupService } from '../../product-group/product-group.service';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
@@ -32,7 +32,7 @@ export interface TemporalProductFormData {
fractionUnits: string;
isPurchased: boolean;
isSold: boolean;
productGroup: string;
productGroup: ProductGroup;
allergen: string;
protein: number;
carbohydrate: number;
@@ -78,7 +78,7 @@ function createEmptyProductForm(): TemporalProductFormData {
fractionUnits: '',
isPurchased: true,
isSold: false,
productGroup: '',
productGroup: new ProductGroup(),
allergen: '',
protein: 0,
carbohydrate: 0,
@@ -170,7 +170,7 @@ export class TemporalProductDetailComponent {
fractionUnits: p.fractionUnits || '',
isPurchased: p.isPurchased ?? true,
isSold: p.isSold ?? false,
productGroup: p.productGroup?.id ?? '',
productGroup: p.productGroup ?? new ProductGroup(),
allergen: p.allergen || '',
protein: p.protein || 0,
carbohydrate: p.carbohydrate || 0,
@@ -185,8 +185,8 @@ export class TemporalProductDetailComponent {
otherSolids: p.otherSolids || 0,
totalSolids: p.totalSolids || 0,
water: p.water || 0,
validFrom: p.validFrom ? moment(p.validFrom, 'DD-MMM-YYYY').toDate() : null,
validTill: p.validTill ? moment(p.validTill, 'DD-MMM-YYYY').toDate() : null,
validFrom: parseDate(p.validFrom, null),
validTill: parseDate(p.validTill, null),
}));
const skus: TemporalProductSkuFormData[] = (itemVal.skus || []).map((s) => ({
@@ -197,8 +197,8 @@ export class TemporalProductDetailComponent {
productYield: s.productYield ?? 1,
costPrice: s.costPrice ?? 0,
salePrice: s.salePrice ?? 0,
validFrom: s.validFrom ? moment(s.validFrom, 'DD-MMM-YYYY').toDate() : null,
validTill: s.validTill ? moment(s.validTill, 'DD-MMM-YYYY').toDate() : null,
validFrom: parseDate(s.validFrom, null),
validTill: parseDate(s.validTill, null),
}));
return {
@@ -215,25 +215,14 @@ export class TemporalProductDetailComponent {
productGroupsResource = this.pgSer.list();
productGroups = computed(() => this.productGroupsResource.value() || []);
selectedProductGroup = computed(() => {
const pgId = this.form.productForm.productGroup().value();
return this.productGroups().find((x: ProductGroup) => x.id === pgId);
});
constructor() {
afterNextRender(() => {
this.form.productForm.name().focusBoundControl();
});
}
getPGName(pgId?: string): string {
if (!pgId) return '';
return this.productGroups().find((x: ProductGroup) => x.id === pgId)?.name ?? '';
}
formatDateOrNull(d: Date | string | null | undefined): string {
if (!d) return '';
return moment(d).format('DD-MMM-YYYY');
return formatDisplayDate(parseDate(d, null));
}
editProduct(row: TemporalProductFormData) {
@@ -338,7 +327,7 @@ export class TemporalProductDetailComponent {
prod.fractionUnits = p.fractionUnits ?? '';
prod.isPurchased = p.isPurchased ?? true;
prod.isSold = p.isSold ?? false;
prod.productGroup = this.productGroups().find((x: ProductGroup) => x.id === p.productGroup);
prod.productGroup = p.productGroup;
prod.allergen = p.allergen ?? '';
prod.protein = p.protein || 0;
@@ -356,8 +345,8 @@ export class TemporalProductDetailComponent {
prod.totalSolids = p.totalSolids || 0;
prod.water = p.water || 0;
prod.validFrom = p.validFrom ? this.formatDateOrNull(p.validFrom) : null;
prod.validTill = p.validTill ? this.formatDateOrNull(p.validTill) : null;
prod.validFrom = p.validFrom ? formatIsoDate(p.validFrom) : null;
prod.validTill = p.validTill ? formatIsoDate(p.validTill) : null;
return prod;
});
@@ -370,8 +359,8 @@ export class TemporalProductDetailComponent {
sku.productYield = Number(s.productYield) || 1;
sku.costPrice = Number(s.costPrice) || 0;
sku.salePrice = Number(s.salePrice) || 0;
sku.validFrom = s.validFrom ? this.formatDateOrNull(s.validFrom) : null;
sku.validTill = s.validTill ? this.formatDateOrNull(s.validTill) : null;
sku.validFrom = s.validFrom ? formatIsoDate(s.validFrom) : null;
sku.validTill = s.validTill ? formatIsoDate(s.validTill) : null;
return sku;
});
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more