fix: resolved merge conflicts

This commit is contained in:
Daizy Modi 2022-11-17 19:13:10 +05:30
commit 678a4c33da
125 changed files with 4810 additions and 1122 deletions

View File

@ -59,7 +59,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.10' python-version: '3.11'
- name: Check for valid Python & Merge Conflicts - name: Check for valid Python & Merge Conflicts
run: | run: |

View File

@ -16,8 +16,8 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-ast - id: check-ast
- repo: https://gitlab.com/pycqa/flake8 - repo: https://github.com/PyCQA/flake8
rev: 3.9.2 rev: 5.0.4
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [ additional_dependencies: [

View File

@ -3,10 +3,6 @@
frappe.ui.form.on('Accounting Dimension Filter', { frappe.ui.form.on('Accounting Dimension Filter', {
refresh: function(frm, cdt, cdn) { refresh: function(frm, cdt, cdn) {
if (frm.doc.accounting_dimension) {
frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
}
let help_content = let help_content =
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);"> `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td> <tr><td>
@ -68,6 +64,7 @@ frappe.ui.form.on('Accounting Dimension Filter', {
frm.clear_table("dimensions"); frm.clear_table("dimensions");
let row = frm.add_child("dimensions"); let row = frm.add_child("dimensions");
row.accounting_dimension = frm.doc.accounting_dimension; row.accounting_dimension = frm.doc.accounting_dimension;
frm.fields_dict["dimensions"].grid.update_docfield_property("dimension_value", "label", frm.doc.accounting_dimension);
frm.refresh_field("dimensions"); frm.refresh_field("dimensions");
frm.trigger('setup_filters'); frm.trigger('setup_filters');
}, },

View File

@ -43,20 +43,13 @@ frappe.ui.form.on('Bank Guarantee', {
reference_docname: function(frm) { reference_docname: function(frm) {
if (frm.doc.reference_docname && frm.doc.reference_doctype) { if (frm.doc.reference_docname && frm.doc.reference_doctype) {
let fields_to_fetch = ["grand_total"];
let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier"; let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier";
if (frm.doc.reference_doctype == "Sales Order") {
fields_to_fetch.push("project");
}
fields_to_fetch.push(party_field);
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials", method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_voucher_details",
args: { args: {
"column_list": fields_to_fetch, "bank_guarantee_type": frm.doc.bg_type,
"doctype": frm.doc.reference_doctype, "reference_name": frm.doc.reference_docname
"docname": frm.doc.reference_docname
}, },
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {

View File

@ -2,11 +2,8 @@
# For license information, please see license.txt # For license information, please see license.txt
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.search import sanitize_searchfield
from frappe.model.document import Document from frappe.model.document import Document
@ -25,14 +22,18 @@ class BankGuarantee(Document):
@frappe.whitelist() @frappe.whitelist()
def get_vouchar_detials(column_list, doctype, docname): def get_voucher_details(bank_guarantee_type: str, reference_name: str):
column_list = json.loads(column_list) if not isinstance(reference_name, str):
for col in column_list: raise TypeError("reference_name must be a string")
sanitize_searchfield(col)
return frappe.db.sql( fields_to_fetch = ["grand_total"]
""" select {columns} from `tab{doctype}` where name=%s""".format(
columns=", ".join(column_list), doctype=doctype if bank_guarantee_type == "Receiving":
), doctype = "Sales Order"
docname, fields_to_fetch.append("customer")
as_dict=1, fields_to_fetch.append("project")
)[0] else:
doctype = "Purchase Order"
fields_to_fetch.append("supplier")
return frappe.db.get_value(doctype, reference_name, fields_to_fetch, as_dict=True)

View File

@ -12,6 +12,9 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}, },
}; };
}); });
let no_bank_transactions_text =
`<div class="text-muted text-center">${__("No Matching Bank Transactions Found")}</div>`
set_field_options("no_bank_transactions", no_bank_transactions_text);
}, },
onload: function (frm) { onload: function (frm) {

View File

@ -81,8 +81,7 @@
}, },
{ {
"fieldname": "no_bank_transactions", "fieldname": "no_bank_transactions",
"fieldtype": "HTML", "fieldtype": "HTML"
"options": "<div class=\"text-muted text-center\">No Matching Bank Transactions Found</div>"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
@ -109,4 +108,4 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC"
} }

View File

@ -100,7 +100,7 @@ frappe.ui.form.on("Bank Statement Import", {
if (frm.doc.status.includes("Success")) { if (frm.doc.status.includes("Success")) {
frm.add_custom_button( frm.add_custom_button(
__("Go to {0} List", [frm.doc.reference_doctype]), __("Go to {0} List", [__(frm.doc.reference_doctype)]),
() => frappe.set_route("List", frm.doc.reference_doctype) () => frappe.set_route("List", frm.doc.reference_doctype)
); );
} }

View File

@ -52,7 +52,7 @@ def validate_company(company):
if parent_company and (not allow_account_creation_against_child_company): if parent_company and (not allow_account_creation_against_child_company):
msg = _("{} is a child company.").format(frappe.bold(company)) + " " msg = _("{} is a child company.").format(frappe.bold(company)) + " "
msg += _("Please import accounts against parent company or enable {} in company master.").format( msg += _("Please import accounts against parent company or enable {} in company master.").format(
frappe.bold("Allow Account Creation Against Child Company") frappe.bold(_("Allow Account Creation Against Child Company"))
) )
frappe.throw(msg, title=_("Wrong Company")) frappe.throw(msg, title=_("Wrong Company"))

View File

@ -45,21 +45,6 @@ frappe.ui.form.on("Journal Entry Template", {
frm.trigger("clear_child"); frm.trigger("clear_child");
switch(frm.doc.voucher_type){ switch(frm.doc.voucher_type){
case "Opening Entry":
frm.set_value("is_opening", "Yes");
frappe.call({
type:"GET",
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
args: {
"company": frm.doc.company
},
callback: function(r) {
if(r.message) {
add_accounts(frm.doc, r.message);
}
}
});
break;
case "Bank Entry": case "Bank Entry":
case "Cash Entry": case "Cash Entry":
frappe.call({ frappe.call({

View File

@ -62,7 +62,6 @@ class PaymentEntry(AccountsController):
self.set_missing_values() self.set_missing_values()
self.validate_payment_type() self.validate_payment_type()
self.validate_party_details() self.validate_party_details()
self.validate_bank_accounts()
self.set_exchange_rate() self.set_exchange_rate()
self.validate_mandatory() self.validate_mandatory()
self.validate_reference_documents() self.validate_reference_documents()
@ -243,23 +242,6 @@ class PaymentEntry(AccountsController):
if not frappe.db.exists(self.party_type, self.party): if not frappe.db.exists(self.party_type, self.party):
frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party)) frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party))
if self.party_account and self.party_type in ("Customer", "Supplier"):
self.validate_account_type(
self.party_account, [erpnext.get_party_account_type(self.party_type)]
)
def validate_bank_accounts(self):
if self.payment_type in ("Pay", "Internal Transfer"):
self.validate_account_type(self.paid_from, ["Bank", "Cash"])
if self.payment_type in ("Receive", "Internal Transfer"):
self.validate_account_type(self.paid_to, ["Bank", "Cash"])
def validate_account_type(self, account, account_types):
account_type = frappe.get_cached_value("Account", account, "account_type")
# if account_type not in account_types:
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
def set_exchange_rate(self, ref_doc=None): def set_exchange_rate(self, ref_doc=None):
self.set_source_exchange_rate(ref_doc) self.set_source_exchange_rate(ref_doc)
self.set_target_exchange_rate(ref_doc) self.set_target_exchange_rate(ref_doc)

View File

@ -8,6 +8,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"barcode", "barcode",
"has_item_scanned",
"item_code", "item_code",
"col_break1", "col_break1",
"item_name", "item_name",
@ -808,11 +809,19 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Grant Commission", "label": "Grant Commission",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"depends_on": "barcode",
"fieldname": "has_item_scanned",
"fieldtype": "Check",
"label": "Has Item Scanned",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-10-05 12:23:47.506290", "modified": "2022-11-02 12:52:39.125295",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Item", "name": "POS Invoice Item",
@ -820,5 +829,6 @@
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -23,6 +23,7 @@
"fetch_customers", "fetch_customers",
"column_break_6", "column_break_6",
"primary_mandatory", "primary_mandatory",
"show_net_values_in_party_account",
"column_break_17", "column_break_17",
"customers", "customers",
"preferences", "preferences",
@ -291,10 +292,16 @@
"fieldname": "include_break", "fieldname": "include_break",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Page Break After Each SoA" "label": "Page Break After Each SoA"
},
{
"default": "0",
"fieldname": "show_net_values_in_party_account",
"fieldtype": "Check",
"label": "Show Net Values in Party Account"
} }
], ],
"links": [], "links": [],
"modified": "2022-10-17 17:47:08.662475", "modified": "2022-11-10 17:44:17.165991",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Statement Of Accounts", "name": "Process Statement Of Accounts",

View File

@ -95,6 +95,7 @@ def get_report_pdf(doc, consolidated=True):
"show_opening_entries": 0, "show_opening_entries": 0,
"include_default_book_entries": 0, "include_default_book_entries": 0,
"tax_id": tax_id if tax_id else None, "tax_id": tax_id if tax_id else None,
"show_net_values_in_party_account": doc.show_net_values_in_party_account,
} }
) )
col, res = get_soa(filters) col, res = get_soa(filters)

View File

@ -57,6 +57,8 @@
"column_break_28", "column_break_28",
"total", "total",
"net_total", "net_total",
"tax_withholding_net_total",
"base_tax_withholding_net_total",
"taxes_section", "taxes_section",
"taxes_and_charges", "taxes_and_charges",
"column_break_58", "column_break_58",
@ -89,7 +91,6 @@
"section_break_44", "section_break_44",
"apply_discount_on", "apply_discount_on",
"base_discount_amount", "base_discount_amount",
"additional_discount_account",
"column_break_46", "column_break_46",
"additional_discount_percentage", "additional_discount_percentage",
"discount_amount", "discount_amount",
@ -1421,6 +1422,26 @@
"label": "Is Old Subcontracting Flow", "label": "Is Old Subcontracting Flow",
"read_only": 1 "read_only": 1
}, },
{
"default": "0",
"fieldname": "tax_withholding_net_total",
"fieldtype": "Currency",
"hidden": 1,
"label": "Tax Withholding Net Total",
"no_copy": 1,
"options": "currency",
"read_only": 1
},
{
"fieldname": "base_tax_withholding_net_total",
"fieldtype": "Currency",
"hidden": 1,
"label": "Base Tax Withholding Net Total",
"no_copy": 1,
"options": "currency",
"print_hide": 1,
"read_only": 1
},
{ {
"collapsible_depends_on": "tax_withheld_vouchers", "collapsible_depends_on": "tax_withheld_vouchers",
"fieldname": "tax_withheld_vouchers_section", "fieldname": "tax_withheld_vouchers_section",
@ -1519,7 +1540,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-10-11 13:04:44.304389", "modified": "2022-11-04 01:02:44.544878",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -1410,7 +1410,7 @@ class PurchaseInvoice(BuyingController):
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.update_project() self.update_project()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ( self.ignore_linked_doctypes = (
@ -1463,6 +1463,7 @@ class PurchaseInvoice(BuyingController):
def update_billing_status_in_pr(self, update_modified=True): def update_billing_status_in_pr(self, update_modified=True):
updated_pr = [] updated_pr = []
po_details = []
for d in self.get("items"): for d in self.get("items"):
if d.pr_detail: if d.pr_detail:
billed_amt = frappe.db.sql( billed_amt = frappe.db.sql(
@ -1480,7 +1481,10 @@ class PurchaseInvoice(BuyingController):
) )
updated_pr.append(d.purchase_receipt) updated_pr.append(d.purchase_receipt)
elif d.po_detail: elif d.po_detail:
updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified) po_details.append(d.po_detail)
if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
for pr in set(updated_pr): for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage

View File

@ -40,6 +40,7 @@
"discount_amount", "discount_amount",
"base_rate_with_margin", "base_rate_with_margin",
"sec_break2", "sec_break2",
"apply_tds",
"rate", "rate",
"amount", "amount",
"item_tax_template", "item_tax_template",
@ -868,6 +869,12 @@
"label": "Product Bundle", "label": "Product Bundle",
"options": "Product Bundle", "options": "Product Bundle",
"read_only": 1 "read_only": 1
},
{
"default": "1",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply TDS"
} }
], ],
"idx": 1, "idx": 1,

View File

@ -0,0 +1,53 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Repost Payment Ledger', {
setup: function(frm) {
frm.set_query("voucher_type", () => {
return {
filters: {
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']]
}
};
});
frm.fields_dict['repost_vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
return {
filters: {
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']]
}
}
}
frm.fields_dict['repost_vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
if (doc.company) {
return {
filters: {
company: doc.company,
docstatus: 1
}
}
}
}
},
refresh: function(frm) {
if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.repost_status)) {
frm.set_intro(__("Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status."));
var btn_label = __("Repost in background")
frm.add_custom_button(btn_label, () => {
frappe.call({
method: 'erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.execute_repost_payment_ledger',
args: {
docname: frm.doc.name,
}
});
frappe.msgprint(__('Reposting in the background.'));
});
}
}
});

View File

@ -0,0 +1,159 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-10-19 21:59:33.553852",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"filters_section",
"company",
"posting_date",
"column_break_4",
"voucher_type",
"add_manually",
"status_section",
"repost_status",
"repost_error_log",
"selected_vouchers_section",
"repost_vouchers",
"amended_from"
],
"fields": [
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Repost Payment Ledger",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "selected_vouchers_section",
"fieldtype": "Section Break",
"label": "Vouchers"
},
{
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "repost_vouchers",
"fieldtype": "Table",
"label": "Selected Vouchers",
"options": "Repost Payment Ledger Items"
},
{
"fieldname": "repost_status",
"fieldtype": "Select",
"label": "Repost Status",
"options": "\nQueued\nFailed\nCompleted",
"read_only": 1
},
{
"fieldname": "status_section",
"fieldtype": "Section Break",
"label": "Status"
},
{
"default": "0",
"description": "Ignore Voucher Type filter and Select Vouchers Manually",
"fieldname": "add_manually",
"fieldtype": "Check",
"label": "Add Manually"
},
{
"depends_on": "eval:doc.repost_error_log",
"fieldname": "repost_error_log",
"fieldtype": "Long Text",
"label": "Repost Error Log"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-11-08 07:38:40.079038",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,111 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils.background_jobs import is_job_queued
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
if voucher_type and voucher_no and gle_map:
_delete_pl_entries(voucher_type, voucher_no)
create_payment_ledger_entry(gle_map, cancel=0)
@frappe.whitelist()
def start_payment_ledger_repost(docname=None):
"""
Repost Payment Ledger Entries for Vouchers through Background Job
"""
if docname:
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
if repost_doc.docstatus == 1 and repost_doc.repost_status in ["Queued", "Failed"]:
try:
for entry in repost_doc.repost_vouchers:
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
else:
gle_map = doc.get_gl_entries()
repost_ple_for_voucher(entry.voucher_type, entry.voucher_no, gle_map)
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", "")
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Completed")
except Exception as e:
frappe.db.rollback()
traceback = frappe.get_traceback()
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_status", "Failed")
class RepostPaymentLedger(Document):
def __init__(self, *args, **kwargs):
super(RepostPaymentLedger, self).__init__(*args, **kwargs)
self.vouchers = []
def before_validate(self):
self.load_vouchers_based_on_filters()
self.set_status()
def load_vouchers_based_on_filters(self):
if not self.add_manually:
self.repost_vouchers.clear()
self.get_vouchers()
self.extend("repost_vouchers", copy.deepcopy(self.vouchers))
def get_vouchers(self):
self.vouchers.clear()
filter_on_voucher_types = [self.voucher_type] if self.voucher_type else VOUCHER_TYPES
for vtype in filter_on_voucher_types:
doc = qb.DocType(vtype)
doctype_name = ConstantColumn(vtype)
query = (
qb.from_(doc)
.select(doctype_name.as_("voucher_type"), doc.name.as_("voucher_no"))
.where(
(doc.docstatus == 1)
& (doc.company == self.company)
& (doc.posting_date.gte(self.posting_date))
)
)
entries = query.run(as_dict=True)
self.vouchers.extend(entries)
def set_status(self):
if self.docstatus == 0:
self.repost_status = "Queued"
def on_submit(self):
execute_repost_payment_ledger(self.name)
frappe.msgprint(_("Repost started in the background"))
@frappe.whitelist()
def execute_repost_payment_ledger(docname):
"""Repost Payment Ledger Entries by background job."""
job_name = "payment_ledger_repost_" + docname
if not is_job_queued(job_name):
frappe.enqueue(
method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
docname=docname,
is_async=True,
job_name=job_name,
)

View File

@ -0,0 +1,12 @@
frappe.listview_settings["Repost Payment Ledger"] = {
add_fields: ["repost_status"],
get_indicator: function(doc) {
var colors = {
'Queued': 'orange',
'Completed': 'green',
'Failed': 'red',
};
let status = doc.repost_status;
return [__(status), colors[status], 'status,=,'+status];
},
};

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestRepostPaymentLedger(FrappeTestCase):
pass

View File

@ -0,0 +1,35 @@
{
"actions": [],
"creation": "2022-10-20 10:44:18.796489",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"voucher_type",
"voucher_no"
],
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": "Voucher No",
"options": "voucher_type"
}
],
"istable": 1,
"links": [],
"modified": "2022-10-28 14:47:11.838109",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger Items",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RepostPaymentLedgerItems(Document):
pass

View File

