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: ignore_title_keywords:
- "sync translations" - "sync translations"
- "update POT file" - "update POT file"
review_status: false

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
-> Resolves dunning automatically -> Resolves dunning automatically
""" """
import json import json
import frappe import frappe
@@ -163,43 +164,66 @@ class Dunning(AccountsController):
] ]
def resolve_dunning(doc, state): def update_linked_dunnings(doc, previous_outstanding_amount):
""" if (
Check if all payments have been made and resolve dunning, if yes. Called doc.doctype != "Sales Invoice"
when a Payment Entry is submitted. or doc.is_return
""" or previous_outstanding_amount == doc.outstanding_amount
for reference in doc.references: ):
# Consider partial and full payments: return
# 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
if reference.reference_doctype == "Sales Invoice" and ( to_resolve = doc.outstanding_amount < previous_outstanding_amount
submit_condition if doc.docstatus == 1 else cancel_condition state = "Unresolved" if to_resolve else "Resolved"
): dunnings = get_linked_dunnings_as_per_state(doc.name, state)
state = "Resolved" if doc.docstatus == 2 else "Unresolved" if not dunnings:
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state) return
for dunning in dunnings: dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
resolve = True invoices = set()
dunning = frappe.get_doc("Dunning", dunning.get("name")) payment_schedule_ids = set()
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)
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: invoice_outstanding_amounts = dict(
dunning.status = new_status frappe.get_all(
dunning.save() "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): 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(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved") 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): def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -1 * overdue_days) 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() @frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None): 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 from frappe.model.mapper import get_mapped_doc
def post_process(source, target): def post_process(source, target):

View File

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

View File

@@ -200,9 +200,9 @@ class PaymentEntry(AccountsController):
if self.difference_amount: if self.difference_amount:
frappe.throw(_("Difference Amount must be zero")) frappe.throw(_("Difference Amount must be zero"))
self.update_payment_requests() self.update_payment_requests()
self.update_payment_schedule()
self.make_gl_entries() self.make_gl_entries()
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_payment_schedule()
self.set_status() self.set_status()
def validate_for_repost(self): def validate_for_repost(self):
@@ -303,10 +303,10 @@ class PaymentEntry(AccountsController):
) )
super().on_cancel() super().on_cancel()
self.update_payment_requests(cancel=True) self.update_payment_requests(cancel=True)
self.update_payment_schedule(cancel=1)
self.make_gl_entries(cancel=1) self.make_gl_entries(cancel=1)
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.delink_advance_entry_references() self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_status() self.set_status()
def update_payment_requests(self, cancel=False): 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); 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) { 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.meta.default_print_format = r.message.print_format || "";
this.frm.doc.campaign = r.message.campaign; this.frm.doc.campaign = r.message.campaign;
this.frm.allow_print_before_pay = r.message.allow_print_before_pay; 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.frm.script_manager.trigger("update_stock");
this.calculate_taxes_and_totals(); this.calculate_taxes_and_totals();

View File

@@ -717,7 +717,13 @@ class POSInvoice(SalesInvoice):
"Account", self.debit_to, "account_currency" "Account", self.debit_to, "account_currency"
) )
if not self.due_date and self.customer: 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) super(SalesInvoice, self).set_missing_values(for_validate)
@@ -732,6 +738,7 @@ class POSInvoice(SalesInvoice):
"utm_campaign": profile.get("utm_campaign"), "utm_campaign": profile.get("utm_campaign"),
"utm_medium": profile.get("utm_medium"), "utm_medium": profile.get("utm_medium"),
"allow_print_before_pay": profile.get("allow_print_before_pay"), "allow_print_before_pay": profile.get("allow_print_before_pay"),
"set_default_payment": profile.get("set_grand_total_to_default_mop"),
} }
@frappe.whitelist() @frappe.whitelist()

View File

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

View File

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

View File

@@ -343,7 +343,12 @@ class PurchaseInvoice(BuyingController):
) )
if not self.due_date: if not self.due_date:
self.due_date = get_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") 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.script_manager.trigger("is_pos");
me.frm.refresh_fields(); 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); erpnext.queries.setup_warehouse_query(this.frm);
} }
@@ -512,8 +519,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}, },
callback: function (r) { callback: function (r) {
if (!r.exc) { if (!r.exc) {
if (r.message && r.message.print_format) { if (r.message) {
me.frm.pos_print_format = r.message.print_format; me.frm.pos_print_format = r.message.print_format;
me.frm.set_default_payment = r.message.set_default_payment;
} }
me.frm.trigger("update_stock"); me.frm.trigger("update_stock");
if (me.frm.doc.taxes_and_charges) { if (me.frm.doc.taxes_and_charges) {

View File

@@ -764,6 +764,7 @@ class SalesInvoice(SellingController):
"utm_campaign": pos.get("utm_campaign"), "utm_campaign": pos.get("utm_campaign"),
"utm_medium": pos.get("utm_medium"), "utm_medium": pos.get("utm_medium"),
"allow_print_before_pay": pos.get("allow_print_before_pay"), "allow_print_before_pay": pos.get("allow_print_before_pay"),
"set_default_payment": pos.get("set_grand_total_to_default_mop", 1),
} }
@frappe.whitelist() @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) asset_categories = get_asset_categories_for_grouped_by_category(filters)
assets = get_assets_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: for asset_category in asset_categories:
row = frappe._dict() row = frappe._dict()
row.update(asset_category) 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 = ( row.value_as_on_to_date = (
flt(row.value_as_on_from_date) flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase) + flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset) - flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset) - flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset) - flt(row.value_of_capitalized_asset)
+ flt(row.adjustment_during_period)
) )
row.update( 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): def get_group_by_asset_data(filters):
data = [] data = []
asset_details = get_asset_details_for_grouped_by_category(filters) asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(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: for asset_detail in asset_details:
row = frappe._dict() row = frappe._dict()
row.update(asset_detail) 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 = ( row.value_as_on_to_date = (
flt(row.value_as_on_from_date) flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase) + flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset) - flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset) - flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_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 = ( row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date) flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period) + 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): def get_columns(filters):
columns = [] columns = []

