Merge branch 'develop' into prevent-negative-repair-cost

This commit is contained in:
KerollesFathy
2025-08-20 22:58:19 +00:00
109 changed files with 21168 additions and 17434 deletions

View File

@@ -3,3 +3,4 @@ reviews:
ignore_title_keywords:
- "sync translations"
- "update POT file"
review_status: false

View File

@@ -8,6 +8,9 @@ on:
- '**.md'
- '**.html'
- '**.csv'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
workflow_dispatch:
permissions:

View File

@@ -9,6 +9,9 @@ on:
- '**.css'
- '**.md'
- '**.html'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"

View File

@@ -6,6 +6,9 @@ on:
- '**.js'
- '**.md'
- '**.html'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
types: [opened, labelled, synchronize, reopened]
concurrency:

View File

@@ -32,8 +32,6 @@ repos:
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$

View File

@@ -20,4 +20,4 @@ erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
erpnext/patches/ @ruthra-kumar
.github/ @ruthra-kumar
pyproject.toml @akhilnarang
pyproject.toml @ruthra-kumar

View File

@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"accounting_dimension",
"fieldname",
"disabled",
"column_break_2",
"company",
@@ -90,11 +91,17 @@
"fieldname": "apply_restriction_on_values",
"fieldtype": "Check",
"label": "Apply restriction on dimension values"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldname"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:05:57.199186",
"modified": "2025-08-08 14:13:22.203011",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Filter",
@@ -139,8 +146,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,17 +17,16 @@ class AccountingDimensionFilter(Document):
from frappe.types import DF
from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import (
ApplicableOnAccount,
)
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount
accounting_dimension: DF.Literal
accounting_dimension: DF.Literal[None]
accounts: DF.Table[ApplicableOnAccount]
allow_or_restrict: DF.Literal["Allow", "Restrict"]
apply_restriction_on_values: DF.Check
company: DF.Link
dimensions: DF.Table[AllowedDimension]
disabled: DF.Check
fieldname: DF.Data | None
# end: auto-generated types
def before_save(self):
@@ -37,6 +36,10 @@ class AccountingDimensionFilter(Document):
self.set("dimensions", [])
def validate(self):
self.fieldname = frappe.db.get_value(
"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension
self.validate_applicable_accounts()
def validate_applicable_accounts(self):
@@ -71,7 +74,7 @@ def get_dimension_filter_map():
"""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
p.allow_or_restrict, p.fieldname, a.is_mandatory
FROM
`tabApplicable On Account` a,
`tabAccounting Dimension Filter` p
@@ -86,8 +89,6 @@ def get_dimension_filter_map():
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
build_map(
dimension_filter_map,
f.fieldname,

View File

@@ -462,9 +462,8 @@ def unset_existing_data(company):
"Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template",
]:
frappe.db.sql(
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
)
dt = frappe.qb.DocType(doctype)
frappe.qb.from_(dt).where(dt.company == company).delete().run()
def set_default_accounts(company):

View File

@@ -11,6 +11,7 @@
-> Resolves dunning automatically
"""
import json
import frappe
@@ -163,43 +164,66 @@ class Dunning(AccountsController):
]
def resolve_dunning(doc, state):
"""
Check if all payments have been made and resolve dunning, if yes. Called
when a Payment Entry is submitted.
"""
for reference in doc.references:
# Consider partial and full payments:
# Submitting full payment: outstanding_amount will be 0
# Submitting 1st partial payment: outstanding_amount will be the pending installment
# Cancelling full payment: outstanding_amount will revert to total amount
# Cancelling last partial payment: outstanding_amount will revert to pending amount
submit_condition = reference.outstanding_amount < reference.total_amount
cancel_condition = reference.outstanding_amount <= reference.total_amount
def update_linked_dunnings(doc, previous_outstanding_amount):
if (
doc.doctype != "Sales Invoice"
or doc.is_return
or previous_outstanding_amount == doc.outstanding_amount
):
return
if reference.reference_doctype == "Sales Invoice" and (
submit_condition if doc.docstatus == 1 else cancel_condition
):
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
to_resolve = doc.outstanding_amount < previous_outstanding_amount
state = "Unresolved" if to_resolve else "Resolved"
dunnings = get_linked_dunnings_as_per_state(doc.name, state)
if not dunnings:
return
for dunning in dunnings:
resolve = True
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = resolve and (False if (outstanding_ps > 0 and outstanding_inv > 0) else True)
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
invoices = set()
payment_schedule_ids = set()
new_status = "Resolved" if resolve else "Unresolved"
for dunning in dunnings:
for overdue_payment in dunning.overdue_payments:
invoices.add(overdue_payment.sales_invoice)
if overdue_payment.payment_schedule:
payment_schedule_ids.add(overdue_payment.payment_schedule)
if dunning.status != new_status:
dunning.status = new_status
dunning.save()
invoice_outstanding_amounts = dict(
frappe.get_all(
"Sales Invoice",
filters={"name": ["in", list(invoices)]},
fields=["name", "outstanding_amount"],
as_list=True,
)
)
ps_outstanding_amounts = (
dict(
frappe.get_all(
"Payment Schedule",
filters={"name": ["in", list(payment_schedule_ids)]},
fields=["name", "outstanding"],
as_list=True,
)
)
if payment_schedule_ids
else {}
)
for dunning in dunnings:
has_outstanding = False
for overdue_payment in dunning.overdue_payments:
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
if has_outstanding:
break
new_status = "Resolved" if not has_outstanding else "Unresolved"
if dunning.status != new_status:
dunning.status = new_status
dunning.save()
def get_linked_dunnings_as_per_state(sales_invoice, state):

View File

@@ -139,6 +139,64 @@ class TestDunning(IntegrationTestCase):
self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved")
def test_dunning_resolution_from_credit_note(self):
"""
Test that dunning is resolved when a credit note is issued against the original invoice.
"""
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -10), qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning.submit()
self.assertEqual(dunning.status, "Unresolved")
credit_note = frappe.copy_doc(sales_invoice)
credit_note.is_return = 1
credit_note.return_against = sales_invoice.name
credit_note.update_outstanding_for_self = 0
for item in credit_note.items:
item.qty = -item.qty
credit_note.save()
credit_note.submit()
dunning.reload()
self.assertEqual(dunning.status, "Resolved")
credit_note.cancel()
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
def test_dunning_not_affected_by_standalone_credit_note(self):
"""
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
"""
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -10), qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning.submit()
self.assertEqual(dunning.status, "Unresolved")
credit_note = frappe.copy_doc(sales_invoice)
credit_note.is_return = 1
credit_note.return_against = sales_invoice.name
credit_note.update_outstanding_for_self = 1
for item in credit_note.items:
item.qty = -item.qty
credit_note.save()
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
credit_note.submit()
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -1 * overdue_days)

View File

@@ -1796,6 +1796,14 @@ def make_inter_company_journal_entry(name, voucher_type, company):
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None):
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
if existing_reverse:
frappe.throw(
_("A Reverse Journal Entry {0} already exists for this Journal Entry.").format(
get_link_to_form("Journal Entry", existing_reverse)
)
)
from frappe.model.mapper import get_mapped_doc
def post_process(source, target):

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, today
@@ -55,22 +56,30 @@ def get_loyalty_details(
if not expiry_date:
expiry_date = today()
condition = ""
if company:
condition = " and company=%s " % frappe.db.escape(company)
if not include_expired_entry:
condition += " and expiry_date>='%s' " % expiry_date
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
loyalty_point_details = frappe.db.sql(
f"""select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
group by customer""",
(customer, loyalty_program, expiry_date),
as_dict=1,
query = (
frappe.qb.from_(LoyaltyPointEntry)
.select(
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
)
.where(
(LoyaltyPointEntry.customer == customer)
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
& (LoyaltyPointEntry.posting_date <= expiry_date)
)
.groupby(LoyaltyPointEntry.customer)
)
if company:
query = query.where(LoyaltyPointEntry.company == company)
if not include_expired_entry:
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
loyalty_point_details = query.run(as_dict=True)
if loyalty_point_details:
return loyalty_point_details[0]
else:

View File

@@ -200,9 +200,9 @@ class PaymentEntry(AccountsController):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.update_payment_requests()
self.update_payment_schedule()
self.make_gl_entries()
self.update_outstanding_amounts()
self.update_payment_schedule()
self.set_status()
def validate_for_repost(self):
@@ -303,10 +303,10 @@ class PaymentEntry(AccountsController):
)
super().on_cancel()
self.update_payment_requests(cancel=True)
self.update_payment_schedule(cancel=1)
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_status()
def update_payment_requests(self, cancel=False):

View File

@@ -55,6 +55,16 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
});
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
if (this.frm.doc.pos_profile) {
frappe.db
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
.then((r) => {
if (!r.exc) {
this.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
}
});
}
}
onload_post_render(frm) {
@@ -120,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
this.frm.meta.default_print_format = r.message.print_format || "";
this.frm.doc.campaign = r.message.campaign;
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
this.frm.set_default_payment = r.message.set_default_payment;
}
this.frm.script_manager.trigger("update_stock");
this.calculate_taxes_and_totals();

View File

@@ -717,7 +717,13 @@ class POSInvoice(SalesInvoice):
"Account", self.debit_to, "account_currency"
)
if not self.due_date and self.customer:
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
self.due_date = get_due_date(
self.posting_date,
"Customer",
self.customer,
self.company,
template_name=self.payment_terms_template,
)
super(SalesInvoice, self).set_missing_values(for_validate)
@@ -732,6 +738,7 @@ class POSInvoice(SalesInvoice):
"utm_campaign": profile.get("utm_campaign"),
"utm_medium": profile.get("utm_medium"),
"allow_print_before_pay": profile.get("allow_print_before_pay"),
"set_default_payment": profile.get("set_grand_total_to_default_mop"),
}
@frappe.whitelist()

View File

@@ -174,6 +174,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.apply_on != 'Transaction'",
"fieldname": "is_cumulative",
"fieldtype": "Check",
"label": "Is Cumulative"
@@ -656,7 +657,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2025-02-17 18:15:39.824639",
"modified": "2025-08-20 11:40:07.096854",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@@ -93,12 +93,14 @@
},
{
"default": "0",
"depends_on": "eval:doc.apply_on != 'Transaction'",
"fieldname": "mixed_conditions",
"fieldtype": "Check",
"label": "Mixed Conditions"
},
{
"default": "0",
"depends_on": "eval:doc.apply_on != 'Transaction'",
"fieldname": "is_cumulative",
"fieldtype": "Check",
"label": "Is Cumulative"
@@ -278,7 +280,7 @@
}
],
"links": [],
"modified": "2024-03-27 13:10:22.103686",
"modified": "2025-08-20 11:48:23.231081",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Promotional Scheme",

View File