@ -64,6 +64,25 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return); this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update."));
this.frm.add_custom_button(__('Repost Accounting Entries'),
() => {
this.frm.call({
doc: this.frm.doc,
method: 'repost_accounting_entries',
freeze: true,
freeze_message: __('Reposting...'),
callback: (r) => {
if (!r.exc) {
frappe.msgprint(__('Accounting Entries are reposted'));
me.frm.refresh();
}
}
});
}).removeClass('btn-default').addClass('btn-warning');
}
if (this.frm.doc.is_return) { if (this.frm.doc.is_return) {
this.frm.return_print_format = "Sales Invoice Return"; this.frm.return_print_format = "Sales Invoice Return";
} }

View File

@ -207,6 +207,7 @@
"is_internal_customer", "is_internal_customer",
"is_discounted", "is_discounted",
"remarks", "remarks",
"repost_required",
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
@ -1035,6 +1036,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "redeem_loyalty_points", "depends_on": "redeem_loyalty_points",
"fieldname": "loyalty_redemption_account", "fieldname": "loyalty_redemption_account",
"fieldtype": "Link", "fieldtype": "Link",
@ -1333,6 +1335,7 @@
"options": "fa fa-money" "options": "fa fa-money"
}, },
{ {
"allow_on_submit": 1,
"depends_on": "is_pos", "depends_on": "is_pos",
"fieldname": "cash_bank_account", "fieldname": "cash_bank_account",
"fieldtype": "Link", "fieldtype": "Link",
@ -1432,6 +1435,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "is_pos", "depends_on": "is_pos",
"fieldname": "account_for_change_amount", "fieldname": "account_for_change_amount",
"fieldtype": "Link", "fieldtype": "Link",
@ -1480,6 +1484,7 @@
"hide_seconds": 1 "hide_seconds": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "write_off_account", "fieldname": "write_off_account",
"fieldtype": "Link", "fieldtype": "Link",
"hide_days": 1, "hide_days": 1,
@ -1703,6 +1708,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"default": "No", "default": "No",
"fieldname": "is_opening", "fieldname": "is_opening",
"fieldtype": "Select", "fieldtype": "Select",
@ -1917,6 +1923,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "eval:doc.is_internal_customer", "depends_on": "eval:doc.is_internal_customer",
"description": "Unrealized Profit / Loss account for intra-company transfers", "description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account", "fieldname": "unrealized_profit_loss_account",
@ -1959,6 +1966,7 @@
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "additional_discount_account", "fieldname": "additional_discount_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Discount Account", "label": "Discount Account",
@ -2090,13 +2098,22 @@
{ {
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "write_off_amount", "collapsible_depends_on": "write_off_amount",
"depends_on": "grand_total", "depends_on": "is_pos",
"fieldname": "write_off_section", "fieldname": "write_off_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Write Off", "label": "Write Off",
"width": "50%" "width": "50%"
},
{
"default": "0",
"fieldname": "repost_required",
"fieldtype": "Check",
"hidden": 1,
"label": "Repost Required",
"no_copy": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -2109,7 +2126,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-10-11 13:07:36.488095", "modified": "2022-11-15 09:33:47.870616",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -11,6 +11,9 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form
import erpnext import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points, get_loyalty_program_details_with_points,
validate_loyalty_points, validate_loyalty_points,
@ -100,13 +103,11 @@ class SalesInvoice(SellingController):
self.validate_debit_to_acc() self.validate_debit_to_acc()
self.clear_unallocated_advances("Sales Invoice Advance", "advances") self.clear_unallocated_advances("Sales Invoice Advance", "advances")
self.add_remarks() self.add_remarks()
self.validate_write_off_account()
self.validate_account_for_change_amount()
self.validate_fixed_asset() self.validate_fixed_asset()
self.set_income_account_for_fixed_assets() self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers() self.validate_item_cost_centers()
self.validate_income_account()
self.check_conversion_rate() self.check_conversion_rate()
self.validate_accounts()
validate_inter_company_party( validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_invoice_reference self.doctype, self.customer, self.company, self.inter_company_invoice_reference
@ -170,6 +171,11 @@ class SalesInvoice(SellingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_accounts(self):
self.validate_write_off_account()
self.validate_account_for_change_amount()
self.validate_income_account()
def validate_fixed_asset(self): def validate_fixed_asset(self):
for d in self.get("items"): for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
@ -367,7 +373,8 @@ class SalesInvoice(SellingController):
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.db_set("repost_required", 0)
if ( if (
frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction"
@ -514,6 +521,93 @@ class SalesInvoice(SellingController):
def on_update(self): def on_update(self):
self.set_paid_amount() self.set_paid_amount()
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
needs_repost = 0
# Check if any field affecting accounting entry is altered
doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if opening entry check updated
if doc_before_update.get("is_opening") != self.is_opening:
needs_repost = 1
if not needs_repost:
# Parent Level Accounts excluding party account
for field in (
"additional_discount_account",
"cash_bank_account",
"account_for_change_amount",
"write_off_account",
"loyalty_redemption_account",
"unrealized_profit_loss_account",
):
if doc_before_update.get(field) != self.get(field):
needs_repost = 1
break
# Check for parent accounting dimensions
for dimension in accounting_dimensions:
if doc_before_update.get(dimension) != self.get(dimension):
needs_repost = 1
break
# Check for child tables
if self.check_if_child_table_updated(
"items",
doc_before_update,
("income_account", "expense_account", "discount_account"),
accounting_dimensions,
):
needs_repost = 1
if self.check_if_child_table_updated(
"taxes", doc_before_update, ("account_head",), accounting_dimensions
):
needs_repost = 1
self.validate_accounts()
# validate if deferred revenue is enabled for any item
# Don't allow to update the invoice if deferred revenue is enabled
if needs_repost:
for item in self.get("items"):
if item.enable_deferred_revenue:
frappe.throw(
_(
"Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission."
).format(item.item_code)
)
self.db_set("repost_required", needs_repost)
def check_if_child_table_updated(
self, child_table, doc_before_update, fields_to_check, accounting_dimensions
):
# Check if any field affecting accounting entry is altered
for index, item in enumerate(self.get(child_table)):
for field in fields_to_check:
if doc_before_update.get(child_table)[index].get(field) != item.get(field):
return True
for dimension in accounting_dimensions:
if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension):
return True
return False
@frappe.whitelist()
def repost_accounting_entries(self):
if self.repost_required:
self.docstatus = 2
self.make_gl_entries_on_cancel()
self.docstatus = 1
self.make_gl_entries()
self.db_set("repost_required", 0)
else:
frappe.throw(_("No updates pending for reposting"))
def set_paid_amount(self): def set_paid_amount(self):
paid_amount = 0.0 paid_amount = 0.0
base_paid_amount = 0.0 base_paid_amount = 0.0
@ -1300,7 +1394,11 @@ class SalesInvoice(SellingController):
def make_write_off_gl_entry(self, gl_entries): def make_write_off_gl_entry(self, gl_entries):
# write off entries, applicable if only pos # write off entries, applicable if only pos
if self.write_off_account and flt(self.write_off_amount, self.precision("write_off_amount")): if (
self.is_pos
and self.write_off_account
and flt(self.write_off_amount, self.precision("write_off_amount"))
):
write_off_account_currency = get_account_currency(self.write_off_account) write_off_account_currency = get_account_currency(self.write_off_account)
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center") default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
@ -2306,7 +2404,7 @@ def get_loyalty_programs(customer):
lp_details = get_loyalty_programs(customer) lp_details = get_loyalty_programs(customer)
if len(lp_details) == 1: if len(lp_details) == 1:
frappe.db.set(customer, "loyalty_program", lp_details[0]) customer.db_set("loyalty_program", lp_details[0])
return lp_details return lp_details
else: else:
return lp_details return lp_details

View File

@ -2729,6 +2729,31 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
# Update Invoice post submit and then check GL Entries again
si.load_from_db()
si.items[0].income_account = "Service - _TC"
si.additional_discount_account = "_Test Account Sales - _TC"
si.taxes[0].account_head = "TDS Payable - _TC"
si.save()
si.load_from_db()
self.assertTrue(si.repost_required)
si.repost_accounting_entries()
expected_gle = [
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
["Debtors - _TC", 88, 0.0, nowdate()],
["Service - _TC", 0.0, 100.0, nowdate()],
["TDS Payable - _TC", 0.0, 10.0, nowdate()],
]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
si.load_from_db()
self.assertFalse(si.repost_required)
def test_asset_depreciation_on_sale_with_pro_rata(self): def test_asset_depreciation_on_sale_with_pro_rata(self):
""" """
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
@ -3286,6 +3311,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
"""select account, debit, credit, posting_date """select account, debit, credit, posting_date
from `tabGL Entry` from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s
and is_cancelled = 0
order by posting_date asc, account asc""", order by posting_date asc, account asc""",
(voucher_no, posting_date), (voucher_no, posting_date),
as_dict=1, as_dict=1,

View File

@ -8,6 +8,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"barcode", "barcode",
"has_item_scanned",
"item_code", "item_code",
"col_break1", "col_break1",
"item_name", "item_name",
@ -437,6 +438,7 @@
"label": "Accounting Details" "label": "Accounting Details"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "income_account", "fieldname": "income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Income Account", "label": "Income Account",
@ -449,6 +451,7 @@
"width": "120px" "width": "120px"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "expense_account", "fieldname": "expense_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Expense Account", "label": "Expense Account",
@ -468,6 +471,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"allow_on_submit": 1,
"default": ":Company", "default": ":Company",
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
@ -799,6 +803,7 @@
"options": "Finance Book" "options": "Finance Book"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
@ -821,7 +826,6 @@
"label": "Incoming Rate (Costing)", "label": "Incoming Rate (Costing)",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1 "print_hide": 1
}, },
{ {
@ -834,6 +838,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "discount_account", "fieldname": "discount_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Discount Account", "label": "Discount Account",
@ -872,12 +877,20 @@
"label": "Purchase Order Item", "label": "Purchase Order Item",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"default": "0",
"depends_on": "barcode",
"fieldname": "has_item_scanned",
"fieldtype": "Check",
"label": "Has Item Scanned",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-10-26 11:38:36.119339", "modified": "2022-11-02 12:53:12.693217",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -51,6 +51,7 @@
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
{ {
"allow_on_submit": 1,
"columns": 2, "columns": 2,
"fieldname": "account_head", "fieldname": "account_head",
"fieldtype": "Link", "fieldtype": "Link",
@ -63,6 +64,7 @@
"search_index": 1 "search_index": 1
}, },
{ {
"allow_on_submit": 1,
"default": ":Company", "default": ":Company",
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
@ -216,12 +218,13 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-05 20:04:01.726867", "modified": "2022-10-17 13:08:17.776528",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Taxes and Charges", "name": "Sales Taxes and Charges",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@ -61,6 +61,9 @@ def get_party_details(inv):
def get_party_tax_withholding_details(inv, tax_withholding_category=None): def get_party_tax_withholding_details(inv, tax_withholding_category=None):
if inv.doctype == "Payment Entry":
inv.tax_withholding_net_total = inv.net_total
pan_no = "" pan_no = ""
parties = [] parties = []
party_type, party = get_party_details(inv) party_type, party = get_party_details(inv)
@ -242,7 +245,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if party_type == "Supplier": if party_type == "Supplier":
ldc = get_lower_deduction_certificate(tax_details, pan_no) ldc = get_lower_deduction_certificate(tax_details, pan_no)
if tax_deducted: if tax_deducted:
net_total = inv.net_total net_total = inv.tax_withholding_net_total
if ldc: if ldc:
tax_amount = get_tds_amount_from_ldc( tax_amount = get_tds_amount_from_ldc(
ldc, parties, pan_no, tax_details, posting_date, net_total ldc, parties, pan_no, tax_details, posting_date, net_total
@ -272,6 +275,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
field = (
"base_tax_withholding_net_total as base_net_total"
if party_type == "Supplier"
else "base_net_total"
)
voucher_wise_amount = {} voucher_wise_amount = {}
vouchers = [] vouchers = []
@ -288,7 +296,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
) )
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"]) invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field])
for d in invoices_details: for d in invoices_details:
vouchers.append(d.name) vouchers.append(d.name)
@ -392,7 +400,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
tds_amount = 0 tds_amount = 0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
field = "sum(net_total)" field = "sum(tax_withholding_net_total)"
if cint(tax_details.consider_party_ledger_amount): if cint(tax_details.consider_party_ledger_amount):
invoice_filters.pop("apply_tds", None) invoice_filters.pop("apply_tds", None)
@ -415,12 +423,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
) )
supp_credit_amt += supp_jv_credit_amt supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.net_total supp_credit_amt += inv.tax_withholding_net_total
threshold = tax_details.get("threshold", 0) threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0)
if (threshold and inv.net_total >= threshold) or ( if (threshold and inv.tax_withholding_net_total >= threshold) or (
cumulative_threshold and supp_credit_amt >= cumulative_threshold cumulative_threshold and supp_credit_amt >= cumulative_threshold
): ):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint( if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
@ -428,11 +436,11 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
): ):
# Get net total again as TDS is calculated on net total # Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach # Grand is used to just check for threshold breach
net_total = 0 net_total = (
if vouchers: frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)")
net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0
)
net_total += inv.net_total net_total += inv.tax_withholding_net_total
supp_credit_amt = net_total - cumulative_threshold supp_credit_amt = net_total - cumulative_threshold
if ldc and is_valid_certificate( if ldc and is_valid_certificate(
@ -440,7 +448,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
ldc.valid_upto, ldc.valid_upto,
inv.get("posting_date") or inv.get("transaction_date"), inv.get("posting_date") or inv.get("transaction_date"),
tax_deducted, tax_deducted,
inv.net_total, inv.tax_withholding_net_total,
ldc.certificate_limit, ldc.certificate_limit,
): ):
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
@ -523,7 +531,7 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
limit_consumed = frappe.db.get_value( limit_consumed = frappe.db.get_value(
"Purchase Invoice", "Purchase Invoice",
{"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1}, {"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1},
"sum(net_total)", "sum(tax_withholding_net_total)",
) )
if is_valid_certificate( if is_valid_certificate(

View File

@ -186,6 +186,46 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in reversed(invoices): for d in reversed(invoices):
d.cancel() d.cancel()
def test_tds_calculation_on_net_total_partial_tds(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
)
invoices = []
pi = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000, do_not_save=True)
pi.extend(
"items",
[
{
"doctype": "Purchase Invoice Item",
"item_code": frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name"),
"qty": 1,
"rate": 20000,
"cost_center": "Main - _TC",
"expense_account": "Stock Received But Not Billed - _TC",
"apply_tds": 0,
},
{
"doctype": "Purchase Invoice Item",
"item_code": frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name"),
"qty": 1,
"rate": 35000,
"cost_center": "Main - _TC",
"expense_account": "Stock Received But Not Billed - _TC",
"apply_tds": 1,
},
],
)
pi.save()
pi.submit()
invoices.append(pi)
self.assertEqual(pi.taxes[0].tax_amount, 5500)
# cancel invoices to avoid clashing
for d in reversed(invoices):
d.cancel()
def test_multi_category_single_supplier(self): def test_multi_category_single_supplier(self):
frappe.db.set_value( frappe.db.set_value(
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category" "Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"

View File

@ -3,7 +3,8 @@
import frappe import frappe
from frappe import _, scrub from frappe import _, qb, scrub
from frappe.query_builder import Order
from frappe.utils import cint, flt, formatdate from frappe.utils import cint, flt, formatdate
from erpnext.controllers.queries import get_match_cond from erpnext.controllers.queries import get_match_cond
@ -398,6 +399,7 @@ class GrossProfitGenerator(object):
self.average_buying_rate = {} self.average_buying_rate = {}
self.filters = frappe._dict(filters) self.filters = frappe._dict(filters)
self.load_invoice_items() self.load_invoice_items()
self.get_delivery_notes()
if filters.group_by == "Invoice": if filters.group_by == "Invoice":
self.group_items_by_invoice() self.group_items_by_invoice()
@ -591,6 +593,21 @@ class GrossProfitGenerator(object):
return flt(buying_amount, self.currency_precision) return flt(buying_amount, self.currency_precision)
def calculate_buying_amount_from_sle(self, row, my_sle, parenttype, parent, item_row, item_code):
for i, sle in enumerate(my_sle):
# find the stock valution rate from stock ledger entry
if (
sle.voucher_type == parenttype
and parent == sle.voucher_no
and sle.voucher_detail_no == item_row
):
previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0
if previous_stock_value:
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
def get_buying_amount(self, row, item_code): def get_buying_amount(self, row, item_code):
# IMP NOTE # IMP NOTE
# stock_ledger_entries should already be filtered by item_code and warehouse and # stock_ledger_entries should already be filtered by item_code and warehouse and
@ -607,19 +624,22 @@ class GrossProfitGenerator(object):
if row.dn_detail: if row.dn_detail:
parenttype, parent = "Delivery Note", row.delivery_note parenttype, parent = "Delivery Note", row.delivery_note
for i, sle in enumerate(my_sle): return self.calculate_buying_amount_from_sle(
# find the stock valution rate from stock ledger entry row, my_sle, parenttype, parent, row.item_row, item_code
if ( )
sle.voucher_type == parenttype elif self.delivery_notes.get((row.parent, row.item_code), None):
and parent == sle.voucher_no # check if Invoice has delivery notes
and sle.voucher_detail_no == row.item_row dn = self.delivery_notes.get((row.parent, row.item_code))
): parenttype, parent, item_row, warehouse = (
previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 "Delivery Note",
dn["delivery_note"],
if previous_stock_value: dn["item_row"],
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) dn["warehouse"],
else: )
return flt(row.qty) * self.get_average_buying_rate(row, item_code) my_sle = self.sle.get((item_code, warehouse))
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
else: else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code) return flt(row.qty) * self.get_average_buying_rate(row, item_code)
@ -753,6 +773,29 @@ class GrossProfitGenerator(object):
as_dict=1, as_dict=1,
) )
def get_delivery_notes(self):
self.delivery_notes = frappe._dict({})
if self.si_list:
invoices = [x.parent for x in self.si_list]
dni = qb.DocType("Delivery Note Item")
delivery_notes = (
qb.from_(dni)
.select(
dni.against_sales_invoice.as_("sales_invoice"),
dni.item_code,
dni.warehouse,
dni.parent.as_("delivery_note"),
dni.name.as_("item_row"),
)
.where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices)))
.groupby(dni.against_sales_invoice, dni.item_code)
.orderby(dni.creation, order=Order.desc)
.run(as_dict=True)
)
for entry in delivery_notes:
self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry
def group_items_by_invoice(self): def group_items_by_invoice(self):
""" """
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.

