Merge pull request #39245 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
Deepesh Garg 2024-01-10 16:04:56 +05:30 committed by GitHub
commit e4e3313a0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 932 additions and 308 deletions

View File

@ -1,4 +1,6 @@
{ {
"country_code": "hu",
"name": "Hungary - Chart of Accounts for Microenterprises",
"tree": { "tree": {
"SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": { "SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": {
"account_number": 1, "account_number": 1,

View File

@ -11,6 +11,7 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
@ -19,7 +20,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-01 12:32:34.044911", "modified": "2024-01-03 11:13:02.669632",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Allowed To Transact With", "name": "Allowed To Transact With",
@ -28,5 +29,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -444,6 +444,10 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
vouchers = json.loads(vouchers) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers) transaction.add_payment_entries(vouchers)
transaction.validate_duplicate_references()
transaction.allocate_payment_entries()
transaction.update_allocated_amount()
transaction.set_status()
transaction.save() transaction.save()
return transaction return transaction

View File

@ -3,12 +3,11 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document
from frappe.utils import flt from frappe.utils import flt
from erpnext.controllers.status_updater import StatusUpdater
class BankTransaction(Document):
class BankTransaction(StatusUpdater):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # This code is auto-generated. Do not modify anything in this block.
@ -50,6 +49,15 @@ class BankTransaction(StatusUpdater):
def validate(self): def validate(self):
self.validate_duplicate_references() self.validate_duplicate_references()
def set_status(self):
if self.docstatus == 2:
self.db_set("status", "Cancelled")
elif self.docstatus == 1:
if self.unallocated_amount > 0:
self.db_set("status", "Unreconciled")
elif self.unallocated_amount <= 0:
self.db_set("status", "Reconciled")
def validate_duplicate_references(self): def validate_duplicate_references(self):
"""Make sure the same voucher is not allocated twice within the same Bank Transaction""" """Make sure the same voucher is not allocated twice within the same Bank Transaction"""
if not self.payment_entries: if not self.payment_entries:
@ -83,12 +91,13 @@ class BankTransaction(StatusUpdater):
self.validate_duplicate_references() self.validate_duplicate_references()
self.allocate_payment_entries() self.allocate_payment_entries()
self.update_allocated_amount() self.update_allocated_amount()
self.set_status()
def on_cancel(self): def on_cancel(self):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel=True) self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.set_status(update=True) self.set_status()
def add_payment_entries(self, vouchers): def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance" "Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
@ -366,15 +375,17 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
and len(get_reconciled_bank_transactions(doctype, docname)) < 2 and len(get_reconciled_bank_transactions(doctype, docname)) < 2
): ):
return return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Sales Invoice": if doctype == "Sales Invoice":
frappe.db.set_value( frappe.db.set_value(
"Sales Invoice Payment", "Sales Invoice Payment",
dict(parenttype=doctype, parent=docname), dict(parenttype=doctype, parent=docname),
"clearance_date", "clearance_date",
clearance_date, clearance_date,
) )
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Bank Transaction": elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund # For when a second bank transaction has fixed another, e.g. refund

View File

@ -747,6 +747,10 @@ frappe.ui.form.on('Payment Entry', {
args["get_orders_to_be_billed"] = true; args["get_orders_to_be_billed"] = true;
} }
if (frm.doc.book_advance_payments_in_separate_party_account) {
args["book_advance_payments_in_separate_party_account"] = true;
}
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount']; frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({ return frappe.call({

View File

@ -256,6 +256,7 @@ class PaymentEntry(AccountsController):
"get_outstanding_invoices": True, "get_outstanding_invoices": True,
"get_orders_to_be_billed": True, "get_orders_to_be_billed": True,
"vouchers": vouchers, "vouchers": vouchers,
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
}, },
validate=True, validate=True,
) )
@ -1614,11 +1615,16 @@ def get_outstanding_reference_documents(args, validate=False):
outstanding_invoices = [] outstanding_invoices = []
negative_outstanding_invoices = [] negative_outstanding_invoices = []
if args.get("book_advance_payments_in_separate_party_account"):
party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
else:
party_account = args.get("party_account")
if args.get("get_outstanding_invoices"): if args.get("get_outstanding_invoices"):
outstanding_invoices = get_outstanding_invoices( outstanding_invoices = get_outstanding_invoices(
args.get("party_type"), args.get("party_type"),
args.get("party"), args.get("party"),
get_party_account(args.get("party_type"), args.get("party"), args.get("company")), party_account,
common_filter=common_filter, common_filter=common_filter,
posting_date=posting_and_due_date, posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"), min_outstanding=args.get("outstanding_amt_greater_than"),

View File

@ -527,7 +527,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
values.extend(warehouses) values.extend(warehouses)
if items: if items:
condition = " and `tab{child_doc}`.{apply_on} in ({items})".format( condition += " and `tab{child_doc}`.{apply_on} in ({items})".format(
child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items)) child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items))
) )

View File

@ -1084,17 +1084,6 @@ class PurchaseInvoice(BuyingController):
item=item, item=item,
) )
) )
# update gross amount of asset bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
)
if ( if (
self.auto_accounting_for_stock self.auto_accounting_for_stock
and self.is_opening == "No" and self.is_opening == "No"
@ -1134,17 +1123,24 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount") item.item_tax_amount, item.precision("item_tax_amount")
) )
if item.is_fixed_asset and item.landed_cost_voucher_amount:
self.update_gross_purchase_amount_for_linked_assets(item)
def update_gross_purchase_amount_for_linked_assets(self, item):
assets = frappe.db.get_all( assets = frappe.db.get_all(
"Asset", "Asset",
filters={"purchase_invoice": self.name, "item_code": item.item_code}, filters={"purchase_invoice": self.name, "item_code": item.item_code},
fields=["name", "asset_quantity"], fields=["name", "asset_quantity"],
) )
for asset in assets: for asset in assets:
purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
frappe.db.set_value( frappe.db.set_value(
"Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity "Asset",
) asset.name,
frappe.db.set_value( {
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity "gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
},
) )
def make_stock_adjustment_entry( def make_stock_adjustment_entry(

View File

@ -1227,11 +1227,11 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self): def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value( unlink_enabled = frappe.db.get_single_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" "Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
) )
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value( frappe.db.set_value(
@ -1422,7 +1422,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pay.cancel() pay.cancel()
frappe.db.set_single_value( frappe.db.set_single_value(
"Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled "Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled
) )
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)

View File

@ -898,8 +898,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.events.append_time_log(frm, timesheet, 1.0); frm.events.append_time_log(frm, timesheet, 1.0);
} }
}); });
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals"); frm.trigger("calculate_timesheet_totals");
frm.refresh();
}, },
async get_exchange_rate(frm, from_currency, to_currency) { async get_exchange_rate(frm, from_currency, to_currency) {

View File

@ -114,14 +114,12 @@ def _get_party_details(
set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype) set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)
) )
party = party_details[party_type.lower()] party = party_details[party_type.lower()]
if not ignore_permissions and not (
frappe.has_permission(party_type, "read", party)
or frappe.has_permission(party_type, "select", party)
):
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party) party = frappe.get_doc(party_type, party)
if not ignore_permissions:
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
currency = party.get("default_currency") or currency or get_company_currency(company) currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details( party_address, shipping_address = set_address_details(
@ -637,9 +635,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
return due_date return due_date
def validate_due_date( def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
):
if getdate(due_date) < getdate(posting_date): if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date")) frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else: else:

View File

@ -177,8 +177,8 @@ def add_solvency_ratios(
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"} return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
for year in years: for year in years:
profit_after_tax = total_income[year] + total_expense[year] profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
share_holder_fund = total_asset[year] - total_liability[year] share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
debt_equity_ratio[year] = calculate_ratio( debt_equity_ratio[year] = calculate_ratio(
total_liability.get(year), share_holder_fund, precision total_liability.get(year), share_holder_fund, precision

View File

@ -251,6 +251,7 @@ def get_journal_entries(filters, args):
) )
.where( .where(
(je.voucher_type == "Journal Entry") (je.voucher_type == "Journal Entry")
& (je.docstatus == 1)
& (journal_account.party == filters.get(args.party)) & (journal_account.party == filters.get(args.party))
& (journal_account.account.isin(args.party_account)) & (journal_account.account.isin(args.party_account))
) )
@ -281,7 +282,9 @@ def get_payment_entries(filters, args):
pe.cost_center, pe.cost_center,
) )
.where( .where(
(pe.party == filters.get(args.party)) & (pe[args.account_fieldname].isin(args.party_account)) (pe.docstatus == 1)
& (pe.party == filters.get(args.party))
& (pe[args.account_fieldname].isin(args.party_account))
) )
.orderby(pe.posting_date, pe.name, order=Order.desc) .orderby(pe.posting_date, pe.name, order=Order.desc)
) )

View File