@@ -343,7 +343,12 @@ class PurchaseInvoice(BuyingController):
)
if not self.due_date:
self.due_date = get_due_date(
self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
self.posting_date,
"Supplier",
self.supplier,
self.company,
self.bill_date,
template_name=self.payment_terms_template,
)
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")

View File

@@ -58,6 +58,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
me.frm.script_manager.trigger("is_pos");
me.frm.refresh_fields();
frappe.db
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
.then((r) => {
if (!r.exc) {
me.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
}
});
}
erpnext.queries.setup_warehouse_query(this.frm);
}
@@ -512,8 +519,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
},
callback: function (r) {
if (!r.exc) {
if (r.message && r.message.print_format) {
if (r.message) {
me.frm.pos_print_format = r.message.print_format;
me.frm.set_default_payment = r.message.set_default_payment;
}
me.frm.trigger("update_stock");
if (me.frm.doc.taxes_and_charges) {

View File

@@ -764,6 +764,7 @@ class SalesInvoice(SellingController):
"utm_campaign": pos.get("utm_campaign"),
"utm_medium": pos.get("utm_medium"),
"allow_print_before_pay": pos.get("allow_print_before_pay"),
"set_default_payment": pos.get("set_grand_total_to_default_mop", 1),
}
@frappe.whitelist()

View File

@@ -25,17 +25,25 @@ def get_group_by_asset_category_data(filters):
asset_categories = get_asset_categories_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_category(filters)
asset_value_adjustment_map = get_asset_value_adjustment_map_by_category(filters)
for asset_category in asset_categories:
row = frappe._dict()
row.update(asset_category)
adjustments = asset_value_adjustment_map.get(asset_category.get("asset_category"), {})
row.adjustment_before_from_date = flt(adjustments.get("adjustment_before_from_date", 0))
row.adjustment_till_to_date = flt(adjustments.get("adjustment_till_to_date", 0))
row.adjustment_during_period = row.adjustment_till_to_date - row.adjustment_before_from_date
row.value_as_on_from_date += row.adjustment_before_from_date
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
+ flt(row.adjustment_during_period)
)
row.update(
@@ -229,26 +237,93 @@ def get_assets_for_grouped_by_category(filters):
)
def get_asset_value_adjustment_map_by_category(filters):
asset_value_adjustments = frappe.db.sql(
"""
SELECT
a.asset_category AS asset_category,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date
FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
GROUP BY a.asset_category
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
as_dict=1,
)
category_value_adjustment_map = {}
for r in asset_value_adjustments:
category_value_adjustment_map[r["asset_category"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}
return category_value_adjustment_map
def get_group_by_asset_data(filters):
data = []
asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(filters)
asset_value_adjustment_map = get_asset_value_adjustment_map(filters)
for asset_detail in asset_details:
row = frappe._dict()
row.update(asset_detail)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
adjustments = asset_value_adjustment_map.get(
asset_detail.get("name", ""),
{
"adjustment_before_from_date": 0.0,
"adjustment_till_to_date": 0.0,
},
)
row.adjustment_before_from_date = adjustments["adjustment_before_from_date"]
row.adjustment_till_to_date = adjustments["adjustment_till_to_date"]
row.adjustment_during_period = flt(row.adjustment_till_to_date) - flt(row.adjustment_before_from_date)
row.value_as_on_from_date += row.adjustment_before_from_date
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
+ flt(row.adjustment_during_period)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
@@ -432,6 +507,59 @@ def get_assets_for_grouped_by_asset(filters):
)
def get_asset_value_adjustment_map(filters):
asset_with_value_adjustments = frappe.db.sql(
"""
SELECT
a.name AS asset,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date
FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
GROUP BY a.name
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
as_dict=1,
)
asset_value_adjustment_map = {}
for r in asset_with_value_adjustments:
asset_value_adjustment_map[r["asset"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}
return asset_value_adjustment_map
def get_columns(filters):
columns = []

View File

@@ -1,29 +1,34 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-12-06 13:22:23",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2017-02-24 20:17:51.995451",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General Ledger",
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "General Ledger",
"report_type": "Script Report",
"add_total_row": 1,
"add_translate_data": 0,
"columns": [],
"creation": "2013-12-06 13:22:23",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-08-13 12:47:27.645023",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General Ledger",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "General Ledger",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -356,7 +356,13 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
query = query.where(si.posting_date <= filters.get("to_date"))
if filters.get("mode_of_payment"):
query = query.where(sip.mode_of_payment == filters.get("mode_of_payment"))
subquery = (
frappe.qb.from_(sip)
.select(sip.parent)
.where(sip.mode_of_payment == filters.get("mode_of_payment"))
.groupby(sip.parent)
)
query = query.where(si.name.isin(subquery))
if filters.get("warehouse"):
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
@@ -425,8 +431,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
frappe.qb.from_(si)
.join(sii)
.on(si.name == sii.parent)
.left_join(sip)
.on(sip.parent == si.name)
.left_join(item)
.on(sii.item_code == item.name)
.select(
@@ -466,7 +470,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
si.update_stock,
sii.uom,
sii.qty,
sip.mode_of_payment,
)
.where(si.docstatus == 1)
.where(sii.parenttype == doctype)

View File

@@ -1940,6 +1940,8 @@ def create_payment_ledger_entry(
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
from erpnext.accounts.doctype.dunning.dunning import update_linked_dunnings
if not voucher_type or not voucher_no:
return
@@ -1969,6 +1971,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
outstanding = voucher_outstanding[0]
ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no)
previous_outstanding_amount = ref_doc.outstanding_amount
outstanding_amount = flt(
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
)
@@ -1982,6 +1985,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
outstanding_amount,
)
update_linked_dunnings(ref_doc, previous_outstanding_amount)
ref_doc.set_status(update=True)
ref_doc.notify_update()

View File

@@ -1108,7 +1108,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
def make_journal_entry(asset_name):
asset = frappe.get_doc("Asset", asset_name)
(
_,
fixed_asset_account,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset.asset_category, asset.company)

View File

@@ -627,6 +627,9 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
docstatus: 1,
status: ["not in", ["Stopped", "Expired"]],
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "rate", "amount"],
});
},
__("Get Items From")

View File