View File

@ -0,0 +1,209 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.gross_profit.gross_profit import execute
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestGrossProfit(FrappeTestCase):
def setUp(self):
self.create_company()
self.create_item()
self.create_customer()
self.create_sales_invoice()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def create_company(self):
company_name = "_Test Gross Profit"
abbr = "_GP"
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.creditors = "Creditors - " + abbr
def create_item(self):
item = create_item(
item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
name = "_Test GP Customer"
if frappe.db.exists("Customer", name):
self.customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.save()
self.customer = customer.name
def create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
sinv = create_sales_invoice(
qty=qty,
rate=rate,
company=self.company,
customer=self.customer,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return sinv
def clear_old_entries(self):
doctype_list = [
"Sales Invoice",
"GL Entry",
"Payment Ledger Entry",
"Stock Entry",
"Stock Ledger Entry",
"Delivery Note",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def test_invoice_without_only_delivery_note(self):
"""
Test buying amount for Invoice without `update_stock` flag set but has Delivery Note
"""
se = make_stock_entry(
company=self.company,
item_code=self.item,
target=self.warehouse,
qty=1,
basic_rate=100,
do_not_submit=True,
)
item = se.items[0]
se.append(
"items",
{
"item_code": item.item_code,
"s_warehouse": item.s_warehouse,
"t_warehouse": item.t_warehouse,
"qty": 1,
"basic_rate": 200,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"cost_center": item.cost_center,
"expense_account": item.expense_account,
},
)
se = se.save().submit()
sinv = create_sales_invoice(
qty=1,
rate=100,
company=self.company,
customer=self.customer,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
income_account=self.income_account,
expense_account=self.expense_account,
)
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
# Without Delivery Note, buying rate should be 150
expected_entry_without_dn = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 150.0,
"selling_amount": 100.0,
"buying_amount": 150.0,
"gross_profit": -50.0,
"gross_profit_%": -50.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry_without_dn, gp_entry[0])
# make delivery note
dn = make_delivery_note(sinv.name)
dn.items[0].qty = 1
dn = dn.save().submit()
columns, data = execute(filters=filters)
# Without Delivery Note, buying rate should be 100
expected_entry_with_dn = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 100.0,
"selling_amount": 100.0,
"buying_amount": 100.0,
"gross_profit": 0.0,
"gross_profit_%": 0.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])

View File

@ -1146,10 +1146,10 @@ def repost_gle_for_stock_vouchers(
if not existing_gle or not compare_existing_and_expected_gle( if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision existing_gle, expected_gle, precision
): ):
_delete_gl_entries(voucher_type, voucher_no) _delete_accounting_ledger_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else: else:
_delete_gl_entries(voucher_type, voucher_no) _delete_accounting_ledger_entries(voucher_type, voucher_no)
if not frappe.flags.in_test: if not frappe.flags.in_test:
frappe.db.commit() frappe.db.commit()
@ -1161,18 +1161,28 @@ def repost_gle_for_stock_vouchers(
) )
def _delete_gl_entries(voucher_type, voucher_no): def _delete_pl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
qb.from_(ple).delete().where( qb.from_(ple).delete().where(
(ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no) (ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)
).run() ).run()
def _delete_gl_entries(voucher_type, voucher_no):
gle = qb.DocType("GL Entry")
qb.from_(gle).delete().where(
(gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)
).run()
def _delete_accounting_ledger_entries(voucher_type, voucher_no):
"""
Remove entries from both General and Payment Ledger for specified Voucher
"""
_delete_gl_entries(voucher_type, voucher_no)
_delete_pl_entries(voucher_type, voucher_no)
def sort_stock_vouchers_by_posting_date( def sort_stock_vouchers_by_posting_date(
stock_vouchers: List[Tuple[str, str]] stock_vouchers: List[Tuple[str, str]]
) -> List[Tuple[str, str]]: ) -> List[Tuple[str, str]]:

View File

@ -230,9 +230,17 @@ class TestAsset(AssetSetup):
self.assertTrue(asset.journal_entry_for_scrap) self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0), (
"_Test Accumulated Depreciations - _TC",
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 82000.0 - pro_rata_amount, 0.0), (
"_Test Gain/Loss on Asset Disposal - _TC",
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
) )
gle = frappe.db.sql( gle = frappe.db.sql(
@ -288,9 +296,17 @@ class TestAsset(AssetSetup):
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0), (
"_Test Accumulated Depreciations - _TC",
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 57000.0 - pro_rata_amount, 0.0), (
"_Test Gain/Loss on Asset Disposal - _TC",
flt(57000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
("Debtors - _TC", 25000.0, 0.0), ("Debtors - _TC", 25000.0, 0.0),
) )

View File

@ -200,11 +200,11 @@ def get_children(doctype, parent=None, location=None, is_root=False):
name as value, name as value,
is_group as expandable is_group as expandable
from from
`tab{doctype}` comp `tabLocation` comp
where where
ifnull(parent_location, "")={parent} ifnull(parent_location, "")={parent}
""".format( """.format(
doctype=doctype, parent=frappe.db.escape(parent) parent=frappe.db.escape(parent)
), ),
as_dict=1, as_dict=1,
) )

View File