View File

@@ -1,17 +1,21 @@
{ {
"add_total_row": 0, "add_total_row": 1,
"apply_user_permissions": 1, "add_translate_data": 0,
"columns": [],
"creation": "2013-12-06 13:22:23", "creation": "2013-12-06 13:22:23",
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"filters": [],
"idx": 3, "idx": 3,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2017-02-24 20:17:51.995451", "letterhead": null,
"modified": "2025-08-13 12:47:27.645023",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "General Ledger", "name": "General Ledger",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry", "ref_doctype": "GL Entry",
"report_name": "General Ledger", "report_name": "General Ledger",
"report_type": "Script Report", "report_type": "Script Report",
@@ -25,5 +29,6 @@
{ {
"role": "Auditor" "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")) query = query.where(si.posting_date <= filters.get("to_date"))
if filters.get("mode_of_payment"): 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 filters.get("warehouse"):
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"): 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) frappe.qb.from_(si)
.join(sii) .join(sii)
.on(si.name == sii.parent) .on(si.name == sii.parent)
.left_join(sip)
.on(sip.parent == si.name)
.left_join(item) .left_join(item)
.on(sii.item_code == item.name) .on(sii.item_code == item.name)
.select( .select(
@@ -466,7 +470,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
si.update_stock, si.update_stock,
sii.uom, sii.uom,
sii.qty, sii.qty,
sip.mode_of_payment,
) )
.where(si.docstatus == 1) .where(si.docstatus == 1)
.where(sii.parenttype == doctype) .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): 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: if not voucher_type or not voucher_no:
return return
@@ -1969,6 +1971,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
outstanding = voucher_outstanding[0] outstanding = voucher_outstanding[0]
ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no) ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no)
previous_outstanding_amount = ref_doc.outstanding_amount
outstanding_amount = flt( outstanding_amount = flt(
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount") 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, outstanding_amount,
) )
update_linked_dunnings(ref_doc, previous_outstanding_amount)
ref_doc.set_status(update=True) ref_doc.set_status(update=True)
ref_doc.notify_update() 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): def make_journal_entry(asset_name):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
( (
_, fixed_asset_account,
accumulated_depreciation_account, accumulated_depreciation_account,
depreciation_expense_account, depreciation_expense_account,
) = get_depreciation_accounts(asset.asset_category, asset.company) ) = get_depreciation_accounts(asset.asset_category, asset.company)

View File

@@ -627,6 +627,9 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
docstatus: 1, docstatus: 1,
status: ["not in", ["Stopped", "Expired"]], 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") __("Get Items From")