@@ -9,6 +9,7 @@ from frappe import _
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Order
from frappe.utils import get_url
from frappe.utils.print_format import download_pdf
from frappe.utils.user import get_user_fullname
@@ -582,35 +583,32 @@ def get_supplier_tag():
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
conditions = ""
if txt:
conditions += "and rfq.name like '%%" + txt + "%%' "
rfq = frappe.qb.DocType("Request for Quotation")
rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
if filters.get("transaction_date"):
conditions += "and rfq.transaction_date = '{}'".format(filters.get("transaction_date"))
rfq_data = frappe.db.sql(
f"""
select
distinct rfq.name, rfq.transaction_date,
rfq.company
from
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
where
rfq.name = rfq_supplier.parent
and rfq_supplier.supplier = %(supplier)s
and rfq.docstatus = 1
and rfq.company = %(company)s
{conditions}
order by rfq.transaction_date ASC
limit %(page_len)s offset %(start)s """,
{
"page_len": page_len,
"start": start,
"company": filters.get("company"),
"supplier": filters.get("supplier"),
},
as_dict=1,
query = (
frappe.qb.from_(rfq)
.from_(rfq_supplier)
.select(rfq.name)
.distinct()
.select(rfq.transaction_date, rfq.company)
.where(
(rfq.name == rfq_supplier.parent)
& (rfq_supplier.supplier == filters.get("supplier"))
& (rfq.docstatus == 1)
& (rfq.company == filters.get("company"))
)
.orderby(rfq.transaction_date, order=Order.asc)
.limit(page_len)
.offset(start)
)
if txt:
query = query.where(rfq.name.like(f"%%{txt}%%"))
if filters.get("transaction_date"):
query = query.where(rfq.transaction_date == filters.get("transaction_date"))
rfq_data = query.run(as_dict=1)
return rfq_data

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
@@ -235,7 +237,12 @@ def get_list_context(context=None):
@frappe.whitelist()
def make_purchase_order(source_name, target_doc=None):
def make_purchase_order(source_name, target_doc=None, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
def set_missing_values(source, target):
target.run_method("set_missing_values")
target.run_method("get_schedule_dates")
@@ -244,6 +251,11 @@ def make_purchase_order(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor)
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
doclist = get_mapped_doc(
"Supplier Quotation",
source_name,
@@ -265,6 +277,7 @@ def make_purchase_order(source_name, target_doc=None):
["sales_order", "sales_order"],
],
"postprocess": update_item,
"condition": select_item,
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",

View File

@@ -588,21 +588,27 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
"""select distinct bo.name, bo.blanket_order_type, bo.to_date
from `tabBlanket Order` bo, `tabBlanket Order Item` boi
where
boi.parent = bo.name
and boi.item_code = {item_code}
and bo.blanket_order_type = '{blanket_order_type}'
and bo.company = {company}
and bo.docstatus = 1""".format(
item_code=frappe.db.escape(filters.get("item")),
blanket_order_type=filters.get("blanket_order_type"),
company=frappe.db.escape(filters.get("company")),
bo = frappe.qb.DocType("Blanket Order")
bo_item = frappe.qb.DocType("Blanket Order Item")
blanket_orders = (
frappe.qb.from_(bo)
.from_(bo_item)
.select(bo.name)
.distinct()
.select(bo.blanket_order_type, bo.to_date)
.where(
(bo_item.parent == bo.name)
& (bo_item.item_code == filters.get("item"))
& (bo.blanket_order_type == filters.get("blanket_order_type"))
& (bo.company == filters.get("company"))
& (bo.docstatus == 1)
)
.run()
)
return blanket_orders
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -620,7 +626,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
if filters.get("company"):
condition += "and tabAccount.company = %(company)s"
condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}"
condition += " and tabAccount.disabled = %(disabled)s"
return frappe.db.sql(
f"""select tabAccount.name from `tabAccount`
@@ -630,7 +636,11 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
and tabAccount.`{searchfield}` LIKE %(txt)s
{condition} {get_match_cond(doctype)}
order by idx desc, name""",
{"txt": "%" + txt + "%", "company": filters.get("company", "")},
{
"txt": "%" + txt + "%",
"company": filters.get("company", ""),
"disabled": cint(filters.get("disabled", 0)),
},
)

View File

@@ -116,7 +116,7 @@ status_map = {
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
[
"Ordered",
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'",
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture']",
],
[
"Transferred",
@@ -142,10 +142,6 @@ status_map = {
"Partially Ordered",
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'",
],
[
"Manufactured",
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'",
],
],
"POS Opening Entry": [
["Draft", None],

View File

@@ -1963,6 +1963,7 @@ def make_bundle_for_material_transfer(**kwargs):
row.warehouse = kwargs.warehouse
bundle_doc.set_incoming_rate()
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_validate = True

View File

@@ -360,7 +360,9 @@ doc_events = {
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
],
"on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"],
"on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
],
"on_trash": "erpnext.regional.check_deletion_permission",
},
"Purchase Invoice": {
@@ -372,9 +374,7 @@ doc_events = {
"Payment Entry": {
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
],
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
"on_trash": "erpnext.regional.check_deletion_permission",
},
"Address": {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1421,11 +1421,11 @@ def add_additional_cost(stock_entry, work_order):
as_dict=1,
)
expecnse_account = (
expense_account = (
company_account.default_operating_cost_account or company_account.default_expense_account
)
add_non_stock_items_cost(stock_entry, work_order, expecnse_account)
add_operations_cost(stock_entry, work_order, expecnse_account)
add_non_stock_items_cost(stock_entry, work_order, expense_account)
add_operations_cost(stock_entry, work_order, expense_account)
def add_non_stock_items_cost(stock_entry, work_order, expense_account):
@@ -1460,21 +1460,74 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account):
)
def add_operating_cost_component_wise(
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None
):
if not work_order:
return False
cost_added = False
for row in work_order.operations:
workstation_cost = frappe.get_all(
"Workstation Cost",
fields=["operating_component", "operating_cost"],
filters={
"parent": row.workstation,
"parenttype": "Workstation",
},
)
for wc in workstation_cost:
expense_account = get_component_account(wc.operating_component) or op_expense_account
actual_cp_operating_cost = flt(
flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0),
row.precision("actual_operating_cost"),
)
per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty)
if per_unit_cost and expense_account:
stock_entry.append(
"additional_costs",
{
"expense_account": expense_account,
"description": _("{0} Operating Cost for operation {1}").format(
wc.operating_component, row.operation
),
"amount": per_unit_cost * flt(stock_entry.fg_completed_qty),
},
)
cost_added = True
return cost_added
@frappe.request_cache
def get_component_account(parent):
return frappe.db.get_value("Workstation Operating Component Account", parent, "expense_account")
def add_operations_cost(stock_entry, work_order=None, expense_account=None):
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
if operating_cost_per_unit:
stock_entry.append(
"additional_costs",
{
"expense_account": expense_account,
"description": _("Operating Cost as per Work Order / BOM"),
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
},
cost_added = add_operating_cost_component_wise(
stock_entry, work_order, operating_cost_per_unit, expense_account
)
if not cost_added:
stock_entry.append(
"additional_costs",
{
"expense_account": expense_account,
"description": _("Operating Cost as per Work Order / BOM"),
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
},
)
if work_order and work_order.additional_operating_cost and work_order.qty:
additional_operating_cost_per_unit = flt(work_order.additional_operating_cost) / flt(work_order.qty)

View File

@@ -31,6 +31,16 @@ frappe.ui.form.on("Job Card", {
};
});
frm.set_query("operation", "time_logs", () => {
let operations = (frm.doc.sub_operations || []).map((d) => d.sub_operation);
return {
filters: {
name: ["in", operations],
},
};
});
frm.events.set_company_filters(frm, "target_warehouse");
frm.events.set_company_filters(frm, "source_warehouse");
frm.events.set_company_filters(frm, "wip_warehouse");
frm.set_query("source_warehouse", "items", () => {
@@ -184,7 +194,12 @@ frappe.ui.form.on("Job Card", {
!frm.doc.finished_good ||
!has_items?.length)
) {
if (!frm.doc.time_logs?.length) {
let last_row = {};
if (frm.doc.sub_operations?.length && frm.doc.time_logs?.length) {
last_row = get_last_row(frm.doc.time_logs);
}
if (!frm.doc.time_logs?.length || (frm.doc.sub_operations?.length && last_row?.to_time)) {
frm.add_custom_button(__("Start Job"), () => {
let from_time = frappe.datetime.now_datetime();
if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
@@ -313,7 +328,12 @@ frappe.ui.form.on("Job Card", {
];
let last_completed_row = get_last_completed_row(frm.doc.time_logs);
if (!last_completed_row || !last_completed_row.to_time) {
let last_row = {};
if (frm.doc.sub_operations?.length && frm.doc.time_logs?.length) {
last_row = get_last_row(frm.doc.time_logs);
}
if (!last_completed_row || !last_completed_row.to_time || !last_row.to_time) {
fields.push({
fieldtype: "Datetime",
label: __("End Time"),
@@ -758,3 +778,7 @@ function get_last_completed_row(time_logs) {
return last_completed_row;
}
}
function get_last_row(time_logs) {
return time_logs[time_logs.length - 1] || {};
}

View File

@@ -157,6 +157,9 @@ class JobCard(Document):
self.validate_sequence_id()
self.set_sub_operations()
self.update_sub_operation_status()
if self.sub_operations:
self.set_total_completed_qty_from_sub_operations()
self.validate_work_order()
def on_update(self):
@@ -280,8 +283,13 @@ class JobCard(Document):
}
)
def set_total_completed_qty_from_sub_operations(self):
sub_op_total_qty = []
for row in self.sub_operations:
self.total_completed_qty += row.completed_qty
sub_op_total_qty.append(flt(row.completed_qty))
if sub_op_total_qty:
self.total_completed_qty = min(sub_op_total_qty)
def get_overlap_for(self, args, open_job_cards=None):
time_logs = []
@@ -613,7 +621,7 @@ class JobCard(Document):
self.save()
def update_sub_operation_status(self):
if not (self.sub_operations and self.time_logs):
if not self.sub_operations:
return
operation_wise_completed_time = {}

View File

@@ -6,9 +6,9 @@
"engine": "InnoDB",
"field_order": [
"sub_operation",
"completed_qty",
"completed_time",
"status",
"completed_qty"
"status"
],
"fields": [
{
@@ -39,6 +39,7 @@
{
"fieldname": "completed_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Completed Qty",
"read_only": 1
}
@@ -46,15 +47,16 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:09:57.090298",
"modified": "2025-08-20 21:44:43.941434",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Operation",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -16,6 +16,7 @@
"fields": [
{
"allow_on_submit": 1,
"columns": 3,
"fieldname": "from_time",
"fieldtype": "Datetime",
"in_list_view": 1,
@@ -58,17 +59,17 @@
{
"fieldname": "operation",
"fieldtype": "Link",
"label": "Operation",
"in_list_view": 1,
"label": "Sub Operation",
"no_copy": 1,
"options": "Operation",
"read_only": 1
"options": "Operation"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-04 15:47:11.748937",
"modified": "2025-08-20 21:49:59.084876",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Time Log",

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
import frappe
from frappe import _
from frappe.tests import IntegrationTestCase
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
@@ -75,14 +76,22 @@ class TestWorkstation(IntegrationTestCase):
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
w1 = frappe.get_doc("Workstation", "_Test Workstation A")
# resets values
w1.hour_rate_rent = 300
w1.hour_rate_labour = 0
for row in w1.workstation_costs:
if row.operating_component == _("Rent"):
row.operating_cost = 300
break
w1.save()
bom_doc.update_cost()
bom_doc.reload()
self.assertEqual(w1.hour_rate, 300)
self.assertEqual(bom_doc.operations[0].hour_rate, 300)
w1.hour_rate_rent = 250
for row in w1.workstation_costs:
if row.operating_component == _("Rent"):
row.operating_cost = 250
break
w1.save()
# updating after setting new rates in workstations
bom_doc.update_cost()
@@ -102,8 +111,24 @@ def make_workstation(*args, **kwargs):
workstation_name = args.workstation_name or args.workstation
if not frappe.db.exists("Workstation", workstation_name):
doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name})
doc.hour_rate_rent = args.get("hour_rate_rent")
doc.hour_rate_labour = args.get("hour_rate_labour")
if args.get("hour_rate_rent"):
doc.append(
"workstation_costs",
{
"operating_component": _("Rent"),
"operating_cost": args.get("hour_rate_rent"),
},
)
if args.get("hour_rate_labour"):
doc.append(
"workstation_costs",
{
"operating_component": _("Wages"),
"operating_cost": args.get("hour_rate_labour"),
},
)
doc.workstation_type = args.get("workstation_type")
doc.insert()

View File

@@ -27,11 +27,8 @@
"column_break_etmc",
"off_status_image",
"over_heads",
"hour_rate_electricity",
"hour_rate_consumable",
"column_break_11",
"hour_rate_rent",
"hour_rate_labour",
"section_break_auzm",
"workstation_costs",
"section_break_11",
"hour_rate",
"workstaion_description",
@@ -68,50 +65,6 @@
"label": "Operating Costs",
"oldfieldtype": "Section Break"
},
{
"bold": 1,
"description": "per hour",
"fieldname": "hour_rate_electricity",
"fieldtype": "Currency",
"label": "Electricity Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_electricity",
"oldfieldtype": "Currency"
},
{
"bold": 1,
"description": "per hour",
"fieldname": "hour_rate_consumable",
"fieldtype": "Currency",
"label": "Consumable Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_consumable",
"oldfieldtype": "Currency"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"bold": 1,
"description": "per hour",
"fieldname": "hour_rate_rent",
"fieldtype": "Currency",
"label": "Rent Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_rent",
"oldfieldtype": "Currency"
},
{
"bold": 1,
"description": "Wages per hour",
"fieldname": "hour_rate_labour",
"fieldtype": "Currency",
"label": "Wages",
"non_negative": 1,
"oldfieldname": "hour_rate_labour",
"oldfieldtype": "Currency"
},
{
"description": "per hour",
"fieldname": "hour_rate",
@@ -252,6 +205,17 @@
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "section_break_auzm",
"fieldtype": "Section Break",
"label": "Operating Costs (Per Hour)"
},
{
"fieldname": "workstation_costs",
"fieldtype": "Table",
"label": "Operating Components Cost",
"options": "Workstation Cost"
}
],
"hide_toolbar": 1,
@@ -259,7 +223,7 @@
"idx": 1,
"image_field": "on_status_image",
"links": [],
"modified": "2025-07-13 16:02:13.615001",
"modified": "2025-08-19 12:07:05.374386",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe import _, bold
from frappe.model.document import Document
from frappe.utils import (
add_days,
@@ -44,6 +44,7 @@ class Workstation(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.manufacturing.doctype.workstation_cost.workstation_cost import WorkstationCost
from erpnext.manufacturing.doctype.workstation_working_hour.workstation_working_hour import (
WorkstationWorkingHour,
)
@@ -52,10 +53,6 @@ class Workstation(Document):
disabled: DF.Check
holiday_list: DF.Link | None
hour_rate: DF.Currency
hour_rate_consumable: DF.Currency
hour_rate_electricity: DF.Currency
hour_rate_labour: DF.Currency
hour_rate_rent: DF.Currency
off_status_image: DF.AttachImage | None
on_status_image: DF.AttachImage | None
plant_floor: DF.Link | None
@@ -64,10 +61,26 @@ class Workstation(Document):
total_working_hours: DF.Float
warehouse: DF.Link | None
working_hours: DF.Table[WorkstationWorkingHour]
workstation_costs: DF.Table[WorkstationCost]
workstation_name: DF.Data
workstation_type: DF.Link | None
# end: auto-generated types
def validate(self):
self.validate_duplicate_operating_component()
def validate_duplicate_operating_component(self):
components = []
for row in self.workstation_costs:
if row.operating_component not in components:
components.append(row.operating_component)
else:
frappe.throw(
_("Duplicate Operating Component {0} found in Operating Components").format(
bold(row.operating_component)
)
)
def before_save(self):
self.set_data_based_on_workstation_type()
self.set_hour_rate()
@@ -95,36 +108,33 @@ class Workstation(Document):
frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
def set_hour_rate(self):
self.hour_rate = (
flt(self.hour_rate_labour)
+ flt(self.hour_rate_electricity)
+ flt(self.hour_rate_consumable)
+ flt(self.hour_rate_rent)
)
self.hour_rate = 0.0
for row in self.workstation_costs:
if row.operating_cost:
self.hour_rate += flt(row.operating_cost)
@frappe.whitelist()
def set_data_based_on_workstation_type(self):
if self.workstation_costs:
return
if self.workstation_type:
fields = [
"hour_rate_labour",
"hour_rate_electricity",
"hour_rate_consumable",
"hour_rate_rent",
"hour_rate",
"description",
]
data = frappe.get_all(
"Workstation Cost",
fields=["operating_component", "operating_cost", "idx"],
filters={"parent": self.workstation_type, "parenttype": "Workstation Type"},
order_by="idx",
)
data = frappe.get_cached_value("Workstation Type", self.workstation_type, fields, as_dict=True)
if not data:
return
for field in fields:
if self.get(field):
continue
if value := data.get(field):
self.set(field, value)
for row in data:
self.append(
"workstation_costs",
{
"operating_component": row.operating_component,
"operating_cost": row.operating_cost,
"idx": row.idx,
},
)
def on_update(self):
self.validate_overlap_for_operation_timings()

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestWorkstationCost(IntegrationTestCase):
"""
Integration tests for WorkstationCost.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Workstation Cost", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,43 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-17 16:43:13.542333",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"operating_component",
"operating_cost"
],
"fields": [
{
"fieldname": "operating_cost",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Operating Cost",
"reqd": 1
},
{
"fieldname": "operating_component",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Operating Component",
"options": "Workstation Operating Component",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-17 19:21:02.725365",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Cost",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,24 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class WorkstationCost(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
operating_component: DF.Link
operating_cost: DF.Currency
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestWorkstationOperatingComponent(IntegrationTestCase):
"""
Integration tests for WorkstationOperatingComponent.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Workstation Operating Component", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,84 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:component_name",
"creation": "2025-08-17 16:49:30.711201",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"component_name",
"section_break_ewdg",
"accounts"
],
"fields": [
{
"fieldname": "component_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Component Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "section_break_ewdg",
"fieldtype": "Section Break"
},
{
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Component Expense Account",
"options": "Workstation Operating Component Account"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-17 19:23:47.510540",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Operating Component",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class WorkstationOperatingComponent(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.manufacturing.doctype.workstation_operating_component_account.workstation_operating_component_account import (
WorkstationOperatingComponentAccount,
)
accounts: DF.Table[WorkstationOperatingComponentAccount]
component_name: DF.Data
# end: auto-generated types
pass

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestWorkstationOperatingComponentAccount(IntegrationTestCase):
"""
Integration tests for WorkstationOperatingComponentAccount.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Workstation Operating Component Account", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,43 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-17 19:21:36.356779",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"expense_account"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "expense_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Expense Account",
"options": "Account"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-17 19:24:01.487406",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Operating Component Account",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,24 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class WorkstationOperatingComponentAccount(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link
expense_account: DF.Link | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View File

@@ -10,12 +10,8 @@
"engine": "InnoDB",
"field_order": [
"workstation_type",
"over_heads",
"hour_rate_electricity",
"hour_rate_consumable",
"column_break_5",
"hour_rate_rent",
"hour_rate_labour",
"section_break_auzm",
"workstation_costs",
"section_break_8",
"hour_rate",
"description_tab",
@@ -32,44 +28,6 @@
"reqd": 1,
"unique": 1
},
{
"fieldname": "over_heads",
"fieldtype": "Section Break",
"label": "Operating Costs",
"oldfieldtype": "Section Break"
},
{
"description": "per hour",
"fieldname": "hour_rate_electricity",
"fieldtype": "Currency",
"label": "Electricity Cost",
"oldfieldname": "hour_rate_electricity",
"oldfieldtype": "Currency"
},
{
"description": "per hour",
"fieldname": "hour_rate_consumable",
"fieldtype": "Currency",
"label": "Consumable Cost",
"oldfieldname": "hour_rate_consumable",
"oldfieldtype": "Currency"
},
{
"description": "per hour",
"fieldname": "hour_rate_rent",
"fieldtype": "Currency",
"label": "Rent Cost",
"oldfieldname": "hour_rate_rent",
"oldfieldtype": "Currency"
},
{
"description": "Wages per hour",
"fieldname": "hour_rate_labour",
"fieldtype": "Currency",
"label": "Wages",
"oldfieldname": "hour_rate_labour",
"oldfieldtype": "Currency"
},
{
"description": "per hour",
"fieldname": "hour_rate",
@@ -88,10 +46,6 @@
"oldfieldtype": "Text",
"width": "300px"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "description_tab",
@@ -101,11 +55,22 @@
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_auzm",
"fieldtype": "Section Break",
"label": "Operating Costs (Per Hour)"
},
{
"fieldname": "workstation_costs",
"fieldtype": "Table",
"label": "Operating Components Cost",
"options": "Workstation Cost"
}
],
"icon": "icon-wrench",
"links": [],
"modified": "2024-03-27 13:11:00.946367",
"modified": "2025-08-19 12:06:56.683558",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Type",
@@ -125,9 +90,10 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -2,6 +2,7 @@
# For license information, please see license.txt
import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.utils import flt
@@ -15,25 +16,38 @@ class WorkstationType(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.manufacturing.doctype.workstation_cost.workstation_cost import WorkstationCost
description: DF.SmallText | None
hour_rate: DF.Currency
hour_rate_consumable: DF.Currency
hour_rate_electricity: DF.Currency
hour_rate_labour: DF.Currency
hour_rate_rent: DF.Currency
workstation_costs: DF.Table[WorkstationCost]
workstation_type: DF.Data
# end: auto-generated types
def validate(self):
self.validate_duplicate_operating_component()
def validate_duplicate_operating_component(self):
components = []
for row in self.workstation_costs:
if row.operating_component not in components:
components.append(row.operating_component)
else:
frappe.throw(
_("Duplicate Operating Component {0} found in Operating Components").format(
bold(row.operating_component)
)
)
def before_save(self):
self.set_hour_rate()
def set_hour_rate(self):
self.hour_rate = (
flt(self.hour_rate_labour)
+ flt(self.hour_rate_electricity)
+ flt(self.hour_rate_consumable)
+ flt(self.hour_rate_rent)
)
self.hour_rate = 0.0
for row in self.workstation_costs:
if row.operating_cost:
self.hour_rate += flt(row.operating_cost)
def get_workstations(workstation_type):

View File

@@ -434,3 +434,5 @@ erpnext.patches.v15_0.update_uae_zero_rated_fetch
erpnext.patches.v15_0.add_company_payment_gateway_account
erpnext.patches.v16_0.update_serial_no_reference_name
erpnext.patches.v16_0.set_invoice_type_in_pos_settings
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
erpnext.patches.v16_0.make_workstation_operating_components #1

View File

@@ -48,6 +48,7 @@ def execute():
dunning.validate()
dunning.flags.ignore_validate_update_after_submit = True
dunning.flags.ignore_links = True
dunning.save()
# Reverse entries only if dunning is submitted and not resolved

View File

@@ -0,0 +1,36 @@
import frappe
from frappe.query_builder import DocType
def execute():
default_accounting_dimension()
ADF = DocType("Accounting Dimension Filter")
AD = DocType("Accounting Dimension")
accounting_dimension_filter = (
frappe.qb.from_(ADF)
.join(AD)
.on(AD.document_type == ADF.accounting_dimension)
.select(ADF.name, AD.fieldname, ADF.accounting_dimension)
).run(as_dict=True)
for doc in accounting_dimension_filter:
value = doc.fieldname or frappe.scrub(doc.accounting_dimension)
frappe.db.set_value(
"Accounting Dimension Filter",
doc.name,
"fieldname",
value,
update_modified=False,
)
def default_accounting_dimension():
ADF = DocType("Accounting Dimension Filter")
for dim in ("Cost Center", "Project"):
(
frappe.qb.update(ADF)
.set(ADF.fieldname, frappe.scrub(dim))
.where(ADF.accounting_dimension == dim)
.run()
)

View File

@@ -0,0 +1,79 @@
import frappe
from frappe import _
def get_operating_cost_account(company):
company_details = frappe.db.get_value(
"Company", company, ["default_operating_cost_account", "default_expense_account"], as_dict=True
)
return company_details.get("default_operating_cost_account") or company_details.get(
"default_expense_account"
)
def execute():
components = [
"Electricity",
"Consumables",
"Rent",
"Wages",
]
companies = frappe.get_all("Company", filters={"is_group": 0}, pluck="name")
for component in components:
component = _(component)
if not frappe.db.exists("Workstation Operating Component", component):
doc = frappe.new_doc("Workstation Operating Component")
doc.component_name = component
for company in companies:
operating_cost_account = get_operating_cost_account(company)
doc.append("accounts", {"company": company, "expense_account": operating_cost_account})
doc.insert()
workstations = frappe.get_all("Workstation", filters={"hour_rate": (">", 0.0)}, pluck="name") or []
workstation_types = (
frappe.get_all("Workstation Type", filters={"hour_rate": (">", 0.0)}, pluck="name") or []
)
if not workstations and not workstation_types:
return
components_map = {
"hour_rate_electricity": _("Electricity"),
"hour_rate_consumable": _("Consumables"),
"hour_rate_rent": _("Rent"),
"hour_rate_labour": _("Wages"),
}
for workstation in workstations:
doc = frappe.get_doc("Workstation", workstation)
for field, component in components_map.items():
if doc.get(field):
doc.append(
"workstation_costs",
{
"operating_component": component,
"operating_cost": doc.get(field),
},
)
doc.save()
for workstation_type in workstation_types:
doc = frappe.get_doc("Workstation Type", workstation_type)
for field, component in components_map.items():
if doc.get(field):
doc.append(
"workstation_costs",
{
"operating_component": component,
"operating_cost": doc.get(field),
},
)
doc.save()

View File

@@ -329,12 +329,16 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
@frappe.whitelist()
def get_timesheet_detail_rate(timelog, currency):
timelog_detail = frappe.db.sql(
f"""SELECT tsd.billing_amount as billing_amount,
ts.currency as currency FROM `tabTimesheet Detail` tsd
INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent
WHERE tsd.name = '{timelog}'""",
as_dict=1,
ts = frappe.qb.DocType("Timesheet")
ts_detail = frappe.qb.DocType("Timesheet Detail")
timelog_detail = (
frappe.qb.from_(ts_detail)
.inner_join(ts)
.on(ts.name == ts_detail.parent)
.select(ts_detail.billing_amount.as_("billing_amount"), ts.currency.as_("currency"))
.where(ts_detail.name == timelog)
.run(as_dict=1)
)[0]
if timelog_detail.currency:

View File

@@ -5,61 +5,69 @@
frappe.provide("erpnext.taxes");
erpnext.accounts.taxes = {
setup_tax_validations: function(doctype) {
setup_tax_validations: function (doctype) {
let me = this;
frappe.ui.form.on(doctype, {
setup: function(frm) {
setup: function (frm) {
// set conditional display for rate column in taxes
$(frm.wrapper).on('grid-row-render', function(e, grid_row) {
if(['Sales Taxes and Charges', 'Purchase Taxes and Charges'].includes(grid_row.doc.doctype)) {
$(frm.wrapper).on("grid-row-render", function (e, grid_row) {
if (
["Sales Taxes and Charges", "Purchase Taxes and Charges"].includes(
grid_row.doc.doctype
)
) {
me.set_conditional_mandatory_rate_or_amount(grid_row);
}
});
},
onload: function(frm) {
if(frm.get_field("taxes")) {
frm.set_query("account_head", "taxes", function(doc) {
if(frm.cscript.tax_table == "Sales Taxes and Charges") {
onload: function (frm) {
if (frm.get_field("taxes")) {
frm.set_query("account_head", "taxes", function (doc) {
if (frm.cscript.tax_table == "Sales Taxes and Charges") {
var account_type = ["Tax", "Chargeable", "Expense Account"];
} else {
var account_type = ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation"];
var account_type = [
"Tax",
"Chargeable",
"Income Account",
"Expenses Included In Valuation",
];
}
return {
query: "erpnext.controllers.queries.tax_account_query",
filters: {
"account_type": account_type,
"company": doc.company,
}
}
account_type: account_type,
company: doc.company,
},
};
});
frm.set_query("cost_center", "taxes", function(doc) {
frm.set_query("cost_center", "taxes", function (doc) {
return {
filters: {
"company": doc.company,
"is_group": 0
}
company: doc.company,
is_group: 0,
},
};
});
}
},
validate: function(frm) {
validate: function (frm) {
// neither is absolutely mandatory
if(frm.get_docfield("taxes")) {
if (frm.get_docfield("taxes")) {
frm.get_docfield("taxes", "rate").reqd = 0;
frm.get_docfield("taxes", "tax_amount").reqd = 0;
}
},
taxes_on_form_rendered: function(frm) {
taxes_on_form_rendered: function (frm) {
me.set_conditional_mandatory_rate_or_amount(frm.open_grid_row());
},
});
},
set_conditional_mandatory_rate_or_amount: function(grid_row) {
if(grid_row) {
if(grid_row.doc.charge_type==="Actual") {
set_conditional_mandatory_rate_or_amount: function (grid_row) {
if (grid_row) {
if (grid_row.doc.charge_type === "Actual") {
grid_row.toggle_editable("tax_amount", true);
grid_row.toggle_reqd("tax_amount", true);
grid_row.toggle_editable("rate", false);
@@ -73,31 +81,45 @@ erpnext.accounts.taxes = {
}
},
validate_taxes_and_charges: function(cdt, cdn) {
validate_taxes_and_charges: function (cdt, cdn) {
let d = locals[cdt][cdn];
let msg = "";
if (d.account_head && !d.description) {
// set description from account head
d.description = d.account_head.split(' - ').slice(0, -1).join(' - ');
d.description = d.account_head.split(" - ").slice(0, -1).join(" - ");
}
if (!d.charge_type && (d.row_id || d.rate || d.tax_amount)) {
msg = __("Please select Charge Type first");
d.row_id = "";
d.rate = d.tax_amount = 0.0;
} else if ((d.charge_type == 'Actual' || d.charge_type == 'On Net Total' || d.charge_type == 'On Paid Amount') && d.row_id) {
msg = __("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'");
} else if (
(d.charge_type == "Actual" ||
d.charge_type == "On Net Total" ||
d.charge_type == "On Paid Amount") &&
d.row_id
) {
msg = __(
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
);
d.row_id = "";
} else if ((d.charge_type == 'On Previous Row Amount' || d.charge_type == 'On Previous Row Total') && d.row_id) {
} else if (
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
d.row_id
) {
if (d.idx == 1) {
msg = __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row");
d.charge_type = '';
msg = __(
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
);
d.charge_type = "";
} else if (!d.row_id) {
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
d.row_id = "";
} else if (d.row_id && d.row_id >= d.idx) {
msg = __("Cannot refer row number greater than or equal to current row number for this Charge type");
msg = __(
"Cannot refer row number greater than or equal to current row number for this Charge type"
);
d.row_id = "";
}
}
@@ -106,13 +128,12 @@ erpnext.accounts.taxes = {
refresh_field("taxes");
frappe.throw(msg);
}
},
setup_tax_filters: function(doctype) {
setup_tax_filters: function (doctype) {
let me = this;
frappe.ui.form.on(doctype, {
account_head: function(frm, cdt, cdn) {
account_head: function (frm, cdt, cdn) {
let d = locals[cdt][cdn];
if (d.docstatus == 1) {
@@ -120,150 +141,157 @@ erpnext.accounts.taxes = {
return;
}
if(!d.charge_type && d.account_head){
if (!d.charge_type && d.account_head) {
frappe.msgprint(__("Please select Charge Type first"));
frappe.model.set_value(cdt, cdn, "account_head", "");
} else if (d.account_head) {
frappe.call({
type:"GET",
type: "GET",
method: "erpnext.controllers.accounts_controller.get_tax_rate",
args: {"account_head":d.account_head},
callback: function(r) {
if (d.charge_type!=="Actual") {
args: { account_head: d.account_head },
callback: function (r) {
if (d.charge_type !== "Actual") {
frappe.model.set_value(cdt, cdn, "rate", r.message.tax_rate || 0);
}
frappe.model.set_value(cdt, cdn, "description", r.message.account_name);
}
})
},
});
}
},
row_id: function(frm, cdt, cdn) {
row_id: function (frm, cdt, cdn) {
me.validate_taxes_and_charges(cdt, cdn);
},
rate: function(frm, cdt, cdn) {
rate: function (frm, cdt, cdn) {
me.validate_taxes_and_charges(cdt, cdn);
},
tax_amount: function(frm, cdt, cdn) {
tax_amount: function (frm, cdt, cdn) {
me.validate_taxes_and_charges(cdt, cdn);
},
charge_type: function(frm, cdt, cdn) {
charge_type: function (frm, cdt, cdn) {
me.validate_taxes_and_charges(cdt, cdn);
let open_form = frm.open_grid_row();
if(open_form) {
if (open_form) {
me.set_conditional_mandatory_rate_or_amount(open_form);
} else {
// apply in current row
me.set_conditional_mandatory_rate_or_amount(frm.get_field('taxes').grid.get_row(cdn));
me.set_conditional_mandatory_rate_or_amount(frm.get_field("taxes").grid.get_row(cdn));
}
},
included_in_print_rate: function(frm, cdt, cdn) {
included_in_print_rate: function (frm, cdt, cdn) {
let tax = frappe.get_doc(cdt, cdn);
try {
me.validate_taxes_and_charges(cdt, cdn);
me.validate_inclusive_tax(tax, frm);
} catch(e) {
} catch (e) {
tax.included_in_print_rate = 0;
refresh_field("included_in_print_rate", tax.name, tax.parentfield);
throw e;
}
}
},
});
},
validate_inclusive_tax: function(tax, frm) {
validate_inclusive_tax: function (tax, frm) {
this.frm = this.frm || frm;
let actual_type_error = function() {
var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
let actual_type_error = function () {
var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx]);
frappe.throw(msg);
};
let on_previous_row_error = function(row_range) {
var msg = __("For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included",
[tax.idx, __(tax.doctype), tax.charge_type, row_range])
let on_previous_row_error = function (row_range) {
var msg = __("For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included", [
tax.idx,
__(tax.doctype),
tax.charge_type,
row_range,
]);
frappe.throw(msg);
};
if(cint(tax.included_in_print_rate)) {
if(tax.charge_type == "Actual") {
if (cint(tax.included_in_print_rate)) {
if (tax.charge_type == "Actual") {
// inclusive tax cannot be of type Actual
actual_type_error();
} else if (tax.charge_type == "On Previous Row Amount" && this.frm &&
} else if (
tax.charge_type == "On Previous Row Amount" &&
this.frm &&
!cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_print_rate)
) {
// referred row should also be an inclusive tax
on_previous_row_error(tax.row_id);
} else if (tax.charge_type == "On Previous Row Total" && this.frm) {
var taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id),
function(t) { return cint(t.included_in_print_rate) ? null : t; });
if(taxes_not_included.length > 0) {
var taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id), function (t) {
return cint(t.included_in_print_rate) ? null : t;
});
if (taxes_not_included.length > 0) {
// all rows above this tax should be inclusive
on_previous_row_error(tax.row_id == 1 ? "1" : "1 - " + tax.row_id);
}
} else if(tax.category == "Valuation") {
} else if (tax.category == "Valuation") {
frappe.throw(__("Valuation type charges can not marked as Inclusive"));
}
}
}
}
},
};
erpnext.accounts.payment_triggers = {
setup: function(doctype) {
setup: function (doctype) {
frappe.ui.form.on(doctype, {
allocate_advances_automatically(frm) {
frm.trigger('fetch_advances');
frm.trigger("fetch_advances");
},
only_include_allocated_payments(frm) {
frm.trigger('fetch_advances');
frm.trigger("fetch_advances");
},
fetch_advances(frm) {
if(frm.doc.allocate_advances_automatically) {
if (frm.doc.allocate_advances_automatically) {
frappe.call({
doc: frm.doc,
method: "set_advances",
callback: function(r, rt) {
callback: function (r, rt) {
refresh_field("advances");
}
})
},
});
}
}
},
});
},
}
};
erpnext.accounts.pos = {
setup: function(doctype) {
setup: function (doctype) {
frappe.ui.form.on(doctype, {
mode_of_payment: function(frm, cdt, cdn) {
mode_of_payment: function (frm, cdt, cdn) {
var d = locals[cdt][cdn];
get_payment_mode_account(frm, d.mode_of_payment, function(account){
frappe.model.set_value(cdt, cdn, 'account', account)
})
}
get_payment_mode_account(frm, d.mode_of_payment, function (account) {
frappe.model.set_value(cdt, cdn, "account", account);
});
},
});
},
get_payment_mode_account: function(frm, mode_of_payment, callback) {
if(!frm.doc.company) {
frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")});
get_payment_mode_account: function (frm, mode_of_payment, callback) {
if (!frm.doc.company) {
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
}
if(!mode_of_payment) {
if (!mode_of_payment) {
return;
}
return frappe.call({
return frappe.call({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_bank_cash_account",
args: {
"mode_of_payment": mode_of_payment,
"company": frm.doc.company
mode_of_payment: mode_of_payment,
company: frm.doc.company,
},
callback: function(r, rt) {
if(r.message) {
callback(r.message.account)
callback: function (r, rt) {
if (r.message) {
callback(r.message.account);
}
}
},
});
}
}
},
};

View File

@@ -5,7 +5,7 @@ frappe.provide("erpnext.buying");
// cur_frm.add_fetch('project', 'cost_center', 'cost_center');
erpnext.buying = {
setup_buying_controller: function() {
setup_buying_controller: function () {
erpnext.buying.BuyingController = class BuyingController extends erpnext.TransactionController {
setup() {
super.setup();
@@ -17,11 +17,11 @@ erpnext.buying = {
this.setup_queries(doc, cdt, cdn);
super.onload();
this.frm.set_query('shipping_rule', function() {
this.frm.set_query("shipping_rule", function () {
return {
filters: {
"shipping_rule_type": "Buying"
}
shipping_rule_type: "Buying",
},
};
});
@@ -33,29 +33,28 @@ erpnext.buying = {
};
});
if (this.frm.doc.__islocal
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
var df = frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total");
var disable = cint(df.default) || cint(frappe.sys_defaults.disable_rounded_total);
this.frm.set_value("disable_rounded_total", disable);
if (
this.frm.doc.__islocal &&
frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")
) {
var df = frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total");
var disable = cint(df.default) || cint(frappe.sys_defaults.disable_rounded_total);
this.frm.set_value("disable_rounded_total", disable);
}
// no idea where me is coming from
if(this.frm.get_field('shipping_address')) {
if (this.frm.get_field("shipping_address")) {
this.frm.set_query("shipping_address", () => {
if(this.frm.doc.customer) {
if (this.frm.doc.customer) {
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: { link_doctype: 'Customer', link_name: this.frm.doc.customer }
query: "frappe.contacts.doctype.address.address.address_query",
filters: { link_doctype: "Customer", link_name: this.frm.doc.customer },
};
} else
return erpnext.queries.company_address_query(this.frm.doc)
} else return erpnext.queries.company_address_query(this.frm.doc);
});
}
if(this.frm.get_field('dispatch_address')) {
if (this.frm.get_field("dispatch_address")) {
this.frm.set_query("dispatch_address", () => {
return erpnext.queries.address_query(this.frm.doc);
});
@@ -65,76 +64,77 @@ erpnext.buying = {
setup_queries(doc, cdt, cdn) {
var me = this;
if(this.frm.fields_dict.buying_price_list) {
this.frm.set_query("buying_price_list", function() {
return{
filters: { 'buying': 1 }
}
if (this.frm.fields_dict.buying_price_list) {
this.frm.set_query("buying_price_list", function () {
return {
filters: { buying: 1 },
};
});
}
if(this.frm.fields_dict.tc_name) {
this.frm.set_query("tc_name", function() {
return{
filters: { 'buying': 1 }
}
if (this.frm.fields_dict.tc_name) {
this.frm.set_query("tc_name", function () {
return {
filters: { buying: 1 },
};
});
}
me.frm.set_query('supplier', erpnext.queries.supplier);
me.frm.set_query('contact_person', erpnext.queries.contact_query);
me.frm.set_query('supplier_address', erpnext.queries.address_query);
me.frm.set_query("supplier", erpnext.queries.supplier);
me.frm.set_query("contact_person", erpnext.queries.contact_query);
me.frm.set_query("supplier_address", erpnext.queries.address_query);
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
me.frm.set_query("billing_address", erpnext.queries.company_address_query);
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
this.frm.set_query("item_code", "items", function() {
this.frm.set_query("item_code", "items", function () {
if (me.frm.doc.is_subcontracted) {
var filters = {'supplier': me.frm.doc.supplier};
var filters = { supplier: me.frm.doc.supplier };
if (me.frm.doc.is_old_subcontracting_flow) {
filters["is_sub_contracted_item"] = 1;
}
else {
} else {
filters["is_stock_item"] = 0;
}
return{
return {
query: "erpnext.controllers.queries.item_query",
filters: filters
}
}
else {
return{
filters: filters,
};
} else {
return {
query: "erpnext.controllers.queries.item_query",
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1, 'has_variants': 0}
}
filters: { supplier: me.frm.doc.supplier, is_purchase_item: 1, has_variants: 0 },
};
}
});
this.frm.set_query("manufacturer", "items", function(doc, cdt, cdn) {
this.frm.set_query("manufacturer", "items", function (doc, cdt, cdn) {
const row = locals[cdt][cdn];
return {
query: "erpnext.controllers.queries.item_manufacturer_query",
filters:{ 'item_code': row.item_code }
}
filters: { item_code: row.item_code },
};
});
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
return me.set_query_for_item_tax_template(doc, cdt, cdn)
if (this.frm.fields_dict["items"].grid.get_field("item_code")) {
this.frm.set_query("item_tax_template", "items", function (doc, cdt, cdn) {
return me.set_query_for_item_tax_template(doc, cdt, cdn);
});
}
}
refresh(doc) {
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier'};
frappe.dynamic_link = { doc: this.frm.doc, fieldname: "supplier", doctype: "Supplier" };
this.frm.toggle_display("supplier_name",
(this.frm.doc.supplier_name && this.frm.doc.supplier_name!==this.frm.doc.supplier));
this.frm.toggle_display(
"supplier_name",
this.frm.doc.supplier_name && this.frm.doc.supplier_name !== this.frm.doc.supplier
);
if(this.frm.doc.docstatus==0 &&
(this.frm.doctype==="Purchase Order" || this.frm.doctype==="Material Request")) {
if (
this.frm.doc.docstatus == 0 &&
(this.frm.doctype === "Purchase Order" || this.frm.doctype === "Material Request")
) {
this.set_from_product_bundle();
}
@@ -143,45 +143,53 @@ erpnext.buying = {
}
toggle_subcontracting_fields() {
if (['Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty',
'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM');
if (["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
this.frm.fields_dict.supplied_items.grid.update_docfield_property(
"consumed_qty",
"read_only",
this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === "BOM"
);
this.frm.set_df_property('supplied_items', 'cannot_add_rows', 1);
this.frm.set_df_property('supplied_items', 'cannot_delete_rows', 1);
this.frm.set_df_property("supplied_items", "cannot_add_rows", 1);
this.frm.set_df_property("supplied_items", "cannot_delete_rows", 1);
}
}
supplier() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function(){
erpnext.utils.get_party_details(this.frm, null, null, function () {
me.apply_price_list();
});
}
company(){
if(!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
company() {
if (!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
frappe.call({
method: "erpnext.setup.doctype.company.company.get_billing_shipping_address",
args: {
name: this.frm.doc.company,
billing_address:this.frm.doc.billing_address,
shipping_address: this.frm.doc.shipping_address
billing_address: this.frm.doc.billing_address,
shipping_address: this.frm.doc.shipping_address,
},
callback: (r) => {
this.frm.set_value("billing_address", r.message.primary_address || "");
if(!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
this.frm.set_value("shipping_address", r.message.shipping_address || "");
},
});
erpnext.utils.set_letter_head(this.frm)
erpnext.utils.set_letter_head(this.frm);
}
supplier_address() {
erpnext.utils.get_address_display(this.frm);
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");
erpnext.utils.set_taxes_from_address(
this.frm,
"supplier_address",
"supplier_address",
"supplier_address"
);
}
buying_price_list() {
@@ -201,24 +209,33 @@ erpnext.buying = {
}
qty(doc, cdt, cdn) {
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && doc.update_stock)) {
this.calculate_received_qty(doc, cdt, cdn)
if (
doc.doctype == "Purchase Receipt" ||
(doc.doctype == "Purchase Invoice" && doc.update_stock)
) {
this.calculate_received_qty(doc, cdt, cdn);
}
super.qty(doc, cdt, cdn);
}
rejected_qty(doc, cdt, cdn) {
this.calculate_received_qty(doc, cdt, cdn)
this.calculate_received_qty(doc, cdt, cdn);
}
calculate_received_qty(doc, cdt, cdn){
calculate_received_qty(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
frappe.model.round_floats_in(item, ["qty", "rejected_qty"]);
if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["qty", "rejected_qty"])){ return }
if (
!doc.is_return &&
this.validate_negative_quantity(cdt, cdn, item, ["qty", "rejected_qty"])
) {
return;
}
let received_qty = flt(item.qty + item.rejected_qty, precision("received_qty", item));
let received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(received_qty);
let received_stock_qty =
flt(item.conversion_factor, precision("conversion_factor", item)) * flt(received_qty);
frappe.model.set_value(cdt, cdn, "received_qty", received_qty);
frappe.model.set_value(cdt, cdn, "received_stock_qty", received_stock_qty);
@@ -228,24 +245,32 @@ erpnext.buying = {
super.batch_no(doc, cdt, cdn);
}
validate_negative_quantity(cdt, cdn, item, fieldnames){
if(!item || !fieldnames) { return }
validate_negative_quantity(cdt, cdn, item, fieldnames) {
if (!item || !fieldnames) {
return;
}
var is_negative_qty = false;
for(var i = 0; i<fieldnames.length; i++) {
if(item[fieldnames[i]] < 0){
frappe.msgprint(__("Row #{0}: {1} can not be negative for item {2}", [item.idx,__(frappe.meta.get_label(cdt, fieldnames[i], cdn)), item.item_code]));
for (var i = 0; i < fieldnames.length; i++) {
if (item[fieldnames[i]] < 0) {
frappe.msgprint(
__("Row #{0}: {1} can not be negative for item {2}", [
item.idx,
__(frappe.meta.get_label(cdt, fieldnames[i], cdn)),
item.item_code,
])
);
is_negative_qty = true;
break;
}
}
return is_negative_qty
return is_negative_qty;
}
warehouse(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
if(item.item_code && item.warehouse) {
if (item.item_code && item.warehouse) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_bin_details",
child: item,
@@ -253,22 +278,21 @@ erpnext.buying = {
item_code: item.item_code,
warehouse: item.warehouse,
company: doc.company,
include_child_warehouses: true
}
include_child_warehouses: true,
},
});
}
}
project(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
if(item.project) {
$.each(this.frm.doc["items"] || [],
function(i, other_item) {
if(!other_item.project) {
other_item.project = item.project;
refresh_field("project", other_item.name, other_item.parentfield);
}
});
if (item.project) {
$.each(this.frm.doc["items"] || [], function (i, other_item) {
if (!other_item.project) {
other_item.project = item.project;
refresh_field("project", other_item.name, other_item.parentfield);
}
});
}
}
@@ -281,7 +305,7 @@ erpnext.buying = {
category(doc, cdt, cdn) {
// should be the category field of tax table
if(cdt != doc.doctype) {
if (cdt != doc.doctype) {
this.calculate_taxes_and_totals();
}
}
@@ -291,26 +315,42 @@ erpnext.buying = {
set_from_product_bundle() {
var me = this;
this.frm.add_custom_button(__("Product Bundle"), function() {
erpnext.buying.get_items_from_product_bundle(me.frm);
}, __("Get Items From"));
this.frm.add_custom_button(
__("Product Bundle"),
function () {
erpnext.buying.get_items_from_product_bundle(me.frm);
},
__("Get Items From")
);
}
shipping_address(){
shipping_address() {
var me = this;
erpnext.utils.get_address_display(this.frm, "shipping_address",
"shipping_address_display", true);
erpnext.utils.get_address_display(
this.frm,
"shipping_address",
"shipping_address_display",
true
);
}
dispatch_address(){
dispatch_address() {
var me = this;
erpnext.utils.get_address_display(this.frm, "dispatch_address",
"dispatch_address_display", true);
erpnext.utils.get_address_display(
this.frm,
"dispatch_address",
"dispatch_address_display",
true
);
}
billing_address() {
erpnext.utils.get_address_display(this.frm, "billing_address",
"billing_address_display", true);
erpnext.utils.get_address_display(
this.frm,
"billing_address",
"billing_address_display",
true
);
}
tc_name() {
@@ -320,37 +360,43 @@ erpnext.buying = {
update_auto_repeat_reference(doc) {
if (doc.auto_repeat) {
frappe.call({
method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference",
args:{
method: "frappe.automation.doctype.auto_repeat.auto_repeat.update_reference",
args: {
docname: doc.auto_repeat,
reference:doc.name
reference: doc.name,
},
callback: function(r){
if (r.message=="success") {
frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'});
callback: function (r) {
if (r.message == "success") {
frappe.show_alert({
message: __("Auto repeat document updated"),
indicator: "green",
});
} else {
frappe.show_alert({message:__("An error occurred during the update process"), indicator:'red'});
frappe.show_alert({
message: __("An error occurred during the update process"),
indicator: "red",
});
}
}
})
},
});
}
}
manufacturer(doc, cdt, cdn) {
const row = locals[cdt][cdn];
if(row.manufacturer) {
if (row.manufacturer) {
frappe.call({
method: "erpnext.stock.doctype.item_manufacturer.item_manufacturer.get_item_manufacturer_part_no",
args: {
'item_code': row.item_code,
'manufacturer': row.manufacturer
item_code: row.item_code,
manufacturer: row.manufacturer,
},
callback: function(r) {
callback: function (r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, 'manufacturer_part_no', r.message);
frappe.model.set_value(cdt, cdn, "manufacturer_part_no", r.message);
}
}
},
});
}
}
@@ -359,19 +405,22 @@ erpnext.buying = {
const row = locals[cdt][cdn];
if (row.manufacturer_part_no) {
frappe.model.get_value('Item Manufacturer',
frappe.model.get_value(
"Item Manufacturer",
{
'item_code': row.item_code,
'manufacturer': row.manufacturer,
'manufacturer_part_no': row.manufacturer_part_no
item_code: row.item_code,
manufacturer: row.manufacturer,
manufacturer_part_no: row.manufacturer_part_no,
},
'name',
function(data) {
"name",
function (data) {
if (!data) {
let msg = {
message: __("Manufacturer Part Number <b>{0}</b> is invalid", [row.manufacturer_part_no]),
title: __("Invalid Part Number")
}
message: __("Manufacturer Part Number <b>{0}</b> is invalid", [
row.manufacturer_part_no,
]),
title: __("Invalid Part Number"),
};
frappe.throw(msg);
}
}
@@ -384,40 +433,42 @@ erpnext.buying = {
let me = this;
let fields = ["has_batch_no", "has_serial_no"];
frappe.db.get_value("Item", item.item_code, fields)
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
fields.forEach((field) => {
item[field] = r.message[field];
});
frappe.db.get_value("Item", item.item_code, fields).then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
fields.forEach((field) => {
item[field] = r.message[field];
});
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false;
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false;
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
);
}
});
let update_values = {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
qty:
qty /
flt(
item.conversion_factor || 1,
precision("conversion_factor", item)
),
};
if (r.warehouse) {
update_values["warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
});
}
});
}
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
@@ -425,142 +476,147 @@ erpnext.buying = {
let me = this;
let fields = ["has_batch_no", "has_serial_no"];
frappe.db.get_value("Item", item.item_code, fields)
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
fields.forEach((field) => {
item[field] = r.message[field];
});
frappe.db.get_value("Item", item.item_code, fields).then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
fields.forEach((field) => {
item[field] = r.message[field];
});
item.type_of_transaction = !doc.is_return > 0 ? "Inward" : "Outward";
item.is_rejected = true;
item.type_of_transaction = !doc.is_return > 0 ? "Inward" : "Outward";
item.is_rejected = true;
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
let update_values = {
"rejected_serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["rejected_warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
);
}
});
let update_values = {
rejected_serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
rejected_qty:
qty /
flt(
item.conversion_factor || 1,
precision("conversion_factor", item)
),
};
if (r.warehouse) {
update_values["rejected_warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
});
}
});
}
};
}
}
},
};
erpnext.buying.link_to_mrs = function(frm) {
erpnext.buying.link_to_mrs = function (frm) {
frappe.call({
method: "erpnext.buying.utils.get_linked_material_requests",
args:{
items: frm.doc.items.map((item) => item.item_code)
args: {
items: frm.doc.items.map((item) => item.item_code),
},
callback: function(r) {
callback: function (r) {
if (!r.message || r.message.length == 0) {
frappe.throw({
message: __("No pending Material Requests found to link for the given items."),
title: __("Note")
title: __("Note"),
});
}
var item_length = frm.doc.items.length;
for (let item of frm.doc.items) {
var qty = item.qty;
(r.message[0] || []).forEach(function(d) {
if (d.qty > 0 && qty > 0 && item.item_code == d.item_code && !item.material_request_item)
{
(r.message[0] || []).forEach(function (d) {
if (
d.qty > 0 &&
qty > 0 &&
item.item_code == d.item_code &&
!item.material_request_item
) {
item.material_request = d.mr_name;
item.material_request_item = d.mr_item;
var my_qty = Math.min(qty, d.qty);
qty = qty - my_qty;
d.qty = d.qty - my_qty;
item.stock_qty = my_qty*item.conversion_factor;
item.stock_qty = my_qty * item.conversion_factor;
item.qty = my_qty;
frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + item.idx + ")");
if (qty > 0)
{
frappe.msgprint(
"Assigning " + d.mr_name + " to " + d.item_code + " (row " + item.idx + ")"
);
if (qty > 0) {
frappe.msgprint("Splitting " + qty + " units of " + d.item_code);
var newrow = frappe.model.add_child(frm.doc, item.doctype, "items");
item_length++;
for (var key in item)
{
for (var key in item) {
newrow[key] = item[key];
}
newrow.idx = item_length;
newrow["stock_qty"] = newrow.conversion_factor*qty;
newrow["stock_qty"] = newrow.conversion_factor * qty;
newrow["qty"] = qty;
newrow["material_request"] = "";
newrow["material_request_item"] = "";
}
}
});
}
refresh_field("items");
}
},
});
}
};
erpnext.buying.get_default_bom = function(frm) {
$.each(frm.doc["items"] || [], function(i, d) {
erpnext.buying.get_default_bom = function (frm) {
$.each(frm.doc["items"] || [], function (i, d) {
if (d.item_code && d.bom === "") {
return frappe.call({
type: "GET",
method: "erpnext.stock.get_item_details.get_default_bom",
args: {
"item_code": d.item_code,
item_code: d.item_code,
},
callback: function(r) {
if(r) {
callback: function (r) {
if (r) {
frappe.model.set_value(d.doctype, d.name, "bom", r.message);
}
}
})
},
});
}
});
}
};
erpnext.buying.get_items_from_product_bundle = function(frm) {
erpnext.buying.get_items_from_product_bundle = function (frm) {
var dialog = new frappe.ui.Dialog({
title: __("Get Items from Product Bundle"),
fields: [
{
"fieldtype": "Link",
"label": __("Product Bundle"),
"fieldname": "product_bundle",
"options":"Product Bundle",
"reqd": 1
fieldtype: "Link",
label: __("Product Bundle"),
fieldname: "product_bundle",
options: "Product Bundle",
reqd: 1,
},
{
"fieldtype": "Currency",
"label": __("Quantity"),
"fieldname": "quantity",
"reqd": 1,
"default": 1
}
fieldtype: "Currency",
label: __("Quantity"),
fieldname: "quantity",
reqd: 1,
default: 1,
},
],
primary_action_label: 'Get Items',
primary_action(args){
if(!args) return;
primary_action_label: "Get Items",
primary_action(args) {
if (!args) return;
dialog.hide();
return frappe.call({
type: "GET",
@@ -581,44 +637,44 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
is_subcontracted: frm.doc.is_subcontracted,
transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
doctype: frm.doc.doctype
doctype: frm.doc.doctype,
},
},
freeze: true,
callback: function(r) {
const first_row_is_empty = function(child_table){
if($.isArray(child_table) && child_table.length > 0) {
callback: function (r) {
const first_row_is_empty = function (child_table) {
if ($.isArray(child_table) && child_table.length > 0) {
return !child_table[0].item_code;
}
return false;
};
const remove_empty_first_row = function(frm){
if (first_row_is_empty(frm.doc.items)){
frm.doc.items = frm.doc.items.splice(1);
const remove_empty_first_row = function (frm) {
if (first_row_is_empty(frm.doc.items)) {
frm.doc.items = frm.doc.items.splice(1);
}
};
if(!r.exc && r.message) {
if (!r.exc && r.message) {
remove_empty_first_row(frm);
for (var i=0; i< r.message.length; i++) {
for (var i = 0; i < r.message.length; i++) {
var d = frm.add_child("items");
var item = r.message[i];
for (var key in item) {
for (var key in item) {
if (!is_null(item[key]) && key !== "doctype") {
d[key] = item[key];
}
}
if(frappe.meta.get_docfield(d.doctype, "price_list_rate", d.name)) {
if (frappe.meta.get_docfield(d.doctype, "price_list_rate", d.name)) {
frm.script_manager.trigger("price_list_rate", d.doctype, d.name);
}
}
frm.refresh_field("items");
}
}
})
}
},
});
},
});
dialog.show();
}
};

View File

@@ -11,13 +11,13 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
}
}
barcode(doc, cdt, cdn) {
barcode(doc, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.barcode) {
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
frappe.model.set_value(cdt, cdn, {
"item_code": r.message.item_code,
"qty": 1,
item_code: r.message.item_code,
qty: 1,
});
});
}
@@ -25,74 +25,81 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
setup_warehouse_query() {
var me = this;
erpnext.queries.setup_queries(this.frm, "Warehouse", function() {
erpnext.queries.setup_queries(this.frm, "Warehouse", function () {
return erpnext.queries.warehouse(me.frm.doc);
});
}
setup_posting_date_time_check() {
// make posting date default and read only unless explictly checked
frappe.ui.form.on(this.frm.doctype, 'set_posting_date_and_time_read_only', function(frm) {
if(frm.doc.docstatus == 0 && frm.doc.set_posting_time) {
frm.set_df_property('posting_date', 'read_only', 0);
frm.set_df_property('posting_time', 'read_only', 0);
frappe.ui.form.on(this.frm.doctype, "set_posting_date_and_time_read_only", function (frm) {
if (frm.doc.docstatus == 0 && frm.doc.set_posting_time) {
frm.set_df_property("posting_date", "read_only", 0);
frm.set_df_property("posting_time", "read_only", 0);
} else {
frm.set_df_property('posting_date', 'read_only', 1);
frm.set_df_property('posting_time', 'read_only', 1);
frm.set_df_property("posting_date", "read_only", 1);
frm.set_df_property("posting_time", "read_only", 1);
}
})
frappe.ui.form.on(this.frm.doctype, 'set_posting_time', function(frm) {
frm.trigger('set_posting_date_and_time_read_only');
});
frappe.ui.form.on(this.frm.doctype, 'refresh', function(frm) {
frappe.ui.form.on(this.frm.doctype, "set_posting_time", function (frm) {
frm.trigger("set_posting_date_and_time_read_only");
});
frappe.ui.form.on(this.frm.doctype, "refresh", function (frm) {
// set default posting date / time
if(frm.doc.docstatus==0) {
if(!frm.doc.posting_date) {
frm.set_value('posting_date', frappe.datetime.nowdate());
if (frm.doc.docstatus == 0) {
if (!frm.doc.posting_date) {
frm.set_value("posting_date", frappe.datetime.nowdate());
}
if(!frm.doc.posting_time) {
frm.set_value('posting_time', frappe.datetime.now_time());
if (!frm.doc.posting_time) {
frm.set_value("posting_time", frappe.datetime.now_time());
}
frm.trigger('set_posting_date_and_time_read_only');
frm.trigger("set_posting_date_and_time_read_only");
}
});
}
show_stock_ledger() {
var me = this;
if(this.frm.doc.docstatus > 0) {
cur_frm.add_custom_button(__("Stock Ledger"), function() {
frappe.route_options = {
voucher_no: me.frm.doc.name,
from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company,
show_cancelled_entries: me.frm.doc.docstatus === 2,
ignore_prepared_report: true
};
frappe.set_route("query-report", "Stock Ledger");
}, __("View"));
if (this.frm.doc.docstatus > 0) {
cur_frm.add_custom_button(
__("Stock Ledger"),
function () {
frappe.route_options = {
voucher_no: me.frm.doc.name,
from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format("YYYY-MM-DD"),
company: me.frm.doc.company,
show_cancelled_entries: me.frm.doc.docstatus === 2,
ignore_prepared_report: true,
};
frappe.set_route("query-report", "Stock Ledger");
},
__("View")
);
}
}
show_general_ledger() {
let me = this;
if(this.frm.doc.docstatus > 0) {
cur_frm.add_custom_button(__('Accounting Ledger'), function() {
frappe.route_options = {
voucher_no: me.frm.doc.name,
from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company,
categorize_by: "Categorize by Voucher (Consolidated)",
show_cancelled_entries: me.frm.doc.docstatus === 2,
ignore_prepared_report: true
};
frappe.set_route("query-report", "General Ledger");
}, __("View"));
if (this.frm.doc.docstatus > 0) {
cur_frm.add_custom_button(
__("Accounting Ledger"),
function () {
frappe.route_options = {
voucher_no: me.frm.doc.name,
from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format("YYYY-MM-DD"),
company: me.frm.doc.company,
categorize_by: "Categorize by Voucher (Consolidated)",
show_cancelled_entries: me.frm.doc.docstatus === 2,
ignore_prepared_report: true,
};
frappe.set_route("query-report", "General Ledger");
},
__("View")
);
}
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1828,7 +1828,7 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
wo.reload()
so.reload()
self.assertEqual(so.items[0].work_order_qty, wo.produced_qty)
self.assertEqual(mr.status, "Manufactured")
self.assertEqual(mr.status, "Ordered")
def test_sales_order_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule

View File

@@ -450,7 +450,6 @@ erpnext.PointOfSale.Payment = class {
}
render_payment_section() {
this.grand_total_to_default_mop();
this.render_payment_mode_dom();
this.make_invoice_field_dialog();
this.update_totals_section();
@@ -498,17 +497,6 @@ erpnext.PointOfSale.Payment = class {
}
}
grand_total_to_default_mop() {
if (this.set_gt_to_default_mop) return;
const doc = this.events.get_frm().doc;
const payments = doc.payments;
payments.forEach((p) => {
if (p.default) {
frappe.model.set_value(p.doctype, p.name, "amount", 0);
}
});
}
render_payment_mode_dom() {
const doc = this.events.get_frm().doc;
const payments = doc.payments;

View File

@@ -294,6 +294,10 @@ def install(country=None):
{"doctype": "Market Segment", "market_segment": _("Upper Income")},
# Warehouse Type
{"doctype": "Warehouse Type", "name": "Transit"},
{"doctype": "Workstation Operating Component", "component_name": _("Electricity")},
{"doctype": "Workstation Operating Component", "component_name": _("Consumables")},
{"doctype": "Workstation Operating Component", "component_name": _("Rent")},
{"doctype": "Workstation Operating Component", "component_name": _("Wages")},
]
for doctype, title_field, filename in (
@@ -481,14 +485,19 @@ def install_defaults(args=None): # nosemgrep
create_bank_account(args)
def set_global_defaults(args):
def set_global_defaults(kwargs):
global_defaults = frappe.get_doc("Global Defaults", "Global Defaults")
company = frappe.db.get_value(
"Company",
{"company_name": kwargs.get("company_name")},
"name",
)
global_defaults.update(
{
"default_currency": args.get("currency"),
"default_company": args.get("company_name"),
"country": args.get("country"),
"default_currency": kwargs.get("currency"),
"default_company": company,
"country": kwargs.get("country"),
}
)

View File

@@ -667,13 +667,13 @@ def prepare_data_for_internal_transfer():
company = "_Test Company with perpetual inventory"
customer = create_internal_customer(
"_Test Internal Customer 3",
"_Test Internal Customer 2",
company,
company,
)
supplier = create_internal_supplier(
"_Test Internal Supplier 3",
"_Test Internal Supplier 2",
company,
company,
)

View File

@@ -11,6 +11,7 @@ import frappe
import frappe.defaults
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Order
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
@@ -624,39 +625,44 @@ def get_items_based_on_default_supplier(supplier):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters):
conditions = ""
if txt:
conditions += "and mr.name like '%%" + txt + "%%' "
if filters.get("transaction_date"):
date = filters.get("transaction_date")[1]
conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' "
supplier = filters.get("supplier")
supplier_items = get_items_based_on_default_supplier(supplier)
if not supplier_items:
frappe.throw(_("{0} is not the default supplier for any items.").format(supplier))
material_requests = frappe.db.sql(
"""select distinct mr.name, transaction_date,company
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr.name = mr_item.parent
and mr_item.item_code in ({})
and mr.material_request_type = 'Purchase'
and mr.per_ordered < 99.99
and mr.docstatus = 1
and mr.status != 'Stopped'
and mr.company = %s
{}
order by mr_item.item_code ASC
limit {} offset {} """.format(
", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start)
),
(*tuple(supplier_items), filters.get("company")),
as_dict=1,
mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")
query = (
frappe.qb.from_(mr)
.from_(mr_item)
.select(mr.name)
.distinct()
.select(mr.transaction_date, mr.company)
.where(
(mr.name == mr_item.parent)
& (mr_item.item_code.isin(supplier_items))
& (mr.material_request_type == "Purchase")
& (mr.per_ordered < 99.99)
& (mr.docstatus == 1)
& (mr.status != "Stopped")
& (mr.company == filters.get("company"))
)
.orderby(mr_item.item_code, order=Order.asc)
.limit(cint(page_len))
.offset(cint(start))
)
if txt:
query = query.where(mr.name.like(f"%%{txt}%%"))
if filters.get("transaction_date"):
date = filters.get("transaction_date")[1]
query = query.where(mr.transaction_date[date[0] : date[1]])
material_requests = query.run(as_dict=True)
return material_requests

View File

@@ -35,7 +35,7 @@ frappe.listview_settings["Material Request"] = {
return [__("Partially Received"), "yellow", "per_received,<,100"];
} else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) {
return [__("Received"), "green", "per_received,=,100"];
} else if (doc.material_request_type == "Purchase") {
} else if (["Purchase", "Manufacture"].includes(doc.material_request_type)) {
return [__("Ordered"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Material Transfer") {
return [__("Transferred"), "green", "per_ordered,=,100"];
@@ -43,8 +43,6 @@ frappe.listview_settings["Material Request"] = {
return [__("Issued"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Customer Provided") {
return [__("Received"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Manufacture") {
return [__("Manufactured"), "green", "per_ordered,=,100"];
}
}
},

View File

@@ -916,6 +916,23 @@ class TestMaterialRequest(IntegrationTestCase):
for perm in permissions:
perm.delete()
def test_manufacture_type_status_over_wo(self):
from erpnext.stock.doctype.material_request.material_request import raise_work_orders
mr = make_material_request(
item_code="_Test FG Item", material_request_type="Manufacture", do_not_submit=False
)
work_order = raise_work_orders(mr.name)
wo = frappe.get_doc("Work Order", work_order[0])
wo.wip_warehouse = "_Test Warehouse 1 - _TC"
wo.submit()
mr.reload()
self.assertEqual(mr.per_ordered, 100)
self.assertEqual(mr.status, "Ordered")
def get_in_transit_warehouse(company):
if not frappe.db.exists("Warehouse Type", "Transit"):

Some files were not shown because too many files have changed in this diff Show More