@ -571,10 +571,16 @@ frappe.ui.form.on('Asset', {
indicator: 'red' indicator: 'red'
}); });
} }
frm.set_value('gross_purchase_amount', item.base_net_rate + item.item_tax_amount); var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
frm.set_value('purchase_receipt_amount', item.base_net_rate + item.item_tax_amount); var asset_quantity = is_grouped_asset ? item.qty : 1;
item.asset_location && frm.set_value('location', item.asset_location); var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
frm.set_value('gross_purchase_amount', purchase_amount);
frm.set_value('purchase_receipt_amount', purchase_amount);
frm.set_value('asset_quantity', asset_quantity);
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center); frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
if(item.asset_location) { frm.set_value('location', item.asset_location); }
}, },
set_depreciation_rate: function(frm, row) { set_depreciation_rate: function(frm, row) {

View File

@ -202,9 +202,9 @@
"fieldname": "purchase_date", "fieldname": "purchase_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Purchase Date", "label": "Purchase Date",
"mandatory_depends_on": "eval:!doc.is_existing_asset",
"read_only": 1, "read_only": 1,
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
"reqd": 1
}, },
{ {
"fieldname": "disposal_date", "fieldname": "disposal_date",
@ -227,15 +227,15 @@
"fieldname": "gross_purchase_amount", "fieldname": "gross_purchase_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Gross Purchase Amount", "label": "Gross Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only_depends_on": "eval:!doc.is_existing_asset", "read_only_depends_on": "eval:!doc.is_existing_asset"
"reqd": 1
}, },
{ {
"fieldname": "available_for_use_date", "fieldname": "available_for_use_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Available-for-use Date", "label": "Available-for-use Date",
"reqd": 1 "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)"
}, },
{ {
"default": "0", "default": "0",
@ -590,7 +590,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2023-12-21 16:46:20.732869", "modified": "2024-01-05 17:36:53.131512",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -57,7 +57,7 @@ class Asset(AccountsController):
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"] asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
asset_owner_company: DF.Link | None asset_owner_company: DF.Link | None
asset_quantity: DF.Int asset_quantity: DF.Int
available_for_use_date: DF.Date available_for_use_date: DF.Date | None
booked_fixed_asset: DF.Check booked_fixed_asset: DF.Check
calculate_depreciation: DF.Check calculate_depreciation: DF.Check
capitalized_in: DF.Link | None capitalized_in: DF.Link | None
@ -92,7 +92,7 @@ class Asset(AccountsController):
number_of_depreciations_booked: DF.Int number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency opening_accumulated_depreciation: DF.Currency
policy_number: DF.Data | None policy_number: DF.Data | None
purchase_date: DF.Date purchase_date: DF.Date | None
purchase_invoice: DF.Link | None purchase_invoice: DF.Link | None
purchase_receipt: DF.Link | None purchase_receipt: DF.Link | None
purchase_receipt_amount: DF.Currency purchase_receipt_amount: DF.Currency
@ -316,7 +316,12 @@ class Asset(AccountsController):
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category): if is_cwip_accounting_enabled(self.asset_category):
if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice): if (
not self.is_existing_asset
and not self.is_composite_asset
and not self.purchase_receipt
and not self.purchase_invoice
):
frappe.throw( frappe.throw(
_("Please create purchase receipt or purchase invoice for the item {0}").format( _("Please create purchase receipt or purchase invoice for the item {0}").format(
self.item_code self.item_code
@ -329,7 +334,7 @@ class Asset(AccountsController):
and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock") and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock")
): ):
frappe.throw( frappe.throw(
_("Update stock must be enable for the purchase invoice {0}").format(self.purchase_invoice) _("Update stock must be enabled for the purchase invoice {0}").format(self.purchase_invoice)
) )
if not self.calculate_depreciation: if not self.calculate_depreciation:

View File

@ -3,6 +3,7 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import now_datetime
class AssetActivity(Document): class AssetActivity(Document):
@ -30,5 +31,6 @@ def add_asset_activity(asset, subject):
"asset": asset, "asset": asset,
"subject": subject, "subject": subject,
"user": frappe.session.user, "user": frappe.session.user,
"date": now_datetime(),
} }
).insert(ignore_permissions=True, ignore_links=True) ).insert(ignore_permissions=True, ignore_links=True)

View File

@ -86,12 +86,12 @@ class AssetCategory(Document):
if selected_key_type not in expected_key_types: if selected_key_type not in expected_key_types:
frappe.throw( frappe.throw(
_( _(
"Row #{}: {} of {} should be {}. Please modify the account or select a different account." "Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account."
).format( ).format(
d.idx, d.idx,
frappe.unscrub(key_to_match), frappe.unscrub(key_to_match),
frappe.bold(selected_account), frappe.bold(selected_account),
frappe.bold(expected_key_types), frappe.bold(" or ".join(expected_key_types)),
), ),
title=_("Invalid Account"), title=_("Invalid Account"),
) )

View File