View File

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

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
@@ -235,7 +237,12 @@ def get_list_context(context=None):
@frappe.whitelist() @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): def set_missing_values(source, target):
target.run_method("set_missing_values") target.run_method("set_missing_values")
target.run_method("get_schedule_dates") 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): def update_item(obj, target, source_parent):
target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) 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( doclist = get_mapped_doc(
"Supplier Quotation", "Supplier Quotation",
source_name, source_name,
@@ -265,6 +277,7 @@ def make_purchase_order(source_name, target_doc=None):
["sales_order", "sales_order"], ["sales_order", "sales_order"],
], ],
"postprocess": update_item, "postprocess": update_item,
"condition": select_item,
}, },
"Purchase Taxes and Charges": { "Purchase Taxes and Charges": {
"doctype": "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.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql( bo = frappe.qb.DocType("Blanket Order")
"""select distinct bo.name, bo.blanket_order_type, bo.to_date bo_item = frappe.qb.DocType("Blanket Order Item")
from `tabBlanket Order` bo, `tabBlanket Order Item` boi
where blanket_orders = (
boi.parent = bo.name frappe.qb.from_(bo)
and boi.item_code = {item_code} .from_(bo_item)
and bo.blanket_order_type = '{blanket_order_type}' .select(bo.name)
and bo.company = {company} .distinct()
and bo.docstatus = 1""".format( .select(bo.blanket_order_type, bo.to_date)
item_code=frappe.db.escape(filters.get("item")), .where(
blanket_order_type=filters.get("blanket_order_type"), (bo_item.parent == bo.name)
company=frappe.db.escape(filters.get("company")), & (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.whitelist()
@frappe.validate_and_sanitize_search_inputs @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"): if filters.get("company"):
condition += "and tabAccount.company = %(company)s" 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( return frappe.db.sql(
f"""select tabAccount.name from `tabAccount` 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 and tabAccount.`{searchfield}` LIKE %(txt)s
{condition} {get_match_cond(doctype)} {condition} {get_match_cond(doctype)}
order by idx desc, name""", 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"], ["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
[ [
"Ordered", "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", "Transferred",
@@ -142,10 +142,6 @@ status_map = {
"Partially Ordered", "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'", "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": [ "POS Opening Entry": [
["Draft", None], ["Draft", None],

View File

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

View File

@@ -360,7 +360,9 @@ doc_events = {
"erpnext.regional.create_transaction_log", "erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit", "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", "on_trash": "erpnext.regional.check_deletion_permission",
}, },
"Purchase Invoice": { "Purchase Invoice": {
@@ -372,9 +374,7 @@ doc_events = {
"Payment Entry": { "Payment Entry": {
"on_submit": [ "on_submit": [
"erpnext.regional.create_transaction_log", "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", "on_trash": "erpnext.regional.check_deletion_permission",
}, },
"Address": { "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, as_dict=1,
) )
expecnse_account = ( expense_account = (
company_account.default_operating_cost_account or company_account.default_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_non_stock_items_cost(stock_entry, work_order, expense_account)
add_operations_cost(stock_entry, work_order, expecnse_account) add_operations_cost(stock_entry, work_order, expense_account)
def add_non_stock_items_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): 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 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) operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
if operating_cost_per_unit: if operating_cost_per_unit:
stock_entry.append( cost_added = add_operating_cost_component_wise(
"additional_costs", stock_entry, work_order, operating_cost_per_unit, expense_account
{
"expense_account": expense_account,
"description": _("Operating Cost as per Work Order / BOM"),
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
},
) )
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: 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) 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, "source_warehouse");
frm.events.set_company_filters(frm, "wip_warehouse"); frm.events.set_company_filters(frm, "wip_warehouse");
frm.set_query("source_warehouse", "items", () => { frm.set_query("source_warehouse", "items", () => {
@@ -184,7 +194,12 @@ frappe.ui.form.on("Job Card", {
!frm.doc.finished_good || !frm.doc.finished_good ||
!has_items?.length) !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"), () => { frm.add_custom_button(__("Start Job"), () => {
let from_time = frappe.datetime.now_datetime(); let from_time = frappe.datetime.now_datetime();
if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) { 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); 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({ fields.push({
fieldtype: "Datetime", fieldtype: "Datetime",
label: __("End Time"), label: __("End Time"),
@@ -758,3 +778,7 @@ function get_last_completed_row(time_logs) {
return last_completed_row; 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.validate_sequence_id()
self.set_sub_operations() self.set_sub_operations()
self.update_sub_operation_status() self.update_sub_operation_status()
if self.sub_operations:
self.set_total_completed_qty_from_sub_operations()
self.validate_work_order() self.validate_work_order()
def on_update(self): 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: 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): def get_overlap_for(self, args, open_job_cards=None):
time_logs = [] time_logs = []
@@ -613,7 +621,7 @@ class JobCard(Document):
self.save() self.save()
def update_sub_operation_status(self): def update_sub_operation_status(self):
if not (self.sub_operations and self.time_logs): if not self.sub_operations:
return return
operation_wise_completed_time = {} operation_wise_completed_time = {}

View File

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

View File

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

View File

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

View File

@@ -27,11 +27,8 @@
"column_break_etmc", "column_break_etmc",
"off_status_image", "off_status_image",
"over_heads", "over_heads",
"hour_rate_electricity", "section_break_auzm",
"hour_rate_consumable", "workstation_costs",
"column_break_11",
"hour_rate_rent",
"hour_rate_labour",
"section_break_11", "section_break_11",
"hour_rate", "hour_rate",
"workstaion_description", "workstaion_description",
@@ -68,50 +65,6 @@
"label": "Operating Costs", "label": "Operating Costs",
"oldfieldtype": "Section Break" "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", "description": "per hour",
"fieldname": "hour_rate", "fieldname": "hour_rate",
@@ -252,6 +205,17 @@
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disabled" "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, "hide_toolbar": 1,
@@ -259,7 +223,7 @@
"idx": 1, "idx": 1,
"image_field": "on_status_image", "image_field": "on_status_image",
"links": [], "links": [],
"modified": "2025-07-13 16:02:13.615001", "modified": "2025-08-19 12:07:05.374386",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Workstation", "name": "Workstation",

View File

@@ -3,7 +3,7 @@
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
@@ -44,6 +44,7 @@ class Workstation(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF 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 ( from erpnext.manufacturing.doctype.workstation_working_hour.workstation_working_hour import (
WorkstationWorkingHour, WorkstationWorkingHour,
) )
@@ -52,10 +53,6 @@ class Workstation(Document):
disabled: DF.Check disabled: DF.Check
holiday_list: DF.Link | None holiday_list: DF.Link | None
hour_rate: DF.Currency 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 off_status_image: DF.AttachImage | None
on_status_image: DF.AttachImage | None on_status_image: DF.AttachImage | None
plant_floor: DF.Link | None plant_floor: DF.Link | None
@@ -64,10 +61,26 @@ class Workstation(Document):
total_working_hours: DF.Float total_working_hours: DF.Float
warehouse: DF.Link | None warehouse: DF.Link | None
working_hours: DF.Table[WorkstationWorkingHour] working_hours: DF.Table[WorkstationWorkingHour]
workstation_costs: DF.Table[WorkstationCost]
workstation_name: DF.Data workstation_name: DF.Data
workstation_type: DF.Link | None workstation_type: DF.Link | None
# end: auto-generated types # 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): def before_save(self):
self.set_data_based_on_workstation_type() self.set_data_based_on_workstation_type()
self.set_hour_rate() 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)) frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
def set_hour_rate(self): def set_hour_rate(self):
self.hour_rate = ( self.hour_rate = 0.0
flt(self.hour_rate_labour) for row in self.workstation_costs:
+ flt(self.hour_rate_electricity) if row.operating_cost:
+ flt(self.hour_rate_consumable) self.hour_rate += flt(row.operating_cost)
+ flt(self.hour_rate_rent)
)
@frappe.whitelist() @frappe.whitelist()
def set_data_based_on_workstation_type(self): def set_data_based_on_workstation_type(self):
if self.workstation_costs:
return
if self.workstation_type: if self.workstation_type:
fields = [ data = frappe.get_all(
"hour_rate_labour", "Workstation Cost",
"hour_rate_electricity", fields=["operating_component", "operating_cost", "idx"],
"hour_rate_consumable", filters={"parent": self.workstation_type, "parenttype": "Workstation Type"},
"hour_rate_rent", order_by="idx",
"hour_rate", )
"description",
]
data = frappe.get_cached_value("Workstation Type", self.workstation_type, fields, as_dict=True) for row in data:
self.append(
if not data: "workstation_costs",
return {
"operating_component": row.operating_component,
for field in fields: "operating_cost": row.operating_cost,
if self.get(field): "idx": row.idx,
continue },
)
if value := data.get(field):
self.set(field, value)
def on_update(self): def on_update(self):
self.validate_overlap_for_operation_timings() 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", "engine": "InnoDB",
"field_order": [ "field_order": [
"workstation_type", "workstation_type",
"over_heads", "section_break_auzm",
"hour_rate_electricity", "workstation_costs",
"hour_rate_consumable",
"column_break_5",
"hour_rate_rent",
"hour_rate_labour",
"section_break_8", "section_break_8",
"hour_rate", "hour_rate",
"description_tab", "description_tab",
@@ -32,44 +28,6 @@
"reqd": 1, "reqd": 1,
"unique": 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", "description": "per hour",
"fieldname": "hour_rate", "fieldname": "hour_rate",
@@ -88,10 +46,6 @@
"oldfieldtype": "Text", "oldfieldtype": "Text",
"width": "300px" "width": "300px"
}, },
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "description_tab", "fieldname": "description_tab",
@@ -101,11 +55,22 @@
{ {
"fieldname": "section_break_8", "fieldname": "section_break_8",
"fieldtype": "Section Break" "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", "icon": "icon-wrench",
"links": [], "links": [],
"modified": "2024-03-27 13:11:00.946367", "modified": "2025-08-19 12:06:56.683558",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Workstation Type", "name": "Workstation Type",
@@ -125,6 +90,7 @@
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "creation", "sort_field": "creation",
"sort_order": "ASC", "sort_order": "ASC",

View File

@@ -2,6 +2,7 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt from frappe.utils import flt
@@ -15,25 +16,38 @@ class WorkstationType(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
from erpnext.manufacturing.doctype.workstation_cost.workstation_cost import WorkstationCost
description: DF.SmallText | None description: DF.SmallText | None
hour_rate: DF.Currency hour_rate: DF.Currency
hour_rate_consumable: DF.Currency workstation_costs: DF.Table[WorkstationCost]
hour_rate_electricity: DF.Currency
hour_rate_labour: DF.Currency
hour_rate_rent: DF.Currency
workstation_type: DF.Data workstation_type: DF.Data
# end: auto-generated types # 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): def before_save(self):
self.set_hour_rate() self.set_hour_rate()
def set_hour_rate(self): def set_hour_rate(self):
self.hour_rate = ( self.hour_rate = 0.0
flt(self.hour_rate_labour)
+ flt(self.hour_rate_electricity) for row in self.workstation_costs:
+ flt(self.hour_rate_consumable) if row.operating_cost:
+ flt(self.hour_rate_rent) self.hour_rate += flt(row.operating_cost)
)
def get_workstations(workstation_type): 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.v15_0.add_company_payment_gateway_account
erpnext.patches.v16_0.update_serial_no_reference_name erpnext.patches.v16_0.update_serial_no_reference_name
erpnext.patches.v16_0.set_invoice_type_in_pos_settings 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.validate()
dunning.flags.ignore_validate_update_after_submit = True dunning.flags.ignore_validate_update_after_submit = True
dunning.flags.ignore_links = True
dunning.save() dunning.save()
# Reverse entries only if dunning is submitted and not resolved # 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() @frappe.whitelist()
def get_timesheet_detail_rate(timelog, currency): def get_timesheet_detail_rate(timelog, currency):
timelog_detail = frappe.db.sql( ts = frappe.qb.DocType("Timesheet")
f"""SELECT tsd.billing_amount as billing_amount, ts_detail = frappe.qb.DocType("Timesheet Detail")
ts.currency as currency FROM `tabTimesheet Detail` tsd
INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent timelog_detail = (
WHERE tsd.name = '{timelog}'""", frappe.qb.from_(ts_detail)
as_dict=1, .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] )[0]
if timelog_detail.currency: if timelog_detail.currency:

View File

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

View File

@@ -5,7 +5,7 @@ frappe.provide("erpnext.buying");
// cur_frm.add_fetch('project', 'cost_center', 'cost_center'); // cur_frm.add_fetch('project', 'cost_center', 'cost_center');
erpnext.buying = { erpnext.buying = {
setup_buying_controller: function() { setup_buying_controller: function () {
erpnext.buying.BuyingController = class BuyingController extends erpnext.TransactionController { erpnext.buying.BuyingController = class BuyingController extends erpnext.TransactionController {
setup() { setup() {
super.setup(); super.setup();
@@ -17,11 +17,11 @@ erpnext.buying = {
this.setup_queries(doc, cdt, cdn); this.setup_queries(doc, cdt, cdn);
super.onload(); super.onload();
this.frm.set_query('shipping_rule', function() { this.frm.set_query("shipping_rule", function () {
return { return {
filters: { filters: {
"shipping_rule_type": "Buying" shipping_rule_type: "Buying",
} },
}; };
}); });
@@ -33,29 +33,28 @@ erpnext.buying = {
}; };
}); });
if (this.frm.doc.__islocal if (
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) { 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); var df = frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total");
this.frm.set_value("disable_rounded_total", disable); 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 // 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", () => { this.frm.set_query("shipping_address", () => {
if(this.frm.doc.customer) { if (this.frm.doc.customer) {
return { return {
query: 'frappe.contacts.doctype.address.address.address_query', query: "frappe.contacts.doctype.address.address.address_query",
filters: { link_doctype: 'Customer', link_name: this.frm.doc.customer } filters: { link_doctype: "Customer", link_name: this.frm.doc.customer },
}; };
} else } else return erpnext.queries.company_address_query(this.frm.doc);
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", () => { this.frm.set_query("dispatch_address", () => {
return erpnext.queries.address_query(this.frm.doc); return erpnext.queries.address_query(this.frm.doc);
}); });
@@ -65,76 +64,77 @@ erpnext.buying = {
setup_queries(doc, cdt, cdn) { setup_queries(doc, cdt, cdn) {
var me = this; var me = this;
if(this.frm.fields_dict.buying_price_list) { if (this.frm.fields_dict.buying_price_list) {
this.frm.set_query("buying_price_list", function() { this.frm.set_query("buying_price_list", function () {
return{ return {
filters: { 'buying': 1 } filters: { buying: 1 },
} };
}); });
} }
if(this.frm.fields_dict.tc_name) { if (this.frm.fields_dict.tc_name) {
this.frm.set_query("tc_name", function() { this.frm.set_query("tc_name", function () {
return{ return {
filters: { 'buying': 1 } filters: { buying: 1 },
} };
}); });
} }
me.frm.set_query('supplier', erpnext.queries.supplier); me.frm.set_query("supplier", erpnext.queries.supplier);
me.frm.set_query('contact_person', erpnext.queries.contact_query); 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_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); 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) { 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) { if (me.frm.doc.is_old_subcontracting_flow) {
filters["is_sub_contracted_item"] = 1; filters["is_sub_contracted_item"] = 1;
} } else {
else {
filters["is_stock_item"] = 0; filters["is_stock_item"] = 0;
} }
return{ return {
query: "erpnext.controllers.queries.item_query", query: "erpnext.controllers.queries.item_query",
filters: filters filters: filters,
} };
} } else {
else { return {
return{
query: "erpnext.controllers.queries.item_query", 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]; const row = locals[cdt][cdn];
return { return {
query: "erpnext.controllers.queries.item_manufacturer_query", 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')) { if (this.frm.fields_dict["items"].grid.get_field("item_code")) {
this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) { this.frm.set_query("item_tax_template", "items", function (doc, cdt, cdn) {
return me.set_query_for_item_tax_template(doc, cdt, cdn) return me.set_query_for_item_tax_template(doc, cdt, cdn);
}); });
} }
} }
refresh(doc) { 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.toggle_display(
(this.frm.doc.supplier_name && this.frm.doc.supplier_name!==this.frm.doc.supplier)); "supplier_name",
this.frm.doc.supplier_name && this.frm.doc.supplier_name !== this.frm.doc.supplier
);
if(this.frm.doc.docstatus==0 && if (
(this.frm.doctype==="Purchase Order" || this.frm.doctype==="Material Request")) { this.frm.doc.docstatus == 0 &&
(this.frm.doctype === "Purchase Order" || this.frm.doctype === "Material Request")
) {
this.set_from_product_bundle(); this.set_from_product_bundle();
} }
@@ -143,45 +143,53 @@ erpnext.buying = {
} }
toggle_subcontracting_fields() { toggle_subcontracting_fields() {
if (['Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doc.doctype)) { if (["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', this.frm.fields_dict.supplied_items.grid.update_docfield_property(
'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM'); "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_add_rows", 1);
this.frm.set_df_property('supplied_items', 'cannot_delete_rows', 1); this.frm.set_df_property("supplied_items", "cannot_delete_rows", 1);
} }
} }
supplier() { supplier() {
var me = this; 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(); me.apply_price_list();
}); });
} }
company(){ company() {
if(!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return; if (!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
frappe.call({ frappe.call({
method: "erpnext.setup.doctype.company.company.get_billing_shipping_address", method: "erpnext.setup.doctype.company.company.get_billing_shipping_address",
args: { args: {
name: this.frm.doc.company, name: this.frm.doc.company,
billing_address:this.frm.doc.billing_address, billing_address: this.frm.doc.billing_address,
shipping_address: this.frm.doc.shipping_address shipping_address: this.frm.doc.shipping_address,
}, },
callback: (r) => { callback: (r) => {
this.frm.set_value("billing_address", r.message.primary_address || ""); 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 || ""); 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() { supplier_address() {
erpnext.utils.get_address_display(this.frm); 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() { buying_price_list() {
@@ -201,24 +209,33 @@ erpnext.buying = {
} }
qty(doc, cdt, cdn) { qty(doc, cdt, cdn) {
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && doc.update_stock)) { if (
this.calculate_received_qty(doc, cdt, cdn) doc.doctype == "Purchase Receipt" ||
(doc.doctype == "Purchase Invoice" && doc.update_stock)
) {
this.calculate_received_qty(doc, cdt, cdn);
} }
super.qty(doc, cdt, cdn); super.qty(doc, cdt, cdn);
} }
rejected_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); var item = frappe.get_doc(cdt, cdn);
frappe.model.round_floats_in(item, ["qty", "rejected_qty"]); 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_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_qty", received_qty);
frappe.model.set_value(cdt, cdn, "received_stock_qty", received_stock_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); super.batch_no(doc, cdt, cdn);
} }
validate_negative_quantity(cdt, cdn, item, fieldnames){ validate_negative_quantity(cdt, cdn, item, fieldnames) {
if(!item || !fieldnames) { return } if (!item || !fieldnames) {
return;
}
var is_negative_qty = false; var is_negative_qty = false;
for(var i = 0; i<fieldnames.length; i++) { for (var i = 0; i < fieldnames.length; i++) {
if(item[fieldnames[i]] < 0){ 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])); 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; is_negative_qty = true;
break; break;
} }
} }
return is_negative_qty return is_negative_qty;
} }
warehouse(doc, cdt, cdn) { warehouse(doc, cdt, cdn) {
var item = frappe.get_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({ return this.frm.call({
method: "erpnext.stock.get_item_details.get_bin_details", method: "erpnext.stock.get_item_details.get_bin_details",
child: item, child: item,
@@ -253,22 +278,21 @@ erpnext.buying = {
item_code: item.item_code, item_code: item.item_code,
warehouse: item.warehouse, warehouse: item.warehouse,
company: doc.company, company: doc.company,
include_child_warehouses: true include_child_warehouses: true,
} },
}); });
} }
} }
project(doc, cdt, cdn) { project(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if(item.project) { if (item.project) {
$.each(this.frm.doc["items"] || [], $.each(this.frm.doc["items"] || [], function (i, other_item) {
function(i, other_item) { if (!other_item.project) {
if(!other_item.project) { other_item.project = item.project;
other_item.project = item.project; refresh_field("project", other_item.name, other_item.parentfield);
refresh_field("project", other_item.name, other_item.parentfield); }
} });
});
} }
} }
@@ -281,7 +305,7 @@ erpnext.buying = {
category(doc, cdt, cdn) { category(doc, cdt, cdn) {
// should be the category field of tax table // should be the category field of tax table
if(cdt != doc.doctype) { if (cdt != doc.doctype) {
this.calculate_taxes_and_totals(); this.calculate_taxes_and_totals();
} }
} }
@@ -291,26 +315,42 @@ erpnext.buying = {
set_from_product_bundle() { set_from_product_bundle() {
var me = this; var me = this;
this.frm.add_custom_button(__("Product Bundle"), function() { this.frm.add_custom_button(
erpnext.buying.get_items_from_product_bundle(me.frm); __("Product Bundle"),
}, __("Get Items From")); function () {
erpnext.buying.get_items_from_product_bundle(me.frm);
},
__("Get Items From")
);
} }
shipping_address(){ shipping_address() {
var me = this; var me = this;
erpnext.utils.get_address_display(this.frm, "shipping_address", erpnext.utils.get_address_display(
"shipping_address_display", true); this.frm,
"shipping_address",
"shipping_address_display",
true
);
} }
dispatch_address(){ dispatch_address() {
var me = this; var me = this;
erpnext.utils.get_address_display(this.frm, "dispatch_address", erpnext.utils.get_address_display(
"dispatch_address_display", true); this.frm,
"dispatch_address",
"dispatch_address_display",
true
);
} }
billing_address() { billing_address() {
erpnext.utils.get_address_display(this.frm, "billing_address", erpnext.utils.get_address_display(
"billing_address_display", true); this.frm,
"billing_address",
"billing_address_display",
true
);
} }
tc_name() { tc_name() {
@@ -320,37 +360,43 @@ erpnext.buying = {
update_auto_repeat_reference(doc) { update_auto_repeat_reference(doc) {
if (doc.auto_repeat) { if (doc.auto_repeat) {
frappe.call({ frappe.call({
method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference", method: "frappe.automation.doctype.auto_repeat.auto_repeat.update_reference",
args:{ args: {
docname: doc.auto_repeat, docname: doc.auto_repeat,
reference:doc.name reference: doc.name,
}, },
callback: function(r){ callback: function (r) {
if (r.message=="success") { if (r.message == "success") {
frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'}); frappe.show_alert({
message: __("Auto repeat document updated"),
indicator: "green",
});
} else { } 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) { manufacturer(doc, cdt, cdn) {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
if(row.manufacturer) { if (row.manufacturer) {
frappe.call({ frappe.call({
method: "erpnext.stock.doctype.item_manufacturer.item_manufacturer.get_item_manufacturer_part_no", method: "erpnext.stock.doctype.item_manufacturer.item_manufacturer.get_item_manufacturer_part_no",
args: { args: {
'item_code': row.item_code, item_code: row.item_code,
'manufacturer': row.manufacturer manufacturer: row.manufacturer,
}, },
callback: function(r) { callback: function (r) {
if (r.message) { 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]; const row = locals[cdt][cdn];
if (row.manufacturer_part_no) { if (row.manufacturer_part_no) {
frappe.model.get_value('Item Manufacturer', frappe.model.get_value(
"Item Manufacturer",
{ {
'item_code': row.item_code, item_code: row.item_code,
'manufacturer': row.manufacturer, manufacturer: row.manufacturer,
'manufacturer_part_no': row.manufacturer_part_no manufacturer_part_no: row.manufacturer_part_no,
}, },
'name', "name",
function(data) { function (data) {
if (!data) { if (!data) {
let msg = { let msg = {
message: __("Manufacturer Part Number <b>{0}</b> is invalid", [row.manufacturer_part_no]), message: __("Manufacturer Part Number <b>{0}</b> is invalid", [
title: __("Invalid Part Number") row.manufacturer_part_no,
} ]),
title: __("Invalid Part Number"),
};
frappe.throw(msg); frappe.throw(msg);
} }
} }
@@ -384,40 +433,42 @@ erpnext.buying = {
let me = this; let me = this;
let fields = ["has_batch_no", "has_serial_no"]; let fields = ["has_batch_no", "has_serial_no"];
frappe.db.get_value("Item", item.item_code, fields) frappe.db.get_value("Item", item.item_code, fields).then((r) => {
.then((r) => { if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { fields.forEach((field) => {
fields.forEach((field) => { item[field] = r.message[field];
item[field] = r.message[field]; });
});
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false; item.is_rejected = false;
new erpnext.SerialBatchPackageSelector( new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
me.frm, item, (r) => { if (r) {
if (r) { let qty = Math.abs(r.total_qty);
let qty = Math.abs(r.total_qty); if (doc.is_return) {
if (doc.is_return) { qty = qty * -1;
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);
}
} }
);
} 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) { add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
@@ -425,142 +476,147 @@ erpnext.buying = {
let me = this; let me = this;
let fields = ["has_batch_no", "has_serial_no"]; let fields = ["has_batch_no", "has_serial_no"];
frappe.db.get_value("Item", item.item_code, fields) frappe.db.get_value("Item", item.item_code, fields).then((r) => {
.then((r) => { if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { fields.forEach((field) => {
fields.forEach((field) => { item[field] = r.message[field];
item[field] = r.message[field]; });
});
item.type_of_transaction = !doc.is_return > 0 ? "Inward" : "Outward"; item.type_of_transaction = !doc.is_return > 0 ? "Inward" : "Outward";
item.is_rejected = true; item.is_rejected = true;
new erpnext.SerialBatchPackageSelector( new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
me.frm, item, (r) => { if (r) {
if (r) { let qty = Math.abs(r.total_qty);
let qty = Math.abs(r.total_qty); if (doc.is_return) {
if (doc.is_return) { qty = qty * -1;
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);
}
} }
);
} 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({ frappe.call({
method: "erpnext.buying.utils.get_linked_material_requests", method: "erpnext.buying.utils.get_linked_material_requests",
args:{ args: {
items: frm.doc.items.map((item) => item.item_code) items: frm.doc.items.map((item) => item.item_code),
}, },
callback: function(r) { callback: function (r) {
if (!r.message || r.message.length == 0) { if (!r.message || r.message.length == 0) {
frappe.throw({ frappe.throw({
message: __("No pending Material Requests found to link for the given items."), message: __("No pending Material Requests found to link for the given items."),
title: __("Note") title: __("Note"),
}); });
} }
var item_length = frm.doc.items.length; var item_length = frm.doc.items.length;
for (let item of frm.doc.items) { for (let item of frm.doc.items) {
var qty = item.qty; var qty = item.qty;
(r.message[0] || []).forEach(function(d) { (r.message[0] || []).forEach(function (d) {
if (d.qty > 0 && qty > 0 && item.item_code == d.item_code && !item.material_request_item) 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 = d.mr_name;
item.material_request_item = d.mr_item; item.material_request_item = d.mr_item;
var my_qty = Math.min(qty, d.qty); var my_qty = Math.min(qty, d.qty);
qty = qty - my_qty; qty = qty - my_qty;
d.qty = d.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; item.qty = my_qty;
frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + item.idx + ")"); frappe.msgprint(
if (qty > 0) "Assigning " + d.mr_name + " to " + d.item_code + " (row " + item.idx + ")"
{ );
if (qty > 0) {
frappe.msgprint("Splitting " + qty + " units of " + d.item_code); frappe.msgprint("Splitting " + qty + " units of " + d.item_code);
var newrow = frappe.model.add_child(frm.doc, item.doctype, "items"); var newrow = frappe.model.add_child(frm.doc, item.doctype, "items");
item_length++; item_length++;
for (var key in item) for (var key in item) {
{
newrow[key] = item[key]; newrow[key] = item[key];
} }
newrow.idx = item_length; newrow.idx = item_length;
newrow["stock_qty"] = newrow.conversion_factor*qty; newrow["stock_qty"] = newrow.conversion_factor * qty;
newrow["qty"] = qty; newrow["qty"] = qty;
newrow["material_request"] = ""; newrow["material_request"] = "";
newrow["material_request_item"] = ""; newrow["material_request_item"] = "";
} }
} }
}); });
} }
refresh_field("items"); refresh_field("items");
} },
}); });
} };
erpnext.buying.get_default_bom = function(frm) { erpnext.buying.get_default_bom = function (frm) {
$.each(frm.doc["items"] || [], function(i, d) { $.each(frm.doc["items"] || [], function (i, d) {
if (d.item_code && d.bom === "") { if (d.item_code && d.bom === "") {
return frappe.call({ return frappe.call({
type: "GET", type: "GET",
method: "erpnext.stock.get_item_details.get_default_bom", method: "erpnext.stock.get_item_details.get_default_bom",
args: { args: {
"item_code": d.item_code, item_code: d.item_code,
}, },
callback: function(r) { callback: function (r) {
if(r) { if (r) {
frappe.model.set_value(d.doctype, d.name, "bom", r.message); 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({ var dialog = new frappe.ui.Dialog({
title: __("Get Items from Product Bundle"), title: __("Get Items from Product Bundle"),
fields: [ fields: [
{ {
"fieldtype": "Link", fieldtype: "Link",
"label": __("Product Bundle"), label: __("Product Bundle"),
"fieldname": "product_bundle", fieldname: "product_bundle",
"options":"Product Bundle", options: "Product Bundle",
"reqd": 1 reqd: 1,
}, },
{ {
"fieldtype": "Currency", fieldtype: "Currency",
"label": __("Quantity"), label: __("Quantity"),
"fieldname": "quantity", fieldname: "quantity",
"reqd": 1, reqd: 1,
"default": 1 default: 1,
} },
], ],
primary_action_label: 'Get Items', primary_action_label: "Get Items",
primary_action(args){ primary_action(args) {
if(!args) return; if (!args) return;
dialog.hide(); dialog.hide();
return frappe.call({ return frappe.call({
type: "GET", type: "GET",
@@ -581,44 +637,44 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
is_subcontracted: frm.doc.is_subcontracted, is_subcontracted: frm.doc.is_subcontracted,
transaction_date: frm.doc.transaction_date || frm.doc.posting_date, transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
ignore_pricing_rule: frm.doc.ignore_pricing_rule, ignore_pricing_rule: frm.doc.ignore_pricing_rule,
doctype: frm.doc.doctype doctype: frm.doc.doctype,
}, },
}, },
freeze: true, freeze: true,
callback: function(r) { callback: function (r) {
const first_row_is_empty = function(child_table){ const first_row_is_empty = function (child_table) {
if($.isArray(child_table) && child_table.length > 0) { if ($.isArray(child_table) && child_table.length > 0) {
return !child_table[0].item_code; return !child_table[0].item_code;
} }
return false; return false;
}; };
const remove_empty_first_row = function(frm){ const remove_empty_first_row = function (frm) {
if (first_row_is_empty(frm.doc.items)){ if (first_row_is_empty(frm.doc.items)) {
frm.doc.items = frm.doc.items.splice(1); frm.doc.items = frm.doc.items.splice(1);
} }
}; };
if(!r.exc && r.message) { if (!r.exc && r.message) {
remove_empty_first_row(frm); 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 d = frm.add_child("items");
var item = r.message[i]; var item = r.message[i];
for (var key in item) { for (var key in item) {
if (!is_null(item[key]) && key !== "doctype") { if (!is_null(item[key]) && key !== "doctype") {
d[key] = item[key]; 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.script_manager.trigger("price_list_rate", d.doctype, d.name);
} }
} }
frm.refresh_field("items"); frm.refresh_field("items");
} }
} },
}) });
} },
}); });
dialog.show(); 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]; let row = locals[cdt][cdn];
if (row.barcode) { if (row.barcode) {
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => { erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
frappe.model.set_value(cdt, cdn, { frappe.model.set_value(cdt, cdn, {
"item_code": r.message.item_code, item_code: r.message.item_code,
"qty": 1, qty: 1,
}); });
}); });
} }
@@ -25,74 +25,81 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
setup_warehouse_query() { setup_warehouse_query() {
var me = this; 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); return erpnext.queries.warehouse(me.frm.doc);
}); });
} }
setup_posting_date_time_check() { setup_posting_date_time_check() {
// make posting date default and read only unless explictly checked // 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) { 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) { if (frm.doc.docstatus == 0 && frm.doc.set_posting_time) {
frm.set_df_property('posting_date', 'read_only', 0); frm.set_df_property("posting_date", "read_only", 0);
frm.set_df_property('posting_time', 'read_only', 0); frm.set_df_property("posting_time", "read_only", 0);
} else { } else {
frm.set_df_property('posting_date', 'read_only', 1); frm.set_df_property("posting_date", "read_only", 1);
frm.set_df_property('posting_time', '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 // set default posting date / time
if(frm.doc.docstatus==0) { if (frm.doc.docstatus == 0) {
if(!frm.doc.posting_date) { if (!frm.doc.posting_date) {
frm.set_value('posting_date', frappe.datetime.nowdate()); frm.set_value("posting_date", frappe.datetime.nowdate());
} }
if(!frm.doc.posting_time) { if (!frm.doc.posting_time) {
frm.set_value('posting_time', frappe.datetime.now_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() { show_stock_ledger() {
var me = this; var me = this;
if(this.frm.doc.docstatus > 0) { if (this.frm.doc.docstatus > 0) {
cur_frm.add_custom_button(__("Stock Ledger"), function() { cur_frm.add_custom_button(
frappe.route_options = { __("Stock Ledger"),
voucher_no: me.frm.doc.name, function () {
from_date: me.frm.doc.posting_date, frappe.route_options = {
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), voucher_no: me.frm.doc.name,
company: me.frm.doc.company, from_date: me.frm.doc.posting_date,
show_cancelled_entries: me.frm.doc.docstatus === 2, to_date: moment(me.frm.doc.modified).format("YYYY-MM-DD"),
ignore_prepared_report: true company: me.frm.doc.company,
}; show_cancelled_entries: me.frm.doc.docstatus === 2,
frappe.set_route("query-report", "Stock Ledger"); ignore_prepared_report: true,
}, __("View")); };
frappe.set_route("query-report", "Stock Ledger");
},
__("View")
);
} }
} }
show_general_ledger() { show_general_ledger() {
let me = this; let me = this;
if(this.frm.doc.docstatus > 0) { if (this.frm.doc.docstatus > 0) {
cur_frm.add_custom_button(__('Accounting Ledger'), function() { cur_frm.add_custom_button(
frappe.route_options = { __("Accounting Ledger"),
voucher_no: me.frm.doc.name, function () {
from_date: me.frm.doc.posting_date, frappe.route_options = {
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), voucher_no: me.frm.doc.name,
company: me.frm.doc.company, from_date: me.frm.doc.posting_date,
categorize_by: "Categorize by Voucher (Consolidated)", to_date: moment(me.frm.doc.modified).format("YYYY-MM-DD"),
show_cancelled_entries: me.frm.doc.docstatus === 2, company: me.frm.doc.company,
ignore_prepared_report: true categorize_by: "Categorize by Voucher (Consolidated)",
}; show_cancelled_entries: me.frm.doc.docstatus === 2,
frappe.set_route("query-report", "General Ledger"); ignore_prepared_report: true,
}, __("View")); };
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() wo.reload()
so.reload() so.reload()
self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) 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): def test_sales_order_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule 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() { render_payment_section() {
this.grand_total_to_default_mop();
this.render_payment_mode_dom(); this.render_payment_mode_dom();
this.make_invoice_field_dialog(); this.make_invoice_field_dialog();
this.update_totals_section(); 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() { render_payment_mode_dom() {
const doc = this.events.get_frm().doc; const doc = this.events.get_frm().doc;
const payments = doc.payments; const payments = doc.payments;

View File

@@ -294,6 +294,10 @@ def install(country=None):
{"doctype": "Market Segment", "market_segment": _("Upper Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")},
# Warehouse Type # Warehouse Type
{"doctype": "Warehouse Type", "name": "Transit"}, {"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 ( for doctype, title_field, filename in (
@@ -481,14 +485,19 @@ def install_defaults(args=None): # nosemgrep
create_bank_account(args) create_bank_account(args)
def set_global_defaults(args): def set_global_defaults(kwargs):
global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") 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( global_defaults.update(
{ {
"default_currency": args.get("currency"), "default_currency": kwargs.get("currency"),
"default_company": args.get("company_name"), "default_company": company,
"country": args.get("country"), "country": kwargs.get("country"),
} }
) )

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ frappe.listview_settings["Material Request"] = {
return [__("Partially Received"), "yellow", "per_received,<,100"]; return [__("Partially Received"), "yellow", "per_received,<,100"];
} else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) { } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) {
return [__("Received"), "green", "per_received,=,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"]; return [__("Ordered"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Material Transfer") { } else if (doc.material_request_type == "Material Transfer") {
return [__("Transferred"), "green", "per_ordered,=,100"]; return [__("Transferred"), "green", "per_ordered,=,100"];
@@ -43,8 +43,6 @@ frappe.listview_settings["Material Request"] = {
return [__("Issued"), "green", "per_ordered,=,100"]; return [__("Issued"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Customer Provided") { } else if (doc.material_request_type == "Customer Provided") {
return [__("Received"), "green", "per_ordered,=,100"]; 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: for perm in permissions:
perm.delete() 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): def get_in_transit_warehouse(company):
if not frappe.db.exists("Warehouse Type", "Transit"): if not frappe.db.exists("Warehouse Type", "Transit"):

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