mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-27 00:44:45 +00:00
chore: release v13 (#33453)
* fix: typerror on multi warehouse in Packed Items DN(with bundled item with varying warehouses)-> Sales Invoice. (cherry picked from commite684eb32d0) * test: type error on bundled products with different warehouses (cherry picked from commit5918bb03f7) * fix: No permission to read doctype (cherry picked from commitc0da948a4e) * fix: `shipping_address` in PO (cherry picked from commit7e1b6b3c2a) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.json * chore: conflicts * chore: linter * refactor: Customer and Supplier Ledger summary will have hidden fields for better handling of user permission (#33432) * feat: provision to setup opening balances for earnings and deductions while creating SSA * fix: use get_all instead of get_value as get_value api dont supports between condition * fix: patch * fix: provision to set tax_deducted_till_date after document is subnmmited * fix: `shipping_address` for non-drop shipping item (cherry picked from commit67a7ccf3ce) * chore: linter (#33455) fix: linter Co-authored-by: ruthra kumar <ruthra@erpnext.com> Co-authored-by: Nabin Hait <nabinhait@gmail.com> Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Saurabh <saurabh6790@gmail.com>
This commit is contained in:
@@ -22,8 +22,7 @@ def get_columns():
|
|||||||
{
|
{
|
||||||
"label": _("Payment Document Type"),
|
"label": _("Payment Document Type"),
|
||||||
"fieldname": "payment_document_type",
|
"fieldname": "payment_document_type",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Data",
|
||||||
"options": "Doctype",
|
|
||||||
"width": 130,
|
"width": 130,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -33,15 +32,15 @@ def get_columns():
|
|||||||
"options": "payment_document_type",
|
"options": "payment_document_type",
|
||||||
"width": 140,
|
"width": 140,
|
||||||
},
|
},
|
||||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||||
{"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120},
|
{"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120},
|
||||||
{"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100},
|
{"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 120},
|
||||||
{
|
{
|
||||||
"label": _("Against Account"),
|
"label": _("Against Account"),
|
||||||
"fieldname": "against",
|
"fieldname": "against",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"width": 170,
|
"width": 200,
|
||||||
},
|
},
|
||||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class PartyLedgerSummaryReport(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.get_gl_entries()
|
self.get_gl_entries()
|
||||||
|
self.get_additional_columns()
|
||||||
self.get_return_invoices()
|
self.get_return_invoices()
|
||||||
self.get_party_adjustment_amounts()
|
self.get_party_adjustment_amounts()
|
||||||
|
|
||||||
@@ -34,6 +35,42 @@ class PartyLedgerSummaryReport(object):
|
|||||||
data = self.get_data()
|
data = self.get_data()
|
||||||
return columns, data
|
return columns, data
|
||||||
|
|
||||||
|
def get_additional_columns(self):
|
||||||
|
"""
|
||||||
|
Additional Columns for 'User Permission' based access control
|
||||||
|
"""
|
||||||
|
from frappe import qb
|
||||||
|
|
||||||
|
if self.filters.party_type == "Customer":
|
||||||
|
self.territories = frappe._dict({})
|
||||||
|
self.customer_group = frappe._dict({})
|
||||||
|
|
||||||
|
customer = qb.DocType("Customer")
|
||||||
|
result = (
|
||||||
|
frappe.qb.from_(customer)
|
||||||
|
.select(
|
||||||
|
customer.name, customer.territory, customer.customer_group, customer.default_sales_partner
|
||||||
|
)
|
||||||
|
.where((customer.disabled == 0))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
for x in result:
|
||||||
|
self.territories[x.name] = x.territory
|
||||||
|
self.customer_group[x.name] = x.customer_group
|
||||||
|
else:
|
||||||
|
self.supplier_group = frappe._dict({})
|
||||||
|
supplier = qb.DocType("Supplier")
|
||||||
|
result = (
|
||||||
|
frappe.qb.from_(supplier)
|
||||||
|
.select(supplier.name, supplier.supplier_group)
|
||||||
|
.where((supplier.disabled == 0))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
for x in result:
|
||||||
|
self.supplier_group[x.name] = x.supplier_group
|
||||||
|
|
||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
@@ -117,6 +154,35 @@ class PartyLedgerSummaryReport(object):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Hidden columns for handling 'User Permissions'
|
||||||
|
if self.filters.party_type == "Customer":
|
||||||
|
columns += [
|
||||||
|
{
|
||||||
|
"label": _("Territory"),
|
||||||
|
"fieldname": "territory",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Territory",
|
||||||
|
"hidden": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Customer Group"),
|
||||||
|
"fieldname": "customer_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Customer Group",
|
||||||
|
"hidden": 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
columns += [
|
||||||
|
{
|
||||||
|
"label": _("Supplier Group"),
|
||||||
|
"fieldname": "supplier_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Supplier Group",
|
||||||
|
"hidden": 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
@@ -144,6 +210,12 @@ class PartyLedgerSummaryReport(object):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.filters.party_type == "Customer":
|
||||||
|
self.party_data[gle.party].update({"territory": self.territories.get(gle.party)})
|
||||||
|
self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)})
|
||||||
|
else:
|
||||||
|
self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)})
|
||||||
|
|
||||||
amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
|
amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
|
||||||
self.party_data[gle.party].closing_balance += amount
|
self.party_data[gle.party].closing_balance += amount
|
||||||
|
|
||||||
|
|||||||
@@ -551,6 +551,7 @@ class GrossProfitGenerator(object):
|
|||||||
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
||||||
else:
|
else:
|
||||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
def get_buying_amount(self, row, item_code):
|
def get_buying_amount(self, row, item_code):
|
||||||
# IMP NOTE
|
# IMP NOTE
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from frappe.utils import add_days, flt, nowdate
|
|||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
||||||
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_item()
|
self.create_item()
|
||||||
|
self.create_bundle()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
self.create_sales_invoice()
|
self.create_sales_invoice()
|
||||||
self.clear_old_entries()
|
self.clear_old_entries()
|
||||||
@@ -42,6 +45,7 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
self.company = company.name
|
self.company = company.name
|
||||||
self.cost_center = company.cost_center
|
self.cost_center = company.cost_center
|
||||||
self.warehouse = "Stores - " + abbr
|
self.warehouse = "Stores - " + abbr
|
||||||
|
self.finished_warehouse = "Finished Goods - " + abbr
|
||||||
self.income_account = "Sales - " + abbr
|
self.income_account = "Sales - " + abbr
|
||||||
self.expense_account = "Cost of Goods Sold - " + abbr
|
self.expense_account = "Cost of Goods Sold - " + abbr
|
||||||
self.debit_to = "Debtors - " + abbr
|
self.debit_to = "Debtors - " + abbr
|
||||||
@@ -53,6 +57,23 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
self.item = item if isinstance(item, str) else item.item_code
|
self.item = item if isinstance(item, str) else item.item_code
|
||||||
|
|
||||||
|
def create_bundle(self):
|
||||||
|
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||||
|
|
||||||
|
item2 = create_item(
|
||||||
|
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
||||||
|
)
|
||||||
|
self.item2 = item2 if isinstance(item2, str) else item2.item_code
|
||||||
|
|
||||||
|
# This will be parent item
|
||||||
|
bundle = create_item(
|
||||||
|
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||||
|
)
|
||||||
|
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
|
||||||
|
|
||||||
|
# Create Product Bundle
|
||||||
|
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
|
||||||
|
|
||||||
def create_customer(self):
|
def create_customer(self):
|
||||||
name = "_Test GP Customer"
|
name = "_Test GP Customer"
|
||||||
if frappe.db.exists("Customer", name):
|
if frappe.db.exists("Customer", name):
|
||||||
@@ -93,6 +114,28 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
return sinv
|
return sinv
|
||||||
|
|
||||||
|
def create_delivery_note(
|
||||||
|
self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Helper function to populate default values in Delivery Note
|
||||||
|
"""
|
||||||
|
dnote = create_delivery_note(
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
currency="INR",
|
||||||
|
item=item or self.item,
|
||||||
|
qty=qty,
|
||||||
|
rate=rate,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
return_against=None,
|
||||||
|
expense_account=self.expense_account,
|
||||||
|
do_not_save=do_not_save,
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
|
)
|
||||||
|
return dnote
|
||||||
|
|
||||||
def clear_old_entries(self):
|
def clear_old_entries(self):
|
||||||
doctype_list = [
|
doctype_list = [
|
||||||
"Sales Invoice",
|
"Sales Invoice",
|
||||||
@@ -206,3 +249,55 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||||
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
|
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
|
||||||
|
|
||||||
|
def test_bundled_delivery_note_with_different_warehouses(self):
|
||||||
|
"""
|
||||||
|
Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses
|
||||||
|
"""
|
||||||
|
se = make_stock_entry(
|
||||||
|
company=self.company,
|
||||||
|
item_code=self.item,
|
||||||
|
target=self.warehouse,
|
||||||
|
qty=1,
|
||||||
|
basic_rate=100,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
item = se.items[0]
|
||||||
|
se.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": self.item2,
|
||||||
|
"s_warehouse": "",
|
||||||
|
"t_warehouse": self.finished_warehouse,
|
||||||
|
"qty": 1,
|
||||||
|
"basic_rate": 100,
|
||||||
|
"conversion_factor": item.conversion_factor or 1.0,
|
||||||
|
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
||||||
|
"serial_no": item.serial_no,
|
||||||
|
"batch_no": item.batch_no,
|
||||||
|
"cost_center": item.cost_center,
|
||||||
|
"expense_account": item.expense_account,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
se = se.save().submit()
|
||||||
|
|
||||||
|
# Make a Delivery note with Product bundle
|
||||||
|
# Packed Items will have different warehouses
|
||||||
|
dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True)
|
||||||
|
dnote.packed_items[1].warehouse = self.finished_warehouse
|
||||||
|
dnote = dnote.submit()
|
||||||
|
|
||||||
|
# make Sales Invoice for above delivery note
|
||||||
|
sinv = make_sales_invoice(dnote.name)
|
||||||
|
sinv = sinv.save().submit()
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
company=self.company,
|
||||||
|
from_date=nowdate(),
|
||||||
|
to_date=nowdate(),
|
||||||
|
group_by="Invoice",
|
||||||
|
sales_invoice=sinv.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
columns, data = execute(filters=filters)
|
||||||
|
self.assertGreater(len(data), 0)
|
||||||
|
|||||||
@@ -63,24 +63,6 @@ frappe.query_reports["Supplier Ledger Summary"] = {
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Payment Terms Template"
|
"options": "Payment Terms Template"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname":"territory",
|
|
||||||
"label": __("Territory"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Territory"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"sales_partner",
|
|
||||||
"label": __("Sales Partner"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Sales Partner"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"sales_person",
|
|
||||||
"label": __("Sales Person"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Sales Person"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname":"tax_id",
|
"fieldname":"tax_id",
|
||||||
"label": __("Tax Id"),
|
"label": __("Tax Id"),
|
||||||
|
|||||||
@@ -370,7 +370,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "shipping_address",
|
"fieldname": "shipping_address",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Company Shipping Address",
|
"label": "Shipping Address",
|
||||||
"options": "Address",
|
"options": "Address",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@@ -1170,7 +1170,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-17 12:34:36.033363",
|
"modified": "2022-12-25 18:08:59.074182",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order",
|
"name": "Purchase Order",
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assign
|
|||||||
|
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
|
frappe.reload_doc("Payroll", "doctype", "Payroll Settings")
|
||||||
frappe.reload_doc("Payroll", "doctype", "Salary Structure")
|
frappe.reload_doc("Payroll", "doctype", "Salary Structure")
|
||||||
frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment")
|
frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment")
|
||||||
|
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
delete from `tabSalary Structure Assignment`
|
delete from `tabSalary Structure Assignment`
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"max_working_hours_against_timesheet",
|
"max_working_hours_against_timesheet",
|
||||||
"include_holidays_in_total_working_days",
|
"include_holidays_in_total_working_days",
|
||||||
"disable_rounded_total",
|
"disable_rounded_total",
|
||||||
|
"define_opening_balance_for_earning_and_deductions",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"daily_wages_fraction_for_half_day",
|
"daily_wages_fraction_for_half_day",
|
||||||
"email_salary_slip_to_employee",
|
"email_salary_slip_to_employee",
|
||||||
@@ -91,13 +92,20 @@
|
|||||||
"fieldname": "show_leave_balances_in_salary_slip",
|
"fieldname": "show_leave_balances_in_salary_slip",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Leave Balances in Salary Slip"
|
"label": "Show Leave Balances in Salary Slip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If checked, then the system will enable the provision to set the opening balance for earnings and deductions till date while creating a Salary Structure Assignment (if any)",
|
||||||
|
"fieldname": "define_opening_balance_for_earning_and_deductions",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Define Opening Balance for Earning and Deductions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-03 17:49:59.579723",
|
"modified": "2022-12-21 17:30:08.704247",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Payroll Settings",
|
"name": "Payroll Settings",
|
||||||
|
|||||||
@@ -1063,7 +1063,25 @@ class SalarySlip(TransactionBase):
|
|||||||
)
|
)
|
||||||
exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0
|
exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0
|
||||||
|
|
||||||
return taxable_earnings - exempted_amount
|
opening_taxable_earning = self.get_opening_balance_for(
|
||||||
|
"taxable_earnings_till_date", start_date, end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
return (taxable_earnings + opening_taxable_earning) - exempted_amount
|
||||||
|
|
||||||
|
def get_opening_balance_for(self, field_to_select, start_date, end_date):
|
||||||
|
opening_balance = frappe.db.get_all(
|
||||||
|
"Salary Structure Assignment",
|
||||||
|
{
|
||||||
|
"employee": self.employee,
|
||||||
|
"salary_structure": self.salary_structure,
|
||||||
|
"from_date": ["between", (start_date, end_date)],
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
field_to_select,
|
||||||
|
)
|
||||||
|
|
||||||
|
return opening_balance[0].get(field_to_select) if opening_balance else 0.0
|
||||||
|
|
||||||
def get_tax_paid_in_period(self, start_date, end_date, tax_component):
|
def get_tax_paid_in_period(self, start_date, end_date, tax_component):
|
||||||
# find total_tax_paid, tax paid for benefit, additional_salary
|
# find total_tax_paid, tax paid for benefit, additional_salary
|
||||||
@@ -1092,7 +1110,11 @@ class SalarySlip(TransactionBase):
|
|||||||
)[0][0]
|
)[0][0]
|
||||||
)
|
)
|
||||||
|
|
||||||
return total_tax_paid
|
tax_deducted_till_date = self.get_opening_balance_for(
|
||||||
|
"tax_deducted_till_date", start_date, end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
return total_tax_paid + tax_deducted_till_date
|
||||||
|
|
||||||
def get_taxable_earnings(
|
def get_taxable_earnings(
|
||||||
self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None
|
self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None
|
||||||
|
|||||||
@@ -1030,6 +1030,104 @@ class TestSalarySlip(FrappeTestCase):
|
|||||||
activity_type.wage_rate = 25
|
activity_type.wage_rate = 25
|
||||||
activity_type.save()
|
activity_type.save()
|
||||||
|
|
||||||
|
def test_salary_slip_generation_against_opening_entries_in_ssa(self):
|
||||||
|
import math
|
||||||
|
|
||||||
|
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
payroll_period = frappe.db.get_value(
|
||||||
|
"Payroll Period",
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"start_date": ["<=", "2023-03-31"],
|
||||||
|
"end_date": [">=", "2022-04-01"],
|
||||||
|
},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payroll_period:
|
||||||
|
payroll_period = create_payroll_period(
|
||||||
|
name="_Test Payroll Period for Tax",
|
||||||
|
company="_Test Company",
|
||||||
|
start_date="2022-04-01",
|
||||||
|
end_date="2023-03-31",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
payroll_period = frappe.get_cached_doc("Payroll Period", payroll_period)
|
||||||
|
|
||||||
|
emp = make_employee(
|
||||||
|
"test_employee_ss_with_opening_balance@salary.com",
|
||||||
|
company="_Test Company",
|
||||||
|
**{"date_of_joining": "2021-12-01"},
|
||||||
|
)
|
||||||
|
employee_doc = frappe.get_doc("Employee", emp)
|
||||||
|
|
||||||
|
create_tax_slab(payroll_period, allow_tax_exemption=True)
|
||||||
|
|
||||||
|
salary_structure_name = "Test Salary Structure for Opening Balance"
|
||||||
|
if not frappe.db.exists("Salary Structure", salary_structure_name):
|
||||||
|
salary_structure_doc = make_salary_structure(
|
||||||
|
salary_structure_name,
|
||||||
|
"Monthly",
|
||||||
|
company="_Test Company",
|
||||||
|
employee=emp,
|
||||||
|
from_date="2022-04-01",
|
||||||
|
payroll_period=payroll_period,
|
||||||
|
test_tax=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# validate no salary slip exists for the employee
|
||||||
|
self.assertTrue(
|
||||||
|
frappe.db.count(
|
||||||
|
"Salary Slip",
|
||||||
|
{
|
||||||
|
"employee": emp,
|
||||||
|
"salary_structure": salary_structure_doc.name,
|
||||||
|
"docstatus": 1,
|
||||||
|
"start_date": [">=", "2022-04-01"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
|
||||||
|
remaining_sub_periods = get_period_factor(
|
||||||
|
emp,
|
||||||
|
get_first_day("2022-10-01"),
|
||||||
|
get_last_day("2022-10-01"),
|
||||||
|
"Monthly",
|
||||||
|
payroll_period,
|
||||||
|
depends_on_payment_days=0,
|
||||||
|
)[1]
|
||||||
|
|
||||||
|
prev_period = math.ceil(remaining_sub_periods)
|
||||||
|
|
||||||
|
annual_tax = 93036 # 89220 #data[0].get('applicable_tax')
|
||||||
|
monthly_tax_amount = 7732.40 # 7435 #annual_tax/12
|
||||||
|
annual_earnings = 933600 # data[0].get('ctc')
|
||||||
|
monthly_earnings = 77800 # annual_earnings/12
|
||||||
|
|
||||||
|
# Get Salary Structure Assignment
|
||||||
|
ssa = frappe.get_value(
|
||||||
|
"Salary Structure Assignment",
|
||||||
|
{"employee": emp, "salary_structure": salary_structure_doc.name},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa)
|
||||||
|
|
||||||
|
# Set opening balance for earning and tax deduction in Salary Structure Assignment
|
||||||
|
ssa_doc.taxable_earnings_till_date = monthly_earnings * prev_period
|
||||||
|
ssa_doc.tax_deducted_till_date = monthly_tax_amount * prev_period
|
||||||
|
ssa_doc.save()
|
||||||
|
|
||||||
|
# Create Salary Slip
|
||||||
|
salary_slip = make_salary_slip(
|
||||||
|
salary_structure_doc.name, employee=employee_doc.name, posting_date=getdate("2022-10-01")
|
||||||
|
)
|
||||||
|
for deduction in salary_slip.deductions:
|
||||||
|
if deduction.salary_component == "TDS":
|
||||||
|
self.assertEqual(deduction.amount, rounded(monthly_tax_amount))
|
||||||
|
|
||||||
|
|
||||||
def get_no_of_days():
|
def get_no_of_days():
|
||||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ frappe.ui.form.on('Salary Structure Assignment', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refresh: function(frm) {
|
||||||
|
if(frm.doc.__onload){
|
||||||
|
frm.unhide_earnings_and_taxation_section = frm.doc.__onload.earning_and_deduction_entries_does_not_exists;
|
||||||
|
frm.trigger("set_earnings_and_taxation_section_visibility");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
employee: function(frm) {
|
employee: function(frm) {
|
||||||
if(frm.doc.employee){
|
if(frm.doc.employee){
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -59,6 +66,8 @@ frappe.ui.form.on('Salary Structure Assignment', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.trigger("valiadte_joining_date_and_salary_slips");
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
frm.set_value("company", null);
|
frm.set_value("company", null);
|
||||||
@@ -71,5 +80,33 @@ frappe.ui.form.on('Salary Structure Assignment', {
|
|||||||
frm.set_value("payroll_payable_account", r.default_payroll_payable_account);
|
frm.set_value("payroll_payable_account", r.default_payroll_payable_account);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
valiadte_joining_date_and_salary_slips: function(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: "earning_and_deduction_entries_does_not_exists",
|
||||||
|
doc: frm.doc,
|
||||||
|
callback: function(data) {
|
||||||
|
let earning_and_deduction_entries_does_not_exists = data.message;
|
||||||
|
frm.unhide_earnings_and_taxation_section = earning_and_deduction_entries_does_not_exists;
|
||||||
|
frm.trigger("set_earnings_and_taxation_section_visibility");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
set_earnings_and_taxation_section_visibility: function(frm) {
|
||||||
|
if(frm.unhide_earnings_and_taxation_section){
|
||||||
|
frm.set_df_property('earnings_and_taxation_section', 'hidden', 0);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
frm.set_df_property('earnings_and_taxation_section', 'hidden', 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
from_date: function(frm) {
|
||||||
|
if (frm.doc.from_date) {
|
||||||
|
frm.trigger("valiadte_joining_date_and_salary_slips" );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
"base",
|
"base",
|
||||||
"column_break_9",
|
"column_break_9",
|
||||||
"variable",
|
"variable",
|
||||||
|
"earnings_and_taxation_section",
|
||||||
|
"taxable_earnings_till_date",
|
||||||
|
"column_break_18",
|
||||||
|
"tax_deducted_till_date",
|
||||||
"amended_from"
|
"amended_from"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -141,11 +145,31 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Payroll Payable Account",
|
"label": "Payroll Payable Account",
|
||||||
"options": "Account"
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "earnings_and_taxation_section",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "tax_deducted_till_date",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Tax Deducted Till Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_18",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "taxable_earnings_till_date",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Taxable Earnings Till Date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-31 22:44:46.267974",
|
"modified": "2022-12-26 12:47:42.521891",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Salary Structure Assignment",
|
"name": "Salary Structure Assignment",
|
||||||
|
|||||||
@@ -13,10 +13,32 @@ class DuplicateAssignment(frappe.ValidationError):
|
|||||||
|
|
||||||
|
|
||||||
class SalaryStructureAssignment(Document):
|
class SalaryStructureAssignment(Document):
|
||||||
|
def onload(self):
|
||||||
|
if self.employee:
|
||||||
|
self.set_onload(
|
||||||
|
"earning_and_deduction_entries_exists", self.earning_and_deduction_entries_does_not_exists()
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_dates()
|
self.validate_dates()
|
||||||
self.validate_income_tax_slab()
|
self.validate_income_tax_slab()
|
||||||
self.set_payroll_payable_account()
|
self.set_payroll_payable_account()
|
||||||
|
self.valiadte_missing_taxable_earnings_and_deductions_till_date()
|
||||||
|
|
||||||
|
def valiadte_missing_taxable_earnings_and_deductions_till_date(self):
|
||||||
|
if self.earning_and_deduction_entries_does_not_exists():
|
||||||
|
if not self.taxable_earnings_till_date and not self.tax_deducted_till_date:
|
||||||
|
frappe.msgprint(
|
||||||
|
_(
|
||||||
|
"""Not found any salary slip record(s) for the employee {0}.<br><br>Please specify {1} and {2} (if any), for the correct tax calculation in future salary slips."""
|
||||||
|
).format(
|
||||||
|
self.employee,
|
||||||
|
"<b>" + _("Taxable Earnings Till Date") + "</b>",
|
||||||
|
"<b>" + _("Tax Deducted Till Date") + "</b>",
|
||||||
|
),
|
||||||
|
indicator="orange",
|
||||||
|
title=_("Warning"),
|
||||||
|
)
|
||||||
|
|
||||||
def validate_dates(self):
|
def validate_dates(self):
|
||||||
joining_date, relieving_date = frappe.db.get_value(
|
joining_date, relieving_date = frappe.db.get_value(
|
||||||
@@ -76,6 +98,56 @@ class SalaryStructureAssignment(Document):
|
|||||||
)
|
)
|
||||||
self.payroll_payable_account = payroll_payable_account
|
self.payroll_payable_account = payroll_payable_account
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def earning_and_deduction_entries_does_not_exists(self):
|
||||||
|
if self.enabled_settings_to_specify_earnings_and_deductions_till_date():
|
||||||
|
if not self.joined_in_the_same_month() and not self.have_salary_slips():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if self.docstatus in [1, 2] and (
|
||||||
|
self.taxable_earnings_till_date or self.tax_deducted_till_date
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def enabled_settings_to_specify_earnings_and_deductions_till_date(self):
|
||||||
|
"""returns True if settings are enabled to specify earnings and deductions till date else False"""
|
||||||
|
|
||||||
|
if frappe.db.get_single_value(
|
||||||
|
"Payroll Settings", "define_opening_balance_for_earning_and_deductions"
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def have_salary_slips(self):
|
||||||
|
"""returns True if salary structure assignment has salary slips else False"""
|
||||||
|
|
||||||
|
salary_slip = frappe.db.get_value(
|
||||||
|
"Salary Slip", filters={"employee": self.employee, "docstatus": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
if salary_slip:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def joined_in_the_same_month(self):
|
||||||
|
"""returns True if employee joined in same month as salary structure assignment from date else False"""
|
||||||
|
|
||||||
|
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
||||||
|
from_date = getdate(self.from_date)
|
||||||
|
|
||||||
|
if not self.from_date or not date_of_joining:
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif date_of_joining.month == from_date.month:
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_assigned_salary_structure(employee, on_date):
|
def get_assigned_salary_structure(employee, on_date):
|
||||||
if not employee or not on_date:
|
if not employee or not on_date:
|
||||||
|
|||||||
@@ -1090,6 +1090,15 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
|||||||
]
|
]
|
||||||
items_to_map = list(set(items_to_map))
|
items_to_map = list(set(items_to_map))
|
||||||
|
|
||||||
|
def is_drop_ship_order(target):
|
||||||
|
drop_ship = True
|
||||||
|
for item in target.items:
|
||||||
|
if not item.delivered_by_supplier:
|
||||||
|
drop_ship = False
|
||||||
|
break
|
||||||
|
|
||||||
|
return drop_ship
|
||||||
|
|
||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
target.supplier = ""
|
target.supplier = ""
|
||||||
target.apply_discount_on = ""
|
target.apply_discount_on = ""
|
||||||
@@ -1097,8 +1106,14 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
|||||||
target.discount_amount = 0.0
|
target.discount_amount = 0.0
|
||||||
target.inter_company_order_reference = ""
|
target.inter_company_order_reference = ""
|
||||||
target.shipping_rule = ""
|
target.shipping_rule = ""
|
||||||
target.customer = ""
|
|
||||||
target.customer_name = ""
|
if is_drop_ship_order(target):
|
||||||
|
target.customer = source.customer
|
||||||
|
target.customer_name = source.customer_name
|
||||||
|
target.shipping_address = source.shipping_address_name
|
||||||
|
else:
|
||||||
|
target.customer = target.customer_name = target.shipping_address = None
|
||||||
|
|
||||||
target.run_method("set_missing_values")
|
target.run_method("set_missing_values")
|
||||||
target.run_method("calculate_taxes_and_totals")
|
target.run_method("calculate_taxes_and_totals")
|
||||||
|
|
||||||
|
|||||||
@@ -139,10 +139,11 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-02-08 17:01:52.162202",
|
"modified": "2022-12-24 11:15:17.142746",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Customer Group",
|
"name": "Customer Group",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"nsm_parent_field": "parent_customer_group",
|
"nsm_parent_field": "parent_customer_group",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -198,10 +199,19 @@
|
|||||||
"role": "Customer",
|
"role": "Customer",
|
||||||
"select": 1,
|
"select": 1,
|
||||||
"share": 1
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"search_fields": "parent_customer_group",
|
"search_fields": "parent_customer_group",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"creation": "2013-01-10 16:34:24",
|
"creation": "2013-01-10 16:34:24",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"supplier_group_name",
|
"supplier_group_name",
|
||||||
"parent_supplier_group",
|
"parent_supplier_group",
|
||||||
@@ -106,10 +107,11 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-03-18 18:10:49.228407",
|
"modified": "2022-12-24 11:16:12.486719",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Supplier Group",
|
"name": "Supplier Group",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"nsm_parent_field": "parent_supplier_group",
|
"nsm_parent_field": "parent_supplier_group",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -156,8 +158,18 @@
|
|||||||
"permlevel": 1,
|
"permlevel": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Purchase User"
|
"role": "Purchase User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_order": "ASC"
|
"sort_field": "modified",
|
||||||
|
"sort_order": "ASC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -123,11 +123,12 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-02-08 17:10:03.767426",
|
"modified": "2022-12-24 11:16:39.964956",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Territory",
|
"name": "Territory",
|
||||||
"name_case": "Title Case",
|
"name_case": "Title Case",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"nsm_parent_field": "parent_territory",
|
"nsm_parent_field": "parent_territory",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -175,10 +176,19 @@
|
|||||||
"role": "Customer",
|
"role": "Customer",
|
||||||
"select": 1,
|
"select": 1,
|
||||||
"share": 1
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"search_fields": "parent_territory,territory_manager",
|
"search_fields": "parent_territory,territory_manager",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,6 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from six import iteritems, itervalues, string_types
|
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
cint,
|
cint,
|
||||||
@@ -23,6 +22,7 @@ from frappe.utils import (
|
|||||||
nowdate,
|
nowdate,
|
||||||
today,
|
today,
|
||||||
)
|
)
|
||||||
|
from six import iteritems, itervalues, string_types
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.general_ledger import process_gl_map
|
from erpnext.accounts.general_ledger import process_gl_map
|
||||||
@@ -2625,4 +2625,3 @@ def get_incorrect_stock_entries() -> Dict:
|
|||||||
stock_entries.setdefault(row.name, row)
|
stock_entries.setdefault(row.name, row)
|
||||||
|
|
||||||
return stock_entries
|
return stock_entries
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user