@ -9,6 +9,7 @@
"field_order": [ "field_order": [
"asset", "asset",
"naming_series", "naming_series",
"company",
"column_break_2", "column_break_2",
"gross_purchase_amount", "gross_purchase_amount",
"opening_accumulated_depreciation", "opening_accumulated_depreciation",
@ -193,12 +194,20 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Depreciate based on shifts", "label": "Depreciate based on shifts",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "asset.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-11-29 00:57:00.461998", "modified": "2024-01-08 16:31:04.533928",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Depreciation Schedule", "name": "Asset Depreciation Schedule",

View File

@ -35,6 +35,7 @@ class AssetDepreciationSchedule(Document):
amended_from: DF.Link | None amended_from: DF.Link | None
asset: DF.Link asset: DF.Link
company: DF.Link | None
daily_prorata_based: DF.Check daily_prorata_based: DF.Check
depreciation_method: DF.Literal[ depreciation_method: DF.Literal[
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual" "", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"

View File

@ -214,7 +214,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-11-28 13:01:18.403492", "modified": "2024-01-05 15:26:02.320942",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",
@ -238,6 +238,41 @@
"role": "Purchase Manager", "role": "Purchase Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts User",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Stock Manager",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Stock User",
"share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Purchase User",
"share": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",

View File

@ -452,6 +452,7 @@ class PurchaseOrder(BuyingController):
self.update_requested_qty() self.update_requested_qty()
self.update_ordered_qty() self.update_ordered_qty()
self.update_reserved_qty_for_subcontract() self.update_reserved_qty_for_subcontract()
self.update_subcontracting_order_status()
self.notify_update() self.notify_update()
clear_doctype_notifications(self) clear_doctype_notifications(self)
@ -613,6 +614,17 @@ class PurchaseOrder(BuyingController):
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"): if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
make_subcontracting_order(self.name, save=True, notify=True) make_subcontracting_order(self.name, save=True, notify=True)
def update_subcontracting_order_status(self):
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
update_subcontracting_order_status as update_sco_status,
)
if self.is_subcontracted and not self.is_old_subcontracting_flow:
sco = frappe.db.get_value("Subcontracting Order", {"purchase_order": self.name, "docstatus": 1})
if sco:
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item""" """get last purchase rate for an item"""

View File

@ -129,6 +129,17 @@ class AccountsController(TransactionBase):
if self.doctype in relevant_docs: if self.doctype in relevant_docs:
self.set_payment_schedule() self.set_payment_schedule()
def remove_bundle_for_non_stock_invoices(self):
has_sabb = False
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.update_stock:
for item in self.get("items"):
if item.serial_and_batch_bundle:
item.serial_and_batch_bundle = None
has_sabb = True
if has_sabb:
self.remove_serial_and_batch_bundle()
def ensure_supplier_is_not_blocked(self): def ensure_supplier_is_not_blocked(self):
is_supplier_payment = self.doctype == "Payment Entry" and self.party_type == "Supplier" is_supplier_payment = self.doctype == "Payment Entry" and self.party_type == "Supplier"
is_buying_invoice = self.doctype in ["Purchase Invoice", "Purchase Order"] is_buying_invoice = self.doctype in ["Purchase Invoice", "Purchase Order"]
@ -156,6 +167,9 @@ class AccountsController(TransactionBase):
if self.get("_action") and self._action != "update_after_submit": if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True) self.set_missing_values(for_validate=True)
if self.get("_action") == "submit":
self.remove_bundle_for_non_stock_invoices()
self.ensure_supplier_is_not_blocked() self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year() self.validate_date_with_fiscal_year()
@ -561,18 +575,12 @@ class AccountsController(TransactionBase):
validate_due_date( validate_due_date(
self.posting_date, self.posting_date,
self.due_date, self.due_date,
"Customer",
self.customer,
self.company,
self.payment_terms_template, self.payment_terms_template,
) )
elif self.doctype == "Purchase Invoice": elif self.doctype == "Purchase Invoice":
validate_due_date( validate_due_date(
self.bill_date or self.posting_date, self.bill_date or self.posting_date,
self.due_date, self.due_date,
"Supplier",
self.supplier,
self.company,
self.bill_date, self.bill_date,
self.payment_terms_template, self.payment_terms_template,
) )

View File

@ -744,11 +744,8 @@ class BuyingController(SubcontractingController):
item_data = frappe.db.get_value( item_data = frappe.db.get_value(
"Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1 "Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1
) )
asset_quantity = row.qty if is_grouped_asset else 1
if is_grouped_asset: purchase_amount = flt(row.valuation_rate) * asset_quantity
purchase_amount = flt(row.base_amount + row.item_tax_amount)
else:
purchase_amount = flt(row.base_rate + row.item_tax_amount)
asset = frappe.get_doc( asset = frappe.get_doc(
{ {
@ -764,7 +761,7 @@ class BuyingController(SubcontractingController):
"calculate_depreciation": 0, "calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount, "purchase_receipt_amount": purchase_amount,
"gross_purchase_amount": purchase_amount, "gross_purchase_amount": purchase_amount,
"asset_quantity": row.qty if is_grouped_asset else 1, "asset_quantity": asset_quantity,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
} }

View File

@ -10,7 +10,7 @@ from frappe.utils import flt, format_datetime, get_datetime
import erpnext import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class StockOverReturnError(frappe.ValidationError): class StockOverReturnError(frappe.ValidationError):
@ -116,7 +116,12 @@ def validate_returned_items(doc):
ref = valid_items.get(d.item_code, frappe._dict()) ref = valid_items.get(d.item_code, frappe._dict())
validate_quantity(doc, d, ref, valid_items, already_returned_items) validate_quantity(doc, d, ref, valid_items, already_returned_items)
if ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") and flt(d.rate) > ref.rate: if (
ref.rate
and flt(d.rate) > ref.rate
and doc.doctype in ("Delivery Note", "Sales Invoice")
and get_valuation_method(ref.item_code) != "Moving Average"
):
frappe.throw( frappe.throw(
_("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format( _("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format(
d.idx, doc.doctype, doc.return_against d.idx, doc.doctype, doc.return_against

View File

@ -432,6 +432,9 @@ class SellingController(StockController):
items = self.get("items") + (self.get("packed_items") or []) items = self.get("items") + (self.get("packed_items") or [])
for d in items: for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
continue
if not self.get("return_against") or ( if not self.get("return_against") or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
): ):

View File

@ -131,11 +131,6 @@ status_map = {
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'",
], ],
], ],
"Bank Transaction": [
["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"],
["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"],
["Cancelled", "eval:self.docstatus == 2"],
],
"POS Opening Entry": [ "POS Opening Entry": [
["Draft", None], ["Draft", None],
["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"], ["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],

View File

@ -494,6 +494,7 @@ bank_reconciliation_doctypes = [
"Payment Entry", "Payment Entry",
"Journal Entry", "Journal Entry",
"Purchase Invoice", "Purchase Invoice",
"Sales Invoice",
] ]
accounting_dimension_doctypes = [ accounting_dimension_doctypes = [

View File

@ -117,7 +117,7 @@ class MaintenanceSchedule(TransactionBase):
self.update_amc_date(serial_nos, d.end_date) self.update_amc_date(serial_nos, d.end_date)
no_email_sp = [] no_email_sp = []
if d.sales_person not in email_map: if d.sales_person and d.sales_person not in email_map:
sp = frappe.get_doc("Sales Person", d.sales_person) sp = frappe.get_doc("Sales Person", d.sales_person)
try: try:
email_map[d.sales_person] = sp.get_email_id() email_map[d.sales_person] = sp.get_email_id()
@ -131,12 +131,11 @@ class MaintenanceSchedule(TransactionBase):
).format(self.owner, "<br>" + "<br>".join(no_email_sp)) ).format(self.owner, "<br>" + "<br>".join(no_email_sp))
) )
scheduled_date = frappe.db.sql( scheduled_date = frappe.db.get_all(
"""select scheduled_date from "Maintenance Schedule Detail",
`tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and {"parent": self.name, "item_code": d.item_code},
parent=%s""", ["scheduled_date"],
(d.sales_person, d.item_code, self.name), as_list=False,
as_dict=1,
) )
for key in scheduled_date: for key in scheduled_date:
@ -232,8 +231,6 @@ class MaintenanceSchedule(TransactionBase):
throw(_("Please select Start Date and End Date for Item {0}").format(d.item_code)) throw(_("Please select Start Date and End Date for Item {0}").format(d.item_code))
elif not d.no_of_visits: elif not d.no_of_visits:
throw(_("Please mention no of visits required")) throw(_("Please mention no of visits required"))
elif not d.sales_person:
throw(_("Please select a Sales Person for item: {0}").format(d.item_name))
if getdate(d.start_date) >= getdate(d.end_date): if getdate(d.start_date) >= getdate(d.end_date):
throw(_("Start date should be less than end date for Item {0}").format(d.item_code)) throw(_("Start date should be less than end date for Item {0}").format(d.item_code))
@ -452,15 +449,23 @@ def get_serial_nos_from_schedule(item_code, schedule=None):
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None): def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
def condition(doc):
if s_id:
return doc.name == s_id
elif item_name:
return doc.item_name == item_name
return True
def update_status_and_detail(source, target, parent): def update_status_and_detail(source, target, parent):
target.maintenance_type = "Scheduled" target.maintenance_type = "Scheduled"
target.maintenance_schedule_detail = s_id
def update_serial(source, target, parent): def update_serial(source, target, parent):
if source.serial_and_batch_bundle: if source.item_reference:
serial_nos = frappe.get_doc( if sbb := frappe.db.get_value(
"Serial and Batch Bundle", source.serial_and_batch_bundle "Maintenance Schedule Item", source.item_reference, "serial_and_batch_bundle"
).get_serial_nos() ):
serial_nos = frappe.get_doc("Serial and Batch Bundle", sbb).get_serial_nos()
if len(serial_nos) == 1: if len(serial_nos) == 1:
target.serial_no = serial_nos[0] target.serial_no = serial_nos[0]
@ -477,10 +482,13 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"validation": {"docstatus": ["=", 1]}, "validation": {"docstatus": ["=", 1]},
"postprocess": update_status_and_detail, "postprocess": update_status_and_detail,
}, },
"Maintenance Schedule Item": { "Maintenance Schedule Detail": {
"doctype": "Maintenance Visit Purpose", "doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name if item_name else True, "condition": condition,
"field_map": {"sales_person": "service_person"}, "field_map": {
"sales_person": "service_person",
"name": "maintenance_schedule_detail",
},
"postprocess": update_serial, "postprocess": update_serial,
}, },
}, },

View File

@ -56,7 +56,8 @@ class MaintenanceVisit(TransactionBase):
frappe.throw(_("Add Items in the Purpose Table"), title=_("Purposes Required")) frappe.throw(_("Add Items in the Purpose Table"), title=_("Purposes Required"))
def validate_maintenance_date(self): def validate_maintenance_date(self):
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail: if self.maintenance_type == "Scheduled":
if self.maintenance_schedule_detail:
item_ref = frappe.db.get_value( item_ref = frappe.db.get_value(
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference" "Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference"
) )
@ -70,6 +71,24 @@ class MaintenanceVisit(TransactionBase):
frappe.throw( frappe.throw(
_("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date)) _("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date))
) )
else:
for purpose in self.purposes:
if purpose.maintenance_schedule_detail:
item_ref = frappe.db.get_value(
"Maintenance Schedule Detail", purpose.maintenance_schedule_detail, "item_reference"
)
if item_ref:
start_date, end_date = frappe.db.get_value(
"Maintenance Schedule Item", item_ref, ["start_date", "end_date"]
)
if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(
self.mntc_date
) > get_datetime(end_date):
frappe.throw(
_("Date must be between {0} and {1}").format(
format_date(start_date), format_date(end_date)
)
)
def validate(self): def validate(self):
self.validate_serial_no() self.validate_serial_no()
@ -82,6 +101,7 @@ class MaintenanceVisit(TransactionBase):
if not cancel: if not cancel:
status = self.completion_status status = self.completion_status
actual_date = self.mntc_date actual_date = self.mntc_date
if self.maintenance_schedule_detail: if self.maintenance_schedule_detail:
frappe.db.set_value( frappe.db.set_value(
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status "Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status
@ -89,6 +109,21 @@ class MaintenanceVisit(TransactionBase):
frappe.db.set_value( frappe.db.set_value(
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date "Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date
) )
else:
for purpose in self.purposes:
if purpose.maintenance_schedule_detail:
frappe.db.set_value(
"Maintenance Schedule Detail",
purpose.maintenance_schedule_detail,
"completion_status",
status,
)
frappe.db.set_value(
"Maintenance Schedule Detail",
purpose.maintenance_schedule_detail,
"actual_date",
actual_date,
)
def update_customer_issue(self, flag): def update_customer_issue(self, flag):
if not self.maintenance_schedule: if not self.maintenance_schedule:

View File

@ -17,7 +17,8 @@
"work_details", "work_details",
"work_done", "work_done",
"prevdoc_doctype", "prevdoc_doctype",
"prevdoc_docname" "prevdoc_docname",
"maintenance_schedule_detail"
], ],
"fields": [ "fields": [
{ {
@ -49,6 +50,8 @@
"options": "Serial No" "options": "Serial No"
}, },
{ {
"fetch_from": "item_code.description",
"fetch_if_empty": 1,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"in_list_view": 1, "in_list_view": 1,
@ -56,7 +59,6 @@
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Small Text", "oldfieldtype": "Small Text",
"print_width": "300px", "print_width": "300px",
"reqd": 1,
"width": "300px" "width": "300px"
}, },
{ {
@ -103,12 +105,19 @@
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break" "fieldtype": "Section Break"
},
{
"fieldname": "maintenance_schedule_detail",
"fieldtype": "Data",
"hidden": 1,
"label": "Maintenance Schedule Detail",
"options": "Maintenance Schedule Detail"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-02-27 11:09:33.114458", "modified": "2024-01-05 21:46:53.239830",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Visit Purpose", "name": "Maintenance Visit Purpose",

View File

@ -14,9 +14,10 @@ class MaintenanceVisitPurpose(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
description: DF.TextEditor description: DF.TextEditor | None
item_code: DF.Link | None item_code: DF.Link | None
item_name: DF.Data | None item_name: DF.Data | None
maintenance_schedule_detail: DF.Data | None
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data

View File

@ -12,12 +12,13 @@
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"last_synced_on": "2020-07-21 16:57:09.767009", "last_synced_on": "2020-07-21 16:57:09.767009",
"modified": "2020-07-21 16:57:55.719802", "modified": "2024-01-10 12:21:25.134075",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Completed Operation", "name": "Completed Operation",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"parent_document_type": "Work Order",
"time_interval": "Quarterly", "time_interval": "Quarterly",
"timeseries": 1, "timeseries": 1,
"timespan": "Last Year", "timespan": "Last Year",

View File

@ -86,10 +86,12 @@ def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
if new_bom == d.parent: if new_bom == d.parent:
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
if d.parent not in tuple(bom_list):
bom_list.append(d.parent) bom_list.append(d.parent)
get_ancestor_boms(d.parent, bom_list) get_ancestor_boms(d.parent, bom_list)
return list(set(bom_list)) return bom_list
def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:

View File

@ -57,6 +57,68 @@ class TestBOMUpdateLog(FrappeTestCase):
log.reload() log.reload()
self.assertEqual(log.status, "Completed") self.assertEqual(log.status, "Completed")
def test_bom_replace_for_root_bom(self):
"""
- B-Item A (Root Item)
- B-Item B
- B-Item C
- B-Item D
- B-Item E
- B-Item F
Create New BOM for B-Item E with B-Item G and replace it in the above BOM.
"""
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.item.test_item import make_item
items = ["B-Item A", "B-Item B", "B-Item C", "B-Item D", "B-Item E", "B-Item F", "B-Item G"]
for item_code in items:
if not frappe.db.exists("Item", item_code):
make_item(item_code)
for item_code in items:
remove_bom(item_code)
bom_tree = {
"B-Item A": {"B-Item B": {"B-Item C": {}}, "B-Item D": {"B-Item E": {"B-Item F": {}}}}
}
root_bom = create_nested_bom(bom_tree, prefix="")
exploded_items = frappe.get_all(
"BOM Explosion Item", filters={"parent": root_bom.name}, fields=["item_code"]
)
exploded_items = [item.item_code for item in exploded_items]
expected_exploded_items = ["B-Item C", "B-Item F"]
self.assertEqual(sorted(exploded_items), sorted(expected_exploded_items))
old_bom = frappe.db.get_value("BOM", {"item": "B-Item E"}, "name")
bom_tree = {"B-Item E": {"B-Item G": {}}}
new_bom = create_nested_bom(bom_tree, prefix="")
enqueue_replace_bom(boms=frappe._dict(current_bom=old_bom, new_bom=new_bom.name))
exploded_items = frappe.get_all(
"BOM Explosion Item", filters={"parent": root_bom.name}, fields=["item_code"]
)
exploded_items = [item.item_code for item in exploded_items]
expected_exploded_items = ["B-Item C", "B-Item G"]
self.assertEqual(sorted(exploded_items), sorted(expected_exploded_items))
def remove_bom(item_code):
boms = frappe.get_all("BOM", fields=["docstatus", "name"], filters={"item": item_code})
for row in boms:
if row.docstatus == 1:
frappe.get_doc("BOM", row.name).cancel()
frappe.delete_doc("BOM", row.name)
def update_cost_in_all_boms_in_test(): def update_cost_in_all_boms_in_test():
""" """

View File

@ -646,6 +646,10 @@ class ProductionPlan(Document):
"project": self.project, "project": self.project,
} }
key = (d.item_code, d.sales_order, d.warehouse)
if not d.sales_order:
key = (d.name, d.item_code, d.warehouse)
if not item_details["project"] and d.sales_order: if not item_details["project"] and d.sales_order:
item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project") item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
@ -654,12 +658,9 @@ class ProductionPlan(Document):
item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
else: else:
item_details.update( item_details.update(
{ {"qty": flt(item_dict.get(key, {}).get("qty")) + (flt(d.planned_qty) - flt(d.ordered_qty))}
"qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse), {}).get("qty"))
+ (flt(d.planned_qty) - flt(d.ordered_qty))
}
) )
item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details item_dict[key] = item_details
return item_dict return item_dict

View File

@ -672,7 +672,7 @@ class TestProductionPlan(FrappeTestCase):
items_data = pln.get_production_items() items_data = pln.get_production_items()
# Update qty # Update qty
items_data[(item, None, None)]["qty"] = qty items_data[(pln.po_items[0].name, item, None)]["qty"] = qty
# Create and Submit Work Order for each item in items_data # Create and Submit Work Order for each item in items_data
for key, item in items_data.items(): for key, item in items_data.items():
@ -1522,6 +1522,45 @@ class TestProductionPlan(FrappeTestCase):
for d in mr_items: for d in mr_items:
self.assertEqual(d.get("quantity"), 1000.0) self.assertEqual(d.get("quantity"), 1000.0)
def test_fg_item_quantity(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_or_make_bin
fg_item = make_item(properties={"is_stock_item": 1}).name
rm_item = make_item(properties={"is_stock_item": 1}).name
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1)
pln.append(
"po_items",
{
"item_code": rm_item,
"planned_qty": 20,
"bom_no": pln.po_items[0].bom_no,
"warehouse": pln.po_items[0].warehouse,
"planned_start_date": add_to_date(nowdate(), days=1),
},
)
pln.submit()
wo_qty = {}
for row in pln.po_items:
wo_qty[row.name] = row.planned_qty
pln.make_work_order()
work_orders = frappe.get_all(
"Work Order",
fields=["qty", "production_plan_item as name"],
filters={"production_plan": pln.name},
)
self.assertEqual(len(work_orders), 2)
for row in work_orders:
self.assertEqual(row.qty, wo_qty[row.name])
def create_production_plan(**args): def create_production_plan(**args):
""" """

View File

@ -4,5 +4,5 @@ import frappe
def execute(): def execute():
subscription = frappe.qb.DocType("Subscription") subscription = frappe.qb.DocType("Subscription")
frappe.qb.update(subscription).set( frappe.qb.update(subscription).set(
subscription.generate_invoice_at, "Beginning of the currency subscription period" subscription.generate_invoice_at, "Beginning of the current subscription period"
).where(subscription.generate_invoice_at_period_start == 1).run() ).where(subscription.generate_invoice_at_period_start == 1).run()

View File

@ -131,6 +131,7 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"bold": 1,
"fieldname": "expected_start_date", "fieldname": "expected_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Expected Start Date", "label": "Expected Start Date",
@ -453,7 +454,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"max_attachments": 4, "max_attachments": 4,
"modified": "2023-08-28 22:27:28.370849", "modified": "2024-01-08 16:01:34.598258",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Project", "name": "Project",

View File

@ -19,6 +19,62 @@ from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
class Project(Document): class Project(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.projects.doctype.project_user.project_user import ProjectUser
actual_end_date: DF.Date | None
actual_start_date: DF.Date | None
actual_time: DF.Float
collect_progress: DF.Check
company: DF.Link
copied_from: DF.Data | None
cost_center: DF.Link | None
customer: DF.Link | None
daily_time_to_send: DF.Time | None
day_to_send: DF.Literal[
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
]
department: DF.Link | None
estimated_costing: DF.Currency
expected_end_date: DF.Date | None
expected_start_date: DF.Date | None
first_email: DF.Time | None
frequency: DF.Literal["Hourly", "Twice Daily", "Daily", "Weekly"]
from_time: DF.Time | None
gross_margin: DF.Currency
holiday_list: DF.Link | None
is_active: DF.Literal["Yes", "No"]
message: DF.Text | None
naming_series: DF.Literal["PROJ-.####"]
notes: DF.TextEditor | None
per_gross_margin: DF.Percent
percent_complete: DF.Percent
percent_complete_method: DF.Literal["Manual", "Task Completion", "Task Progress", "Task Weight"]
priority: DF.Literal["Medium", "Low", "High"]
project_name: DF.Data
project_template: DF.Link | None
project_type: DF.Link | None
sales_order: DF.Link | None
second_email: DF.Time | None
status: DF.Literal["Open", "Completed", "Cancelled"]
to_time: DF.Time | None
total_billable_amount: DF.Currency
total_billed_amount: DF.Currency
total_consumed_material_cost: DF.Currency
total_costing_amount: DF.Currency
total_purchase_cost: DF.Currency
total_sales_amount: DF.Currency
users: DF.Table[ProjectUser]
weekly_time_to_send: DF.Time | None
# end: auto-generated types
def onload(self): def onload(self):
self.set_onload( self.set_onload(
"activity_summary", "activity_summary",
@ -314,17 +370,15 @@ def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
def get_project_list( def get_project_list(
doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"
): ):
user = frappe.session.user
customers, suppliers = get_customers_suppliers("Project", frappe.session.user) customers, suppliers = get_customers_suppliers("Project", frappe.session.user)
ignore_permissions = False ignore_permissions = False
if is_website_user(): if is_website_user() and frappe.session.user != "Guest":
if not filters: if not filters:
filters = [] filters = []
if customers: if customers:
filters.append([doctype, "customer", "in", customers]) filters.append([doctype, "customer", "in", customers])
ignore_permissions = True ignore_permissions = True
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)

View File

@ -153,6 +153,7 @@
"label": "Timeline" "label": "Timeline"
}, },
{ {
"bold": 1,
"fieldname": "exp_start_date", "fieldname": "exp_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Expected Start Date", "label": "Expected Start Date",
@ -398,7 +399,7 @@
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"max_attachments": 5, "max_attachments": 5,
"modified": "2023-11-20 11:42:41.884069", "modified": "2024-01-08 16:00:41.296203",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Task", "name": "Task",

View File

@ -454,7 +454,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.weight_uom = ''; item.weight_uom = '';
item.conversion_factor = 0; item.conversion_factor = 0;
if(['Sales Invoice'].includes(this.frm.doc.doctype)) { if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
update_stock = cint(me.frm.doc.update_stock); update_stock = cint(me.frm.doc.update_stock);
show_batch_dialog = update_stock; show_batch_dialog = update_stock;
@ -545,7 +545,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}, },
() => me.toggle_conversion_factor(item), () => me.toggle_conversion_factor(item),
() => { () => {
if (show_batch_dialog) if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => { .then((r) => {
if (r.message && if (r.message &&
@ -790,7 +790,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (me.frm.doc.price_list_currency == company_currency) { if (me.frm.doc.price_list_currency == company_currency) {
me.frm.set_value('plc_conversion_rate', 1.0); me.frm.set_value('plc_conversion_rate', 1.0);
} }
if (company_doc.default_letter_head) { if (company_doc && company_doc.default_letter_head) {
if(me.frm.fields_dict.letter_head) { if(me.frm.fields_dict.letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head); me.frm.set_value("letter_head", company_doc.default_letter_head);
} }
@ -1239,6 +1239,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
} }
sync_bundle_data() {
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.sync_bundle_data();
barcode_scanner.remove_item_from_localstorage();
}
}
before_save(doc) {
this.sync_bundle_data();
}
service_start_date(frm, cdt, cdn) { service_start_date(frm, cdt, cdn) {
var child = locals[cdt][cdn]; var child = locals[cdt][cdn];
@ -1576,6 +1590,18 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return item_list; return item_list;
} }
items_delete() {
this.update_localstorage_scanned_data();
}
update_localstorage_scanned_data() {
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.update_localstorage_scanned_data();
}
}
_set_values_for_item_list(children) { _set_values_for_item_list(children) {
const items_rule_dict = {}; const items_rule_dict = {};

View File

@ -7,8 +7,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name]; this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];
this.barcode_field = opts.barcode_field || "barcode"; this.barcode_field = opts.barcode_field || "barcode";
this.serial_no_field = opts.serial_no_field || "serial_no";
this.batch_no_field = opts.batch_no_field || "batch_no";
this.uom_field = opts.uom_field || "uom"; this.uom_field = opts.uom_field || "uom";
this.qty_field = opts.qty_field || "qty"; this.qty_field = opts.qty_field || "qty";
// field name on row which defines max quantity to be scanned e.g. picklist // field name on row which defines max quantity to be scanned e.g. picklist
@ -84,6 +82,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
update_table(data) { update_table(data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid; let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
frappe.flags.trigger_from_barcode_scanner = true;
const {item_code, barcode, batch_no, serial_no, uom} = data; const {item_code, barcode, batch_no, serial_no, uom} = data;
@ -106,50 +105,38 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.frm.has_items = false; this.frm.has_items = false;
} }
if (this.is_duplicate_serial_no(row, serial_no)) { if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) {
this.clean_up(); this.clean_up();
reject(); reject();
return; return;
} }
frappe.run_serially([ frappe.run_serially([
() => this.set_selector_trigger_flag(data), () => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode), () => this.set_barcode(row, barcode),
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
this.show_scan_message(row.idx, row.item_code, qty); this.show_scan_message(row.idx, row.item_code, qty);
}), }),
() => this.set_barcode_uom(row, uom), () => this.set_barcode_uom(row, uom),
() => this.clean_up(), () => this.clean_up(),
() => this.revert_selector_flag(), () => resolve(row),
() => resolve(row) () => {
if (row.serial_and_batch_bundle && !this.frm.is_new()) {
this.frm.save();
}
frappe.flags.trigger_from_barcode_scanner = false;
}
]); ]);
}); });
} }
// batch and serial selector is reduandant when all info can be added by scan
// this flag on item row is used by transaction.js to avoid triggering selector
set_selector_trigger_flag(data) {
const {has_batch_no, has_serial_no} = data;
const require_selecting_batch = has_batch_no;
const require_selecting_serial = has_serial_no;
if (!(require_selecting_batch || require_selecting_serial)) {
frappe.flags.hide_serial_batch_dialog = true;
}
}
revert_selector_flag() {
frappe.flags.hide_serial_batch_dialog = false;
}
set_item(row, item_code, barcode, batch_no, serial_no) { set_item(row, item_code, barcode, batch_no, serial_no) {
return new Promise(resolve => { return new Promise(resolve => {
const increment = async (value = 1) => { const increment = async (value = 1) => {
const item_data = {item_code: item_code}; const item_data = {item_code: item_code};
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
frappe.flags.trigger_from_barcode_scanner = true;
await frappe.model.set_value(row.doctype, row.name, item_data); await frappe.model.set_value(row.doctype, row.name, item_data);
return value; return value;
}; };
@ -158,8 +145,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => { frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
increment(value).then((value) => resolve(value)); increment(value).then((value) => resolve(value));
}); });
} else if (this.frm.has_items) {
this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no);
} else { } else {
increment().then((value) => resolve(value)); increment().then((value) => resolve(value));
} }
@ -182,9 +167,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.model.set_value(row.doctype, row.name, item_data); frappe.model.set_value(row.doctype, row.name, item_data);
frappe.run_serially([ frappe.run_serially([
() => this.set_batch_no(row, this.dialog.get_value("batch_no")),
() => this.set_barcode(row, this.dialog.get_value("barcode")), () => this.set_barcode(row, this.dialog.get_value("barcode")),
() => this.set_serial_no(row, this.dialog.get_value("serial_no")), () => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")),
() => this.add_child_for_remaining_qty(row), () => this.add_child_for_remaining_qty(row),
() => this.clean_up() () => this.clean_up()
]); ]);
@ -338,18 +322,136 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
} }
} }
async set_serial_no(row, serial_no) { async set_serial_and_batch(row, item_code, serial_no, batch_no) {
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) { if (this.frm.is_new() || !row.serial_and_batch_bundle) {
const existing_serial_nos = row[this.serial_no_field]; this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no);
let new_serial_nos = ""; } else if(row.serial_and_batch_bundle) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch",
args: {
bundle_id: row.serial_and_batch_bundle,
serial_no: serial_no,
batch_no: batch_no,
},
})
}
}
if (!!existing_serial_nos) { get_key_for_localstorage() {
new_serial_nos = existing_serial_nos + "\n" + serial_no; let parts = this.frm.doc.name.split("-");
} else { return parts[parts.length - 1] + this.frm.doc.doctype;
new_serial_nos = serial_no;
} }
await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
update_localstorage_scanned_data() {
let docname = this.frm.doc.name
if (localStorage[docname]) {
let items = JSON.parse(localStorage[docname]);
let existing_items = this.frm.doc.items.map(d => d.item_code);
if (!existing_items.length) {
localStorage.removeItem(docname);
return;
} }
for (let item_code in items) {
if (!existing_items.includes(item_code)) {
delete items[item_code];
}
}
localStorage[docname] = JSON.stringify(items);
}
}
async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) {
let docname = this.frm.doc.name
let entries = JSON.parse(localStorage.getItem(docname));
if (!entries) {
entries = {};
}
let key = item_code;
if (!entries[key]) {
entries[key] = [];
}
let existing_row = [];
if (!serial_no && batch_no) {
existing_row = entries[key].filter((e) => e.batch_no === batch_no);
if (existing_row.length) {
existing_row[0].qty += 1;
}
} else if (serial_no) {
existing_row = entries[key].filter((e) => e.serial_no === serial_no);
if (existing_row.length) {
frappe.throw(__("Serial No {0} has already scanned.", [serial_no]));
}
}
if (!existing_row.length) {
entries[key].push({
"serial_no": serial_no,
"batch_no": batch_no,
"qty": 1
});
}
localStorage.setItem(docname, JSON.stringify(entries));
// Auto remove from localstorage after 1 hour
setTimeout(() => {
localStorage.removeItem(docname);
}, 3600000)
}
remove_item_from_localstorage() {
let docname = this.frm.doc.name;
if (localStorage[docname]) {
localStorage.removeItem(docname);
}
}
async sync_bundle_data() {
let docname = this.frm.doc.name;
if (localStorage[docname]) {
let entries = JSON.parse(localStorage[docname]);
if (entries) {
for (let entry in entries) {
let row = this.frm.doc.items.filter((item) => {
if (item.item_code === entry) {
return true;
}
})[0];
if (row) {
this.create_serial_and_batch_bundle(row, entries, entry)
.then(() => {
if (!entries) {
localStorage.removeItem(docname);
}
});
}
}
}
}
}
async create_serial_and_batch_bundle(row, entries, key) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
args: {
entries: entries[key],
child_row: row,
doc: this.frm.doc,
warehouse: row.warehouse,
do_not_save: 1
},
callback: function(r) {
row.serial_and_batch_bundle = r.message.name;
delete entries[key];
}
})
} }
async set_barcode_uom(row, uom) { async set_barcode_uom(row, uom) {
@ -358,12 +460,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
} }
} }
async set_batch_no(row, batch_no) {
if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
}
}
async set_barcode(row, barcode) { async set_barcode(row, barcode) {
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) { if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode); await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
@ -379,13 +475,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
} }
} }
is_duplicate_serial_no(row, serial_no) { is_duplicate_serial_no(row, item_code, serial_no) {
const is_duplicate = row[this.serial_no_field]?.includes(serial_no); if (this.frm.is_new() || !row.serial_and_batch_bundle) {
let is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
if (is_duplicate) { if (is_duplicate) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
} }
return is_duplicate; return is_duplicate;
} else if (row.serial_and_batch_bundle) {
this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
if (r.message) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
return r.message;
})
}
}
async check_duplicate_serial_no_in_db(row, serial_no, response) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
args: {
serial_no: serial_no,
bundle_id: row.serial_and_batch_bundle
},
callback(r) {
response(r);
}
})
}
check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
let docname = this.frm.doc.name
let entries = JSON.parse(localStorage.getItem(docname));
if (!entries) {
return false;
}
let existing_row = [];
if (entries[item_code]) {
existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
}
return existing_row.length;
} }
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) { get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {

View File

@ -370,6 +370,7 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_
) )
# sales team # sales team
if not target.get("sales_team"):
for d in customer.get("sales_team") or []: for d in customer.get("sales_team") or []:
target.append( target.append(
"sales_team", "sales_team",

View File

@ -582,17 +582,17 @@ class SalesOrder(SellingController):
def set_indicator(self): def set_indicator(self):
"""Set indicator for portal""" """Set indicator for portal"""
if self.per_billed < 100 and self.per_delivered < 100: self.indicator_color = {
self.indicator_color = "orange" "Draft": "red",
self.indicator_title = _("Not Paid and Not Delivered") "On Hold": "orange",
"To Deliver and Bill": "orange",
"To Bill": "orange",
"To Deliver": "orange",
"Completed": "green",
"Cancelled": "red",
}.get(self.status, "blue")
elif self.per_billed == 100 and self.per_delivered < 100: self.indicator_title = _(self.status)
self.indicator_color = "orange"
self.indicator_title = _("Paid and Not Delivered")
else:
self.indicator_color = "green"
self.indicator_title = _("Paid")
def on_recurring(self, reference_doc, auto_repeat_doc): def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):

View File

@ -149,6 +149,11 @@ def convert_order_to_invoices():
invoice.set_posting_time = 1 invoice.set_posting_time = 1
invoice.posting_date = order.transaction_date invoice.posting_date = order.transaction_date
invoice.due_date = order.transaction_date invoice.due_date = order.transaction_date
invoice.bill_date = order.transaction_date
if invoice.get("payment_schedule"):
invoice.payment_schedule[0].due_date = order.transaction_date
invoice.update_stock = 1 invoice.update_stock = 1
invoice.submit() invoice.submit()

View File

@ -616,8 +616,8 @@
"fieldname": "relieving_date", "fieldname": "relieving_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Relieving Date", "label": "Relieving Date",
"no_copy": 1,
"mandatory_depends_on": "eval:doc.status == \"Left\"", "mandatory_depends_on": "eval:doc.status == \"Left\"",
"no_copy": 1,
"oldfieldname": "relieving_date", "oldfieldname": "relieving_date",
"oldfieldtype": "Date" "oldfieldtype": "Date"
}, },
@ -822,12 +822,14 @@
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"is_tree": 1,
"links": [], "links": [],
"modified": "2023-10-04 10:57:05.174592", "modified": "2024-01-03 17:36:20.984421",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Employee", "name": "Employee",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"nsm_parent_field": "reports_to",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -860,7 +862,6 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"set_user_permissions": 1,
"share": 1, "share": 1,
"write": 1 "write": 1
} }

View File

@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
& ( & (
(table.from_date.between(self.from_date, self.to_date)) (table.from_date.between(self.from_date, self.to_date))
| (table.to_date.between(self.from_date, self.to_date)) | (table.to_date.between(self.from_date, self.to_date))
| (table.from_date >= self.from_date and table.to_date <= self.to_date) | (table.from_date >= self.from_date and table.to_date >= self.to_date)
) )
) )
) )

View File

@ -1518,6 +1518,25 @@ class TestDeliveryNote(FrappeTestCase):
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0
) )
def test_internal_transfer_for_non_stock_item(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
item = make_item(properties={"is_stock_item": 0}).name
warehouse = "_Test Warehouse - _TC"
target = "Stores - _TC"
company = "_Test Company"
customer = create_internal_customer(represents_company=company)
rate = 100
so = make_sales_order(item_code=item, qty=1, rate=rate, customer=customer, warehouse=warehouse)
dn = make_delivery_note(so.name)
dn.items[0].target_warehouse = target
dn.save().submit()
self.assertEqual(so.items[0].rate, rate)
self.assertEqual(dn.items[0].rate, so.items[0].rate)
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

@ -305,7 +305,7 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None):
dimensions = get_document_wise_inventory_dimensions(doc.doctype) dimensions = get_document_wise_inventory_dimensions(doc.doctype)
filter_dimensions = [] filter_dimensions = []
for row in dimensions: for row in dimensions:
if row.type_of_transaction: if row.type_of_transaction and row.type_of_transaction != "Both":
if ( if (
row.type_of_transaction == "Inward" row.type_of_transaction == "Inward"
if doc.docstatus == 1 if doc.docstatus == 1

View File

@ -429,6 +429,14 @@ class TestInventoryDimension(FrappeTestCase):
) )
warehouse = create_warehouse("Negative Stock Warehouse") warehouse = create_warehouse("Negative Stock Warehouse")
doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True)
doc.items[0].inv_site = "Site 1"
self.assertRaises(frappe.ValidationError, doc.submit)
doc.reload()
if doc.docstatus == 1:
doc.cancel()
doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True) doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
doc.items[0].to_inv_site = "Site 1" doc.items[0].to_inv_site = "Site 1"

View File

@ -202,6 +202,7 @@
"label": "Allow Alternative Item" "label": "Allow Alternative Item"
}, },
{ {
"allow_in_quick_entry": 1,
"bold": 1, "bold": 1,
"default": "1", "default": "1",
"depends_on": "eval:!doc.is_fixed_asset", "depends_on": "eval:!doc.is_fixed_asset",
@ -239,6 +240,7 @@
"label": "Standard Selling Rate" "label": "Standard Selling Rate"
}, },
{ {
"allow_in_quick_entry": 1,
"default": "0", "default": "0",
"fieldname": "is_fixed_asset", "fieldname": "is_fixed_asset",
"fieldtype": "Check", "fieldtype": "Check",
@ -246,6 +248,7 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"allow_in_quick_entry": 1,
"depends_on": "is_fixed_asset", "depends_on": "is_fixed_asset",
"fieldname": "asset_category", "fieldname": "asset_category",
"fieldtype": "Link", "fieldtype": "Link",
@ -888,7 +891,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-09-18 15:41:32.688051", "modified": "2024-01-08 18:09:30.225085",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -194,6 +194,7 @@ class LandedCostVoucher(Document):
for d in self.get("purchase_receipts"): for d in self.get("purchase_receipts"):
doc = frappe.get_doc(d.receipt_document_type, d.receipt_document) doc = frappe.get_doc(d.receipt_document_type, d.receipt_document)
# check if there are {qty} assets created and linked to this receipt document # check if there are {qty} assets created and linked to this receipt document
if self.docstatus != 2:
self.validate_asset_qty_and_status(d.receipt_document_type, doc) self.validate_asset_qty_and_status(d.receipt_document_type, doc)
# set landed cost voucher amount in pr item # set landed cost voucher amount in pr item
@ -235,20 +236,20 @@ class LandedCostVoucher(Document):
filters={receipt_document_type: item.receipt_document, "item_code": item.item_code}, filters={receipt_document_type: item.receipt_document, "item_code": item.item_code},
fields=["name", "docstatus"], fields=["name", "docstatus"],
) )
if not docs or len(docs) != item.qty: if not docs or len(docs) < item.qty:
frappe.throw( frappe.throw(
_( _(
"There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document." "There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document."
).format(item.receipt_document, item.qty) ).format(len(docs), item.receipt_document, item.qty)
) )
if docs: if docs:
for d in docs: for d in docs:
if d.docstatus == 1: if d.docstatus == 1:
frappe.throw( frappe.throw(
_( _(
"{2} <b>{0}</b> has submitted Assets. Remove Item <b>{1}</b> from table to continue." "{0} <b>{1}</b> has submitted Assets. Remove Item <b>{2}</b> from table to continue."
).format( ).format(
item.receipt_document, item.item_code, item.receipt_document_type item.receipt_document_type, item.receipt_document, item.item_code
) )
) )

View File

@ -717,7 +717,7 @@ class PurchaseReceipt(BuyingController):
): ):
warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse) warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
if d.is_fixed_asset: if d.is_fixed_asset and d.landed_cost_voucher_amount:
self.update_assets(d, d.valuation_rate) self.update_assets(d, d.valuation_rate)
if warehouse_with_no_account: if warehouse_with_no_account:
@ -849,11 +849,14 @@ class PurchaseReceipt(BuyingController):
) )
for asset in assets: for asset in assets:
purchase_amount = flt(valuation_rate) * asset.asset_quantity
frappe.db.set_value( frappe.db.set_value(
"Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity "Asset",
) asset.name,
frappe.db.set_value( {
"Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity "gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
},
) )
def update_status(self, status): def update_status(self, status):

View File

@ -729,19 +729,13 @@ class SerialandBatchBundle(Document):
def before_cancel(self): def before_cancel(self):
self.delink_serial_and_batch_bundle() self.delink_serial_and_batch_bundle()
self.clear_table()
def delink_serial_and_batch_bundle(self): def delink_serial_and_batch_bundle(self):
self.voucher_no = None
sles = frappe.get_all("Stock Ledger Entry", filters={"serial_and_batch_bundle": self.name}) sles = frappe.get_all("Stock Ledger Entry", filters={"serial_and_batch_bundle": self.name})
for sle in sles: for sle in sles:
frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_and_batch_bundle", None) frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_and_batch_bundle", None)
def clear_table(self):
self.set("entries", [])
@property @property
def child_table(self): def child_table(self):
if self.voucher_type == "Job Card": if self.voucher_type == "Job Card":
@ -876,7 +870,6 @@ class SerialandBatchBundle(Document):
self.validate_voucher_no_docstatus() self.validate_voucher_no_docstatus()
self.delink_refernce_from_voucher() self.delink_refernce_from_voucher()
self.delink_reference_from_batch() self.delink_reference_from_batch()
self.clear_table()
@frappe.whitelist() @frappe.whitelist()
def add_serial_batch(self, data): def add_serial_batch(self, data):
@ -1011,13 +1004,17 @@ def make_serial_nos(item_code, serial_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1) item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")] serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
existing_serial_nos = frappe.get_all("Serial No", filters={"name": ("in", serial_nos)})
existing_serial_nos = [d.get("name") for d in existing_serial_nos if d.get("name")]
serial_nos = list(set(serial_nos) - set(existing_serial_nos))
if not serial_nos:
return
serial_nos_details = [] serial_nos_details = []
user = frappe.session.user user = frappe.session.user
for serial_no in serial_nos: for serial_no in serial_nos:
if frappe.db.exists("Serial No", serial_no):
continue
serial_nos_details.append( serial_nos_details.append(
( (
serial_no, serial_no,
@ -1053,9 +1050,16 @@ def make_serial_nos(item_code, serial_nos):
def make_batch_nos(item_code, batch_nos): def make_batch_nos(item_code, batch_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1) item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
batch_nos = [d.get("batch_no") for d in batch_nos if d.get("batch_no")] batch_nos = [d.get("batch_no") for d in batch_nos if d.get("batch_no")]
existing_batches = frappe.get_all("Batch", filters={"name": ("in", batch_nos)})
existing_batches = [d.get("name") for d in existing_batches if d.get("name")]
batch_nos = list(set(batch_nos) - set(existing_batches))
if not batch_nos:
return
batch_nos_details = [] batch_nos_details = []
user = frappe.session.user user = frappe.session.user
for batch_no in batch_nos: for batch_no in batch_nos:
@ -1156,7 +1160,7 @@ def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name
@frappe.whitelist() @frappe.whitelist()
def add_serial_batch_ledgers(entries, child_row, doc, warehouse) -> object: def add_serial_batch_ledgers(entries, child_row, doc, warehouse, do_not_save=False) -> object:
if isinstance(child_row, str): if isinstance(child_row, str):
child_row = frappe._dict(parse_json(child_row)) child_row = frappe._dict(parse_json(child_row))
@ -1170,20 +1174,23 @@ def add_serial_batch_ledgers(entries, child_row, doc, warehouse) -> object:
if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle): if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
sb_doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse) sb_doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
else: else:
sb_doc = create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse) sb_doc = create_serial_batch_no_ledgers(
entries, child_row, parent_doc, warehouse, do_not_save=do_not_save
)
return sb_doc return sb_doc
def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object: def create_serial_batch_no_ledgers(
entries, child_row, parent_doc, warehouse=None, do_not_save=False
) -> object:
warehouse = warehouse or ( warehouse = warehouse or (
child_row.rejected_warehouse if child_row.is_rejected else child_row.warehouse child_row.rejected_warehouse if child_row.is_rejected else child_row.warehouse
) )
type_of_transaction = child_row.type_of_transaction type_of_transaction = get_type_of_transaction(parent_doc, child_row)
if parent_doc.get("doctype") == "Stock Entry": if parent_doc.get("doctype") == "Stock Entry":
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse
doc = frappe.get_doc( doc = frappe.get_doc(
@ -1214,6 +1221,7 @@ def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
doc.save() doc.save()
if do_not_save:
frappe.db.set_value(child_row.doctype, child_row.name, "serial_and_batch_bundle", doc.name) frappe.db.set_value(child_row.doctype, child_row.name, "serial_and_batch_bundle", doc.name)
frappe.msgprint(_("Serial and Batch Bundle created"), alert=True) frappe.msgprint(_("Serial and Batch Bundle created"), alert=True)
@ -1221,6 +1229,22 @@ def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
return doc return doc
def get_type_of_transaction(parent_doc, child_row):
type_of_transaction = child_row.type_of_transaction
if parent_doc.get("doctype") == "Stock Entry":
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
if not type_of_transaction:
type_of_transaction = "Outward"
if parent_doc.get("doctype") in ["Purchase Receipt", "Purchase Invoice"]:
type_of_transaction = "Inward"
if parent_doc.get("is_return"):
type_of_transaction = "Inward" if type_of_transaction == "Outward" else "Outward"
return type_of_transaction
def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object: def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle) doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
doc.voucher_detail_no = child_row.name doc.voucher_detail_no = child_row.name
@ -1247,6 +1271,25 @@ def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
return doc return doc
@frappe.whitelist()
def update_serial_or_batch(bundle_id, serial_no=None, batch_no=None):
if batch_no and not serial_no:
if qty := frappe.db.get_value(
"Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty"
):
frappe.db.set_value(
"Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty", qty + 1
)
return
doc = frappe.get_cached_doc("Serial and Batch Bundle", bundle_id)
if not serial_no and not batch_no:
return
doc.append("entries", {"serial_no": serial_no, "batch_no": batch_no, "qty": 1})
doc.save(ignore_permissions=True)
def get_serial_and_batch_ledger(**kwargs): def get_serial_and_batch_ledger(**kwargs):
kwargs = frappe._dict(kwargs) kwargs = frappe._dict(kwargs)
@ -2032,3 +2075,8 @@ def get_stock_ledgers_batches(kwargs):
@frappe.whitelist() @frappe.whitelist()
def get_batch_no_from_serial_no(serial_no): def get_batch_no_from_serial_no(serial_no):
return frappe.get_cached_value("Serial No", serial_no, "batch_no") return frappe.get_cached_value("Serial No", serial_no, "batch_no")
@frappe.whitelist()
def is_duplicate_serial_no(bundle_id, serial_no):
return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no})

View File

@ -10,6 +10,8 @@ from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
add_serial_batch_ledgers, add_serial_batch_ledgers,
make_batch_nos,
make_serial_nos,
) )
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@ -481,6 +483,38 @@ class TestSerialandBatchBundle(FrappeTestCase):
docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus") docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
self.assertEqual(docstatus, 2) self.assertEqual(docstatus, 2)
def test_batch_duplicate_entry(self):
item_code = make_item(properties={"has_batch_no": 1}).name
batch_id = "TEST-BATTCCH-VAL-00001"
batch_nos = [{"batch_no": batch_id, "qty": 1}]
make_batch_nos(item_code, batch_nos)
self.assertTrue(frappe.db.exists("Batch", batch_id))
batch_id = "TEST-BATTCCH-VAL-00001"
batch_nos = [{"batch_no": batch_id, "qty": 1}]
# Shouldn't throw duplicate entry error
make_batch_nos(item_code, batch_nos)
self.assertTrue(frappe.db.exists("Batch", batch_id))
def test_serial_no_duplicate_entry(self):
item_code = make_item(properties={"has_serial_no": 1}).name
serial_no_id = "TEST-SNID-VAL-00001"
serial_nos = [{"serial_no": serial_no_id, "qty": 1}]
make_serial_nos(item_code, serial_nos)
self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
serial_no_id = "TEST-SNID-VAL-00001"
serial_nos = [{"batch_no": serial_no_id, "qty": 1}]
# Shouldn't throw duplicate entry error
make_serial_nos(item_code, serial_nos)
self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
def get_batch_from_bundle(bundle): def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos from erpnext.stock.serial_batch_bundle import get_batch_nos

View File

@ -24,6 +24,7 @@ from frappe.utils import (
import erpnext import erpnext
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import ( from erpnext.manufacturing.doctype.bom.bom import (
add_additional_cost, add_additional_cost,
@ -208,7 +209,6 @@ class StockEntry(StockController):
self.validate_bom() self.validate_bom()
self.set_process_loss_qty() self.set_process_loss_qty()
self.validate_purchase_order() self.validate_purchase_order()
self.validate_subcontracting_order()
if self.purpose in ("Manufacture", "Repack"): if self.purpose in ("Manufacture", "Repack"):
self.mark_finished_and_scrap_items() self.mark_finished_and_scrap_items()
@ -274,6 +274,7 @@ class StockEntry(StockController):
return False return False
def on_submit(self): def on_submit(self):
self.validate_closed_subcontracting_order()
self.update_stock_ledger() self.update_stock_ledger()
self.update_work_order() self.update_work_order()
self.validate_subcontract_order() self.validate_subcontract_order()
@ -294,6 +295,7 @@ class StockEntry(StockController):
self.set_material_request_transfer_status("Completed") self.set_material_request_transfer_status("Completed")
def on_cancel(self): def on_cancel(self):
self.validate_closed_subcontracting_order()
self.update_subcontract_order_supplied_items() self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status() self.update_subcontracting_order_status()
@ -1197,19 +1199,9 @@ class StockEntry(StockController):
) )
) )
def validate_subcontracting_order(self): def validate_closed_subcontracting_order(self):
if self.get("subcontracting_order") and self.purpose in [ if self.get("subcontracting_order"):
"Send to Subcontractor", check_on_hold_or_closed_status("Subcontracting Order", self.subcontracting_order)
"Material Transfer",
]:
sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status")
if sco_status == "Closed":
frappe.throw(
_("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format(
self.subcontracting_order
)
)
def mark_finished_and_scrap_items(self): def mark_finished_and_scrap_items(self):
if self.purpose != "Repack" and any( if self.purpose != "Repack" and any(

View File

@ -111,13 +111,17 @@ class StockLedgerEntry(Document):
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time, "posting_time": self.posting_time,
"company": self.company, "company": self.company,
"sle": self.name,
} }
) )
sle = get_previous_sle(kwargs, extra_cond=extra_cond) sle = get_previous_sle(kwargs, extra_cond=extra_cond)
if sle: qty_after_transaction = 0.0
flt_precision = cint(frappe.db.get_default("float_precision")) or 2 flt_precision = cint(frappe.db.get_default("float_precision")) or 2
diff = sle.qty_after_transaction + flt(self.actual_qty) if sle:
qty_after_transaction = sle.qty_after_transaction
diff = qty_after_transaction + flt(self.actual_qty)
diff = flt(diff, flt_precision) diff = flt(diff, flt_precision)
if diff < 0 and abs(diff) > 0.0001: if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimensions) self.throw_validation_error(diff, dimensions)

View File

@ -209,7 +209,7 @@ class SerialBatchBundle:
frappe.db.set_value( frappe.db.set_value(
"Serial and Batch Bundle", "Serial and Batch Bundle",
{"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type}, {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
{"is_cancelled": 1, "voucher_no": ""}, {"is_cancelled": 1},
) )
if self.sle.serial_and_batch_bundle: if self.sle.serial_and_batch_bundle:

View File

@ -591,6 +591,13 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
as_dict=True, as_dict=True,
) )
if batch_no_data: if batch_no_data:
if frappe.get_cached_value("Item", batch_no_data.item_code, "has_serial_no"):
frappe.throw(
_(
"Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead."
).format(search_value, batch_no_data.item_code)
)
_update_item_info(batch_no_data) _update_item_info(batch_no_data)
set_cache(batch_no_data) set_cache(batch_no_data)
return batch_no_data return batch_no_data

View File

@ -101,9 +101,32 @@ frappe.ui.form.on('Subcontracting Order', {
}, },
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.docstatus == 1 && frm.has_perm("submit")) {
if (frm.doc.status == "Closed") {
frm.add_custom_button(__('Re-open'), () => frm.events.update_subcontracting_order_status(frm), __("Status"));
} else if(flt(frm.doc.per_received, 2) < 100) {
frm.add_custom_button(__('Close'), () => frm.events.update_subcontracting_order_status(frm, "Closed"), __("Status"));
}
}
frm.trigger('get_materials_from_supplier'); frm.trigger('get_materials_from_supplier');
}, },
update_subcontracting_order_status(frm, status) {
frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status",
args: {
sco: frm.doc.name,
status: status,
},
callback: function (r) {
if (!r.exc) {
frm.reload_doc();
}
},
});
},
get_materials_from_supplier: function (frm) { get_materials_from_supplier: function (frm) {
let sco_rm_details = []; let sco_rm_details = [];

View File

@ -370,7 +370,7 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled", "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled\nClosed",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"reqd": 1, "reqd": 1,
@ -454,7 +454,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-03 16:18:17.782538", "modified": "2024-01-03 20:56:04.670380",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Order", "name": "Subcontracting Order",

View File

@ -7,7 +7,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
@ -68,6 +68,7 @@ class SubcontractingOrder(SubcontractingController):
"Material Transferred", "Material Transferred",
"Partial Material Transferred", "Partial Material Transferred",
"Cancelled", "Cancelled",
"Closed",
] ]
supplied_items: DF.Table[SubcontractingOrderSuppliedItem] supplied_items: DF.Table[SubcontractingOrderSuppliedItem]
supplier: DF.Link supplier: DF.Link
@ -112,16 +113,10 @@ class SubcontractingOrder(SubcontractingController):
def on_submit(self): def on_submit(self):
self.update_prevdoc_status() self.update_prevdoc_status()
self.update_requested_qty()
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
self.update_status() self.update_status()
def on_cancel(self): def on_cancel(self):
self.update_prevdoc_status() self.update_prevdoc_status()
self.update_requested_qty()
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
self.update_status() self.update_status()
def validate_purchase_order_for_subcontracting(self): def validate_purchase_order_for_subcontracting(self):
@ -277,6 +272,9 @@ class SubcontractingOrder(SubcontractingController):
self.set_missing_values() self.set_missing_values()
def update_status(self, status=None, update_modified=True): def update_status(self, status=None, update_modified=True):
if self.status == "Closed" and self.status != status:
check_on_hold_or_closed_status("Purchase Order", self.purchase_order)
if self.docstatus >= 1 and not status: if self.docstatus >= 1 and not status:
if self.docstatus == 1: if self.docstatus == 1:
if self.status == "Draft": if self.status == "Draft":
@ -285,11 +283,6 @@ class SubcontractingOrder(SubcontractingController):
status = "Completed" status = "Completed"
elif self.per_received > 0 and self.per_received < 100: elif self.per_received > 0 and self.per_received < 100:
status = "Partially Received" status = "Partially Received"
for item in self.supplied_items:
if not item.returned_qty or (item.supplied_qty - item.consumed_qty - item.returned_qty) > 0:
break
else:
status = "Closed"
else: else:
total_required_qty = total_supplied_qty = 0 total_required_qty = total_supplied_qty = 0
for item in self.supplied_items: for item in self.supplied_items:
@ -304,13 +297,12 @@ class SubcontractingOrder(SubcontractingController):
elif self.docstatus == 2: elif self.docstatus == 2:
status = "Cancelled" status = "Cancelled"
if status: if status and self.status != status:
frappe.db.set_value( self.db_set("status", status, update_modified=update_modified)
"Subcontracting Order", self.name, "status", status, update_modified=update_modified
)
if status == "Closed": self.update_requested_qty()
update_po_status("Closed", self.purchase_order) self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
@frappe.whitelist() @frappe.whitelist()
@ -357,8 +349,8 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None):
@frappe.whitelist() @frappe.whitelist()
def update_subcontracting_order_status(sco): def update_subcontracting_order_status(sco, status=None):
if isinstance(sco, str): if isinstance(sco, str):
sco = frappe.get_doc("Subcontracting Order", sco) sco = frappe.get_doc("Subcontracting Order", sco)
sco.update_status() sco.update_status(status)

View File

@ -10,7 +10,7 @@ frappe.listview_settings['Subcontracting Order'] = {
"Completed": "green", "Completed": "green",
"Partial Material Transferred": "purple", "Partial Material Transferred": "purple",
"Material Transferred": "blue", "Material Transferred": "blue",
"Closed": "red", "Closed": "green",
"Cancelled": "red", "Cancelled": "red",
}; };
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];

View File

@ -95,14 +95,14 @@ class TestSubcontractingOrder(FrappeTestCase):
self.assertEqual(sco.status, "Partially Received") self.assertEqual(sco.status, "Partially Received")
# Closed # Closed
ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items]) sco.update_status("Closed")
ste.save()
ste.submit()
sco.load_from_db()
self.assertEqual(sco.status, "Closed") self.assertEqual(sco.status, "Closed")
ste.cancel() scr = make_subcontracting_receipt(sco.name)
sco.load_from_db() scr.save()
self.assertRaises(frappe.exceptions.ValidationError, scr.submit)
sco.update_status()
self.assertEqual(sco.status, "Partially Received") self.assertEqual(sco.status, "Partially Received")
scr.cancel()
# Completed # Completed
scr = make_subcontracting_receipt(sco.name) scr = make_subcontracting_receipt(sco.name)
@ -564,7 +564,6 @@ class TestSubcontractingOrder(FrappeTestCase):
sco.load_from_db() sco.load_from_db()
self.assertEqual(sco.status, "Closed")
self.assertEqual(sco.supplied_items[0].returned_qty, 5) self.assertEqual(sco.supplied_items[0].returned_qty, 5)
def test_ordered_qty_for_subcontracting_order(self): def test_ordered_qty_for_subcontracting_order(self):

View File

@ -93,7 +93,8 @@ frappe.ui.form.on('Subcontracting Receipt', {
get_query_filters: { get_query_filters: {
docstatus: 1, docstatus: 1,
per_received: ['<', 100], per_received: ['<', 100],
company: frm.doc.company company: frm.doc.company,
status: ['!=', 'Closed'],
} }
}); });
}, __('Get Items From')); }, __('Get Items From'));

View File

@ -8,6 +8,7 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
import erpnext import erpnext
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
@ -142,6 +143,7 @@ class SubcontractingReceipt(SubcontractingController):
self.get_current_stock() self.get_current_stock()
def on_submit(self): def on_submit(self):
self.validate_closed_subcontracting_order()
self.validate_available_qty_for_consumption() self.validate_available_qty_for_consumption()
self.update_status_updater_args() self.update_status_updater_args()
self.update_prevdoc_status() self.update_prevdoc_status()
@ -165,6 +167,7 @@ class SubcontractingReceipt(SubcontractingController):
"Repost Item Valuation", "Repost Item Valuation",
"Serial and Batch Bundle", "Serial and Batch Bundle",
) )
self.validate_closed_subcontracting_order()
self.update_status_updater_args() self.update_status_updater_args()
self.update_prevdoc_status() self.update_prevdoc_status()
self.set_consumed_qty_in_subcontract_order() self.set_consumed_qty_in_subcontract_order()
@ -175,6 +178,11 @@ class SubcontractingReceipt(SubcontractingController):
self.update_status() self.update_status()
self.delete_auto_created_batches() self.delete_auto_created_batches()
def validate_closed_subcontracting_order(self):
for item in self.items:
if item.subcontracting_order:
check_on_hold_or_closed_status("Subcontracting Order", item.subcontracting_order)
def validate_items_qty(self): def validate_items_qty(self):
for item in self.items: for item in self.items:
if not (item.qty or item.rejected_qty): if not (item.qty or item.rejected_qty):

View File

@ -15,7 +15,6 @@ def transaction_processing(data, from_doctype, to_doctype):
length_of_data = len(deserialized_data) length_of_data = len(deserialized_data)
if length_of_data > 10:
frappe.msgprint( frappe.msgprint(
_("Started a background job to create {1} {0}").format(to_doctype, length_of_data) _("Started a background job to create {1} {0}").format(to_doctype, length_of_data)
) )
@ -25,8 +24,6 @@ def transaction_processing(data, from_doctype, to_doctype):
from_doctype=from_doctype, from_doctype=from_doctype,
to_doctype=to_doctype, to_doctype=to_doctype,
) )
else:
job(deserialized_data, from_doctype, to_doctype)
@frappe.whitelist() @frappe.whitelist()