@ -1108,7 +1108,8 @@
"fetch_from": "supplier.is_internal_supplier", "fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier", "fieldname": "is_internal_supplier",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Internal Supplier" "label": "Is Internal Supplier",
"read_only": 1
}, },
{ {
"fetch_from": "supplier.represents_company", "fetch_from": "supplier.represents_company",
@ -1232,7 +1233,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-10-11 13:01:41.674352", "modified": "2022-11-17 12:34:36.033363",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -361,7 +361,7 @@ class PurchaseOrder(BuyingController):
self.update_reserved_qty_for_subcontract() self.update_reserved_qty_for_subcontract()
self.check_on_hold_or_closed_status() self.check_on_hold_or_closed_status()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_prevdoc_status() self.update_prevdoc_status()

View File

@ -31,7 +31,7 @@ class RequestforQuotation(BuyingController):
if self.docstatus < 1: if self.docstatus < 1:
# after amend and save, status still shows as cancelled, until submit # after amend and save, status still shows as cancelled, until submit
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def validate_duplicate_supplier(self): def validate_duplicate_supplier(self):
supplier_list = [d.supplier for d in self.suppliers] supplier_list = [d.supplier for d in self.suppliers]
@ -73,14 +73,14 @@ class RequestforQuotation(BuyingController):
) )
def on_submit(self): def on_submit(self):
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
for supplier in self.suppliers: for supplier in self.suppliers:
supplier.email_sent = 0 supplier.email_sent = 0
supplier.quote_status = "Pending" supplier.quote_status = "Pending"
self.send_to_supplier() self.send_to_supplier()
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
@frappe.whitelist() @frappe.whitelist()
def get_supplier_email_preview(self, supplier): def get_supplier_email_preview(self, supplier):
@ -478,7 +478,7 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt
conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date")) conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date"))
rfq_data = frappe.db.sql( rfq_data = frappe.db.sql(
""" f"""
select select
distinct rfq.name, rfq.transaction_date, distinct rfq.name, rfq.transaction_date,
rfq.company rfq.company
@ -486,15 +486,18 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier `tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
where where
rfq.name = rfq_supplier.parent rfq.name = rfq_supplier.parent
and rfq_supplier.supplier = '{0}' and rfq_supplier.supplier = %(supplier)s
and rfq.docstatus = 1 and rfq.docstatus = 1
and rfq.company = '{1}' and rfq.company = %(company)s
{2} {conditions}
order by rfq.transaction_date ASC order by rfq.transaction_date ASC
limit %(page_len)s offset %(start)s """.format( limit %(page_len)s offset %(start)s """,
filters.get("supplier"), filters.get("company"), conditions {
), "page_len": page_len,
{"page_len": page_len, "start": start}, "start": start,
"company": filters.get("company"),
"supplier": filters.get("supplier"),
},
as_dict=1, as_dict=1,
) )

View File

@ -10,34 +10,37 @@
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"basic_info",
"naming_series", "naming_series",
"supplier_name", "supplier_name",
"country", "country",
"default_bank_account",
"tax_id",
"tax_category",
"tax_withholding_category",
"image",
"column_break0", "column_break0",
"supplier_group", "supplier_group",
"supplier_type", "supplier_type",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"is_internal_supplier",
"represents_company",
"disabled",
"is_transporter", "is_transporter",
"warn_rfqs", "image",
"warn_pos", "defaults_section",
"prevent_rfqs",
"prevent_pos",
"allowed_to_transact_section",
"companies",
"section_break_7",
"default_currency", "default_currency",
"default_bank_account",
"column_break_10", "column_break_10",
"default_price_list", "default_price_list",
"payment_terms",
"internal_supplier_section",
"is_internal_supplier",
"represents_company",
"column_break_16",
"companies",
"column_break2",
"supplier_details",
"column_break_30",
"website",
"language",
"dashboard_tab",
"tax_tab",
"tax_id",
"column_break_27",
"tax_category",
"tax_withholding_category",
"contact_and_address_tab",
"address_contacts", "address_contacts",
"address_html", "address_html",
"column_break1", "column_break1",
@ -49,30 +52,25 @@
"column_break_44", "column_break_44",
"supplier_primary_address", "supplier_primary_address",
"primary_address", "primary_address",
"default_payable_accounts", "accounting_tab",
"accounts", "accounts",
"section_credit_limit", "settings_tab",
"payment_terms", "allow_purchase_invoice_creation_without_purchase_order",
"cb_21", "allow_purchase_invoice_creation_without_purchase_receipt",
"column_break_54",
"is_frozen",
"disabled",
"warn_rfqs",
"warn_pos",
"prevent_rfqs",
"prevent_pos",
"block_supplier_section",
"on_hold", "on_hold",
"hold_type", "hold_type",
"release_date", "column_break_59",
"default_tax_withholding_config", "release_date"
"column_break2",
"website",
"supplier_details",
"column_break_30",
"language",
"is_frozen"
], ],
"fields": [ "fields": [
{
"fieldname": "basic_info",
"fieldtype": "Section Break",
"label": "Name and Type",
"oldfieldtype": "Section Break",
"options": "fa fa-user"
},
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
@ -192,6 +190,7 @@
"default": "0", "default": "0",
"fieldname": "warn_rfqs", "fieldname": "warn_rfqs",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Warn RFQs", "label": "Warn RFQs",
"read_only": 1 "read_only": 1
}, },
@ -199,6 +198,7 @@
"default": "0", "default": "0",
"fieldname": "warn_pos", "fieldname": "warn_pos",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Warn POs", "label": "Warn POs",
"read_only": 1 "read_only": 1
}, },
@ -206,6 +206,7 @@
"default": "0", "default": "0",
"fieldname": "prevent_rfqs", "fieldname": "prevent_rfqs",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Prevent RFQs", "label": "Prevent RFQs",
"read_only": 1 "read_only": 1
}, },
@ -213,15 +214,10 @@
"default": "0", "default": "0",
"fieldname": "prevent_pos", "fieldname": "prevent_pos",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Prevent POs", "label": "Prevent POs",
"read_only": 1 "read_only": 1
}, },
{
"depends_on": "represents_company",
"fieldname": "allowed_to_transact_section",
"fieldtype": "Section Break",
"label": "Allowed To Transact With"
},
{ {
"depends_on": "represents_company", "depends_on": "represents_company",
"fieldname": "companies", "fieldname": "companies",
@ -229,12 +225,6 @@
"label": "Allowed To Transact With", "label": "Allowed To Transact With",
"options": "Allowed To Transact With" "options": "Allowed To Transact With"
}, },
{
"collapsible": 1,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Currency and Price List"
},
{ {
"fieldname": "default_currency", "fieldname": "default_currency",
"fieldtype": "Link", "fieldtype": "Link",
@ -254,22 +244,12 @@
"label": "Price List", "label": "Price List",
"options": "Price List" "options": "Price List"
}, },
{
"collapsible": 1,
"fieldname": "section_credit_limit",
"fieldtype": "Section Break",
"label": "Payment Terms"
},
{ {
"fieldname": "payment_terms", "fieldname": "payment_terms",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Default Payment Terms Template", "label": "Default Payment Terms Template",
"options": "Payment Terms Template" "options": "Payment Terms Template"
}, },
{
"fieldname": "cb_21",
"fieldtype": "Column Break"
},
{ {
"default": "0", "default": "0",
"fieldname": "on_hold", "fieldname": "on_hold",
@ -315,13 +295,6 @@
"label": "Contact HTML", "label": "Contact HTML",
"read_only": 1 "read_only": 1
}, },
{
"collapsible": 1,
"collapsible_depends_on": "accounts",
"fieldname": "default_payable_accounts",
"fieldtype": "Section Break",
"label": "Default Payable Accounts"
},
{ {
"description": "Mention if non-standard payable account", "description": "Mention if non-standard payable account",
"fieldname": "accounts", "fieldname": "accounts",
@ -329,12 +302,6 @@
"label": "Accounts", "label": "Accounts",
"options": "Party Account" "options": "Party Account"
}, },
{
"collapsible": 1,
"fieldname": "default_tax_withholding_config",
"fieldtype": "Section Break",
"label": "Default Tax Withholding Config"
},
{ {
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "supplier_details", "collapsible_depends_on": "supplier_details",
@ -383,7 +350,7 @@
{ {
"fieldname": "primary_address_and_contact_detail_section", "fieldname": "primary_address_and_contact_detail_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Primary Address and Contact Detail" "label": "Primary Address and Contact"
}, },
{ {
"description": "Reselect, if the chosen contact is edited after save", "description": "Reselect, if the chosen contact is edited after save",
@ -420,6 +387,64 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier Primary Address", "label": "Supplier Primary Address",
"options": "Address" "options": "Address"
},
{
"fieldname": "dashboard_tab",
"fieldtype": "Tab Break",
"label": "Dashboard",
"show_dashboard": 1
},
{
"fieldname": "settings_tab",
"fieldtype": "Tab Break",
"label": "Settings"
},
{
"fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break",
"label": "Contact & Address"
},
{
"fieldname": "accounting_tab",
"fieldtype": "Tab Break",
"label": "Accounting"
},
{
"fieldname": "defaults_section",
"fieldtype": "Section Break",
"label": "Defaults"
},
{
"fieldname": "tax_tab",
"fieldtype": "Tab Break",
"label": "Tax"
},
{
"collapsible": 1,
"fieldname": "internal_supplier_section",
"fieldtype": "Section Break",
"label": "Internal Supplier"
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_54",
"fieldtype": "Column Break"
},
{
"fieldname": "block_supplier_section",
"fieldtype": "Section Break",
"label": "Block Supplier"
},
{
"fieldname": "column_break_59",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
@ -432,7 +457,7 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2022-04-16 18:02:27.838623", "modified": "2022-11-09 18:02:59.075203",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier", "name": "Supplier",

View File

@ -145,7 +145,7 @@ class Supplier(TransactionBase):
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name": if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name":
frappe.db.set(self, "supplier_name", newdn) self.db_set("supplier_name", newdn)
@frappe.whitelist() @frappe.whitelist()

View File

@ -30,11 +30,11 @@ class SupplierQuotation(BuyingController):
self.validate_valid_till() self.validate_valid_till()
def on_submit(self): def on_submit(self):
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
self.update_rfq_supplier_status(1) self.update_rfq_supplier_status(1)
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_rfq_supplier_status(0) self.update_rfq_supplier_status(0)
def on_trash(self): def on_trash(self):

View File

@ -151,6 +151,7 @@ class AccountsController(TransactionBase):
self.validate_inter_company_reference() self.validate_inter_company_reference()
self.disable_pricing_rule_on_internal_transfer() self.disable_pricing_rule_on_internal_transfer()
self.disable_tax_included_prices_for_internal_transfer()
self.set_incoming_rate() self.set_incoming_rate()
if self.meta.get_field("currency"): if self.meta.get_field("currency"):
@ -226,7 +227,7 @@ class AccountsController(TransactionBase):
for item in self.get("items"): for item in self.get("items"):
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
if not item.get(field_map.get(self.doctype)): if not item.get(field_map.get(self.doctype)):
default_deferred_account = frappe.db.get_value( default_deferred_account = frappe.get_cached_value(
"Company", self.company, "default_" + field_map.get(self.doctype) "Company", self.company, "default_" + field_map.get(self.doctype)
) )
if not default_deferred_account: if not default_deferred_account:
@ -398,6 +399,20 @@ class AccountsController(TransactionBase):
alert=1, alert=1,
) )
def disable_tax_included_prices_for_internal_transfer(self):
if self.is_internal_transfer():
tax_updated = False
for tax in self.get("taxes"):
if tax.get("included_in_print_rate"):
tax.included_in_print_rate = 0
tax_updated = True
if tax_updated:
frappe.msgprint(
_("Disabled tax included prices since this {} is an internal transfer").format(self.doctype),
alert=1,
)
def validate_due_date(self): def validate_due_date(self):
if self.get("is_pos"): if self.get("is_pos"):
return return
@ -661,7 +676,7 @@ class AccountsController(TransactionBase):
def validate_enabled_taxes_and_charges(self): def validate_enabled_taxes_and_charges(self):
taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges") taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges")
if frappe.db.get_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"): if frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"):
frappe.throw( frappe.throw(
_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges) _("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges)
) )
@ -669,7 +684,7 @@ class AccountsController(TransactionBase):
def validate_tax_account_company(self): def validate_tax_account_company(self):
for d in self.get("taxes"): for d in self.get("taxes"):
if d.account_head: if d.account_head:
tax_account_company = frappe.db.get_value("Account", d.account_head, "company") tax_account_company = frappe.get_cached_value("Account", d.account_head, "company")
if tax_account_company != self.company: if tax_account_company != self.company:
frappe.throw( frappe.throw(
_("Row #{0}: Account {1} does not belong to company {2}").format( _("Row #{0}: Account {1} does not belong to company {2}").format(
@ -804,15 +819,12 @@ class AccountsController(TransactionBase):
self.set("advances", []) self.set("advances", [])
advance_allocated = 0 advance_allocated = 0
for d in res: for d in res:
if d.against_order: if self.get("party_account_currency") == self.company_currency:
allocated_amount = flt(d.amount) amount = self.get("base_rounded_total") or self.base_grand_total
else: else:
if self.get("party_account_currency") == self.company_currency: amount = self.get("rounded_total") or self.grand_total
amount = self.get("base_rounded_total") or self.base_grand_total
else:
amount = self.get("rounded_total") or self.grand_total
allocated_amount = min(amount - advance_allocated, d.amount) allocated_amount = min(amount - advance_allocated, d.amount)
advance_allocated += flt(allocated_amount) advance_allocated += flt(allocated_amount)
advance_row = { advance_row = {
@ -917,7 +929,9 @@ class AccountsController(TransactionBase):
party_account = self.credit_to if is_purchase_invoice else self.debit_to party_account = self.credit_to if is_purchase_invoice else self.debit_to
party_type = "Supplier" if is_purchase_invoice else "Customer" party_type = "Supplier" if is_purchase_invoice else "Customer"
gain_loss_account = frappe.db.get_value("Company", self.company, "exchange_gain_loss_account") gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account"
)
if not gain_loss_account: if not gain_loss_account:
frappe.throw( frappe.throw(
_("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company"))
@ -1014,7 +1028,7 @@ class AccountsController(TransactionBase):
else self.grand_total else self.grand_total
), ),
"outstanding_amount": self.outstanding_amount, "outstanding_amount": self.outstanding_amount,
"difference_account": frappe.db.get_value( "difference_account": frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account" "Company", self.company, "exchange_gain_loss_account"
), ),
"exchange_gain_loss": flt(d.get("exchange_gain_loss")), "exchange_gain_loss": flt(d.get("exchange_gain_loss")),
@ -1382,7 +1396,7 @@ class AccountsController(TransactionBase):
@property @property
def company_abbr(self): def company_abbr(self):
if not hasattr(self, "_abbr"): if not hasattr(self, "_abbr"):
self._abbr = frappe.db.get_value("Company", self.company, "abbr") self._abbr = frappe.get_cached_value("Company", self.company, "abbr")
return self._abbr return self._abbr
@ -1768,7 +1782,7 @@ class AccountsController(TransactionBase):
""" """
if self.is_internal_transfer() and not self.unrealized_profit_loss_account: if self.is_internal_transfer() and not self.unrealized_profit_loss_account:
unrealized_profit_loss_account = frappe.db.get_value( unrealized_profit_loss_account = frappe.get_cached_value(
"Company", self.company, "unrealized_profit_loss_account" "Company", self.company, "unrealized_profit_loss_account"
) )
@ -1883,7 +1897,9 @@ class AccountsController(TransactionBase):
@frappe.whitelist() @frappe.whitelist()
def get_tax_rate(account_head): def get_tax_rate(account_head):
return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) return frappe.get_cached_value(
"Account", account_head, ["tax_rate", "account_name"], as_dict=True
)
@frappe.whitelist() @frappe.whitelist()
@ -1892,7 +1908,7 @@ def get_default_taxes_and_charges(master_doctype, tax_template=None, company=Non
return {} return {}
if tax_template and company: if tax_template and company:
tax_template_company = frappe.db.get_value(master_doctype, tax_template, "company") tax_template_company = frappe.get_cached_value(master_doctype, tax_template, "company")
if tax_template_company == company: if tax_template_company == company:
return return

View File

@ -326,7 +326,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
company = frappe.db.get_value("Delivery Note", source_name, "company") company = frappe.db.get_value("Delivery Note", source_name, "company")
default_warehouse_for_sales_return = frappe.db.get_value( default_warehouse_for_sales_return = frappe.get_cached_value(
"Company", company, "default_warehouse_for_sales_return" "Company", company, "default_warehouse_for_sales_return"
) )
@ -340,11 +340,11 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
# look for Print Heading "Credit Note" # look for Print Heading "Credit Note"
if not doc.select_print_heading: if not doc.select_print_heading:
doc.select_print_heading = frappe.db.get_value("Print Heading", _("Credit Note")) doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Credit Note"))
elif doctype == "Purchase Invoice": elif doctype == "Purchase Invoice":
# look for Print Heading "Debit Note" # look for Print Heading "Debit Note"
doc.select_print_heading = frappe.db.get_value("Print Heading", _("Debit Note")) doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
for tax in doc.get("taxes") or []: for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual": if tax.charge_type == "Actual":
@ -503,7 +503,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
doctype doctype
+ " Item": { + " Item": {
"doctype": doctype + " Item", "doctype": doctype + " Item",
"field_map": {"serial_no": "serial_no", "batch_no": "batch_no"}, "field_map": {"serial_no": "serial_no", "batch_no": "batch_no", "bom": "bom"},
"postprocess": update_item, "postprocess": update_item,
}, },
"Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms}, "Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms},

View File

@ -57,7 +57,7 @@ class StockController(AccountsController):
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
provisional_accounting_for_non_stock_items = cint( provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value( frappe.get_cached_value(
"Company", self.company, "enable_provisional_accounting_for_non_stock_items" "Company", self.company, "enable_provisional_accounting_for_non_stock_items"
) )
) )
@ -200,7 +200,7 @@ class StockController(AccountsController):
elif self.get("is_internal_supplier"): elif self.get("is_internal_supplier"):
warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"] warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"]
expense_account = frappe.db.get_value("Company", self.company, "default_expense_account") expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account")
gl_list.append( gl_list.append(
self.get_gl_dict( self.get_gl_dict(
@ -235,7 +235,7 @@ class StockController(AccountsController):
if warehouse_with_no_account: if warehouse_with_no_account:
for wh in warehouse_with_no_account: for wh in warehouse_with_no_account:
if frappe.db.get_value("Warehouse", wh, "company"): if frappe.get_cached_value("Warehouse", wh, "company"):
frappe.throw( frappe.throw(
_( _(
"Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}."
@ -449,15 +449,15 @@ class StockController(AccountsController):
# Get value based on doctype name # Get value based on doctype name
if not sl_dict.get(dimension.target_fieldname): if not sl_dict.get(dimension.target_fieldname):
fieldname = frappe.get_cached_value( fieldname = next(
"DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" (
field.fieldname
for field in frappe.get_meta(self.doctype).fields
if field.options == dimension.fetch_from_parent
),
None,
) )
if not fieldname:
fieldname = frappe.get_cached_value(
"Custom Field", {"dt": self.doctype, "options": dimension.fetch_from_parent}, "fieldname"
)
if fieldname and self.get(fieldname): if fieldname and self.get(fieldname):
sl_dict[dimension.target_fieldname] = self.get(fieldname) sl_dict[dimension.target_fieldname] = self.get(fieldname)

View File

@ -89,6 +89,9 @@ class SubcontractingController(StockController):
if bom.item != item.item_code: if bom.item != item.item_code:
msg = f"Please select an valid BOM for Item {item.item_name}." msg = f"Please select an valid BOM for Item {item.item_name}."
frappe.throw(_(msg)) frappe.throw(_(msg))
else:
msg = f"Please select a BOM for Item {item.item_name}."
frappe.throw(_(msg))
def __get_data_before_save(self): def __get_data_before_save(self):
item_dict = {} item_dict = {}

View File

@ -58,12 +58,25 @@ class calculate_taxes_and_totals(object):
self.initialize_taxes() self.initialize_taxes()
self.determine_exclusive_rate() self.determine_exclusive_rate()
self.calculate_net_total() self.calculate_net_total()
self.calculate_tax_withholding_net_total()
self.calculate_taxes() self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax() self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals() self.calculate_totals()
self._cleanup() self._cleanup()
self.calculate_total_net_weight() self.calculate_total_net_weight()
def calculate_tax_withholding_net_total(self):
if hasattr(self.doc, "tax_withholding_net_total"):
sum_net_amount = 0
sum_base_net_amount = 0
for item in self.doc.get("items"):
if hasattr(item, "apply_tds") and item.apply_tds:
sum_net_amount += item.net_amount
sum_base_net_amount += item.base_net_amount
self.doc.tax_withholding_net_total = sum_net_amount
self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self): def validate_item_tax_template(self):
for item in self.doc.get("items"): for item in self.doc.get("items"):
if item.item_code and item.get("item_tax_template"): if item.item_code and item.get("item_tax_template"):
@ -1043,7 +1056,7 @@ class init_landed_taxes_and_totals(object):
company_currency = erpnext.get_company_currency(self.doc.company) company_currency = erpnext.get_company_currency(self.doc.company)
for d in self.doc.get(self.tax_field): for d in self.doc.get(self.tax_field):
if not d.account_currency: if not d.account_currency:
account_currency = frappe.db.get_value("Account", d.expense_account, "account_currency") account_currency = frappe.get_cached_value("Account", d.expense_account, "account_currency")
d.account_currency = account_currency or company_currency d.account_currency = account_currency or company_currency
def set_exchange_rate(self): def set_exchange_rate(self):

View File

@ -815,6 +815,7 @@ def add_second_row_in_scr(scr):
"item_name", "item_name",
"qty", "qty",
"uom", "uom",
"bom",
"warehouse", "warehouse",
"stock_uom", "stock_uom",
"subcontracting_order", "subcontracting_order",

View File

@ -80,7 +80,7 @@ def get_data(filters, conditions):
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer": if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
cond += " and t1.quotation_to = 'Customer'" cond += " and t1.quotation_to = 'Customer'"
year_start_date, year_end_date = frappe.db.get_value( year_start_date, year_end_date = frappe.get_cached_value(
"Fiscal Year", filters.get("fiscal_year"), ["year_start_date", "year_end_date"] "Fiscal Year", filters.get("fiscal_year"), ["year_start_date", "year_end_date"]
) )
@ -275,7 +275,7 @@ def get_period_date_ranges(period, fiscal_year=None, year_start_date=None):
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
if not year_start_date: if not year_start_date:
year_start_date, year_end_date = frappe.db.get_value( year_start_date, year_end_date = frappe.get_cached_value(
"Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"] "Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"]
) )

View File

@ -60,7 +60,7 @@ class Opportunity(TransactionBase, CRMNote):
if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field): if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
try: try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field) value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
frappe.db.set(self, field, value) self.db_set(field, value)
except Exception: except Exception:
continue continue

View File

@ -1,5 +1,3 @@
from frappe import _
app_name = "erpnext" app_name = "erpnext"
app_title = "ERPNext" app_title = "ERPNext"
app_publisher = "Frappe Technologies Pvt. Ltd." app_publisher = "Frappe Technologies Pvt. Ltd."
@ -94,7 +92,7 @@ website_route_rules = [
{ {
"from_route": "/orders/<path:name>", "from_route": "/orders/<path:name>",
"to_route": "order", "to_route": "order",
"defaults": {"doctype": "Sales Order", "parents": [{"label": _("Orders"), "route": "orders"}]}, "defaults": {"doctype": "Sales Order", "parents": [{"label": "Orders", "route": "orders"}]},
}, },
{"from_route": "/invoices", "to_route": "Sales Invoice"}, {"from_route": "/invoices", "to_route": "Sales Invoice"},
{ {
@ -102,7 +100,7 @@ website_route_rules = [
"to_route": "order", "to_route": "order",
"defaults": { "defaults": {
"doctype": "Sales Invoice", "doctype": "Sales Invoice",
"parents": [{"label": _("Invoices"), "route": "invoices"}], "parents": [{"label": "Invoices", "route": "invoices"}],
}, },
}, },
{"from_route": "/supplier-quotations", "to_route": "Supplier Quotation"}, {"from_route": "/supplier-quotations", "to_route": "Supplier Quotation"},
@ -111,7 +109,7 @@ website_route_rules = [
"to_route": "order", "to_route": "order",
"defaults": { "defaults": {
"doctype": "Supplier Quotation", "doctype": "Supplier Quotation",
"parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}], "parents": [{"label": "Supplier Quotation", "route": "supplier-quotations"}],
}, },
}, },
{"from_route": "/purchase-orders", "to_route": "Purchase Order"}, {"from_route": "/purchase-orders", "to_route": "Purchase Order"},
@ -120,7 +118,7 @@ website_route_rules = [
"to_route": "order", "to_route": "order",
"defaults": { "defaults": {
"doctype": "Purchase Order", "doctype": "Purchase Order",
"parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}], "parents": [{"label": "Purchase Order", "route": "purchase-orders"}],
}, },
}, },
{"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"}, {"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"},
@ -129,7 +127,7 @@ website_route_rules = [
"to_route": "order", "to_route": "order",
"defaults": { "defaults": {
"doctype": "Purchase Invoice", "doctype": "Purchase Invoice",
"parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}], "parents": [{"label": "Purchase Invoice", "route": "purchase-invoices"}],
}, },
}, },
{"from_route": "/quotations", "to_route": "Quotation"}, {"from_route": "/quotations", "to_route": "Quotation"},
@ -138,7 +136,7 @@ website_route_rules = [
"to_route": "order", "to_route": "order",
"defaults": { "defaults": {
"doctype": "Quotation", "doctype": "Quotation",
"parents": [{"label": _("Quotations"), "route": "quotations"}], "parents": [{"label": "Quotations", "route": "quotations"}],
}, },
}, },
{"from_route": "/shipments", "to_route": "Delivery Note"}, {"from_route": "/shipments", "to_route": "Delivery Note"},
@ -147,7 +145,7 @@ website_route_rules = [
"to_route": "order", "to_route": "order",
"defaults": { "defaults": {
"doctype": "Delivery Note", "doctype": "Delivery Note",
"parents": [{"label": _("Shipments"), "route": "shipments"}], "parents": [{"label": "Shipments", "route": "shipments"}],
}, },
}, },
{"from_route": "/rfq", "to_route": "Request for Quotation"}, {"from_route": "/rfq", "to_route": "Request for Quotation"},
@ -156,14 +154,14 @@ website_route_rules = [
"to_route": "rfq", "to_route": "rfq",
"defaults": { "defaults": {
"doctype": "Request for Quotation", "doctype": "Request for Quotation",
"parents": [{"label": _("Request for Quotation"), "route": "rfq"}], "parents": [{"label": "Request for Quotation", "route": "rfq"}],
}, },
}, },
{"from_route": "/addresses", "to_route": "Address"}, {"from_route": "/addresses", "to_route": "Address"},
{ {
"from_route": "/addresses/<path:name>", "from_route": "/addresses/<path:name>",
"to_route": "addresses", "to_route": "addresses",
"defaults": {"doctype": "Address", "parents": [{"label": _("Addresses"), "route": "addresses"}]}, "defaults": {"doctype": "Address", "parents": [{"label": "Addresses", "route": "addresses"}]},
}, },
{"from_route": "/boms", "to_route": "BOM"}, {"from_route": "/boms", "to_route": "BOM"},
{"from_route": "/timesheets", "to_route": "Timesheet"}, {"from_route": "/timesheets", "to_route": "Timesheet"},
@ -173,78 +171,78 @@ website_route_rules = [
"to_route": "material_request_info", "to_route": "material_request_info",
"defaults": { "defaults": {
"doctype": "Material Request", "doctype": "Material Request",
"parents": [{"label": _("Material Request"), "route": "material-requests"}], "parents": [{"label": "Material Request", "route": "material-requests"}],
}, },
}, },
{"from_route": "/project", "to_route": "Project"}, {"from_route": "/project", "to_route": "Project"},
] ]
standard_portal_menu_items = [ standard_portal_menu_items = [
{"title": _("Projects"), "route": "/project", "reference_doctype": "Project"}, {"title": "Projects", "route": "/project", "reference_doctype": "Project"},
{ {
"title": _("Request for Quotations"), "title": "Request for Quotations",
"route": "/rfq", "route": "/rfq",
"reference_doctype": "Request for Quotation", "reference_doctype": "Request for Quotation",
"role": "Supplier", "role": "Supplier",
}, },
{ {
"title": _("Supplier Quotation"), "title": "Supplier Quotation",
"route": "/supplier-quotations", "route": "/supplier-quotations",
"reference_doctype": "Supplier Quotation", "reference_doctype": "Supplier Quotation",
"role": "Supplier", "role": "Supplier",
}, },
{ {
"title": _("Purchase Orders"), "title": "Purchase Orders",
"route": "/purchase-orders", "route": "/purchase-orders",
"reference_doctype": "Purchase Order", "reference_doctype": "Purchase Order",
"role": "Supplier", "role": "Supplier",
}, },
{ {
"title": _("Purchase Invoices"), "title": "Purchase Invoices",
"route": "/purchase-invoices", "route": "/purchase-invoices",
"reference_doctype": "Purchase Invoice", "reference_doctype": "Purchase Invoice",
"role": "Supplier", "role": "Supplier",
}, },
{ {
"title": _("Quotations"), "title": "Quotations",
"route": "/quotations", "route": "/quotations",
"reference_doctype": "Quotation", "reference_doctype": "Quotation",
"role": "Customer", "role": "Customer",
}, },
{ {
"title": _("Orders"), "title": "Orders",
"route": "/orders", "route": "/orders",
"reference_doctype": "Sales Order", "reference_doctype": "Sales Order",
"role": "Customer", "role": "Customer",
}, },
{ {
"title": _("Invoices"), "title": "Invoices",
"route": "/invoices", "route": "/invoices",
"reference_doctype": "Sales Invoice", "reference_doctype": "Sales Invoice",
"role": "Customer", "role": "Customer",
}, },
{ {
"title": _("Shipments"), "title": "Shipments",
"route": "/shipments", "route": "/shipments",
"reference_doctype": "Delivery Note", "reference_doctype": "Delivery Note",
"role": "Customer", "role": "Customer",
}, },
{"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role": "Customer"}, {"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"},
{"title": _("Addresses"), "route": "/addresses", "reference_doctype": "Address"}, {"title": "Addresses", "route": "/addresses", "reference_doctype": "Address"},
{ {
"title": _("Timesheets"), "title": "Timesheets",
"route": "/timesheets", "route": "/timesheets",
"reference_doctype": "Timesheet", "reference_doctype": "Timesheet",
"role": "Customer", "role": "Customer",
}, },
{"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"}, {"title": "Newsletter", "route": "/newsletters", "reference_doctype": "Newsletter"},
{ {
"title": _("Material Request"), "title": "Material Request",
"route": "/material-requests", "route": "/material-requests",
"reference_doctype": "Material Request", "reference_doctype": "Material Request",
"role": "Customer", "role": "Customer",
}, },
{"title": _("Appointment Booking"), "route": "/book_appointment"}, {"title": "Appointment Booking", "route": "/book_appointment"},
] ]
default_roles = [ default_roles = [

View File

@ -576,8 +576,8 @@ def regenerate_repayment_schedule(loan, cancel=0):
loan_doc = frappe.get_doc("Loan", loan) loan_doc = frappe.get_doc("Loan", loan)
next_accrual_date = None next_accrual_date = None
accrued_entries = 0 accrued_entries = 0
last_repayment_amount = 0 last_repayment_amount = None
last_balance_amount = 0 last_balance_amount = None
for term in reversed(loan_doc.get("repayment_schedule")): for term in reversed(loan_doc.get("repayment_schedule")):
if not term.is_accrued: if not term.is_accrued:
@ -585,9 +585,9 @@ def regenerate_repayment_schedule(loan, cancel=0):
loan_doc.remove(term) loan_doc.remove(term)
else: else:
accrued_entries += 1 accrued_entries += 1
if not last_repayment_amount: if last_repayment_amount is None:
last_repayment_amount = term.total_payment last_repayment_amount = term.total_payment
if not last_balance_amount: if last_balance_amount is None:
last_balance_amount = term.balance_loan_amount last_balance_amount = term.balance_loan_amount
loan_doc.save() loan_doc.save()

View File

@ -119,7 +119,7 @@ class MaintenanceSchedule(TransactionBase):
event.add_participant(self.doctype, self.name) event.add_participant(self.doctype, self.name)
event.insert(ignore_permissions=1) event.insert(ignore_permissions=1)
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person): def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person):
schedule_list = [] schedule_list = []
@ -245,7 +245,7 @@ class MaintenanceSchedule(TransactionBase):
self.generate_schedule() self.generate_schedule()
def on_update(self): def on_update(self):
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def update_amc_date(self, serial_nos, amc_expiry_date=None): def update_amc_date(self, serial_nos, amc_expiry_date=None):
for serial_no in serial_nos: for serial_no in serial_nos:
@ -344,7 +344,7 @@ class MaintenanceSchedule(TransactionBase):
if d.serial_no: if d.serial_no:
serial_nos = get_valid_serial_nos(d.serial_no) serial_nos = get_valid_serial_nos(d.serial_no)
self.update_amc_date(serial_nos) self.update_amc_date(serial_nos)
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
delete_events(self.doctype, self.name) delete_events(self.doctype, self.name)
def on_trash(self): def on_trash(self):

View File

@ -125,12 +125,12 @@ class MaintenanceVisit(TransactionBase):
def on_submit(self): def on_submit(self):
self.update_customer_issue(1) self.update_customer_issue(1)
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
self.update_status_and_actual_date() self.update_status_and_actual_date()
def on_cancel(self): def on_cancel(self):
self.check_if_last_visit() self.check_if_last_visit()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_status_and_actual_date(cancel=True) self.update_status_and_actual_date(cancel=True)
def on_update(self): def on_update(self):

View File

@ -206,8 +206,8 @@ class BOM(WebsiteGenerator):
self.manage_default_bom() self.manage_default_bom()
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, "is_active", 0) self.db_set("is_active", 0)
frappe.db.set(self, "is_default", 0) self.db_set("is_default", 0)
# check if used in any other bom # check if used in any other bom
self.validate_bom_links() self.validate_bom_links()
@ -449,10 +449,10 @@ class BOM(WebsiteGenerator):
not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1)) not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1))
and self.is_active and self.is_active
): ):
frappe.db.set(self, "is_default", 1) self.db_set("is_default", 1)
frappe.db.set_value("Item", self.item, "default_bom", self.name) frappe.db.set_value("Item", self.item, "default_bom", self.name)
else: else:
frappe.db.set(self, "is_default", 0) self.db_set("is_default", 0)
item = frappe.get_doc("Item", self.item) item = frappe.get_doc("Item", self.item)
if item.default_bom == self.name: if item.default_bom == self.name:
frappe.db.set_value("Item", self.item, "default_bom", None) frappe.db.set_value("Item", self.item, "default_bom", None)

View File

@ -9,6 +9,7 @@
"sequence_id", "sequence_id",
"operation", "operation",
"col_break1", "col_break1",
"workstation_type",
"workstation", "workstation",
"time_in_mins", "time_in_mins",
"fixed_time", "fixed_time",
@ -40,9 +41,9 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:!doc.workstation_type",
"fieldname": "workstation", "fieldname": "workstation",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Workstation", "label": "Workstation",
"oldfieldname": "workstation", "oldfieldname": "workstation",
"oldfieldtype": "Link", "oldfieldtype": "Link",
@ -180,13 +181,20 @@
"fieldname": "set_cost_based_on_bom_qty", "fieldname": "set_cost_based_on_bom_qty",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Set Operating Cost Based On BOM Quantity" "label": "Set Operating Cost Based On BOM Quantity"
},
{
"fieldname": "workstation_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Workstation Type",
"options": "Workstation Type"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-04-08 01:18:33.547481", "modified": "2022-11-04 17:17:16.986941",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operation", "name": "BOM Operation",

View File

@ -27,11 +27,14 @@
"operation", "operation",
"operation_row_number", "operation_row_number",
"column_break_18", "column_break_18",
"workstation_type",
"workstation", "workstation",
"employee", "employee",
"section_break_21", "section_break_21",
"sub_operations", "sub_operations",
"timing_detail", "timing_detail",
"expected_start_date",
"expected_end_date",
"time_logs", "time_logs",
"section_break_13", "section_break_13",
"total_completed_qty", "total_completed_qty",
@ -416,11 +419,27 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Quality Inspection Template", "label": "Quality Inspection Template",
"options": "Quality Inspection Template" "options": "Quality Inspection Template"
},
{
"fieldname": "workstation_type",
"fieldtype": "Link",
"label": "Workstation Type",
"options": "Workstation Type"
},
{
"fieldname": "expected_start_date",
"fieldtype": "Datetime",
"label": "Expected Start Date"
},
{
"fieldname": "expected_end_date",
"fieldtype": "Datetime",
"label": "Expected End Date"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-11-24 19:17:40.879235", "modified": "2022-11-09 15:02:44.490731",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",
@ -475,6 +494,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "operation", "title_field": "operation",
"track_changes": 1 "track_changes": 1
} }

View File

@ -2,11 +2,14 @@
# For license information, please see license.txt # For license information, please see license.txt
import datetime import datetime
import json import json
from typing import Optional
import frappe import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Criterion
from frappe.query_builder.functions import IfNull, Max, Min
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
add_to_date, add_to_date,
@ -24,6 +27,7 @@ from frappe.utils import (
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import ( from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
get_mins_between_operations, get_mins_between_operations,
) )
from erpnext.manufacturing.doctype.workstation_type.workstation_type import get_workstations
class OverlapError(frappe.ValidationError): class OverlapError(frappe.ValidationError):
@ -54,6 +58,9 @@ class JobCard(Document):
self.set_onload("job_card_excess_transfer", excess_transfer) self.set_onload("job_card_excess_transfer", excess_transfer)
self.set_onload("work_order_closed", self.is_work_order_closed()) self.set_onload("work_order_closed", self.is_work_order_closed())
def before_validate(self):
self.set_wip_warehouse()
def validate(self): def validate(self):
self.validate_time_logs() self.validate_time_logs()
self.set_status() self.set_status()
@ -109,49 +116,66 @@ class JobCard(Document):
def get_overlap_for(self, args, check_next_available_slot=False): def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1 production_capacity = 1
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log")
time_conditions = [
((jctl.from_time < args.from_time) & (jctl.to_time > args.from_time)),
((jctl.from_time < args.to_time) & (jctl.to_time > args.to_time)),
((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
]
if check_next_available_slot:
time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time)))
query = (
frappe.qb.from_(jctl)
.from_(jc)
.select(jc.name.as_("name"), jctl.to_time, jc.workstation, jc.workstation_type)
.where(
(jctl.parent == jc.name)
& (Criterion.any(time_conditions))
& (jctl.name != f"{args.name or 'No Name'}")
& (jc.name != f"{args.parent or 'No Name'}")
& (jc.docstatus < 2)
)
.orderby(jctl.to_time, order=frappe.qb.desc)
)
if self.workstation_type:
query = query.where(jc.workstation_type == self.workstation_type)
if self.workstation: if self.workstation:
production_capacity = ( production_capacity = (
frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1 frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
) )
validate_overlap_for = " and jc.workstation = %(workstation)s " query = query.where(jc.workstation == self.workstation)
if args.get("employee"): if args.get("employee"):
# override capacity for employee # override capacity for employee
production_capacity = 1 production_capacity = 1
validate_overlap_for = " and jctl.employee = %(employee)s " query = query.where(jctl.employee == args.get("employee"))
extra_cond = "" existing = query.run(as_dict=True)
if check_next_available_slot:
extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)"
existing = frappe.db.sql(
"""select jc.name as name, jctl.to_time from
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
(
(%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or
(%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
)
and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
order by jctl.to_time desc""".format(
extra_cond, validate_overlap_for
),
{
"from_time": args.from_time,
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name",
"employee": args.get("employee"),
"workstation": self.workstation,
},
as_dict=True,
)
if existing and production_capacity > len(existing): if existing and production_capacity > len(existing):
return return
if self.workstation_type:
if workstation := self.get_workstation_based_on_available_slot(existing):
self.workstation = workstation
return None
return existing[0] if existing else None return existing[0] if existing else None
def get_workstation_based_on_available_slot(self, existing) -> Optional[str]:
workstations = get_workstations(self.workstation_type)
if workstations:
busy_workstations = [row.workstation for row in existing]
for workstation in workstations:
if workstation not in busy_workstations:
return workstation
def schedule_time_logs(self, row): def schedule_time_logs(self, row):
row.remaining_time_in_mins = row.time_in_mins row.remaining_time_in_mins = row.time_in_mins
while row.remaining_time_in_mins > 0: while row.remaining_time_in_mins > 0:
@ -164,6 +188,9 @@ class JobCard(Document):
# get the last record based on the to time from the job card # get the last record based on the to time from the job card
data = self.get_overlap_for(args, check_next_available_slot=True) data = self.get_overlap_for(args, check_next_available_slot=True)
if data: if data:
if not self.workstation:
self.workstation = data.workstation
row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations()) row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
def check_workstation_time(self, row): def check_workstation_time(self, row):
@ -485,18 +512,21 @@ class JobCard(Document):
) )
def update_work_order_data(self, for_quantity, time_in_mins, wo): def update_work_order_data(self, for_quantity, time_in_mins, wo):
time_data = frappe.db.sql( jc = frappe.qb.DocType("Job Card")
""" jctl = frappe.qb.DocType("Job Card Time Log")
SELECT
min(from_time) as start_time, max(to_time) as end_time time_data = (
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl frappe.qb.from_(jc)
WHERE .from_(jctl)
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s .select(Min(jctl.from_time).as_("start_time"), Max(jctl.to_time).as_("end_time"))
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0 .where(
""", (jctl.parent == jc.name)
(self.work_order, self.operation_id), & (jc.work_order == self.work_order)
as_dict=1, & (jc.operation_id == self.operation_id)
) & (jc.docstatus == 1)
& (IfNull(jc.is_corrective_job_card, 0) == 0)
)
).run(as_dict=True)
for data in wo.operations: for data in wo.operations:
if data.get("name") == self.operation_id: if data.get("name") == self.operation_id:
@ -639,6 +669,12 @@ class JobCard(Document):
if update_status: if update_status:
self.db_set("status", self.status) self.db_set("status", self.status)
def set_wip_warehouse(self):
if not self.wip_warehouse:
self.wip_warehouse = frappe.db.get_single_value(
"Manufacturing Settings", "default_wip_warehouse"
)
def validate_operation_id(self): def validate_operation_id(self):
if ( if (
self.get("operation_id") self.get("operation_id")

View File

@ -649,23 +649,13 @@ class ProductionPlan(Document):
else: else:
material_request = material_request_map[key] material_request = material_request_map[key]
conversion_factor = 1.0
if (
material_request_type == "Purchase"
and item_doc.purchase_uom
and item_doc.purchase_uom != item_doc.stock_uom
):
conversion_factor = (
get_conversion_factor(item_doc.name, item_doc.purchase_uom).get("conversion_factor") or 1.0
)
# add item # add item
material_request.append( material_request.append(
"items", "items",
{ {
"item_code": item.item_code, "item_code": item.item_code,
"from_warehouse": item.from_warehouse, "from_warehouse": item.from_warehouse,
"qty": item.quantity / conversion_factor, "qty": item.quantity,
"schedule_date": schedule_date, "schedule_date": schedule_date,
"warehouse": item.warehouse, "warehouse": item.warehouse,
"sales_order": item.sales_order, "sales_order": item.sales_order,
@ -1053,11 +1043,25 @@ def get_material_request_items(
if include_safety_stock: if include_safety_stock:
required_qty += flt(row["safety_stock"]) required_qty += flt(row["safety_stock"])
item_details = frappe.get_cached_value(
"Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1
)
conversion_factor = 1.0
if (
row.get("default_material_request_type") == "Purchase"
and item_details.purchase_uom
and item_details.purchase_uom != item_details.stock_uom
):
conversion_factor = (
get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0
)
if required_qty > 0: if required_qty > 0:
return { return {
"item_code": row.item_code, "item_code": row.item_code,
"item_name": row.item_name, "item_name": row.item_name,
"quantity": required_qty, "quantity": required_qty / conversion_factor,
"required_bom_qty": total_qty, "required_bom_qty": total_qty,
"stock_uom": row.get("stock_uom"), "stock_uom": row.get("stock_uom"),
"warehouse": warehouse "warehouse": warehouse

View File

@ -826,6 +826,11 @@ class TestProductionPlan(FrappeTestCase):
) )
pln.make_material_request() pln.make_material_request()
for row in pln.mr_items:
self.assertEqual(row.uom, "Nos")
self.assertEqual(row.quantity, 1)
for row in frappe.get_all( for row in frappe.get_all(
"Material Request Item", "Material Request Item",
filters={"production_plan": pln.name}, filters={"production_plan": pln.name},

View File

@ -5,7 +5,7 @@ import copy
import frappe import frappe
from frappe.tests.utils import FrappeTestCase, change_settings, timeout from frappe.tests.utils import FrappeTestCase, change_settings, timeout
from frappe.utils import add_days, add_months, cint, flt, now, today from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
@ -1480,6 +1480,166 @@ class TestWorkOrder(FrappeTestCase):
for row in return_ste_doc.items: for row in return_ste_doc.items:
self.assertEqual(row.qty, 2) self.assertEqual(row.qty, 2)
def test_workstation_type_for_work_order(self):
prepare_data_for_workstation_type_check()
workstation_types = ["Workstation Type 1", "Workstation Type 2", "Workstation Type 3"]
planned_start_date = "2022-11-14 10:00:00"
wo_order = make_wo_order_test_record(
item="Test FG Item For Workstation Type", planned_start_date=planned_start_date, qty=2
)
job_cards = frappe.get_all(
"Job Card",
fields=[
"`tabJob Card`.`name`",
"`tabJob Card`.`workstation_type`",
"`tabJob Card`.`workstation`",
"`tabJob Card Time Log`.`from_time`",
"`tabJob Card Time Log`.`to_time`",
"`tabJob Card Time Log`.`time_in_mins`",
],
filters=[
["Job Card", "work_order", "=", wo_order.name],
["Job Card Time Log", "docstatus", "=", 1],
],
order_by="`tabJob Card`.`creation` desc",
)
workstations_to_check = ["Workstation 1", "Workstation 3", "Workstation 5"]
for index, row in enumerate(job_cards):
if index != 0:
planned_start_date = add_to_date(planned_start_date, minutes=40)
self.assertEqual(row.workstation_type, workstation_types[index])
self.assertEqual(row.from_time, planned_start_date)
self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30))
self.assertEqual(row.workstation, workstations_to_check[index])
planned_start_date = "2022-11-14 10:00:00"
wo_order = make_wo_order_test_record(
item="Test FG Item For Workstation Type", planned_start_date=planned_start_date, qty=2
)
job_cards = frappe.get_all(
"Job Card",
fields=[
"`tabJob Card`.`name`",
"`tabJob Card`.`workstation_type`",
"`tabJob Card`.`workstation`",
"`tabJob Card Time Log`.`from_time`",
"`tabJob Card Time Log`.`to_time`",
"`tabJob Card Time Log`.`time_in_mins`",
],
filters=[
["Job Card", "work_order", "=", wo_order.name],
["Job Card Time Log", "docstatus", "=", 1],
],
order_by="`tabJob Card`.`creation` desc",
)
workstations_to_check = ["Workstation 2", "Workstation 4", "Workstation 6"]
for index, row in enumerate(job_cards):
if index != 0:
planned_start_date = add_to_date(planned_start_date, minutes=40)
self.assertEqual(row.workstation_type, workstation_types[index])
self.assertEqual(row.from_time, planned_start_date)
self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30))
self.assertEqual(row.workstation, workstations_to_check[index])
def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.workstation_type.test_workstation_type import (
create_workstation_type,
)
workstation_types = ["Workstation Type 1", "Workstation Type 2", "Workstation Type 3"]
for workstation_type in workstation_types:
create_workstation_type(workstation_type=workstation_type)
operations = ["Cutting", "Sewing", "Packing"]
for operation in operations:
make_operation(
{
"operation": operation,
}
)
workstations = [
{
"workstation": "Workstation 1",
"workstation_type": "Workstation Type 1",
},
{
"workstation": "Workstation 2",
"workstation_type": "Workstation Type 1",
},
{
"workstation": "Workstation 3",
"workstation_type": "Workstation Type 2",
},
{
"workstation": "Workstation 4",
"workstation_type": "Workstation Type 2",
},
{
"workstation": "Workstation 5",
"workstation_type": "Workstation Type 3",
},
{
"workstation": "Workstation 6",
"workstation_type": "Workstation Type 3",
},
]
for row in workstations:
make_workstation(row)
fg_item = make_item(
"Test FG Item For Workstation Type",
{
"is_stock_item": 1,
},
)
rm_item = make_item(
"Test RM Item For Workstation Type",
{
"is_stock_item": 1,
},
)
if not frappe.db.exists("BOM", {"item": fg_item.name}):
bom_doc = make_bom(
item=fg_item.name,
source_warehouse="Stores - _TC",
raw_materials=[rm_item.name],
do_not_submit=True,
)
submit_bom = False
for index, operation in enumerate(operations):
if not frappe.db.exists("BOM Operation", {"parent": bom_doc.name, "operation": operation}):
bom_doc.append(
"operations",
{
"operation": operation,
"time_in_mins": 30,
"hour_rate": 100,
"workstation_type": workstation_types[index],
},
)
submit_bom = True
if submit_bom:
bom_doc.submit()
def prepare_data_for_backflush_based_on_materials_transferred(): def prepare_data_for_backflush_based_on_materials_transferred():
batch_item_doc = make_item( batch_item_doc = make_item(

View File

@ -446,7 +446,6 @@ frappe.ui.form.on("Work Order", {
frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true); frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true);
frm.toggle_reqd("transfer_material_against", frm.toggle_reqd("transfer_material_against",
frm.doc.operations && frm.doc.operations.length > 0); frm.doc.operations && frm.doc.operations.length > 0);
frm.fields_dict.operations.grid.toggle_reqd("workstation", frm.doc.operations);
}, },
set_sales_order: function(frm) { set_sales_order: function(frm) {
@ -589,66 +588,69 @@ erpnext.work_order = {
} }
} }
if(!frm.doc.skip_transfer){ if (frm.doc.status != 'Stopped') {
// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption // If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
if (flt(doc.material_transferred_for_manufacturing) > 0 && frm.doc.status != 'Stopped') { if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) { if (flt(doc.material_transferred_for_manufacturing) > 0 || frm.doc.skip_transfer) {
frm.has_finish_btn = true; // Only show "Material Consumption" when required_qty > consumed_qty
var counter = 0;
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) { var tbl = frm.doc.required_items || [];
// Only show "Material Consumption" when required_qty > consumed_qty var tbl_lenght = tbl.length;
var counter = 0; for (var i = 0, len = tbl_lenght; i < len; i++) {
var tbl = frm.doc.required_items || []; let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
var tbl_lenght = tbl.length; if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
for (var i = 0, len = tbl_lenght; i < len; i++) { counter += 1;
let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
counter += 1;
}
}
if (counter > 0) {
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on;
erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on);
});
consumption_btn.addClass('btn-primary');
} }
} }
if (counter > 0) {
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on;
erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on);
});
consumption_btn.addClass('btn-primary');
}
}
}
if(!frm.doc.skip_transfer){
if (flt(doc.material_transferred_for_manufacturing) > 0) {
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
frm.has_finish_btn = true;
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
if(doc.material_transferred_for_manufacturing>=doc.qty) {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
}
} else {
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
let allowance_percentage = doc.overproduction_percentage_for_work_order;
if (allowance_percentage > 0) {
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
if ((flt(doc.produced_qty) < allowed_qty)) {
frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
}
}
});
}
}
} else {
if ((flt(doc.produced_qty) < flt(doc.qty))) {
var finish_btn = frm.add_custom_button(__('Finish'), function() { var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture'); erpnext.work_order.make_se(frm, 'Manufacture');
}); });
finish_btn.addClass('btn-primary');
if(doc.material_transferred_for_manufacturing>=doc.qty) {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
}
} else {
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
let allowance_percentage = doc.overproduction_percentage_for_work_order;
if (allowance_percentage > 0) {
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
if ((flt(doc.produced_qty) < allowed_qty)) {
frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
}
}
});
} }
} }
} else {
if ((flt(doc.produced_qty) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
finish_btn.addClass('btn-primary');
}
} }
} }
}, },
calculate_cost: function(doc) { calculate_cost: function(doc) {
if (doc.operations){ if (doc.operations){

View File

@ -87,11 +87,18 @@ class WorkOrder(Document):
self.validate_transfer_against() self.validate_transfer_against()
self.validate_operation_time() self.validate_operation_time()
self.status = self.get_status() self.status = self.get_status()
self.validate_workstation_type()
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items"))) self.set_required_items(reset_only_qty=len(self.get("required_items")))
def validate_workstation_type(self):
for row in self.operations:
if not row.workstation and not row.workstation_type:
msg = f"Row {row.idx}: Workstation or Workstation Type is mandatory for an operation {row.operation}"
frappe.throw(_(msg))
def validate_sales_order(self): def validate_sales_order(self):
if self.sales_order: if self.sales_order:
self.check_sales_order_on_hold_or_close() self.check_sales_order_on_hold_or_close()
@ -146,7 +153,7 @@ class WorkOrder(Document):
frappe.throw(_("Sales Order {0} is {1}").format(self.sales_order, status)) frappe.throw(_("Sales Order {0} is {1}").format(self.sales_order, status))
def set_default_warehouse(self): def set_default_warehouse(self):
if not self.wip_warehouse: if not self.wip_warehouse and not self.skip_transfer:
self.wip_warehouse = frappe.db.get_single_value( self.wip_warehouse = frappe.db.get_single_value(
"Manufacturing Settings", "default_wip_warehouse" "Manufacturing Settings", "default_wip_warehouse"
) )
@ -373,7 +380,7 @@ class WorkOrder(Document):
def on_cancel(self): def on_cancel(self):
self.validate_cancel() self.validate_cancel()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
if self.production_plan and frappe.db.exists( if self.production_plan and frappe.db.exists(
"Production Plan Item Reference", {"parent": self.production_plan} "Production Plan Item Reference", {"parent": self.production_plan}
@ -491,11 +498,6 @@ class WorkOrder(Document):
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
self.set_operation_start_end_time(index, row) self.set_operation_start_end_time(index, row)
if not row.workstation:
frappe.throw(
_("Row {0}: select the workstation against the operation {1}").format(row.idx, row.operation)
)
original_start_time = row.planned_start_time original_start_time = row.planned_start_time
job_card_doc = create_job_card( job_card_doc = create_job_card(
self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
@ -662,6 +664,7 @@ class WorkOrder(Document):
"description", "description",
"workstation", "workstation",
"idx", "idx",
"workstation_type",
"base_hour_rate as hour_rate", "base_hour_rate as hour_rate",
"time_in_mins", "time_in_mins",
"parent as bom", "parent as bom",
@ -1398,6 +1401,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
doc.update( doc.update(
{ {
"work_order": work_order.name, "work_order": work_order.name,
"workstation_type": row.get("workstation_type"),
"operation": row.get("operation"), "operation": row.get("operation"),
"workstation": row.get("workstation"), "workstation": row.get("workstation"),
"posting_date": nowdate(), "posting_date": nowdate(),

View File

@ -10,6 +10,7 @@
"completed_qty", "completed_qty",
"column_break_4", "column_break_4",
"bom", "bom",
"workstation_type",
"workstation", "workstation",
"sequence_id", "sequence_id",
"section_break_10", "section_break_10",
@ -196,12 +197,18 @@
{ {
"fieldname": "section_break_10", "fieldname": "section_break_10",
"fieldtype": "Section Break" "fieldtype": "Section Break"
},
{
"fieldname": "workstation_type",
"fieldtype": "Link",
"label": "Workstation Type",
"options": "Workstation Type"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-11-29 16:37:18.824489", "modified": "2022-11-09 01:37:56.563068",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",
@ -209,5 +216,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -107,6 +107,7 @@ def make_workstation(*args, **kwargs):
doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name}) doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name})
doc.hour_rate_rent = args.get("hour_rate_rent") doc.hour_rate_rent = args.get("hour_rate_rent")
doc.hour_rate_labour = args.get("hour_rate_labour") doc.hour_rate_labour = args.get("hour_rate_labour")
doc.workstation_type = args.get("workstation_type")
doc.insert() doc.insert()
return doc return doc

View File

@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Workstation", { frappe.ui.form.on("Workstation", {
onload: function(frm) { onload(frm) {
if(frm.is_new()) if(frm.is_new())
{ {
frappe.call({ frappe.call({
@ -15,6 +15,18 @@ frappe.ui.form.on("Workstation", {
} }
}) })
} }
},
workstation_type(frm) {
if (frm.doc.workstation_type) {
frm.call({
method: "set_data_based_on_workstation_type",
doc: frm.doc,
callback: function(r) {
frm.refresh_fields();
}
})
}
} }
}); });

View File

@ -1,26 +1,30 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:workstation_name", "autoname": "field:workstation_name",
"creation": "2013-01-10 16:34:17", "creation": "2013-01-10 16:34:17",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"workstation_name", "workstation_name",
"production_capacity", "production_capacity",
"column_break_3", "column_break_3",
"workstation_type",
"over_heads", "over_heads",
"hour_rate_electricity", "hour_rate_electricity",
"hour_rate_consumable", "hour_rate_consumable",
"column_break_11", "column_break_11",
"hour_rate_rent", "hour_rate_rent",
"hour_rate_labour", "hour_rate_labour",
"section_break_11",
"hour_rate", "hour_rate",
"workstaion_description",
"description",
"working_hours_section", "working_hours_section",
"holiday_list", "holiday_list",
"working_hours", "working_hours"
"workstaion_description",
"description"
], ],
"fields": [ "fields": [
{ {
@ -44,7 +48,7 @@
}, },
{ {
"fieldname": "over_heads", "fieldname": "over_heads",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Operating Costs", "label": "Operating Costs",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
@ -99,7 +103,7 @@
}, },
{ {
"fieldname": "working_hours_section", "fieldname": "working_hours_section",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Working Hours" "label": "Working Hours"
}, },
{ {
@ -128,16 +132,29 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "workstaion_description", "fieldname": "workstaion_description",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Description" "label": "Description"
},
{
"bold": 1,
"fieldname": "workstation_type",
"fieldtype": "Link",
"label": "Workstation Type",
"options": "Workstation Type"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
} }
], ],
"icon": "icon-wrench", "icon": "icon-wrench",
"idx": 1, "idx": 1,
"modified": "2019-11-26 12:39:19.742052", "links": [],
"modified": "2022-11-04 17:39:01.549346",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Workstation", "name": "Workstation",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -154,6 +171,8 @@
], ],
"quick_entry": 1, "quick_entry": 1,
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -32,7 +32,11 @@ class OverlapError(frappe.ValidationError):
class Workstation(Document): class Workstation(Document):
def validate(self): def before_save(self):
self.set_data_based_on_workstation_type()
self.set_hour_rate()
def set_hour_rate(self):
self.hour_rate = ( self.hour_rate = (
flt(self.hour_rate_labour) flt(self.hour_rate_labour)
+ flt(self.hour_rate_electricity) + flt(self.hour_rate_electricity)
@ -40,6 +44,30 @@ class Workstation(Document):
+ flt(self.hour_rate_rent) + flt(self.hour_rate_rent)
) )
@frappe.whitelist()
def set_data_based_on_workstation_type(self):
if self.workstation_type:
fields = [
"hour_rate_labour",
"hour_rate_electricity",
"hour_rate_consumable",
"hour_rate_rent",
"hour_rate",
"description",
]
data = frappe.get_cached_value("Workstation Type", self.workstation_type, fields, as_dict=True)
if not data:
return
for field in fields:
if self.get(field):
continue
if value := data.get(field):
self.set(field, value)
def on_update(self): def on_update(self):
self.validate_overlap_for_operation_timings() self.validate_overlap_for_operation_timings()
self.update_bom_operation() self.update_bom_operation()
@ -100,9 +128,7 @@ def get_default_holiday_list():
def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime): def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime):
if from_datetime and to_datetime: if from_datetime and to_datetime:
if not cint( if not frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"):
frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")
):
check_workstation_for_holiday(workstation, from_datetime, to_datetime) check_workstation_for_holiday(workstation, from_datetime, to_datetime)
if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")): if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")):

View File

@ -0,0 +1,21 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
class TestWorkstationType(FrappeTestCase):
pass
def create_workstation_type(**args):
args = frappe._dict(args)
if workstation_type := frappe.db.exists("Workstation Type", args.workstation_type):
return frappe.get_doc("Workstation Type", workstation_type)
else:
doc = frappe.new_doc("Workstation Type")
doc.update(args)
doc.insert()
return doc

View File

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

View File

@ -0,0 +1,133 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:workstation_type",
"creation": "2022-11-04 17:03:23.334818",
"default_view": "List",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"workstation_type",
"over_heads",
"hour_rate_electricity",
"hour_rate_consumable",
"column_break_5",
"hour_rate_rent",
"hour_rate_labour",
"section_break_8",
"hour_rate",
"description_tab",
"description"
],
"fields": [
{
"fieldname": "workstation_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Workstation Type",
"oldfieldname": "workstation_name",
"oldfieldtype": "Data",
"reqd": 1,
"unique": 1
},
{
"fieldname": "over_heads",
"fieldtype": "Section Break",
"label": "Operating Costs",
"oldfieldtype": "Section Break"
},
{
"description": "per hour",
"fieldname": "hour_rate_electricity",
"fieldtype": "Currency",
"label": "Electricity Cost",
"oldfieldname": "hour_rate_electricity",
"oldfieldtype": "Currency"
},
{
"description": "per hour",
"fieldname": "hour_rate_consumable",
"fieldtype": "Currency",
"label": "Consumable Cost",
"oldfieldname": "hour_rate_consumable",
"oldfieldtype": "Currency"
},
{
"description": "per hour",
"fieldname": "hour_rate_rent",
"fieldtype": "Currency",
"label": "Rent Cost",
"oldfieldname": "hour_rate_rent",
"oldfieldtype": "Currency"
},
{
"description": "Wages per hour",
"fieldname": "hour_rate_labour",
"fieldtype": "Currency",
"label": "Wages",
"oldfieldname": "hour_rate_labour",
"oldfieldtype": "Currency"
},
{
"description": "per hour",
"fieldname": "hour_rate",
"fieldtype": "Currency",
"label": "Net Hour Rate",
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
"read_only": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"width": "300px"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "description_tab",
"fieldtype": "Tab Break",
"label": "Description"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
}
],
"icon": "icon-wrench",
"links": [],
"modified": "2022-11-16 23:11:36.224249",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation Type",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing User",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,25 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils import flt
class WorkstationType(Document):
def before_save(self):
self.set_hour_rate()
def set_hour_rate(self):
self.hour_rate = (
flt(self.hour_rate_labour)
+ flt(self.hour_rate_electricity)
+ flt(self.hour_rate_consumable)
+ flt(self.hour_rate_rent)
)
def get_workstations(workstation_type):
workstations = frappe.get_all("Workstation", filters={"workstation_type": workstation_type})
return [workstation.name for workstation in workstations]

View File

@ -73,168 +73,6 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "Bill of Materials",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Item",
"link_count": 0,
"link_to": "Item",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Item",
"hidden": 0,
"is_query_report": 0,
"label": "Bill of Materials",
"link_count": 0,
"link_to": "BOM",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Workstation",
"link_count": 0,
"link_to": "Workstation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Operation",
"link_count": 0,
"link_to": "Operation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Routing",
"link_count": 0,
"link_to": "Routing",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
"label": "Production Planning Report",
"link_count": 0,
"link_to": "Production Planning Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
"label": "Work Order Summary",
"link_count": 0,
"link_to": "Work Order Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Quality Inspection",
"hidden": 0,
"is_query_report": 1,
"label": "Quality Inspection Summary",
"link_count": 0,
"link_to": "Quality Inspection Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Downtime Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Downtime Analysis",
"link_count": 0,
"link_to": "Downtime Analysis",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Job Card",
"hidden": 0,
"is_query_report": 1,
"label": "Job Card Summary",
"link_count": 0,
"link_to": "Job Card Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "BOM Search",
"link_count": 0,
"link_to": "BOM Search",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "BOM Stock Report",
"link_count": 0,
"link_to": "BOM Stock Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
"label": "Production Analytics",
"link_count": 0,
"link_to": "Production Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "BOM Operations Time",
"link_count": 0,
"link_to": "BOM Operations Time",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@ -400,9 +238,181 @@
"link_type": "Report", "link_type": "Report",
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Bill of Materials",
"link_count": 15,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Item",
"link_count": 0,
"link_to": "Item",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Item",
"hidden": 0,
"is_query_report": 0,
"label": "Bill of Materials",
"link_count": 0,
"link_to": "BOM",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Workstation Type",
"link_count": 0,
"link_to": "Workstation Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Workstation",
"link_count": 0,
"link_to": "Workstation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Operation",
"link_count": 0,
"link_to": "Operation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
"label": "Routing",
"link_count": 0,
"link_to": "Routing",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
"label": "Production Planning Report",
"link_count": 0,
"link_to": "Production Planning Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Quality Inspection",
"hidden": 0,
"is_query_report": 1,
"label": "Work Order Summary",
"link_count": 0,
"link_to": "Work Order Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Downtime Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Quality Inspection Summary",
"link_count": 0,
"link_to": "Quality Inspection Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Job Card",
"hidden": 0,
"is_query_report": 1,
"label": "Downtime Analysis",
"link_count": 0,
"link_to": "Downtime Analysis",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "Job Card Summary",
"link_count": 0,
"link_to": "Job Card Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "BOM Search",
"link_count": 0,
"link_to": "BOM Search",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
"label": "BOM Stock Report",
"link_count": 0,
"link_to": "BOM Stock Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "Production Analytics",
"link_count": 0,
"link_to": "Production Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "BOM Operations Time",
"link_count": 0,
"link_to": "BOM Operations Time",
"link_type": "Report",
"onboard": 0,
"type": "Link"
} }
], ],
"modified": "2022-06-15 15:18:57.062935", "modified": "2022-11-14 14:53:34.616862",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing", "name": "Manufacturing",

View File

@ -317,3 +317,4 @@ erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v13_0.update_schedule_type_in_loans
erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization
erpnext.patches.v14_0.update_tds_fields

View File

@ -1,10 +1,5 @@
import frappe
from erpnext.setup.install import setup_currency_exchange from erpnext.setup.install import setup_currency_exchange
def execute(): def execute():
frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_result")
frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_details")
frappe.reload_doc("accounts", "doctype", "currency_exchange_settings")
setup_currency_exchange() setup_currency_exchange()

View File

@ -0,0 +1,29 @@
import frappe
from frappe.utils import nowdate
from erpnext.accounts.utils import FiscalYearError, get_fiscal_year
def execute():
# Only do for current fiscal year, no need to repost for all years
for company in frappe.get_all("Company"):
try:
fiscal_year_details = get_fiscal_year(date=nowdate(), company=company.name, as_dict=True)
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
frappe.qb.update(purchase_invoice).set(
purchase_invoice.tax_withholding_net_total, purchase_invoice.net_total
).set(
purchase_invoice.base_tax_withholding_net_total, purchase_invoice.base_net_total
).where(
purchase_invoice.company == company.name
).where(
purchase_invoice.apply_tds == 1
).where(
purchase_invoice.posting_date >= fiscal_year_details.year_start_date
).where(
purchase_invoice.docstatus == 1
).run()
except FiscalYearError:
pass

View File

@ -92,18 +92,26 @@ frappe.ui.form.on("Timesheet", {
frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false); frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false);
frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false); frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false);
} }
let filters = {
"status": "Open"
};
if (frm.doc.customer) {
filters["customer"] = frm.doc.customer;
}
frm.set_query('parent_project', function(doc) {
return {
filters: filters
};
});
frm.trigger('setup_filters'); frm.trigger('setup_filters');
frm.trigger('set_dynamic_field_label'); frm.trigger('set_dynamic_field_label');
}, },
customer: function(frm) { customer: function(frm) {
frm.set_query('parent_project', function(doc) {
return {
filters: {
"customer": doc.customer
}
};
});
frm.set_query('project', 'time_logs', function(doc) { frm.set_query('project', 'time_logs', function(doc) {
return { return {
filters: { filters: {

View File

@ -30,28 +30,28 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
get_dt_columns() { get_dt_columns() {
this.columns = [ this.columns = [
{ {
name: "Date", name: __("Date"),
editable: false, editable: false,
width: 100, width: 100,
}, },
{ {
name: "Party Type", name: __("Party Type"),
editable: false, editable: false,
width: 95, width: 95,
}, },
{ {
name: "Party", name: __("Party"),
editable: false, editable: false,
width: 100, width: 100,
}, },
{ {
name: "Description", name: __("Description"),
editable: false, editable: false,
width: 350, width: 350,
}, },
{ {
name: "Deposit", name: __("Deposit"),
editable: false, editable: false,
width: 100, width: 100,
format: (value) => format: (value) =>
@ -60,7 +60,7 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
"</span>", "</span>",
}, },
{ {
name: "Withdrawal", name: __("Withdrawal"),
editable: false, editable: false,
width: 100, width: 100,
format: (value) => format: (value) =>
@ -69,26 +69,26 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
"</span>", "</span>",
}, },
{ {
name: "Unallocated Amount", name: __("Unallocated Amount"),
editable: false, editable: false,
width: 100, width: 100,
format: (value) => format: (value) =>
"<span style='color:blue;'>" + "<span style='color:var(--blue-500);'>" +
format_currency(value, this.currency) + format_currency(value, this.currency) +
"</span>", "</span>",
}, },
{ {
name: "Reference Number", name: __("Reference Number"),
editable: false, editable: false,
width: 140, width: 140,
}, },
{ {
name: "Actions", name: __("Actions"),
editable: false, editable: false,
sortable: false, sortable: false,
focusable: false, focusable: false,
dropdown: false, dropdown: false,
width: 80, width: 100,
}, },
]; ];
} }
@ -118,7 +118,7 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
row["reference_number"], row["reference_number"],
` `
<Button class="btn btn-primary btn-xs center" data-name = ${row["name"]} > <Button class="btn btn-primary btn-xs center" data-name = ${row["name"]} >
Actions ${__("Actions")}
</a> </a>
`, `,
]; ];

View File

@ -87,33 +87,33 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
get_dt_columns() { get_dt_columns() {
this.columns = [ this.columns = [
{ {
name: "Document Type", name: __("Document Type"),
editable: false, editable: false,
width: 125, width: 125,
}, },
{ {
name: "Document Name", name: __("Document Name"),
editable: false, editable: false,
width: 150, width: 150,
}, },
{ {
name: "Reference Date", name: __("Reference Date"),
editable: false, editable: false,
width: 120, width: 120,
}, },
{ {
name: "Amount", name: __("Amount"),
editable: false, editable: false,
width: 100, width: 100,
}, },
{ {
name: "Party", name: __("Party"),
editable: false, editable: false,
width: 120, width: 120,
}, },
{ {
name: "Reference Number", name: __("Reference Number"),
editable: false, editable: false,
width: 140, width: 140,
}, },
@ -222,7 +222,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
{ {
fieldtype: "HTML", fieldtype: "HTML",
fieldname: "no_matching_vouchers", fieldname: "no_matching_vouchers",
options: "<div class=\"text-muted text-center\">No Matching Vouchers Found</div>" options: __("<div class=\"text-muted text-center\">{0}</div>", [__("No Matching Vouchers Found")])
}, },
{ {
fieldtype: "Section Break", fieldtype: "Section Break",
@ -444,10 +444,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
vouchers: vouchers, vouchers: vouchers,
}, },
callback: (response) => { callback: (response) => {
const alert_string = const alert_string = __("Bank Transaction {0} Matched", [this.bank_transaction.name]);
"Bank Transaction " +
this.bank_transaction.name +
" Matched";
frappe.show_alert(alert_string); frappe.show_alert(alert_string);
this.update_dt_cards(response.message); this.update_dt_cards(response.message);
this.dialog.hide(); this.dialog.hide();
@ -471,10 +468,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
cost_center: values.cost_center, cost_center: values.cost_center,
}, },
callback: (response) => { callback: (response) => {
const alert_string = const alert_string = __("Bank Transaction {0} added as Payment Entry", [this.bank_transaction.name]);
"Bank Transaction " +
this.bank_transaction.name +
" added as Payment Entry";
frappe.show_alert(alert_string); frappe.show_alert(alert_string);
this.update_dt_cards(response.message); this.update_dt_cards(response.message);
this.dialog.hide(); this.dialog.hide();
@ -498,10 +492,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
second_account: values.second_account, second_account: values.second_account,
}, },
callback: (response) => { callback: (response) => {
const alert_string = const alert_string = __("Bank Transaction {0} added as Journal Entry", [this.bank_transaction.name]);
"Bank Transaction " +
this.bank_transaction.name +
" added as Journal Entry";
frappe.show_alert(alert_string); frappe.show_alert(alert_string);
this.update_dt_cards(response.message); this.update_dt_cards(response.message);
this.dialog.hide(); this.dialog.hide();
@ -520,10 +511,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
party: values.party, party: values.party,
}, },
callback: (response) => { callback: (response) => {
const alert_string = const alert_string = __("Bank Transaction {0} updated", [this.bank_transaction.name]);
"Bank Transaction " +
this.bank_transaction.name +
" updated";
frappe.show_alert(alert_string); frappe.show_alert(alert_string);
this.update_dt_cards(response.message); this.update_dt_cards(response.message);
this.dialog.hide(); this.dialog.hide();

View File

@ -15,20 +15,20 @@ erpnext.accounts.bank_reconciliation.NumberCardManager = class NumberCardManager
var chart_data = [ var chart_data = [
{ {
value: this.bank_statement_closing_balance, value: this.bank_statement_closing_balance,
label: "Closing Balance as per Bank Statement", label: __("Closing Balance as per Bank Statement"),
datatype: "Currency", datatype: "Currency",
currency: this.currency, currency: this.currency,
}, },
{ {
value: this.cleared_balance, value: this.cleared_balance,
label: "Closing Balance as per ERP", label: __("Closing Balance as per ERP"),
datatype: "Currency", datatype: "Currency",
currency: this.currency, currency: this.currency,
}, },
{ {
value: value:
this.bank_statement_closing_balance - this.cleared_balance, this.bank_statement_closing_balance - this.cleared_balance,
label: "Difference", label: __("Difference"),
datatype: "Currency", datatype: "Currency",
currency: this.currency, currency: this.currency,
}, },

View File

@ -11,7 +11,7 @@ $.extend(erpnext.bulk_transaction_processing, {
}); });
let count_of_rows = checked_items.length; let count_of_rows = checked_items.length;
frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), ()=>{ frappe.confirm(__("Create {0} {1} ?", [count_of_rows, __(to_doctype)]), ()=>{
if (doc_name.length == 0) { if (doc_name.length == 0) {
frappe.call({ frappe.call({
method: "erpnext.utilities.bulk_transaction.transaction_processing", method: "erpnext.utilities.bulk_transaction.transaction_processing",
@ -20,11 +20,11 @@ $.extend(erpnext.bulk_transaction_processing, {
}); });
if (count_of_rows > 10) { if (count_of_rows > 10) {
frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, to_doctype]); frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, __(to_doctype)]);
} }
} else { } else {
frappe.msgprint(__("Selected document must be in submitted state")); frappe.msgprint(__("Selected document must be in submitted state"));
} }
}); });
} }
}); });

View File

@ -143,6 +143,12 @@ var get_payment_mode_account = function(frm, mode_of_payment, callback) {
cur_frm.cscript.account_head = function(doc, cdt, cdn) { cur_frm.cscript.account_head = function(doc, cdt, cdn) {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
if (doc.docstatus == 1) {
// Should not trigger any changes on change post submit
return;
}
if(!d.charge_type && d.account_head){ if(!d.charge_type && d.account_head){
frappe.msgprint(__("Please select Charge Type first")); frappe.msgprint(__("Please select Charge Type first"));
frappe.model.set_value(cdt, cdn, "account_head", ""); frappe.model.set_value(cdt, cdn, "account_head", "");

View File

@ -341,6 +341,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.set_dynamic_labels(); this.set_dynamic_labels();
this.setup_sms(); this.setup_sms();
this.setup_quality_inspection(); this.setup_quality_inspection();
this.validate_has_items();
} }
scan_barcode() { scan_barcode() {
@ -348,6 +349,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
barcode_scanner.process_scan(); barcode_scanner.process_scan();
} }
validate_has_items () {
let table = this.frm.doc.items;
this.frm.has_items = (table && table.length
&& table[0].qty && table[0].item_code);
}
apply_default_taxes() { apply_default_taxes() {
var me = this; var me = this;
var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges", var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges",
@ -1200,7 +1207,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"base_rounding_adjustment"], company_currency); "base_rounding_adjustment"], company_currency);
this.frm.set_currency_labels(["total", "net_total", "total_taxes_and_charges", "discount_amount", this.frm.set_currency_labels(["total", "net_total", "total_taxes_and_charges", "discount_amount",
"grand_total", "taxes_and_charges_added", "taxes_and_charges_deducted", "grand_total", "taxes_and_charges_added", "taxes_and_charges_deducted","tax_withholding_net_total",
"rounded_total", "in_words", "paid_amount", "write_off_amount", "operating_cost", "rounded_total", "in_words", "paid_amount", "write_off_amount", "operating_cost",
"scrap_material_cost", "rounding_adjustment", "raw_material_cost", "scrap_material_cost", "rounding_adjustment", "raw_material_cost",
"total_cost"], this.frm.doc.currency); "total_cost"], this.frm.doc.currency);
@ -1217,7 +1224,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
// toggle fields // toggle fields
this.frm.toggle_display(["conversion_rate", "base_total", "base_net_total", this.frm.toggle_display(["conversion_rate", "base_total", "base_net_total", "base_tax_withholding_net_total",
"base_total_taxes_and_charges", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", "base_total_taxes_and_charges", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted",
"base_grand_total", "base_rounded_total", "base_in_words", "base_discount_amount", "base_grand_total", "base_rounded_total", "base_in_words", "base_discount_amount",
"base_paid_amount", "base_write_off_amount", "base_operating_cost", "base_raw_material_cost", "base_paid_amount", "base_write_off_amount", "base_operating_cost", "base_raw_material_cost",

View File

@ -47,42 +47,49 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
return; return;
} }
frappe this.scan_api_call(input, (r) => {
.call({ const data = r && r.message;
method: this.scan_api, if (!data || Object.keys(data).length === 0) {
args: { this.show_alert(__("Cannot find Item with this Barcode"), "red");
search_value: input, this.clean_up();
}, this.play_fail_sound();
}) reject();
.then((r) => { return;
const data = r && r.message; }
if (!data || Object.keys(data).length === 0) {
this.show_alert(__("Cannot find Item with this Barcode"), "red");
this.clean_up();
this.play_fail_sound();
reject();
return;
}
me.update_table(data).then(row => { me.update_table(data).then(row => {
this.play_success_sound(); this.play_success_sound();
resolve(row); resolve(row);
}).catch(() => { }).catch(() => {
this.play_fail_sound(); this.play_fail_sound();
reject(); reject();
});
}); });
});
}); });
} }
scan_api_call(input, callback) {
frappe
.call({
method: this.scan_api,
args: {
search_value: input,
},
})
.then((r) => {
callback(r);
});
}
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;
const {item_code, barcode, batch_no, serial_no, uom} = data; const {item_code, barcode, batch_no, serial_no, uom} = data;
let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom); let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode);
this.is_new_row = false;
if (!row) { if (!row) {
if (this.dont_allow_new_row) { if (this.dont_allow_new_row) {
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
@ -90,11 +97,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
reject(); reject();
return; return;
} }
this.is_new_row = true;
// add new row if new item/batch is scanned // add new row if new item/batch is scanned
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name); row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
// trigger any row add triggers defined on child table. // trigger any row add triggers defined on child table.
this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name); this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name);
this.frm.has_items = false;
} }
if (this.is_duplicate_serial_no(row, serial_no)) { if (this.is_duplicate_serial_no(row, serial_no)) {
@ -105,7 +114,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.run_serially([ frappe.run_serially([
() => this.set_selector_trigger_flag(data), () => this.set_selector_trigger_flag(data),
() => this.set_item(row, item_code).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),
@ -136,7 +145,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.flags.hide_serial_batch_dialog = false; frappe.flags.hide_serial_batch_dialog = false;
} }
set_item(row, item_code) { 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};
@ -149,12 +158,186 @@ 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));
} }
}); });
} }
prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no) {
var me = this;
this.dialog = new frappe.ui.Dialog({
title: __("Scan barcode for item {0}", [item_code]),
fields: me.get_fields_for_dialog(row, item_code, barcode, batch_no, serial_no),
})
this.dialog.set_primary_action(__("Update"), () => {
const item_data = {item_code: item_code};
item_data[this.qty_field] = this.dialog.get_value("scanned_qty");
item_data["has_item_scanned"] = 1;
this.remaining_qty = flt(this.dialog.get_value("qty")) - flt(this.dialog.get_value("scanned_qty"));
frappe.model.set_value(row.doctype, row.name, item_data);
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_serial_no(row, this.dialog.get_value("serial_no")),
() => this.add_child_for_remaining_qty(row),
() => this.clean_up()
]);
this.dialog.hide();
});
this.dialog.show();
this.$scan_btn = this.dialog.$wrapper.find(".link-btn");
this.$scan_btn.css("display", "inline");
}
get_fields_for_dialog(row, item_code, barcode, batch_no, serial_no) {
let fields = [
{
fieldtype: "Data",
fieldname: "barcode_scanner",
options: "Barcode",
label: __("Scan Barcode"),
onchange: (e) => {
if (!e) {
return;
}
if (e.target.value) {
this.scan_api_call(e.target.value, (r) => {
if (r.message) {
this.update_dialog_values(item_code, r);
}
})
}
}
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Float",
fieldname: "qty",
label: __("Quantity to Scan"),
default: row[this.qty_field] || 1,
},
{
fieldtype: "Column Break",
fieldname: "column_break_1",
},
{
fieldtype: "Float",
read_only: 1,
fieldname: "scanned_qty",
label: __("Scanned Quantity"),
default: 1,
},
{
fieldtype: "Section Break",
}
]
if (batch_no) {
fields.push({
fieldtype: "Link",
fieldname: "batch_no",
options: "Batch No",
label: __("Batch No"),
default: batch_no,
read_only: 1,
hidden: 1
});
}
if (serial_no) {
fields.push({
fieldtype: "Small Text",
fieldname: "serial_no",
label: __("Serial Nos"),
default: serial_no,
read_only: 1,
});
}
if (barcode) {
fields.push({
fieldtype: "Data",
fieldname: "barcode",
options: "Barcode",
label: __("Barcode"),
default: barcode,
read_only: 1,
hidden: 1
});
}
return fields;
}
update_dialog_values(scanned_item, r) {
const {item_code, barcode, batch_no, serial_no} = r.message;
this.dialog.set_value("barcode_scanner", "");
if (item_code === scanned_item &&
(this.dialog.get_value("barcode") === barcode || batch_no || serial_no)) {
if (batch_no) {
this.dialog.set_value("batch_no", batch_no);
}
if (serial_no) {
this.validate_duplicate_serial_no(serial_no);
let serial_nos = this.dialog.get_value("serial_no") + "\n" + serial_no;
this.dialog.set_value("serial_no", serial_nos);
}
let qty = flt(this.dialog.get_value("scanned_qty")) + 1.0;
this.dialog.set_value("scanned_qty", qty);
}
}
validate_duplicate_serial_no(serial_no) {
let serial_nos = this.dialog.get_value("serial_no") ?
this.dialog.get_value("serial_no").split("\n") : [];
if (in_list(serial_nos, serial_no)) {
frappe.throw(__("Serial No {0} already scanned", [serial_no]));
}
}
add_child_for_remaining_qty(prev_row) {
if (this.remaining_qty && this.remaining_qty >0) {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
let row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
let ignore_fields = ["name", "idx", "batch_no", "barcode",
"received_qty", "serial_no", "has_item_scanned"];
for (let key in prev_row) {
if (in_list(ignore_fields, key)) {
continue;
}
row[key] = prev_row[key];
}
row[this.qty_field] = this.remaining_qty;
if (this.qty_field == "qty" && frappe.meta.has_field(row.doctype, "stock_qty")) {
row["stock_qty"] = this.remaining_qty * row.conversion_factor;
}
this.frm.script_manager.trigger("item_code", row.doctype, row.name);
}
}
async set_serial_no(row, serial_no) { async set_serial_no(row, serial_no) {
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) { if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
const existing_serial_nos = row[this.serial_no_field]; const existing_serial_nos = row[this.serial_no_field];
@ -205,7 +388,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
return is_duplicate; return is_duplicate;
} }
get_row_to_modify_on_scan(item_code, batch_no, uom) { get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid; let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
// Check if batch is scanned and table has batch no field // Check if batch is scanned and table has batch no field
@ -214,12 +397,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
const matching_row = (row) => { const matching_row = (row) => {
const item_match = row.item_code == item_code; const item_match = row.item_code == item_code;
const batch_match = row[this.batch_no_field] == batch_no; const batch_match = (!row[this.batch_no_field] || row[this.batch_no_field] == batch_no);
const uom_match = !uom || row[this.uom_field] == uom; const uom_match = !uom || row[this.uom_field] == uom;
const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]); const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
const item_scanned = row.has_item_scanned;
return item_match return item_match
&& uom_match && uom_match
&& !item_scanned
&& (!is_batch_no_scan || batch_match) && (!is_batch_no_scan || batch_match)
&& (!check_max_qty || qty_in_limit) && (!check_max_qty || qty_in_limit)
} }

View File

@ -159,6 +159,12 @@
} }
} }
.item-img {
@extend .image;
border-radius: 8px 8px 0 0;
object-fit: cover;
}
> .item-detail { > .item-detail {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -79,7 +79,7 @@ def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=Fa
] ]
else: else:
return frappe.get_all( return frappe.get_all(
doctype, "Quality Procedure",
fields=["name as value", "is_group as expandable"], fields=["name as value", "is_group as expandable"],
filters=dict(parent_quality_procedure=parent), filters=dict(parent_quality_procedure=parent),
order_by="name asc", order_by="name asc",

View File

@ -49,7 +49,7 @@ def make_custom_fields(update=True):
dict( dict(
fieldname="exempt_from_sales_tax", fieldname="exempt_from_sales_tax",
fieldtype="Check", fieldtype="Check",
insert_after="represents_company", insert_after="dn_required",
label="Is customer exempted from sales tax?", label="Is customer exempted from sales tax?",
) )
], ],

View File

@ -14,30 +14,35 @@
"naming_series", "naming_series",
"salutation", "salutation",
"customer_name", "customer_name",
"customer_type",
"customer_group",
"column_break0",
"territory",
"gender", "gender",
"default_bank_account",
"tax_id",
"tax_category",
"tax_withholding_category",
"lead_name", "lead_name",
"opportunity_name", "opportunity_name",
"image",
"column_break0",
"customer_group",
"customer_type",
"territory",
"account_manager", "account_manager",
"so_required", "image",
"dn_required", "defaults_tab",
"default_price_list",
"default_bank_account",
"column_break_14",
"default_currency",
"internal_customer_section",
"is_internal_customer", "is_internal_customer",
"represents_company", "represents_company",
"disabled", "column_break_70",
"allowed_to_transact_section",
"companies", "companies",
"currency_and_price_list", "more_info",
"default_currency", "market_segment",
"column_break_14", "industry",
"default_price_list", "customer_pos_id",
"website",
"language",
"column_break_45",
"customer_details",
"dashboard_tab",
"contact_and_address_tab",
"address_contacts", "address_contacts",
"address_html", "address_html",
"column_break1", "column_break1",
@ -49,34 +54,39 @@
"column_break_26", "column_break_26",
"customer_primary_address", "customer_primary_address",
"primary_address", "primary_address",
"default_receivable_accounts", "tax_tab",
"accounts", "taxation_section",
"tax_id",
"column_break_21",
"tax_category",
"tax_withholding_category",
"accounting_tab",
"credit_limit_section", "credit_limit_section",
"payment_terms", "payment_terms",
"credit_limits", "credit_limits",
"more_info", "default_receivable_accounts",
"customer_details", "accounts",
"column_break_45", "loyalty_points_tab",
"market_segment",
"industry",
"website",
"language",
"is_frozen",
"column_break_38",
"loyalty_program", "loyalty_program",
"column_break_54",
"loyalty_program_tier", "loyalty_program_tier",
"sales_team_section_break", "sales_team_tab",
"default_sales_partner",
"default_commission_rate",
"sales_team_section",
"sales_team", "sales_team",
"customer_pos_id" "sales_team_section",
"default_sales_partner",
"column_break_66",
"default_commission_rate",
"settings_tab",
"so_required",
"dn_required",
"column_break_53",
"is_frozen",
"disabled"
], ],
"fields": [ "fields": [
{ {
"fieldname": "basic_info", "fieldname": "basic_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Name and Type",
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-user" "options": "fa fa-user"
}, },
@ -215,12 +225,6 @@
"options": "Company", "options": "Company",
"unique": 1 "unique": 1
}, },
{
"depends_on": "represents_company",
"fieldname": "allowed_to_transact_section",
"fieldtype": "Section Break",
"label": "Allowed To Transact With"
},
{ {
"depends_on": "represents_company", "depends_on": "represents_company",
"fieldname": "companies", "fieldname": "companies",
@ -228,12 +232,6 @@
"label": "Allowed To Transact With", "label": "Allowed To Transact With",
"options": "Allowed To Transact With" "options": "Allowed To Transact With"
}, },
{
"collapsible": 1,
"fieldname": "currency_and_price_list",
"fieldtype": "Section Break",
"label": "Currency and Price List"
},
{ {
"fieldname": "default_currency", "fieldname": "default_currency",
"fieldtype": "Link", "fieldtype": "Link",
@ -295,7 +293,7 @@
"description": "Select, to make the customer searchable with these fields", "description": "Select, to make the customer searchable with these fields",
"fieldname": "primary_address_and_contact_detail", "fieldname": "primary_address_and_contact_detail",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Primary Address and Contact Detail" "label": "Primary Address and Contact"
}, },
{ {
"description": "Reselect, if the chosen contact is edited after save", "description": "Reselect, if the chosen contact is edited after save",
@ -334,20 +332,18 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible": 1,
"fieldname": "default_receivable_accounts", "fieldname": "default_receivable_accounts",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Default Receivable Accounts" "label": "Default Receivable Accounts"
}, },
{ {
"description": "Mention if non-standard receivable account", "description": "Mention if a non-standard receivable account",
"fieldname": "accounts", "fieldname": "accounts",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Accounts", "label": "Receivable Accounts",
"options": "Party Account" "options": "Party Account"
}, },
{ {
"collapsible": 1,
"fieldname": "credit_limit_section", "fieldname": "credit_limit_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Credit Limit and Payment Terms" "label": "Credit Limit and Payment Terms"
@ -397,12 +393,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Frozen" "label": "Is Frozen"
}, },
{
"collapsible": 1,
"fieldname": "column_break_38",
"fieldtype": "Section Break",
"label": "Loyalty Points"
},
{ {
"fieldname": "loyalty_program", "fieldname": "loyalty_program",
"fieldtype": "Link", "fieldtype": "Link",
@ -417,15 +407,6 @@
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
}, },
{
"collapsible": 1,
"collapsible_depends_on": "default_sales_partner",
"fieldname": "sales_team_section_break",
"fieldtype": "Section Break",
"label": "Sales Partner and Commission",
"oldfieldtype": "Section Break",
"options": "fa fa-group"
},
{ {
"fieldname": "default_sales_partner", "fieldname": "default_sales_partner",
"fieldtype": "Link", "fieldtype": "Link",
@ -446,13 +427,12 @@
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "sales_team", "collapsible_depends_on": "sales_team",
"fieldname": "sales_team_section", "fieldname": "sales_team_section",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Sales Team"
}, },
{ {
"fieldname": "sales_team", "fieldname": "sales_team",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Sales Team Details", "label": "Sales Team",
"oldfieldname": "sales_team", "oldfieldname": "sales_team",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Sales Team" "options": "Sales Team"
@ -498,6 +478,83 @@
"no_copy": 1, "no_copy": 1,
"options": "Opportunity", "options": "Opportunity",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break",
"label": "Contact & Address"
},
{
"fieldname": "defaults_tab",
"fieldtype": "Section Break",
"label": "Defaults"
},
{
"fieldname": "settings_tab",
"fieldtype": "Tab Break",
"label": "Settings"
},
{
"collapsible": 1,
"collapsible_depends_on": "default_sales_partner",
"fieldname": "sales_team_tab",
"fieldtype": "Tab Break",
"label": "Sales Team",
"oldfieldtype": "Section Break",
"options": "fa fa-group"
},
{
"fieldname": "column_break_66",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"fieldname": "dashboard_tab",
"fieldtype": "Tab Break",
"label": "Dashboard",
"show_dashboard": 1
},
{
"fieldname": "column_break_53",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "loyalty_points_tab",
"fieldtype": "Section Break",
"label": "Loyalty Points"
},
{
"fieldname": "taxation_section",
"fieldtype": "Section Break"
},
{
"fieldname": "accounting_tab",
"fieldtype": "Tab Break",
"label": "Accounting"
},
{
"fieldname": "tax_tab",
"fieldtype": "Tab Break",
"label": "Tax"
},
{
"collapsible": 1,
"collapsible_depends_on": "is_internal_customer",
"fieldname": "internal_customer_section",
"fieldtype": "Section Break",
"label": "Internal Customer"
},
{
"fieldname": "column_break_70",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_54",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
@ -511,7 +568,7 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2022-04-16 20:32:34.000304", "modified": "2022-11-08 15:52:34.462657",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@ -294,7 +294,7 @@ class Customer(TransactionBase):
def after_rename(self, olddn, newdn, merge=False): def after_rename(self, olddn, newdn, merge=False):
if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": if frappe.defaults.get_global_default("cust_master_name") == "Customer Name":
frappe.db.set(self, "customer_name", newdn) self.db_set("customer_name", newdn)
def set_loyalty_program(self): def set_loyalty_program(self):
if self.loyalty_program: if self.loyalty_program:

View File

@ -12,7 +12,7 @@
], ],
"fields": [ "fields": [
{ {
"columns": 4, "columns": 3,
"fieldname": "credit_limit", "fieldname": "credit_limit",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
@ -31,6 +31,7 @@
"options": "Company" "options": "Company"
}, },
{ {
"columns": 3,
"default": "0", "default": "0",
"fieldname": "bypass_credit_limit_check", "fieldname": "bypass_credit_limit_check",
"fieldtype": "Check", "fieldtype": "Check",
@ -40,7 +41,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2019-12-31 15:43:05.822328", "modified": "2022-11-08 15:19:13.927194",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer Credit Limit", "name": "Customer Credit Limit",
@ -48,5 +49,6 @@
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -87,13 +87,13 @@ class InstallationNote(TransactionBase):
frappe.throw(_("Please pull items from Delivery Note")) frappe.throw(_("Please pull items from Delivery Note"))
def on_update(self): def on_update(self):
frappe.db.set(self, "status", "Draft") self.db_set("status", "Draft")
def on_submit(self): def on_submit(self):
self.validate_serial_no() self.validate_serial_no()
self.update_prevdoc_status() self.update_prevdoc_status()
frappe.db.set(self, "status", "Submitted") self.db_set("status", "Submitted")
def on_cancel(self): def on_cancel(self):
self.update_prevdoc_status() self.update_prevdoc_status()
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")

View File

@ -119,10 +119,10 @@ class Quotation(SellingController):
if not (self.is_fully_ordered() or self.is_partially_ordered()): if not (self.is_fully_ordered() or self.is_partially_ordered()):
get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"])
lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons]
frappe.db.set(self, "status", "Lost") self.db_set("status", "Lost")
if detailed_reason: if detailed_reason:
frappe.db.set(self, "order_lost_reason", detailed_reason) self.db_set("order_lost_reason", detailed_reason)
for reason in lost_reasons_list: for reason in lost_reasons_list:
if reason.get("lost_reason") in lost_reasons_lst: if reason.get("lost_reason") in lost_reasons_lst:
@ -247,7 +247,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
"Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}}, "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}},
"Quotation Item": { "Quotation Item": {
"doctype": "Sales Order Item", "doctype": "Sales Order Item",
"field_map": {"parent": "prevdoc_docname"}, "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: doc.qty > 0, "condition": lambda doc: doc.qty > 0,
}, },

View File

@ -193,6 +193,9 @@ class SalesOrder(SellingController):
{"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}}
) )
if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")):
self.validate_rate_with_reference_doc([["Quotation", "prev_docname", "quotation_item"]])
def update_enquiry_status(self, prevdoc, flag): def update_enquiry_status(self, prevdoc, flag):
enq = frappe.db.sql( enq = frappe.db.sql(
"select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", "select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s",
@ -246,7 +249,7 @@ class SalesOrder(SellingController):
self.update_project() self.update_project()
self.update_prevdoc_status("cancel") self.update_prevdoc_status("cancel")
frappe.db.set(self, "status", "Cancelled") self.db_set("status", "Cancelled")
self.update_blanket_order() self.update_blanket_order()

View File

@ -1774,6 +1774,69 @@ class TestSalesOrder(FrappeTestCase):
sales_order.save() sales_order.save()
self.assertEqual(sales_order.taxes[0].tax_amount, 0) self.assertEqual(sales_order.taxes[0].tax_amount, 0)
def test_sales_order_partial_advance_payment(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_entry,
get_payment_entry,
)
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
# Make a customer
customer = get_customer_dict("QA Logistics")
frappe.get_doc(customer).insert()
# Make a Sales Order
so = make_sales_order(
customer="QA Logistics",
item_list=[
{"item_code": "_Test Item", "qty": 1, "rate": 200},
{"item_code": "_Test Item 2", "qty": 1, "rate": 300},
],
)
# Create a advance payment against that Sales Order
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_from_account_currency = so.currency
pe.paid_to_account_currency = so.currency
pe.source_exchange_rate = 1
pe.target_exchange_rate = 1
pe.paid_amount = so.grand_total
pe.save(ignore_permissions=True)
pe.submit()
# Make standalone advance payment entry
create_payment_entry(
payment_type="Receive",
party_type="Customer",
party="QA Logistics",
paid_from="Debtors - _TC",
paid_to="_Test Bank - _TC",
save=1,
submit=1,
)
si = make_sales_invoice(so.name)
item = si.get("items")[1]
si.remove(item)
si.allocate_advances_automatically = 1
si.save()
self.assertEqual(len(si.get("advances")), 1)
self.assertEqual(si.get("advances")[0].allocated_amount, 200)
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
si.submit()
pe.load_from_db()
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(pe.references[0].allocated_amount, 200)
self.assertEqual(pe.references[1].reference_name, so.name)
self.assertEqual(pe.references[1].allocated_amount, 300)
def automatically_fetch_payment_terms(enable=1): def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings = frappe.get_doc("Accounts Settings")

View File

@ -70,6 +70,7 @@
"warehouse", "warehouse",
"target_warehouse", "target_warehouse",
"prevdoc_docname", "prevdoc_docname",
"quotation_item",
"col_break4", "col_break4",
"against_blanket_order", "against_blanket_order",
"blanket_order", "blanket_order",
@ -838,12 +839,20 @@
"label": "Purchase Order Item", "label": "Purchase Order Item",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "quotation_item",
"fieldtype": "Data",
"hidden": 1,
"label": "quotation_item",
"no_copy": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-10-26 16:05:02.712705", "modified": "2022-11-10 18:20:30.137455",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

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