mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-13 02:08:47 +00:00
Compare commits
56 Commits
develop
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66534dc97e | ||
|
|
42ebb7446a | ||
|
|
49760e4542 | ||
|
|
6e1f4d84b6 | ||
|
|
ab482caac9 | ||
|
|
d8506fb2c0 | ||
|
|
d3dfed909e | ||
|
|
ac31c5ca19 | ||
|
|
ccab91b9ed | ||
|
|
541a8b135a | ||
|
|
c0a30a5302 | ||
|
|
accce1fe59 | ||
|
|
f04221417e | ||
|
|
c4c2d35565 | ||
|
|
f1e41f4a4f | ||
|
|
d9326d80de | ||
|
|
5c93bf5798 | ||
|
|
f62ad83d6f | ||
|
|
876e2d4e6e | ||
|
|
4977e06c50 | ||
|
|
e64ae9a8a9 | ||
|
|
6ddf4eee15 | ||
|
|
d27a09cb9f | ||
|
|
86d5939d91 | ||
|
|
768c131073 | ||
|
|
8f77223057 | ||
|
|
8ba470160d | ||
|
|
1a6264d831 | ||
|
|
19a90c0980 | ||
|
|
d57fc49896 | ||
|
|
fe0431a6d0 | ||
|
|
bfd6375508 | ||
|
|
6dade11d8f | ||
|
|
ce421bb1d4 | ||
|
|
84a749e3d0 | ||
|
|
65a1c7086b | ||
|
|
a04da71182 | ||
|
|
cbfc13728b | ||
|
|
9b88275312 | ||
|
|
332673f260 | ||
|
|
e49add20b7 | ||
|
|
c3b0633eda | ||
|
|
a660ed061b | ||
|
|
eb7cebac91 | ||
|
|
3420e21d45 | ||
|
|
211832104c | ||
|
|
4b85d51257 | ||
|
|
0363b01ab7 | ||
|
|
fc517f7fa2 | ||
|
|
18451b69e6 | ||
|
|
50ce61ae02 | ||
|
|
c3fdb191b9 | ||
|
|
93db2ebd6f | ||
|
|
2925f9a04e | ||
|
|
c7bf103c0c | ||
|
|
6dead8fd85 |
2
.github/workflows/initiate_release.yml
vendored
2
.github/workflows/initiate_release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ["14", "15"]
|
||||
version: ["14", "15", "16"]
|
||||
|
||||
steps:
|
||||
- uses: octokit/request-action@v2.x
|
||||
|
||||
5
.github/workflows/patch.yml
vendored
5
.github/workflows/patch.yml
vendored
@@ -113,8 +113,8 @@ jobs:
|
||||
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
|
||||
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
|
||||
|
||||
wget https://erpnext.com/files/v13-erpnext.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
|
||||
wget https://frappe.io/files/erpnext-v14.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
|
||||
|
||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
@@ -142,7 +142,6 @@ jobs:
|
||||
bench --site test_site migrate
|
||||
}
|
||||
|
||||
update_to_version 14 3.11
|
||||
update_to_version 15 3.13
|
||||
|
||||
echo "Updating to latest version"
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
- version-16
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.0.0-dev"
|
||||
__version__ = "16.0.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -184,6 +184,9 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def before_cancel(self):
|
||||
self.has_asset_adjustment_entry()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_cancel")
|
||||
@@ -554,12 +557,27 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
frappe.db.sql(
|
||||
""" update `tabAsset Value Adjustment`
|
||||
set journal_entry = null where journal_entry = %s""",
|
||||
self.name,
|
||||
def has_asset_adjustment_entry(self):
|
||||
if self.flags.get("via_asset_value_adjustment"):
|
||||
return
|
||||
|
||||
asset_value_adjustment = frappe.db.get_value(
|
||||
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
|
||||
)
|
||||
if asset_value_adjustment:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
|
||||
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
|
||||
)
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||
(
|
||||
frappe.qb.update(AssetValueAdjustment)
|
||||
.set(AssetValueAdjustment.journal_entry, None)
|
||||
.where(AssetValueAdjustment.journal_entry == self.name)
|
||||
).run()
|
||||
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
|
||||
@@ -778,8 +778,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
|
||||
"depends_on": "eval:!doc.is_return",
|
||||
"collapsible_depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
|
||||
"fieldname": "time_sheet_list",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
@@ -793,7 +792,6 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Time Sheets",
|
||||
"no_copy": 1,
|
||||
"options": "Sales Invoice Timesheet",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -2092,7 +2090,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)",
|
||||
"depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
|
||||
"fieldname": "section_break_104",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
@@ -2306,7 +2304,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-10-09 14:48:59.472826",
|
||||
"modified": "2025-12-24 18:29:50.242618",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -352,10 +352,22 @@ class SalesInvoice(SellingController):
|
||||
self.is_opening = "No"
|
||||
|
||||
self.set_against_income_account()
|
||||
self.validate_time_sheets_are_submitted()
|
||||
|
||||
if self.is_return and not self.return_against and self.timesheets:
|
||||
frappe.throw(_("Direct return is not allowed for Timesheet."))
|
||||
|
||||
if not self.is_return:
|
||||
self.validate_time_sheets_are_submitted()
|
||||
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
|
||||
if self.is_return:
|
||||
self.timesheets = []
|
||||
|
||||
if self.is_return and self.return_against:
|
||||
for row in self.timesheets:
|
||||
if row.billing_hours:
|
||||
row.billing_hours = -abs(row.billing_hours)
|
||||
if row.billing_amount:
|
||||
row.billing_amount = -abs(row.billing_amount)
|
||||
|
||||
self.update_packing_list()
|
||||
self.set_billing_hours_and_amount()
|
||||
self.update_timesheet_billing_for_project()
|
||||
@@ -484,7 +496,7 @@ class SalesInvoice(SellingController):
|
||||
if cint(self.is_pos) != 1 and not self.is_return:
|
||||
self.update_against_document_in_jv()
|
||||
|
||||
self.update_time_sheet(self.name)
|
||||
self.update_time_sheet(None if (self.is_return and self.return_against) else self.name)
|
||||
|
||||
if frappe.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
|
||||
update_company_current_month_sales(self.company)
|
||||
@@ -564,7 +576,7 @@ class SalesInvoice(SellingController):
|
||||
self.check_if_consolidated_invoice()
|
||||
|
||||
super().before_cancel()
|
||||
self.update_time_sheet(None)
|
||||
self.update_time_sheet(self.return_against if (self.is_return and self.return_against) else None)
|
||||
|
||||
def on_cancel(self):
|
||||
check_if_return_invoice_linked_with_payment_entry(self)
|
||||
@@ -804,8 +816,20 @@ class SalesInvoice(SellingController):
|
||||
for data in timesheet.time_logs:
|
||||
if (
|
||||
(self.project and args.timesheet_detail == data.name)
|
||||
or (not self.project and not data.sales_invoice)
|
||||
or (not sales_invoice and data.sales_invoice == self.name)
|
||||
or (not self.project and not data.sales_invoice and args.timesheet_detail == data.name)
|
||||
or (
|
||||
not sales_invoice
|
||||
and data.sales_invoice == self.name
|
||||
and args.timesheet_detail == data.name
|
||||
)
|
||||
or (
|
||||
self.is_return
|
||||
and self.return_against
|
||||
and data.sales_invoice
|
||||
and data.sales_invoice == self.return_against
|
||||
and not sales_invoice
|
||||
and args.timesheet_detail == data.name
|
||||
)
|
||||
):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
@@ -845,11 +869,26 @@ class SalesInvoice(SellingController):
|
||||
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
|
||||
|
||||
def validate_time_sheets_are_submitted(self):
|
||||
# Note: This validation is skipped for return invoices
|
||||
# to allow returns to reference already-billed timesheet details
|
||||
for data in self.timesheets:
|
||||
# Handle invoice duplication
|
||||
if data.time_sheet and data.timesheet_detail:
|
||||
if sales_invoice := frappe.db.get_value(
|
||||
"Timesheet Detail", data.timesheet_detail, "sales_invoice"
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Sales Invoice {1} is already created for {2}").format(
|
||||
data.idx, frappe.bold(sales_invoice), frappe.bold(data.time_sheet)
|
||||
)
|
||||
)
|
||||
|
||||
if data.time_sheet:
|
||||
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
|
||||
if status not in ["Submitted", "Payslip"]:
|
||||
frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet))
|
||||
if status not in ["Submitted", "Payslip", "Partially Billed"]:
|
||||
frappe.throw(
|
||||
_("Timesheet {0} cannot be invoiced in its current state").format(data.time_sheet)
|
||||
)
|
||||
|
||||
def set_pos_fields(self, for_validate=False):
|
||||
"""Set retail related fields from POS Profiles"""
|
||||
@@ -1283,7 +1322,12 @@ class SalesInvoice(SellingController):
|
||||
timesheet.billing_amount = ts_doc.total_billable_amount
|
||||
|
||||
def update_timesheet_billing_for_project(self):
|
||||
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
|
||||
if (
|
||||
not self.is_return
|
||||
and not self.timesheets
|
||||
and self.project
|
||||
and self.is_auto_fetch_timesheet_enabled()
|
||||
):
|
||||
self.add_timesheet_data()
|
||||
else:
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Timesheet Detail",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -117,15 +116,16 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:36.562795",
|
||||
"modified": "2025-12-23 13:54:17.677187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Timesheet",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +415,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
@@ -506,7 +505,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 200,
|
||||
"description": "Test Gross Tax",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
@@ -541,10 +539,10 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 400,
|
||||
"description": "Test Gross Tax",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
si.reload()
|
||||
si.submit()
|
||||
invoices.append(si)
|
||||
# For amount before threshold (first 8000 + VAT): TCS entry with amount zero
|
||||
@@ -594,7 +592,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "VAT added to test TDS calculation on gross amount",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
@@ -1024,7 +1021,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 1000,
|
||||
"description": "VAT added to test TDS calculation on gross amount",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
@@ -1162,7 +1158,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 8000,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -708,6 +708,10 @@ class TaxWithholdingController:
|
||||
existing_taxes = {row.account_head: row for row in self.doc.taxes if row.is_tax_withholding_account}
|
||||
precision = self.doc.precision("tax_amount", "taxes")
|
||||
conversion_rate = self.get_conversion_rate()
|
||||
add_deduct_tax = "Deduct"
|
||||
|
||||
if self.party_type == "Customer":
|
||||
add_deduct_tax = "Add"
|
||||
|
||||
for account_head, base_amount in account_amount_map.items():
|
||||
tax_amount = flt(base_amount / conversion_rate, precision)
|
||||
@@ -724,6 +728,7 @@ class TaxWithholdingController:
|
||||
tax_row = self._create_tax_row(account_head, tax_amount)
|
||||
for_update = False
|
||||
|
||||
tax_row.add_deduct_tax = add_deduct_tax
|
||||
# Set item-wise tax breakup for this tax row
|
||||
self._set_item_wise_tax_for_tds(
|
||||
tax_row, account_head, category_withholding_map, for_update=for_update
|
||||
@@ -743,7 +748,6 @@ class TaxWithholdingController:
|
||||
"account_head": account_head,
|
||||
"description": account_head,
|
||||
"cost_center": cost_center,
|
||||
"add_deduct_tax": "Deduct",
|
||||
"tax_amount": tax_amount,
|
||||
"dont_recompute_tax": 1,
|
||||
},
|
||||
@@ -807,12 +811,14 @@ class TaxWithholdingController:
|
||||
else:
|
||||
item_tax_amount = 0
|
||||
|
||||
multiplier = -1 if tax_row.add_deduct_tax == "Deduct" else 1
|
||||
|
||||
self.doc._item_wise_tax_details.append(
|
||||
frappe._dict(
|
||||
item=item,
|
||||
tax=tax_row,
|
||||
rate=category.tax_rate,
|
||||
amount=item_tax_amount * -1, # Negative because it's a deduction
|
||||
amount=item_tax_amount * multiplier,
|
||||
taxable_amount=item_base_taxable,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -877,11 +877,15 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
entry_date = row.posting_date
|
||||
|
||||
row.range0 = 0.0
|
||||
|
||||
self.get_ageing_data(entry_date, row)
|
||||
|
||||
# ageing buckets should not have amounts if due date is not reached
|
||||
if getdate(entry_date) > getdate(self.age_as_on):
|
||||
row.range0 = row.outstanding
|
||||
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
|
||||
row.total_due = 0
|
||||
return
|
||||
|
||||
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
|
||||
|
||||
@@ -1281,6 +1285,8 @@ class ReceivablePayableReport:
|
||||
ranges = [*self.ranges, _("Above")]
|
||||
|
||||
prev_range_value = 0
|
||||
self.add_column(label=_("<0"), fieldname="range0", fieldtype="Currency")
|
||||
self.ageing_column_labels.append(_("<0"))
|
||||
for idx, curr_range_value in enumerate(ranges):
|
||||
label = f"{prev_range_value}-{curr_range_value}"
|
||||
self.add_column(label=label, fieldname="range" + str(idx + 1))
|
||||
@@ -1296,7 +1302,9 @@ class ReceivablePayableReport:
|
||||
for row in self.data:
|
||||
row = frappe._dict(row)
|
||||
if not cint(row.bold):
|
||||
values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
|
||||
values = [flt(row.get("range0", 0), precision)] + [
|
||||
flt(row.get(f"range{i}", 0), precision) for i in self.range_numbers
|
||||
]
|
||||
rows.append({"values": values})
|
||||
|
||||
self.chart = {
|
||||
|
||||
@@ -74,7 +74,7 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.get_doc("Journal Entry", self.journal_entry).cancel()
|
||||
self.cancel_asset_revaluation_entry()
|
||||
self.update_asset()
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
@@ -167,6 +167,17 @@ class AssetValueAdjustment(Document):
|
||||
if dimension.get("mandatory_for_pl"):
|
||||
debit_entry.update({dimension["fieldname"]: dimension_value})
|
||||
|
||||
def cancel_asset_revaluation_entry(self):
|
||||
if not self.journal_entry:
|
||||
return
|
||||
|
||||
revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry)
|
||||
if revaluation_entry.docstatus == 1:
|
||||
# Ignore permissions to match Journal Entry submission behavior
|
||||
revaluation_entry.flags.ignore_permissions = True
|
||||
revaluation_entry.flags.via_asset_value_adjustment = True
|
||||
revaluation_entry.cancel()
|
||||
|
||||
def update_asset(self):
|
||||
asset = self.update_asset_value_after_depreciation()
|
||||
note = self.get_adjustment_note()
|
||||
|
||||
@@ -16,6 +16,14 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
return !doc.qty && me.frm.doc.has_unit_price_items ? "yellow" : "";
|
||||
});
|
||||
|
||||
this.frm.set_query("warehouse", "items", (doc, cdt, cdn) => {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
super.setup();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,21 @@
|
||||
"idx": 1,
|
||||
"label": "Accounting",
|
||||
"link_to": "Accounting",
|
||||
<<<<<<< HEAD
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-01-01 20:07:01.203651",
|
||||
=======
|
||||
"link_type": "Workspace",
|
||||
"modified": "2025-12-31 16:06:04.678577",
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"modified_by": "Administrator",
|
||||
"name": "Accounting",
|
||||
"owner": "Administrator",
|
||||
"parent_icon": "Accounts",
|
||||
<<<<<<< HEAD
|
||||
"restrict_removal": 0,
|
||||
=======
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"roles": [],
|
||||
"standard": 1
|
||||
}
|
||||
|
||||
@@ -9,13 +9,21 @@
|
||||
"idx": 0,
|
||||
"label": "Financial Reports",
|
||||
"link_to": "Financial Reports",
|
||||
<<<<<<< HEAD
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-01-01 20:07:01.253367",
|
||||
=======
|
||||
"link_type": "Workspace",
|
||||
"modified": "2025-12-31 16:07:27.060176",
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"modified_by": "Administrator",
|
||||
"name": "Financial Reports",
|
||||
"owner": "Administrator",
|
||||
"parent_icon": "Accounts",
|
||||
<<<<<<< HEAD
|
||||
"restrict_removal": 0,
|
||||
=======
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"roles": [],
|
||||
"sidebar": "",
|
||||
"standard": 1
|
||||
|
||||
@@ -8,9 +8,15 @@
|
||||
"icon_type": "Link",
|
||||
"idx": 3,
|
||||
"label": "Taxes",
|
||||
<<<<<<< HEAD
|
||||
"link_to": "Taxes",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-01-01 20:07:01.356333",
|
||||
=======
|
||||
"link_to": "Sales Taxes and Charges Template",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-12-31 16:59:14.978584",
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"modified_by": "Administrator",
|
||||
"name": "Taxes",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -8,7 +8,7 @@ app_email = "hello@frappe.io"
|
||||
app_license = "GNU General Public License (v3)"
|
||||
source_link = "https://github.com/frappe/erpnext"
|
||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||
app_home = "/app/home"
|
||||
app_home = "/desk"
|
||||
|
||||
add_to_apps_screen = [
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_to_date, now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.projects.doctype.task.test_task import create_task
|
||||
from erpnext.projects.doctype.timesheet.timesheet import OverlapError, make_sales_invoice
|
||||
@@ -272,6 +273,60 @@ class TestTimesheet(ERPNextTestSuite):
|
||||
ts.calculate_percentage_billed()
|
||||
self.assertEqual(ts.per_billed, 100)
|
||||
|
||||
def test_partial_billing_and_return(self):
|
||||
"""
|
||||
Test Timesheet status transitions during partial billing, full billing,
|
||||
sales return, and return cancellation.
|
||||
|
||||
Scenario:
|
||||
1. Create a Timesheet with two billable time logs.
|
||||
2. Create a Sales Invoice billing only one time log → Timesheet becomes Partially Billed.
|
||||
3. Create another Sales Invoice billing the remaining time log → Timesheet becomes Billed.
|
||||
4. Create a Sales Return against the second invoice → Timesheet reverts to Partially Billed.
|
||||
5. Cancel the Sales Return → Timesheet returns to Billed status.
|
||||
|
||||
This test ensures Timesheet status is recalculated correctly
|
||||
across billing and return lifecycle events.
|
||||
"""
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1, do_not_submit=True)
|
||||
timesheet_detail = timesheet.append("time_logs", {})
|
||||
timesheet_detail.is_billable = 1
|
||||
timesheet_detail.activity_type = "_Test Activity Type"
|
||||
timesheet_detail.from_time = timesheet.time_logs[0].to_time + datetime.timedelta(minutes=1)
|
||||
timesheet_detail.hours = 2
|
||||
timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(
|
||||
hours=timesheet_detail.hours
|
||||
)
|
||||
timesheet.save().submit()
|
||||
|
||||
sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
|
||||
sales_invoice.due_date = nowdate()
|
||||
sales_invoice.timesheets.pop()
|
||||
sales_invoice.submit()
|
||||
|
||||
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
|
||||
self.assertEqual(timesheet_status, "Partially Billed")
|
||||
|
||||
sales_invoice2 = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
|
||||
sales_invoice2.due_date = nowdate()
|
||||
sales_invoice2.submit()
|
||||
|
||||
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
|
||||
self.assertEqual(timesheet_status, "Billed")
|
||||
|
||||
sales_return = make_sales_return(sales_invoice2.name).submit()
|
||||
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
|
||||
self.assertEqual(timesheet_status, "Partially Billed")
|
||||
|
||||
sales_return.load_from_db()
|
||||
sales_return.cancel()
|
||||
|
||||
timesheet.load_from_db()
|
||||
self.assertEqual(timesheet.time_logs[1].sales_invoice, sales_invoice2.name)
|
||||
self.assertEqual(timesheet.status, "Billed")
|
||||
|
||||
|
||||
def make_timesheet(
|
||||
employee,
|
||||
@@ -283,6 +338,7 @@ def make_timesheet(
|
||||
company=None,
|
||||
currency=None,
|
||||
exchange_rate=None,
|
||||
do_not_submit=False,
|
||||
):
|
||||
update_activity_type(activity_type)
|
||||
timesheet = frappe.new_doc("Timesheet")
|
||||
@@ -311,7 +367,8 @@ def make_timesheet(
|
||||
else:
|
||||
timesheet.save(ignore_permissions=True)
|
||||
|
||||
timesheet.submit()
|
||||
if not do_not_submit:
|
||||
timesheet.submit()
|
||||
|
||||
return timesheet
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Draft\nSubmitted\nBilled\nPayslip\nCompleted\nCancelled",
|
||||
"options": "Draft\nSubmitted\nPartially Billed\nBilled\nPayslip\nCompleted\nCancelled",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -310,7 +310,7 @@
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:53.551907",
|
||||
"modified": "2025-12-19 13:48:23.453636",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Timesheet",
|
||||
@@ -386,8 +386,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,9 @@ class Timesheet(Document):
|
||||
per_billed: DF.Percent
|
||||
sales_invoice: DF.Link | None
|
||||
start_date: DF.Date | None
|
||||
status: DF.Literal["Draft", "Submitted", "Billed", "Payslip", "Completed", "Cancelled"]
|
||||
status: DF.Literal[
|
||||
"Draft", "Submitted", "Partially Billed", "Billed", "Payslip", "Completed", "Cancelled"
|
||||
]
|
||||
time_logs: DF.Table[TimesheetDetail]
|
||||
title: DF.Data | None
|
||||
total_billable_amount: DF.Currency
|
||||
@@ -128,6 +130,9 @@ class Timesheet(Document):
|
||||
if flt(self.per_billed, self.precision("per_billed")) >= 100.0:
|
||||
self.status = "Billed"
|
||||
|
||||
if 0.0 < flt(self.per_billed, self.precision("per_billed")) < 100.0:
|
||||
self.status = "Partially Billed"
|
||||
|
||||
if self.sales_invoice:
|
||||
self.status = "Completed"
|
||||
|
||||
@@ -433,7 +438,7 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
|
||||
target.append("items", {"item_code": item_code, "qty": hours, "rate": billing_rate})
|
||||
|
||||
for time_log in timesheet.time_logs:
|
||||
if time_log.is_billable:
|
||||
if time_log.is_billable and not time_log.sales_invoice:
|
||||
target.append(
|
||||
"timesheets",
|
||||
{
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
frappe.listview_settings["Timesheet"] = {
|
||||
add_fields: ["status", "total_hours", "start_date", "end_date"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Partially Billed") {
|
||||
return [__("Partially Billed"), "orange", "status,=," + "Partially Billed"];
|
||||
}
|
||||
|
||||
if (doc.status == "Billed") {
|
||||
return [__("Billed"), "green", "status,=," + "Billed"];
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
barcode(doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.barcode) {
|
||||
if (row.barcode && !frappe.flags.trigger_from_barcode_scanner) {
|
||||
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
|
||||
frappe.model.set_value(cdt, cdn, {
|
||||
item_code: r.message.item_code,
|
||||
@@ -1530,8 +1530,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
} else if (
|
||||
this.frm.doc.price_list_currency === this.frm.doc.currency &&
|
||||
this.frm.doc.plc_conversion_rate &&
|
||||
cint(this.frm.doc.plc_conversion_rate) != 1 &&
|
||||
cint(this.frm.doc.plc_conversion_rate) != cint(this.frm.doc.conversion_rate)
|
||||
flt(this.frm.doc.plc_conversion_rate) != 1 &&
|
||||
flt(this.frm.doc.plc_conversion_rate) != flt(this.frm.doc.conversion_rate)
|
||||
) {
|
||||
this.frm.set_value("conversion_rate", this.frm.doc.plc_conversion_rate);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,15 @@ frappe.ui.form.on("Quotation", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("warehouse", "items", (doc, cdt, cdn) => {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_indicator_formatter("item_code", function (doc) {
|
||||
return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : "";
|
||||
});
|
||||
|
||||
@@ -11,7 +11,16 @@ from frappe.cache_manager import clear_defaults_cache
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import make_records
|
||||
from frappe.utils import add_months, cint, formatdate, get_first_day, get_link_to_form, get_timestamp, today
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
get_timestamp,
|
||||
today,
|
||||
)
|
||||
from frappe.utils.nestedset import NestedSet, rebuild_tree
|
||||
|
||||
from erpnext.accounts.doctype.account.account import get_account_currency
|
||||
@@ -866,31 +875,41 @@ def install_country_fixtures(company, country):
|
||||
|
||||
|
||||
def update_company_current_month_sales(company):
|
||||
from_date = get_first_day(today())
|
||||
to_date = get_first_day(add_months(from_date, 1))
|
||||
"""Update Company's Total Monthly Sales.
|
||||
|
||||
results = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
SUM(base_grand_total) AS total,
|
||||
DATE_FORMAT(posting_date, '%%m-%%Y') AS month_year
|
||||
FROM
|
||||
`tabSales Invoice`
|
||||
WHERE
|
||||
posting_date >= %s
|
||||
AND posting_date < %s
|
||||
AND docstatus = 1
|
||||
AND company = %s
|
||||
GROUP BY
|
||||
month_year
|
||||
""",
|
||||
(from_date, to_date, company),
|
||||
as_dict=True,
|
||||
Postgres compatibility:
|
||||
- Avoid MariaDB-only DATE_FORMAT().
|
||||
- Use a date range for the current month instead (portable + index-friendly).
|
||||
"""
|
||||
|
||||
# Local imports so you don't have to touch file-level imports
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
start_date = get_first_day(today())
|
||||
end_date = get_last_day(today())
|
||||
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
|
||||
total_monthly_sales = (
|
||||
frappe.qb.from_(si)
|
||||
.select(Sum(si.base_grand_total))
|
||||
.where(
|
||||
(si.docstatus == 1)
|
||||
& (si.company == company)
|
||||
& (si.posting_date >= start_date)
|
||||
& (si.posting_date <= end_date)
|
||||
)
|
||||
).run(pluck=True)[0] or 0
|
||||
|
||||
# Fieldname in standard ERPNext is `total_monthly_sales`
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
"total_monthly_sales",
|
||||
total_monthly_sales,
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
monthly_total = results[0]["total"] if len(results) > 0 else 0
|
||||
frappe.db.set_value("Company", company, "total_monthly_sales", monthly_total)
|
||||
|
||||
|
||||
def update_company_monthly_sales(company):
|
||||
"""Cache past year monthly sales of every company based on sales invoices"""
|
||||
|
||||
@@ -1,7 +1,2 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
//--------- ONLOAD -------------
|
||||
cur_frm.cscript.onload = function (doc, cdt, cdn) {};
|
||||
|
||||
cur_frm.cscript.refresh = function (doc, cdt, cdn) {};
|
||||
|
||||
@@ -53,12 +53,14 @@ def get_stock_value_by_item_group(company):
|
||||
.inner_join(item_doctype)
|
||||
.on(doctype.item_code == item_doctype.name)
|
||||
.select(item_doctype.item_group, stock_value.as_("stock_value"))
|
||||
.where(doctype.warehouse.isin(warehouses))
|
||||
.groupby(item_doctype.item_group)
|
||||
.orderby(stock_value, order=frappe.qb.desc)
|
||||
.limit(10)
|
||||
)
|
||||
|
||||
if warehouses:
|
||||
query = query.where(doctype.warehouse.isin(warehouses))
|
||||
|
||||
results = query.run(as_dict=True)
|
||||
|
||||
labels = []
|
||||
|
||||
@@ -116,6 +116,11 @@ frappe.ui.form.on("Item", {
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
|
||||
frm.toggle_display(
|
||||
["opening_stock"],
|
||||
frappe.model.can_create("Stock Entry") && frappe.model.can_write("Stock Entry")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.is_fixed_asset) {
|
||||
@@ -239,6 +244,8 @@ frappe.ui.form.on("Item", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.toggle_display(["standard_rate"], frappe.model.can_create("Item Price"));
|
||||
},
|
||||
|
||||
validate: function (frm) {
|
||||
@@ -1063,7 +1070,7 @@ frappe.tour["Item"] = [
|
||||
fieldname: "valuation_rate",
|
||||
title: "Valuation Rate",
|
||||
description: __(
|
||||
"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.erpnext.com/docs/v13/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
|
||||
"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.frappe.io/erpnext/user/manual/en/calculation-of-valuation-rate-in-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -266,7 +266,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
||||
);
|
||||
}
|
||||
cur_frm.add_custom_button(
|
||||
__("Retention Stock Entry"),
|
||||
__("Sample Retention Stock Entry"),
|
||||
this.make_retention_stock_entry,
|
||||
__("Create")
|
||||
);
|
||||
|
||||
@@ -4849,6 +4849,154 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
||||
self.assertEqual(return_entry.items[0].qty, -2)
|
||||
self.assertEqual(return_entry.items[0].rejected_qty, 0) # 3-3=0
|
||||
|
||||
def test_do_not_use_batchwise_valuation_with_fifo(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item_code = make_item(
|
||||
"Test Item Do Not Use Batchwise Valuation with FIFO",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BN-TESTDNUBVWF-.#####",
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
doc = frappe.new_doc("Batch")
|
||||
doc.update(
|
||||
{
|
||||
"batch_id": "BN-TESTDNUBVWF-00001",
|
||||
"item": item_code,
|
||||
}
|
||||
).insert()
|
||||
|
||||
doc.db_set("use_batchwise_valuation", 0)
|
||||
doc.reload()
|
||||
|
||||
self.assertTrue(doc.use_batchwise_valuation == 0)
|
||||
|
||||
doc = frappe.new_doc("Batch")
|
||||
doc.update(
|
||||
{
|
||||
"batch_id": "BN-TESTDNUBVWF-00002",
|
||||
"item": item_code,
|
||||
}
|
||||
).insert()
|
||||
|
||||
self.assertTrue(doc.use_batchwise_valuation == 1)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
target=warehouse,
|
||||
batch_no="BN-TESTDNUBVWF-00001",
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
se1 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=200,
|
||||
target=warehouse,
|
||||
batch_no="BN-TESTDNUBVWF-00001",
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
stock_queue = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"is_cancelled": 0,
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": se1.name,
|
||||
},
|
||||
"stock_queue",
|
||||
)
|
||||
|
||||
stock_queue = frappe.parse_json(stock_queue)
|
||||
|
||||
self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]])
|
||||
|
||||
se2 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=2,
|
||||
target=warehouse,
|
||||
batch_no="BN-TESTDNUBVWF-00002",
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
stock_queue = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"is_cancelled": 0,
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": se2.name,
|
||||
},
|
||||
"stock_queue",
|
||||
)
|
||||
|
||||
stock_queue = frappe.parse_json(stock_queue)
|
||||
self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]])
|
||||
|
||||
se3 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=20,
|
||||
source=warehouse,
|
||||
batch_no="BN-TESTDNUBVWF-00001",
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
ste_details = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"is_cancelled": 0,
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": se3.name,
|
||||
},
|
||||
["stock_queue", "stock_value_difference"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
stock_queue = frappe.parse_json(ste_details.stock_queue)
|
||||
self.assertEqual(stock_queue, [])
|
||||
self.assertEqual(ste_details.stock_value_difference, 3000 * -1)
|
||||
|
||||
se4 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=20,
|
||||
rate=0,
|
||||
target=warehouse,
|
||||
batch_no="BN-TESTDNUBVWF-00001",
|
||||
use_serial_batch_fields=1,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
se4.items[0].basic_rate = 0.0
|
||||
se4.items[0].allow_zero_valuation_rate = 1
|
||||
se4.submit()
|
||||
|
||||
stock_queue = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"is_cancelled": 0,
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": se4.name,
|
||||
},
|
||||
"stock_queue",
|
||||
)
|
||||
|
||||
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -712,17 +712,16 @@ class SerialandBatchBundle(Document):
|
||||
is_packed_item = True
|
||||
|
||||
stock_queue = []
|
||||
batches = []
|
||||
if prev_sle and prev_sle.stock_queue:
|
||||
batches = frappe.get_all(
|
||||
"Batch",
|
||||
filters={
|
||||
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
|
||||
"use_batchwise_valuation": 0,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
batches = frappe.get_all(
|
||||
"Batch",
|
||||
filters={
|
||||
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
|
||||
"use_batchwise_valuation": 0,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if prev_sle and prev_sle.stock_queue and parse_json(prev_sle.stock_queue):
|
||||
if batches and valuation_method == "FIFO":
|
||||
stock_queue = parse_json(prev_sle.stock_queue)
|
||||
|
||||
@@ -749,7 +748,7 @@ class SerialandBatchBundle(Document):
|
||||
if d.qty:
|
||||
d.stock_value_difference = flt(d.qty) * d.incoming_rate
|
||||
|
||||
if stock_queue and valuation_method == "FIFO" and d.batch_no in batches:
|
||||
if valuation_method == "FIFO" and d.batch_no in batches and d.incoming_rate is not None:
|
||||
stock_queue.append([d.qty, d.incoming_rate])
|
||||
d.stock_queue = json.dumps(stock_queue)
|
||||
|
||||
|
||||
@@ -440,12 +440,16 @@ frappe.ui.form.on("Stock Entry", {
|
||||
|
||||
if (
|
||||
frm.doc.docstatus == 1 &&
|
||||
frm.doc.purpose == "Material Receipt" &&
|
||||
["Material Receipt", "Manufacture"].includes(frm.doc.purpose) &&
|
||||
frm.get_sum("items", "sample_quantity")
|
||||
) {
|
||||
frm.add_custom_button(__("Create Sample Retention Stock Entry"), function () {
|
||||
frm.trigger("make_retention_stock_entry");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Sample Retention Stock Entry"),
|
||||
function () {
|
||||
frm.trigger("make_retention_stock_entry");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
frm.trigger("setup_quality_inspection");
|
||||
@@ -568,10 +572,6 @@ frappe.ui.form.on("Stock Entry", {
|
||||
if (r.message) {
|
||||
var doc = frappe.model.sync(r.message)[0];
|
||||
frappe.set_route("Form", doc.doctype, doc.name);
|
||||
} else {
|
||||
frappe.msgprint(
|
||||
__("Retention Stock Entry already created or Sample Quantity not provided")
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1054,7 +1054,7 @@ frappe.ui.form.on("Stock Entry Detail", {
|
||||
|
||||
var validate_sample_quantity = function (frm, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
if (d.sample_quantity && frm.doc.purpose == "Material Receipt") {
|
||||
if (d.sample_quantity && d.transfer_qty && frm.doc.purpose == "Material Receipt") {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.stock_entry.stock_entry.validate_sample_quantity",
|
||||
args: {
|
||||
|
||||
@@ -2570,6 +2570,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
"expense_account": expense_account,
|
||||
"cost_center": item.get("buying_cost_center"),
|
||||
"is_finished_item": 1,
|
||||
"sample_quantity": item.get("sample_quantity"),
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -3103,6 +3104,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
se_child.po_detail = item_row.get("po_detail")
|
||||
se_child.sco_rm_detail = item_row.get("sco_rm_detail")
|
||||
se_child.scio_detail = item_row.get("scio_detail")
|
||||
se_child.sample_quantity = item_row.get("sample_quantity", 0)
|
||||
|
||||
for field in [
|
||||
self.subcontract_data.rm_detail_field,
|
||||
@@ -3408,13 +3410,14 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
|
||||
@frappe.whitelist()
|
||||
def move_sample_to_retention_warehouse(company, items):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
from erpnext.stock.serial_batch_bundle import (
|
||||
SerialBatchCreation,
|
||||
get_batch_nos,
|
||||
)
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
retention_warehouse = frappe.get_single_value("Stock Settings", "sample_retention_warehouse")
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.company = company
|
||||
@@ -3422,38 +3425,64 @@ def move_sample_to_retention_warehouse(company, items):
|
||||
stock_entry.set_stock_entry_type()
|
||||
for item in items:
|
||||
if item.get("sample_quantity") and item.get("serial_and_batch_bundle"):
|
||||
batch_no = get_batch_from_bundle(item.get("serial_and_batch_bundle"))
|
||||
sample_quantity = validate_sample_quantity(
|
||||
item.get("item_code"),
|
||||
item.get("sample_quantity"),
|
||||
item.get("transfer_qty") or item.get("qty"),
|
||||
batch_no,
|
||||
warehouse = item.get("t_warehouse") or item.get("warehouse")
|
||||
total_qty = 0
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": "Outward",
|
||||
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
|
||||
"item_code": item.get("item_code"),
|
||||
"warehouse": warehouse,
|
||||
"do_not_save": True,
|
||||
}
|
||||
)
|
||||
|
||||
if sample_quantity:
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": "Outward",
|
||||
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
|
||||
"item_code": item.get("item_code"),
|
||||
"warehouse": item.get("t_warehouse"),
|
||||
}
|
||||
sabb = cls_obj.duplicate_package()
|
||||
batches = get_batch_nos(item.get("serial_and_batch_bundle"))
|
||||
sabe_list = []
|
||||
for batch_no in batches.keys():
|
||||
sample_quantity = validate_sample_quantity(
|
||||
item.get("item_code"),
|
||||
item.get("sample_quantity"),
|
||||
item.get("transfer_qty") or item.get("qty"),
|
||||
batch_no,
|
||||
)
|
||||
|
||||
cls_obj.duplicate_package()
|
||||
sabe = next(item for item in sabb.entries if item.batch_no == batch_no)
|
||||
if sample_quantity:
|
||||
if sabb.has_serial_no:
|
||||
new_sabe = [
|
||||
entry
|
||||
for entry in sabb.entries
|
||||
if entry.batch_no == batch_no
|
||||
and frappe.db.exists(
|
||||
"Serial No", {"name": entry.serial_no, "warehouse": warehouse}
|
||||
)
|
||||
][: int(sample_quantity)]
|
||||
sabe_list.extend(new_sabe)
|
||||
total_qty += len(new_sabe)
|
||||
else:
|
||||
total_qty += sample_quantity
|
||||
sabe.qty = sample_quantity
|
||||
else:
|
||||
sabb.entries.remove(sabe)
|
||||
|
||||
if total_qty:
|
||||
if sabe_list:
|
||||
sabb.entries = sabe_list
|
||||
sabb.save()
|
||||
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item.get("item_code"),
|
||||
"s_warehouse": item.get("t_warehouse"),
|
||||
"s_warehouse": warehouse,
|
||||
"t_warehouse": retention_warehouse,
|
||||
"qty": item.get("sample_quantity"),
|
||||
"qty": total_qty,
|
||||
"basic_rate": item.get("valuation_rate"),
|
||||
"uom": item.get("uom"),
|
||||
"stock_uom": item.get("stock_uom"),
|
||||
"conversion_factor": item.get("conversion_factor") or 1.0,
|
||||
"serial_and_batch_bundle": cls_obj.serial_and_batch_bundle,
|
||||
"serial_and_batch_bundle": sabb.name,
|
||||
},
|
||||
)
|
||||
if stock_entry.get("items"):
|
||||
|
||||
@@ -190,6 +190,7 @@ def make_stock_entry(**args):
|
||||
"cost_center": args.cost_center,
|
||||
"expense_account": args.expense_account,
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields,
|
||||
"sample_quantity": frappe.get_value("Item", args.item, "sample_quantity") or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -2250,6 +2250,73 @@ class TestStockEntry(IntegrationTestCase):
|
||||
material_request.reload()
|
||||
self.assertEqual(material_request.transfer_status, "Completed")
|
||||
|
||||
def test_manufacture_entry_without_wo(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
fg_item = make_item("_Mobiles", properties={"is_stock_item": 1}).name
|
||||
rm_item1 = make_item("_Temper Glass", properties={"is_stock_item": 1}).name
|
||||
rm_item2 = make_item("_Battery", properties={"is_stock_item": 1}).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
make_stock_entry(item_code=rm_item1, target=warehouse, qty=5, purpose="Material Receipt")
|
||||
make_stock_entry(item_code=rm_item2, target=warehouse, qty=5, purpose="Material Receipt")
|
||||
|
||||
bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name
|
||||
se = make_stock_entry(item_code=fg_item, qty=1, purpose="Manufacture", do_not_save=True)
|
||||
se.from_bom = 1
|
||||
se.use_multi_level_bom = 1
|
||||
se.bom_no = bom_no
|
||||
se.fg_completed_qty = 1
|
||||
se.from_warehouse = warehouse
|
||||
se.to_warehouse = warehouse
|
||||
|
||||
se.get_items()
|
||||
rm_items = {d.item_code: d.qty for d in se.items if d.item_code != fg_item}
|
||||
self.assertEqual(rm_items[rm_item1], 1)
|
||||
self.assertEqual(rm_items[rm_item2], 1)
|
||||
se.calculate_rate_and_amount()
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Stock Settings", {"sample_retention_warehouse": "_Test Warehouse 1 - _TC"}
|
||||
)
|
||||
def test_sample_retention_stock_entry(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
retain_sample_item = make_item(
|
||||
"Retain Sample Item",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"retain_sample": 1,
|
||||
"sample_quantity": 2,
|
||||
"has_batch_no": 1,
|
||||
"has_serial_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "SAMPLE-RET-.#####",
|
||||
"serial_no_series": "SAMPLE-RET-SN-.#####",
|
||||
},
|
||||
)
|
||||
material_receipt = make_stock_entry(
|
||||
item_code=retain_sample_item.item_code, target=warehouse, qty=10, purpose="Material Receipt"
|
||||
)
|
||||
|
||||
source_sabb = frappe.get_doc(
|
||||
"Serial and Batch Bundle", material_receipt.items[0].serial_and_batch_bundle
|
||||
)
|
||||
batch = source_sabb.entries[0].batch_no
|
||||
serial_nos = [entry.serial_no for entry in source_sabb.entries]
|
||||
|
||||
sample_entry = frappe.get_doc(
|
||||
move_sample_to_retention_warehouse(material_receipt.company, material_receipt.items)
|
||||
)
|
||||
sample_entry.submit()
|
||||
target_sabb = frappe.get_doc("Serial and Batch Bundle", sample_entry.items[0].serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(sample_entry.items[0].transfer_qty, 2)
|
||||
self.assertEqual(target_sabb.entries[0].batch_no, batch)
|
||||
self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2])
|
||||
|
||||
|
||||
def make_serialized_item(self, **args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -42,9 +42,37 @@ def get_data(report_filters):
|
||||
gl_data = voucher_wise_gl_data.get(key) or {}
|
||||
d.account_value = gl_data.get("account_value", 0)
|
||||
d.difference_value = d.stock_value - d.account_value
|
||||
d.ledger_type = "Stock Ledger Entry"
|
||||
if abs(d.difference_value) > 0.1:
|
||||
data.append(d)
|
||||
|
||||
if key in voucher_wise_gl_data:
|
||||
del voucher_wise_gl_data[key]
|
||||
|
||||
if voucher_wise_gl_data:
|
||||
data += get_gl_ledgers_with_no_stock_ledger_entries(voucher_wise_gl_data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_gl_ledgers_with_no_stock_ledger_entries(voucher_wise_gl_data):
|
||||
data = []
|
||||
|
||||
for key in voucher_wise_gl_data:
|
||||
gl_data = voucher_wise_gl_data.get(key) or {}
|
||||
data.append(
|
||||
{
|
||||
"name": gl_data.get("name"),
|
||||
"ledger_type": "GL Entry",
|
||||
"voucher_type": gl_data.get("voucher_type"),
|
||||
"voucher_no": gl_data.get("voucher_no"),
|
||||
"posting_date": gl_data.get("posting_date"),
|
||||
"stock_value": 0,
|
||||
"account_value": gl_data.get("account_value", 0),
|
||||
"difference_value": gl_data.get("account_value", 0) * -1,
|
||||
}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -88,6 +116,7 @@ def get_gl_data(report_filters, filters):
|
||||
"name",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"posting_date",
|
||||
{
|
||||
"SUB": [{"SUM": "debit_in_account_currency"}, {"SUM": "credit_in_account_currency"}],
|
||||
"as": "account_value",
|
||||
@@ -109,10 +138,15 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("Stock Ledger ID"),
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"options": "Stock Ledger Entry",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "ledger_type",
|
||||
"width": "80",
|
||||
},
|
||||
{
|
||||
"label": _("Ledger Type"),
|
||||
"fieldname": "ledger_type",
|
||||
"fieldtype": "Data",
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"},
|
||||
{"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"},
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"},
|
||||
|
||||
@@ -989,9 +989,10 @@ def get_batch_nos(serial_and_batch_bundle):
|
||||
|
||||
entries = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
fields=["batch_no", "qty", "name"],
|
||||
fields=["batch_no", {"SUM": "qty", "as": "qty"}],
|
||||
filters={"parent": serial_and_batch_bundle, "batch_no": ("is", "set")},
|
||||
order_by="idx",
|
||||
group_by="batch_no",
|
||||
)
|
||||
|
||||
if not entries:
|
||||
@@ -1116,7 +1117,7 @@ class SerialBatchCreation:
|
||||
|
||||
id = self.serial_and_batch_bundle
|
||||
package = frappe.get_doc("Serial and Batch Bundle", id)
|
||||
new_package = frappe.copy_doc(package)
|
||||
new_package = frappe.copy_doc(package, ignore_no_copy=False)
|
||||
|
||||
if self.get("returned_serial_nos"):
|
||||
self.remove_returned_serial_nos(new_package)
|
||||
|
||||
@@ -67,7 +67,11 @@
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
<<<<<<< HEAD
|
||||
"filters": "[[\"Sales Invoice\",\"is_return\",\"=\",1]]",
|
||||
=======
|
||||
"filters": "",
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
@@ -542,7 +546,7 @@
|
||||
"icon": "",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounts Settings",
|
||||
"label": "Settings",
|
||||
"link_to": "Accounts Settings",
|
||||
"link_type": "DocType",
|
||||
"show_arrow": 0,
|
||||
@@ -571,7 +575,11 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"modified": "2026-01-10 00:06:13.234927",
|
||||
=======
|
||||
"modified": "2025-12-31 16:54:07.693056",
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
|
||||
@@ -258,7 +258,11 @@
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"modified": "2026-01-10 00:06:13.218453",
|
||||
=======
|
||||
"modified": "2025-12-31 16:27:44.936562",
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Assets",
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Tax Template",
|
||||
<<<<<<< HEAD
|
||||
"link_to": "Item Tax Template",
|
||||
=======
|
||||
"link_to": "Sales Taxes and Charges",
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"link_type": "DocType",
|
||||
"navigate_to_tab": "",
|
||||
"show_arrow": 0,
|
||||
@@ -46,6 +50,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
<<<<<<< HEAD
|
||||
"icon": "database",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
@@ -57,6 +62,8 @@
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
=======
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"icon": "triangle",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
@@ -67,7 +74,11 @@
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"child": 1,
|
||||
=======
|
||||
"child": 0,
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"collapsible": 1,
|
||||
"icon": "book-open-text",
|
||||
"indent": 0,
|
||||
@@ -79,7 +90,11 @@
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"child": 1,
|
||||
=======
|
||||
"child": 0,
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"collapsible": 1,
|
||||
"icon": "book-text",
|
||||
"indent": 0,
|
||||
@@ -148,7 +163,11 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"modified": "2026-01-10 00:06:13.005238",
|
||||
=======
|
||||
"modified": "2025-12-31 16:55:52.262432",
|
||||
>>>>>>> b93f920592 (fix: sidebar items for accounts (#51418))
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Taxes",
|
||||
|
||||
@@ -38,7 +38,7 @@ skip_namespaces = [
|
||||
]
|
||||
|
||||
[tool.bench.frappe-dependencies]
|
||||
frappe = ">=16.0.0-dev,<17.0.0"
|
||||
frappe = ">=16.0.0,<17.0.0"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 110
|
||||
|
||||
Reference in New Issue
Block a user