Merge branch 'develop' into fix-employee-onboarding-status
This commit is contained in:
commit
c5c858e428
@ -10,3 +10,6 @@
|
||||
|
||||
# Replace use of Class.extend with native JS class
|
||||
1fe891b287a1b3f225d29ee3d07e7b1824aba9e7
|
||||
|
||||
# This commit just changes spaces to tabs for indentation in some files
|
||||
5f473611bd6ed57703716244a054d3fb5ba9cd23
|
||||
|
1
.github/helper/install.sh
vendored
1
.github/helper/install.sh
vendored
@ -44,3 +44,4 @@ sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
||||
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
||||
bench start &
|
||||
bench --site test_site reinstall --yes
|
||||
bench build --app frappe
|
||||
|
6
.github/workflows/patch.yml
vendored
6
.github/workflows/patch.yml
vendored
@ -66,4 +66,8 @@ jobs:
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
|
||||
- name: Run Patch Tests
|
||||
run: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
wget https://erpnext.com/files/v10-erpnext.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
|
||||
bench --site test_site migrate
|
||||
|
@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.4.1'
|
||||
__version__ = '13.5.1'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
@ -27,7 +27,7 @@ class AccountingDimension(Document):
|
||||
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
|
||||
|
||||
if exists and self.is_new():
|
||||
frappe.throw("Document Type already used as a dimension")
|
||||
frappe.throw(_("Document Type already used as a dimension"))
|
||||
|
||||
if not self.is_new():
|
||||
self.validate_document_type_change()
|
||||
|
@ -18,6 +18,7 @@
|
||||
"delete_linked_ledger_entries",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"post_change_gl_entries",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
@ -253,6 +254,13 @@
|
||||
{
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
|
||||
"fieldname": "post_change_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Ledger Entries for Change Amount"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -260,7 +268,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-30 15:25:10.381008",
|
||||
"modified": "2021-06-17 20:26:03.721202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint
|
||||
from frappe.model.document import Document
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
@ -24,7 +25,7 @@ class AccountsSettings(Document):
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
frappe.msgprint(
|
||||
"Stale Days should start from 1.", title='Error', indicator='red',
|
||||
_("Stale Days should start from 1."), title='Error', indicator='red',
|
||||
raise_exception=1)
|
||||
|
||||
def enable_payment_schedule_in_print(self):
|
||||
|
@ -0,0 +1,197 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2020-09-12 22:26:19.594367",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"add_deduct_tax",
|
||||
"charge_type",
|
||||
"row_id",
|
||||
"account_head",
|
||||
"col_break_1",
|
||||
"description",
|
||||
"included_in_paid_amount",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
"currency",
|
||||
"tax_amount",
|
||||
"total",
|
||||
"allocated_amount",
|
||||
"column_break_13",
|
||||
"base_tax_amount",
|
||||
"base_total",
|
||||
"base_allocated_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "charge_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"oldfieldname": "charge_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nActual\nOn Paid Amount\nOn Previous Row Amount\nOn Previous Row Total",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:[\"On Previous Row Amount\", \"On Previous Row Total\"].indexOf(doc.charge_type)!==-1",
|
||||
"fieldname": "row_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Row #",
|
||||
"oldfieldname": "row_id",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "account_head",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account Head",
|
||||
"oldfieldname": "account_head",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_1",
|
||||
"fieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_width": "300px",
|
||||
"reqd": 1,
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"default": ":Company",
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"oldfieldname": "cost_center_other_charges",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"oldfieldname": "rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "tax_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "total",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Total",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_tax_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount (Company Currency)",
|
||||
"oldfieldname": "tax_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total (Company Currency)",
|
||||
"oldfieldname": "total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "add_deduct_tax",
|
||||
"fieldtype": "Select",
|
||||
"label": "Add Or Deduct",
|
||||
"options": "Add\nDeduct",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "included_in_paid_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Considered In Paid Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fetch_from": "account_head.account_currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-09 11:46:58.373170",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class AdvanceTaxesandCharges(Document):
|
||||
pass
|
@ -120,4 +120,4 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
plaid_success(token, response) {
|
||||
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
||||
}
|
||||
};
|
||||
};
|
@ -54,7 +54,7 @@ class CForm(Document):
|
||||
frappe.throw(_("Please enter atleast 1 invoice in the table"))
|
||||
|
||||
def set_total_invoiced_amount(self):
|
||||
total = sum([flt(d.grand_total) for d in self.get('invoices')])
|
||||
total = sum(flt(d.grand_total) for d in self.get('invoices'))
|
||||
frappe.db.set(self, 'total_invoiced_amount', total)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -22,7 +22,7 @@ def validate_company(company):
|
||||
'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(
|
||||
frappe.bold('Allow Account Creation Against Child Company'))
|
||||
frappe.throw(msg, title=_('Wrong Company'))
|
||||
@ -56,7 +56,7 @@ def get_file(file_name):
|
||||
extension = extension.lstrip(".")
|
||||
|
||||
if extension not in ('csv', 'xlsx', 'xls'):
|
||||
frappe.throw("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")
|
||||
frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"))
|
||||
|
||||
return file_doc, extension
|
||||
|
||||
@ -293,7 +293,7 @@ def validate_accounts(file_name):
|
||||
accounts_dict = {}
|
||||
for account in accounts:
|
||||
accounts_dict.setdefault(account["account_name"], account)
|
||||
if not hasattr(account, "parent_account"):
|
||||
if "parent_account" not in account:
|
||||
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
|
||||
msg += "<br><br>"
|
||||
msg += _("Alternatively, you can download the template and fill your data in.")
|
||||
|
@ -14,7 +14,7 @@ class CouponCode(Document):
|
||||
|
||||
if not self.coupon_code:
|
||||
if self.coupon_type == "Promotional":
|
||||
self.coupon_code =''.join([i for i in self.coupon_name if not i.isdigit()])[0:8].upper()
|
||||
self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
|
||||
elif self.coupon_type == "Gift Card":
|
||||
self.coupon_code = frappe.generate_hash()[:10].upper()
|
||||
|
||||
|
@ -42,18 +42,18 @@ class InvoiceDiscounting(AccountsController):
|
||||
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
|
||||
|
||||
def calculate_total_amount(self):
|
||||
self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices])
|
||||
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
|
||||
|
||||
def on_submit(self):
|
||||
self.update_sales_invoice()
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status()
|
||||
self.set_status(cancel=1)
|
||||
self.update_sales_invoice()
|
||||
self.make_gl_entries()
|
||||
|
||||
def set_status(self, status=None):
|
||||
def set_status(self, status=None, cancel=0):
|
||||
if status:
|
||||
self.status = status
|
||||
self.db_set("status", status)
|
||||
@ -66,6 +66,9 @@ class InvoiceDiscounting(AccountsController):
|
||||
elif self.docstatus == 2:
|
||||
self.status = "Cancelled"
|
||||
|
||||
if cancel:
|
||||
self.db_set('status', self.status, update_modified = True)
|
||||
|
||||
def update_sales_invoice(self):
|
||||
for d in self.invoices:
|
||||
if self.docstatus == 1:
|
||||
|
@ -196,8 +196,8 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account))
|
||||
|
||||
def check_credit_limit(self):
|
||||
customers = list(set([d.party for d in self.get("accounts")
|
||||
if d.party_type=="Customer" and d.party and flt(d.debit) > 0]))
|
||||
customers = list(set(d.party for d in self.get("accounts")
|
||||
if d.party_type=="Customer" and d.party and flt(d.debit) > 0))
|
||||
if customers:
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
for customer in customers:
|
||||
|
@ -21,7 +21,7 @@ class MonthlyDistribution(Document):
|
||||
idx += 1
|
||||
|
||||
def validate(self):
|
||||
total = sum([flt(d.percentage_allocation) for d in self.get("percentages")])
|
||||
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
|
||||
|
||||
if flt(total, 2) != 100.0:
|
||||
frappe.throw(_("Percentage Allocation should be equal to 100%") + \
|
||||
|
@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
||||
doc: frm.doc,
|
||||
btn: $(btn_primary),
|
||||
method: "make_invoices",
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
|
||||
freeze: 1,
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
||||
callback: function(r) {
|
||||
if (r.message.length == 1) {
|
||||
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
|
||||
} else if (r.message.length < 50) {
|
||||
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,7 +216,8 @@ def start_import(invoices):
|
||||
return names
|
||||
|
||||
def publish(index, total, doctype):
|
||||
if total < 5: return
|
||||
if total < 50:
|
||||
return
|
||||
frappe.publish_realtime(
|
||||
"opening_invoice_creation_progress",
|
||||
dict(
|
||||
@ -241,4 +242,3 @@ def get_temporary_opening_account(company=None):
|
||||
|
||||
return accounts[0].name
|
||||
|
||||
|
||||
|
@ -3,8 +3,12 @@
|
||||
{% include "erpnext/public/js/controllers/accounts.js" %}
|
||||
frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
cur_frm.cscript.tax_table = "Advance Taxes and Charges";
|
||||
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);
|
||||
@ -91,6 +95,16 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query("advance_tax_account", function() {
|
||||
return {
|
||||
filters: {
|
||||
"company": frm.doc.company,
|
||||
"root_type": ["in", ["Asset", "Liability"]],
|
||||
"is_group": 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query("reference_doctype", "references", function() {
|
||||
if (frm.doc.party_type == "Customer") {
|
||||
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
|
||||
@ -182,6 +196,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
|
||||
|
||||
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
|
||||
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
|
||||
(frm.doc.paid_from_account_currency != company_currency));
|
||||
|
||||
frm.toggle_display("base_received_amount", (
|
||||
frm.doc.paid_to_account_currency != company_currency
|
||||
@ -216,7 +232,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
|
||||
|
||||
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
|
||||
"difference_amount"], company_currency);
|
||||
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
|
||||
|
||||
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
|
||||
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
|
||||
@ -224,11 +240,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
var party_account_currency = frm.doc.payment_type=="Receive" ?
|
||||
frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency;
|
||||
|
||||
frm.set_currency_labels(["total_allocated_amount", "unallocated_amount"], party_account_currency);
|
||||
frm.set_currency_labels(["total_allocated_amount", "unallocated_amount",
|
||||
"total_taxes_and_charges"], party_account_currency);
|
||||
|
||||
var currency_field = (frm.doc.payment_type=="Receive") ? "paid_from_account_currency" : "paid_to_account_currency"
|
||||
frm.set_df_property("total_allocated_amount", "options", currency_field);
|
||||
frm.set_df_property("unallocated_amount", "options", currency_field);
|
||||
frm.set_df_property("total_taxes_and_charges", "options", currency_field);
|
||||
frm.set_df_property("party_balance", "options", currency_field);
|
||||
|
||||
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
|
||||
@ -364,6 +382,16 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
},
|
||||
|
||||
apply_tax_withholding_amount: function(frm) {
|
||||
if (!frm.doc.apply_tax_withholding_amount) {
|
||||
frm.set_value("tax_withholding_category", '');
|
||||
} else {
|
||||
frappe.db.get_value('Supplier', frm.doc.party, 'tax_withholding_category', (values) => {
|
||||
frm.set_value("tax_withholding_category", values.tax_withholding_category);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
paid_from: function(frm) {
|
||||
if(frm.set_party_account_based_on_party) return;
|
||||
|
||||
@ -843,12 +871,12 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if(frm.doc.payment_type == "Receive"
|
||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
|
||||
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
|
||||
unallocated_amount = (frm.doc.base_received_amount + total_deductions
|
||||
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
|
||||
unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
|
||||
+ frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
|
||||
} else if (frm.doc.payment_type == "Pay"
|
||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
|
||||
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {
|
||||
unallocated_amount = (frm.doc.base_paid_amount - (total_deductions
|
||||
unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions
|
||||
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
|
||||
}
|
||||
}
|
||||
@ -874,7 +902,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
|
||||
function(d) { return flt(d.amount) }));
|
||||
|
||||
frm.set_value("difference_amount", difference_amount - total_deductions);
|
||||
frm.set_value("difference_amount", difference_amount - total_deductions +
|
||||
frm.doc.base_total_taxes_and_charges);
|
||||
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
},
|
||||
@ -1002,7 +1031,266 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
sales_taxes_and_charges_template: function(frm) {
|
||||
frm.trigger('fetch_taxes_from_template');
|
||||
},
|
||||
|
||||
purchase_taxes_and_charges_template: function(frm) {
|
||||
frm.trigger('fetch_taxes_from_template');
|
||||
},
|
||||
|
||||
fetch_taxes_from_template: function(frm) {
|
||||
let master_doctype = '';
|
||||
let taxes_and_charges = '';
|
||||
|
||||
if (frm.doc.party_type == 'Supplier') {
|
||||
master_doctype = 'Purchase Taxes and Charges Template';
|
||||
taxes_and_charges = frm.doc.purchase_taxes_and_charges_template;
|
||||
} else if (frm.doc.party_type == 'Customer') {
|
||||
master_doctype = 'Sales Taxes and Charges Template';
|
||||
taxes_and_charges = frm.doc.sales_taxes_and_charges_template;
|
||||
}
|
||||
|
||||
if (!taxes_and_charges) {
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.accounts_controller.get_taxes_and_charges",
|
||||
args: {
|
||||
"master_doctype": master_doctype,
|
||||
"master_name": taxes_and_charges
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc && r.message) {
|
||||
// set taxes table
|
||||
if(r.message) {
|
||||
for (let tax of r.message) {
|
||||
if (tax.charge_type === 'On Net Total') {
|
||||
tax.charge_type = 'On Paid Amount';
|
||||
}
|
||||
me.frm.add_child("taxes", tax);
|
||||
}
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
apply_taxes: function(frm) {
|
||||
frm.events.initialize_taxes(frm);
|
||||
frm.events.determine_exclusive_rate(frm);
|
||||
frm.events.calculate_taxes(frm);
|
||||
},
|
||||
|
||||
initialize_taxes: function(frm) {
|
||||
$.each(frm.doc["taxes"] || [], function(i, tax) {
|
||||
frm.events.validate_taxes_and_charges(tax);
|
||||
frm.events.validate_inclusive_tax(tax);
|
||||
tax.item_wise_tax_detail = {};
|
||||
let tax_fields = ["total", "tax_fraction_for_current_item",
|
||||
"grand_total_fraction_for_current_item"];
|
||||
|
||||
if (cstr(tax.charge_type) != "Actual") {
|
||||
tax_fields.push("tax_amount");
|
||||
}
|
||||
|
||||
$.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
|
||||
|
||||
frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
|
||||
});
|
||||
},
|
||||
|
||||
validate_taxes_and_charges: function(d) {
|
||||
let msg = "";
|
||||
|
||||
if (d.account_head && !d.description) {
|
||||
// set description from account head
|
||||
d.description = d.account_head.split(' - ').slice(0, -1).join(' - ');
|
||||
}
|
||||
|
||||
if (!d.charge_type && (d.row_id || d.rate || d.tax_amount)) {
|
||||
msg = __("Please select Charge Type first");
|
||||
d.row_id = "";
|
||||
d.rate = d.tax_amount = 0.0;
|
||||
} else if ((d.charge_type == 'Actual' || d.charge_type == 'On Net Total' || d.charge_type == 'On Paid Amount') && d.row_id) {
|
||||
msg = __("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'");
|
||||
d.row_id = "";
|
||||
} else if ((d.charge_type == 'On Previous Row Amount' || d.charge_type == 'On Previous Row Total') && d.row_id) {
|
||||
if (d.idx == 1) {
|
||||
msg = __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row");
|
||||
d.charge_type = '';
|
||||
} else if (!d.row_id) {
|
||||
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
|
||||
d.row_id = "";
|
||||
} else if (d.row_id && d.row_id >= d.idx) {
|
||||
msg = __("Cannot refer row number greater than or equal to current row number for this Charge type");
|
||||
d.row_id = "";
|
||||
}
|
||||
}
|
||||
if (msg) {
|
||||
frappe.validated = false;
|
||||
refresh_field("taxes");
|
||||
frappe.throw(msg);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
validate_inclusive_tax: function(tax) {
|
||||
let actual_type_error = function() {
|
||||
let msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
|
||||
frappe.throw(msg);
|
||||
};
|
||||
|
||||
let on_previous_row_error = function(row_range) {
|
||||
let msg = __("For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included",
|
||||
[tax.idx, __(tax.doctype), tax.charge_type, row_range])
|
||||
frappe.throw(msg);
|
||||
};
|
||||
|
||||
if(cint(tax.included_in_paid_amount)) {
|
||||
if(tax.charge_type == "Actual") {
|
||||
// inclusive tax cannot be of type Actual
|
||||
actual_type_error();
|
||||
} else if(tax.charge_type == "On Previous Row Amount" &&
|
||||
!cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_paid_amount)
|
||||
) {
|
||||
// referred row should also be an inclusive tax
|
||||
on_previous_row_error(tax.row_id);
|
||||
} else if(tax.charge_type == "On Previous Row Total") {
|
||||
let taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id),
|
||||
function(t) { return cint(t.included_in_paid_amount) ? null : t; });
|
||||
if(taxes_not_included.length > 0) {
|
||||
// all rows above this tax should be inclusive
|
||||
on_previous_row_error(tax.row_id == 1 ? "1" : "1 - " + tax.row_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
determine_exclusive_rate: function(frm) {
|
||||
let has_inclusive_tax = false;
|
||||
$.each(frm.doc["taxes"] || [], function(i, row) {
|
||||
if(cint(row.included_in_paid_amount)) has_inclusive_tax = true;
|
||||
});
|
||||
if(has_inclusive_tax==false) return;
|
||||
|
||||
let cumulated_tax_fraction = 0.0;
|
||||
$.each(frm.doc["taxes"] || [], function(i, tax) {
|
||||
tax.tax_fraction_for_current_item = frm.events.get_current_tax_fraction(frm, tax);
|
||||
|
||||
if(i==0) {
|
||||
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item;
|
||||
} else {
|
||||
tax.grand_total_fraction_for_current_item =
|
||||
me.frm.doc["taxes"][i-1].grand_total_fraction_for_current_item +
|
||||
tax.tax_fraction_for_current_item;
|
||||
}
|
||||
|
||||
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
|
||||
frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
|
||||
});
|
||||
},
|
||||
|
||||
get_current_tax_fraction: function(frm, tax) {
|
||||
let current_tax_fraction = 0.0;
|
||||
|
||||
if(cint(tax.included_in_paid_amount)) {
|
||||
let tax_rate = tax.rate;
|
||||
|
||||
if(tax.charge_type == "On Paid Amount") {
|
||||
current_tax_fraction = (tax_rate / 100.0);
|
||||
} else if(tax.charge_type == "On Previous Row Amount") {
|
||||
current_tax_fraction = (tax_rate / 100.0) *
|
||||
frm.doc["taxes"][cint(tax.row_id) - 1].tax_fraction_for_current_item;
|
||||
} else if(tax.charge_type == "On Previous Row Total") {
|
||||
current_tax_fraction = (tax_rate / 100.0) *
|
||||
frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_fraction_for_current_item;
|
||||
}
|
||||
}
|
||||
|
||||
if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") {
|
||||
current_tax_fraction *= -1;
|
||||
}
|
||||
return current_tax_fraction;
|
||||
},
|
||||
|
||||
|
||||
calculate_taxes: function(frm) {
|
||||
frm.doc.total_taxes_and_charges = 0.0;
|
||||
frm.doc.base_total_taxes_and_charges = 0.0;
|
||||
|
||||
let actual_tax_dict = {};
|
||||
|
||||
// maintain actual tax rate based on idx
|
||||
$.each(frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (tax.charge_type == "Actual") {
|
||||
actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax));
|
||||
}
|
||||
});
|
||||
|
||||
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
|
||||
let current_tax_amount = frm.events.get_current_tax_amount(frm, tax);
|
||||
|
||||
// Adjust divisional loss to the last item
|
||||
if (tax.charge_type == "Actual") {
|
||||
actual_tax_dict[tax.idx] -= current_tax_amount;
|
||||
if (i == frm.doc["taxes"].length - 1) {
|
||||
current_tax_amount += actual_tax_dict[tax.idx];
|
||||
}
|
||||
}
|
||||
|
||||
tax.tax_amount = current_tax_amount;
|
||||
tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
|
||||
current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
|
||||
|
||||
if(i==0) {
|
||||
tax.total = flt(frm.doc.paid_amount_after_tax + current_tax_amount, precision("total", tax));
|
||||
} else {
|
||||
tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
|
||||
}
|
||||
|
||||
tax.base_total = tax.total * frm.doc.source_exchange_rate;
|
||||
frm.doc.total_taxes_and_charges += current_tax_amount;
|
||||
frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
|
||||
|
||||
frm.refresh_field('taxes');
|
||||
frm.refresh_field('total_taxes_and_charges');
|
||||
frm.refresh_field('base_total_taxes_and_charges');
|
||||
});
|
||||
},
|
||||
|
||||
get_current_tax_amount: function(frm, tax) {
|
||||
let tax_rate = tax.rate;
|
||||
let current_tax_amount = 0.0;
|
||||
|
||||
// To set row_id by default as previous row.
|
||||
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
|
||||
if (tax.idx === 1) {
|
||||
frappe.throw(
|
||||
__("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"));
|
||||
}
|
||||
}
|
||||
|
||||
if(tax.charge_type == "Actual") {
|
||||
current_tax_amount = flt(tax.tax_amount, precision("tax_amount", tax))
|
||||
} else if(tax.charge_type == "On Paid Amount") {
|
||||
current_tax_amount = flt((tax_rate / 100.0) * frm.doc.paid_amount_after_tax);
|
||||
} else if(tax.charge_type == "On Previous Row Amount") {
|
||||
current_tax_amount = flt((tax_rate / 100.0) *
|
||||
frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount);
|
||||
|
||||
} else if(tax.charge_type == "On Previous Row Total") {
|
||||
current_tax_amount = flt((tax_rate / 100.0) *
|
||||
frm.doc["taxes"][cint(tax.row_id) - 1].total);
|
||||
}
|
||||
|
||||
return current_tax_amount;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1049,6 +1337,38 @@ frappe.ui.form.on('Payment Entry Reference', {
|
||||
}
|
||||
})
|
||||
|
||||
frappe.ui.form.on('Advance Taxes and Charges', {
|
||||
rate: function(frm) {
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
tax_amount : function(frm) {
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
row_id: function(frm) {
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
taxes_remove: function(frm) {
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
included_in_paid_amount: function(frm) {
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
charge_type: function(frm) {
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
})
|
||||
|
||||
frappe.ui.form.on('Payment Entry Deduction', {
|
||||
amount: function(frm) {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
|
@ -35,12 +35,16 @@
|
||||
"paid_to_account_balance",
|
||||
"payment_amounts_section",
|
||||
"paid_amount",
|
||||
"paid_amount_after_tax",
|
||||
"source_exchange_rate",
|
||||
"base_paid_amount",
|
||||
"base_paid_amount_after_tax",
|
||||
"column_break_21",
|
||||
"received_amount",
|
||||
"received_amount_after_tax",
|
||||
"target_exchange_rate",
|
||||
"base_received_amount",
|
||||
"base_received_amount_after_tax",
|
||||
"section_break_14",
|
||||
"get_outstanding_invoice",
|
||||
"references",
|
||||
@ -52,6 +56,17 @@
|
||||
"unallocated_amount",
|
||||
"difference_amount",
|
||||
"write_off_difference_amount",
|
||||
"taxes_and_charges_section",
|
||||
"purchase_taxes_and_charges_template",
|
||||
"sales_taxes_and_charges_template",
|
||||
"advance_tax_account",
|
||||
"column_break_55",
|
||||
"apply_tax_withholding_amount",
|
||||
"tax_withholding_category",
|
||||
"section_break_56",
|
||||
"taxes",
|
||||
"base_total_taxes_and_charges",
|
||||
"total_taxes_and_charges",
|
||||
"deductions_or_loss_section",
|
||||
"deductions",
|
||||
"transaction_references",
|
||||
@ -320,6 +335,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "doc.received_amount",
|
||||
"fieldname": "base_received_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Received Amount (Company Currency)",
|
||||
@ -584,12 +600,114 @@
|
||||
"fieldname": "custom_remarks",
|
||||
"fieldtype": "Check",
|
||||
"label": "Custom Remarks"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.apply_tax_withholding_amount",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
|
||||
"options": "Tax Withholding Category"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.party_type == 'Supplier'",
|
||||
"fieldname": "apply_tax_withholding_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Tax Withholding Amount"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "taxes_and_charges_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Taxes and Charges"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_type == 'Supplier'",
|
||||
"fieldname": "purchase_taxes_and_charges_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Taxes and Charges Template",
|
||||
"options": "Purchase Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.party_type == 'Customer'",
|
||||
"fieldname": "sales_taxes_and_charges_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Taxes and Charges Template",
|
||||
"options": "Sales Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.party_type == 'Supplier' || doc.party_type == 'Customer'",
|
||||
"fieldname": "taxes",
|
||||
"fieldtype": "Table",
|
||||
"label": "Advance Taxes and Charges",
|
||||
"options": "Advance Taxes and Charges"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_total_taxes_and_charges",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Taxes and Charges (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_taxes_and_charges",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Taxes and Charges",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Paid Amount After Tax",
|
||||
"options": "paid_from_account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_paid_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Paid Amount After Tax (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_55",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_56",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.apply_tax_withholding_amount",
|
||||
"description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices",
|
||||
"fieldname": "advance_tax_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Tax Account",
|
||||
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.received_amount",
|
||||
"fieldname": "received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Received Amount After Tax",
|
||||
"options": "paid_to_account_currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "doc.received_amount",
|
||||
"fieldname": "base_received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Received Amount After Tax (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-08 13:05:16.958866",
|
||||
"modified": "2021-06-09 11:55:04.215050",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
@ -633,4 +751,4 @@
|
||||
"sort_order": "DESC",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
@ -4,8 +4,8 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, erpnext, json
|
||||
from frappe import _, scrub, ValidationError
|
||||
from frappe.utils import flt, comma_or, nowdate, getdate
|
||||
from frappe import _, scrub, ValidationError, throw
|
||||
from frappe.utils import flt, comma_or, nowdate, getdate, cint
|
||||
from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
@ -15,9 +15,11 @@ from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amo
|
||||
from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account, get_bank_account_details
|
||||
from erpnext.controllers.accounts_controller import AccountsController, get_supplier_block_status
|
||||
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
|
||||
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
|
||||
from six import string_types, iteritems
|
||||
|
||||
from erpnext.controllers.accounts_controller import validate_taxes_and_charges
|
||||
|
||||
class InvalidPaymentEntry(ValidationError):
|
||||
pass
|
||||
|
||||
@ -52,6 +54,8 @@ class PaymentEntry(AccountsController):
|
||||
self.set_exchange_rate()
|
||||
self.validate_mandatory()
|
||||
self.validate_reference_documents()
|
||||
self.set_tax_withholding()
|
||||
self.apply_taxes()
|
||||
self.set_amounts()
|
||||
self.clear_unallocated_reference_document_rows()
|
||||
self.validate_payment_against_negative_invoice()
|
||||
@ -65,7 +69,6 @@ class PaymentEntry(AccountsController):
|
||||
self.set_status()
|
||||
|
||||
def on_submit(self):
|
||||
self.setup_party_account_field()
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
self.make_gl_entries()
|
||||
@ -78,7 +81,6 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
|
||||
self.setup_party_account_field()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
@ -122,6 +124,11 @@ class PaymentEntry(AccountsController):
|
||||
if flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0:
|
||||
if flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
@ -177,7 +184,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
for field, value in iteritems(ref_details):
|
||||
if field == 'exchange_rate' or not d.get(field) or force:
|
||||
d.set(field, value)
|
||||
d.db_set(field, value)
|
||||
|
||||
def validate_payment_type(self):
|
||||
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
|
||||
@ -303,11 +310,10 @@ class PaymentEntry(AccountsController):
|
||||
for k, v in no_oustanding_refs.items():
|
||||
frappe.msgprint(
|
||||
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
|
||||
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount"))
|
||||
.format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount"))
|
||||
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
|
||||
title=_("Warning"), indicator="orange")
|
||||
|
||||
|
||||
def validate_journal_entry(self):
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype == "Journal Entry":
|
||||
@ -386,12 +392,98 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
self.status = 'Draft'
|
||||
|
||||
self.db_set('status', self.status, update_modified = True)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if not self.party_type == 'Supplier':
|
||||
return
|
||||
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
if not self.advance_tax_account:
|
||||
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
|
||||
|
||||
reference_doclist = []
|
||||
net_total = self.paid_amount
|
||||
included_in_paid_amount = 0
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict({
|
||||
'company': self.company,
|
||||
'doctype': 'Purchase Invoice',
|
||||
'supplier': self.party,
|
||||
'posting_date': self.posting_date,
|
||||
'net_total': net_total
|
||||
})
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
tax_withholding_details.update({
|
||||
'included_in_paid_amount': included_in_paid_amount,
|
||||
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
})
|
||||
|
||||
accounts = []
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_details.get("account_head"):
|
||||
|
||||
# Preserve user updated included in paid amount
|
||||
if d.included_in_paid_amount:
|
||||
tax_withholding_details.update({'included_in_paid_amount': d.included_in_paid_amount})
|
||||
|
||||
d.update(tax_withholding_details)
|
||||
accounts.append(d.account_head)
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append("taxes", tax_withholding_details)
|
||||
|
||||
to_remove = [d for d in self.taxes
|
||||
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
self.calculate_taxes()
|
||||
|
||||
def set_amounts(self):
|
||||
self.set_received_amount()
|
||||
self.set_amounts_in_company_currency()
|
||||
self.set_amounts_after_tax()
|
||||
self.set_total_allocated_amount()
|
||||
self.set_unallocated_amount()
|
||||
self.set_difference_amount()
|
||||
|
||||
def set_received_amount(self):
|
||||
self.base_received_amount = self.base_paid_amount
|
||||
|
||||
def set_amounts_after_tax(self):
|
||||
applicable_tax = 0
|
||||
base_applicable_tax = 0
|
||||
for tax in self.get('taxes'):
|
||||
if not tax.included_in_paid_amount:
|
||||
amount = -1 * tax.tax_amount if tax.add_deduct_tax == 'Deduct' else tax.tax_amount
|
||||
base_amount = -1 * tax.base_tax_amount if tax.add_deduct_tax == 'Deduct' else tax.base_tax_amount
|
||||
|
||||
applicable_tax += amount
|
||||
base_applicable_tax += base_amount
|
||||
|
||||
self.paid_amount_after_tax = flt(flt(self.paid_amount) + flt(applicable_tax),
|
||||
self.precision("paid_amount_after_tax"))
|
||||
self.base_paid_amount_after_tax = flt(flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate),
|
||||
self.precision("base_paid_amount_after_tax"))
|
||||
|
||||
self.received_amount_after_tax = flt(flt(self.received_amount) + flt(applicable_tax),
|
||||
self.precision("paid_amount_after_tax"))
|
||||
self.base_received_amount_after_tax = flt(flt(self.received_amount_after_tax) * flt(self.target_exchange_rate),
|
||||
self.precision("base_paid_amount_after_tax"))
|
||||
|
||||
def set_amounts_in_company_currency(self):
|
||||
self.base_paid_amount, self.base_received_amount, self.difference_amount = 0, 0, 0
|
||||
if self.paid_amount:
|
||||
@ -419,17 +511,17 @@ class PaymentEntry(AccountsController):
|
||||
def set_unallocated_amount(self):
|
||||
self.unallocated_amount = 0
|
||||
if self.party:
|
||||
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
if self.payment_type == "Receive" \
|
||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions \
|
||||
and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate):
|
||||
self.unallocated_amount = (self.base_received_amount + total_deductions -
|
||||
self.base_total_allocated_amount) / self.source_exchange_rate
|
||||
and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \
|
||||
and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate):
|
||||
self.unallocated_amount = (self.received_amount_after_tax + total_deductions -
|
||||
self.base_total_allocated_amount) / self.source_exchange_rate
|
||||
elif self.payment_type == "Pay" \
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \
|
||||
and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate):
|
||||
self.unallocated_amount = (self.base_paid_amount - (total_deductions +
|
||||
self.base_total_allocated_amount)) / self.target_exchange_rate
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \
|
||||
and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate):
|
||||
self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions +
|
||||
self.base_total_allocated_amount)) / self.target_exchange_rate
|
||||
|
||||
def set_difference_amount(self):
|
||||
base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
|
||||
@ -438,13 +530,13 @@ class PaymentEntry(AccountsController):
|
||||
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
self.difference_amount = base_party_amount - self.base_received_amount
|
||||
self.difference_amount = base_party_amount - self.base_received_amount_after_tax
|
||||
elif self.payment_type == "Pay":
|
||||
self.difference_amount = self.base_paid_amount - base_party_amount
|
||||
self.difference_amount = self.base_paid_amount_after_tax - base_party_amount
|
||||
else:
|
||||
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
|
||||
self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax)
|
||||
|
||||
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
|
||||
self.difference_amount = flt(self.difference_amount - total_deductions,
|
||||
self.precision("difference_amount"))
|
||||
@ -460,8 +552,8 @@ class PaymentEntry(AccountsController):
|
||||
if ((self.payment_type=="Pay" and self.party_type=="Customer")
|
||||
or (self.payment_type=="Receive" and self.party_type=="Supplier")):
|
||||
|
||||
total_negative_outstanding = sum([abs(flt(d.outstanding_amount))
|
||||
for d in self.get("references") if flt(d.outstanding_amount) < 0])
|
||||
total_negative_outstanding = sum(abs(flt(d.outstanding_amount))
|
||||
for d in self.get("references") if flt(d.outstanding_amount) < 0)
|
||||
|
||||
paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount
|
||||
additional_charges = sum([flt(d.amount) for d in self.deductions])
|
||||
@ -532,6 +624,7 @@ class PaymentEntry(AccountsController):
|
||||
self.add_party_gl_entries(gl_entries)
|
||||
self.add_bank_gl_entries(gl_entries)
|
||||
self.add_deductions_gl_entries(gl_entries)
|
||||
self.add_tax_gl_entries(gl_entries)
|
||||
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
|
||||
|
||||
@ -571,7 +664,7 @@ class PaymentEntry(AccountsController):
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
base_unallocated_amount = base_unallocated_amount = self.unallocated_amount * \
|
||||
base_unallocated_amount = self.unallocated_amount * \
|
||||
(self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate)
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
@ -590,8 +683,8 @@ class PaymentEntry(AccountsController):
|
||||
"account": self.paid_from,
|
||||
"account_currency": self.paid_from_account_currency,
|
||||
"against": self.party if self.payment_type=="Pay" else self.paid_to,
|
||||
"credit_in_account_currency": self.paid_amount,
|
||||
"credit": self.base_paid_amount,
|
||||
"credit_in_account_currency": self.paid_amount_after_tax,
|
||||
"credit": self.base_paid_amount_after_tax,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
@ -601,12 +694,50 @@ class PaymentEntry(AccountsController):
|
||||
"account": self.paid_to,
|
||||
"account_currency": self.paid_to_account_currency,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
"debit_in_account_currency": self.received_amount,
|
||||
"debit": self.base_received_amount,
|
||||
"debit_in_account_currency": self.received_amount_after_tax,
|
||||
"debit": self.base_received_amount_after_tax,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
|
||||
def add_tax_gl_entries(self, gl_entries):
|
||||
for d in self.get('taxes'):
|
||||
account_currency = get_account_currency(d.account_head)
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
|
||||
|
||||
if self.payment_type == 'Pay':
|
||||
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
|
||||
elif self.payment_type == 'Receive':
|
||||
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
|
||||
|
||||
payment_or_advance_account = self.get_party_account_for_taxes()
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": d.account_head,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
dr_or_cr: d.base_tax_amount,
|
||||
dr_or_cr + "_in_account_currency": d.base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": d.cost_center
|
||||
}, account_currency, item=d))
|
||||
|
||||
#Intentionally use -1 to get net values in party account
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": payment_or_advance_account,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
dr_or_cr: -1 * d.base_tax_amount,
|
||||
dr_or_cr + "_in_account_currency": -1*d.base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party
|
||||
}, account_currency, item=d))
|
||||
|
||||
def add_deductions_gl_entries(self, gl_entries):
|
||||
for d in self.get("deductions"):
|
||||
if d.amount:
|
||||
@ -625,6 +756,14 @@ class PaymentEntry(AccountsController):
|
||||
}, item=d)
|
||||
)
|
||||
|
||||
def get_party_account_for_taxes(self):
|
||||
if self.advance_tax_account:
|
||||
return self.advance_tax_account
|
||||
elif self.payment_type == 'Receive':
|
||||
return self.paid_from
|
||||
elif self.payment_type == 'Pay':
|
||||
return self.paid_to
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
for d in self.get("references"):
|
||||
@ -671,6 +810,139 @@ class PaymentEntry(AccountsController):
|
||||
self.append('deductions', row)
|
||||
self.set_unallocated_amount()
|
||||
|
||||
def initialize_taxes(self):
|
||||
for tax in self.get("taxes"):
|
||||
validate_taxes_and_charges(tax)
|
||||
validate_inclusive_tax(tax, self)
|
||||
|
||||
tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
|
||||
|
||||
if tax.charge_type != "Actual":
|
||||
tax_fields.append("tax_amount")
|
||||
|
||||
for fieldname in tax_fields:
|
||||
tax.set(fieldname, 0.0)
|
||||
|
||||
self.paid_amount_after_tax = self.paid_amount
|
||||
|
||||
def determine_exclusive_rate(self):
|
||||
if not any((cint(tax.included_in_paid_amount) for tax in self.get("taxes"))):
|
||||
return
|
||||
|
||||
cumulated_tax_fraction = 0
|
||||
for i, tax in enumerate(self.get("taxes")):
|
||||
tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax)
|
||||
if i==0:
|
||||
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item
|
||||
else:
|
||||
tax.grand_total_fraction_for_current_item = \
|
||||
self.get("taxes")[i-1].grand_total_fraction_for_current_item \
|
||||
+ tax.tax_fraction_for_current_item
|
||||
|
||||
cumulated_tax_fraction += tax.tax_fraction_for_current_item
|
||||
|
||||
self.paid_amount_after_tax = flt(self.paid_amount/(1+cumulated_tax_fraction))
|
||||
|
||||
def calculate_taxes(self):
|
||||
self.total_taxes_and_charges = 0.0
|
||||
self.base_total_taxes_and_charges = 0.0
|
||||
|
||||
actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
|
||||
for tax in self.get("taxes") if tax.charge_type == "Actual"])
|
||||
|
||||
for i, tax in enumerate(self.get('taxes')):
|
||||
current_tax_amount = self.get_current_tax_amount(tax)
|
||||
|
||||
if tax.charge_type == "Actual":
|
||||
actual_tax_dict[tax.idx] -= current_tax_amount
|
||||
if i == len(self.get("taxes")) - 1:
|
||||
current_tax_amount += actual_tax_dict[tax.idx]
|
||||
|
||||
tax.tax_amount = current_tax_amount
|
||||
tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
|
||||
|
||||
if tax.add_deduct_tax == "Deduct":
|
||||
current_tax_amount *= -1.0
|
||||
else:
|
||||
current_tax_amount *= 1.0
|
||||
|
||||
if i == 0:
|
||||
tax.total = flt(self.paid_amount_after_tax + current_tax_amount, self.precision("total", tax))
|
||||
else:
|
||||
tax.total = flt(self.get('taxes')[i-1].total + current_tax_amount, self.precision("total", tax))
|
||||
|
||||
tax.base_total = tax.total * self.source_exchange_rate
|
||||
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
|
||||
|
||||
if self.get('taxes'):
|
||||
self.paid_amount_after_tax = self.get('taxes')[-1].base_total
|
||||
|
||||
def get_current_tax_amount(self, tax):
|
||||
tax_rate = tax.rate
|
||||
|
||||
# To set row_id by default as previous row.
|
||||
if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]:
|
||||
if tax.idx == 1:
|
||||
frappe.throw(_("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"))
|
||||
|
||||
if not tax.row_id:
|
||||
tax.row_id = tax.idx - 1
|
||||
|
||||
if tax.charge_type == "Actual":
|
||||
current_tax_amount = flt(tax.tax_amount, self.precision("tax_amount", tax))
|
||||
elif tax.charge_type == "On Paid Amount":
|
||||
current_tax_amount = (tax_rate / 100.0) * self.paid_amount_after_tax
|
||||
elif tax.charge_type == "On Previous Row Amount":
|
||||
current_tax_amount = (tax_rate / 100.0) * \
|
||||
self.get('taxes')[cint(tax.row_id) - 1].tax_amount
|
||||
|
||||
elif tax.charge_type == "On Previous Row Total":
|
||||
current_tax_amount = (tax_rate / 100.0) * \
|
||||
self.get('taxes')[cint(tax.row_id) - 1].total
|
||||
|
||||
return current_tax_amount
|
||||
|
||||
def get_current_tax_fraction(self, tax):
|
||||
current_tax_fraction = 0
|
||||
|
||||
if cint(tax.included_in_paid_amount):
|
||||
tax_rate = tax.rate
|
||||
|
||||
if tax.charge_type == "On Paid Amount":
|
||||
current_tax_fraction = tax_rate / 100.0
|
||||
elif tax.charge_type == "On Previous Row Amount":
|
||||
current_tax_fraction = (tax_rate / 100.0) * \
|
||||
self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item
|
||||
elif tax.charge_type == "On Previous Row Total":
|
||||
current_tax_fraction = (tax_rate / 100.0) * \
|
||||
self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item
|
||||
|
||||
if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct":
|
||||
current_tax_fraction *= -1.0
|
||||
|
||||
return current_tax_fraction
|
||||
|
||||
def validate_inclusive_tax(tax, doc):
|
||||
def _on_previous_row_error(row_range):
|
||||
throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
|
||||
|
||||
if cint(getattr(tax, "included_in_paid_amount", None)):
|
||||
if tax.charge_type == "Actual":
|
||||
# inclusive tax cannot be of type Actual
|
||||
throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx))
|
||||
elif tax.charge_type == "On Previous Row Amount" and \
|
||||
not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount):
|
||||
# referred row should also be inclusive
|
||||
_on_previous_row_error(tax.row_id)
|
||||
elif tax.charge_type == "On Previous Row Total" and \
|
||||
not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]):
|
||||
# all rows about the referred tax should be inclusive
|
||||
_on_previous_row_error("1 - %d" % (cint(tax.row_id),))
|
||||
elif tax.get("category") == "Valuation":
|
||||
frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_outstanding_reference_documents(args):
|
||||
|
||||
@ -791,7 +1063,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
|
||||
|
||||
outstanding_invoices.pop(idx - 1)
|
||||
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
|
||||
|
||||
|
||||
return outstanding_invoices
|
||||
|
||||
def get_orders_to_be_billed(posting_date, party_type, party,
|
||||
@ -989,6 +1261,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||
elif reference_doctype == "Donation":
|
||||
total_amount = ref_doc.get("amount")
|
||||
outstanding_amount = total_amount
|
||||
exchange_rate = 1
|
||||
elif reference_doctype == "Dunning":
|
||||
total_amount = ref_doc.get("dunning_amount")
|
||||
@ -1235,6 +1508,13 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
||||
})
|
||||
pe.set_difference_amount()
|
||||
|
||||
if doc.doctype == 'Purchase Order' and doc.apply_tds:
|
||||
pe.apply_tax_withholding_amount = 1
|
||||
pe.tax_withholding_category = doc.tax_withholding_category
|
||||
|
||||
if not pe.advance_tax_account:
|
||||
pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account')
|
||||
|
||||
return pe
|
||||
|
||||
def get_bank_cash_account(doc, bank_account):
|
||||
@ -1353,6 +1633,13 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
|
||||
paid_amount = received_amount * doc.get('conversion_rate', 1)
|
||||
if dt == "Employee Advance":
|
||||
paid_amount = received_amount * doc.get('exchange_rate', 1)
|
||||
|
||||
if dt == "Purchase Order" and doc.apply_tds:
|
||||
if party_account_currency == bank.account_currency:
|
||||
paid_amount = received_amount = doc.base_net_total
|
||||
else:
|
||||
paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1)
|
||||
|
||||
return paid_amount, received_amount
|
||||
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
|
@ -1,140 +1,70 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2016-06-15 15:56:30.815503",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2016-06-15 15:56:30.815503",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"field_order": [
|
||||
"account",
|
||||
"cost_center",
|
||||
"amount",
|
||||
"column_break_2",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Cost Center",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Cost Center",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-01-07 16:52:07.040146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-12 20:38:08.110674",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
@ -101,7 +101,7 @@ class PaymentRequest(Document):
|
||||
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
controller.request_for_payment(**payment_record)
|
||||
|
||||
|
||||
def get_request_amount(self):
|
||||
data_of_completed_requests = frappe.get_all("Integration Request", filters={
|
||||
'reference_doctype': self.doctype,
|
||||
@ -112,7 +112,7 @@ class PaymentRequest(Document):
|
||||
if not data_of_completed_requests:
|
||||
return self.grand_total
|
||||
|
||||
request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
|
||||
request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests)
|
||||
return request_amounts
|
||||
|
||||
def on_cancel(self):
|
||||
@ -492,7 +492,6 @@ def update_payment_req_status(doc, method):
|
||||
status = 'Requested'
|
||||
|
||||
pay_req_doc.db_set('status', status)
|
||||
frappe.db.commit()
|
||||
|
||||
def get_dummy_message(doc):
|
||||
return frappe.render_template("""{% if doc.contact_person -%}
|
||||
|
@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document):
|
||||
def check_duplicate_terms(self):
|
||||
terms = []
|
||||
for term in self.terms:
|
||||
term_info = (term.credit_days, term.credit_months, term.due_date_based_on)
|
||||
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
|
||||
if term_info in terms:
|
||||
frappe.msgprint(
|
||||
_('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),
|
||||
|
@ -1,350 +1,138 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "ACC-PCV-.YYYY.-.#####",
|
||||
"beta": 0,
|
||||
"creation": "2013-01-10 16:34:07",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"autoname": "ACC-PCV-.YYYY.-.#####",
|
||||
"creation": "2013-01-10 16:34:07",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"transaction_date",
|
||||
"posting_date",
|
||||
"fiscal_year",
|
||||
"amended_from",
|
||||
"company",
|
||||
"cost_center_wise_pnl",
|
||||
"column_break1",
|
||||
"closing_account_head",
|
||||
"remarks"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Transaction Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "transaction_date",
|
||||
"oldfieldtype": "Date",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Transaction Date",
|
||||
"oldfieldname": "transaction_date",
|
||||
"oldfieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Posting Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date",
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Closing Fiscal Year",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "fiscal_year",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Fiscal Year",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Closing Fiscal Year",
|
||||
"oldfieldname": "fiscal_year",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Fiscal Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 1,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Amended From",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "amended_from",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Period Closing Voucher",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "amended_from",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Period Closing Voucher",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "company",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"oldfieldname": "company",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldtype": "Column Break",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "The account head under Liability or Equity, in which Profit/Loss will be booked",
|
||||
"fieldname": "closing_account_head",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Closing Account Head",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "closing_account_head",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"description": "The account head under Liability or Equity, in which Profit/Loss will be booked",
|
||||
"fieldname": "closing_account_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Closing Account Head",
|
||||
"oldfieldname": "closing_account_head",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Remarks",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "remarks",
|
||||
"oldfieldtype": "Small Text",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Remarks",
|
||||
"oldfieldname": "remarks",
|
||||
"oldfieldtype": "Small Text",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "cost_center_wise_pnl",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Cost Center Wise Profit/Loss"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 1,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Period Closing Voucher",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-20 15:27:37.210458",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Period Closing Voucher",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"search_fields": "posting_date, fiscal_year",
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "closing_account_head",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"search_fields": "posting_date, fiscal_year",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "closing_account_head"
|
||||
}
|
@ -51,63 +51,96 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def make_gl_entries(self):
|
||||
gl_entries = []
|
||||
net_pl_balance = 0
|
||||
dimension_fields = ['t1.cost_center']
|
||||
net_pl_balance = 0
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
for dimension in accounting_dimensions:
|
||||
dimension_fields.append('t1.{0}'.format(dimension))
|
||||
|
||||
dimension_filters, default_dimensions = get_dimensions()
|
||||
|
||||
pl_accounts = self.get_pl_balances(dimension_fields)
|
||||
pl_accounts = self.get_pl_balances()
|
||||
|
||||
for acc in pl_accounts:
|
||||
if flt(acc.balance_in_company_currency):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gl_dict({
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \
|
||||
if flt(acc.balance_in_account_currency) < 0 else 0,
|
||||
"debit": abs(flt(acc.balance_in_company_currency)) \
|
||||
if flt(acc.balance_in_company_currency) < 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \
|
||||
if flt(acc.balance_in_account_currency) > 0 else 0,
|
||||
"credit": abs(flt(acc.balance_in_company_currency)) \
|
||||
if flt(acc.balance_in_company_currency) > 0 else 0
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
|
||||
}, item=acc))
|
||||
|
||||
net_pl_balance += flt(acc.balance_in_company_currency)
|
||||
net_pl_balance += flt(acc.bal_in_company_currency)
|
||||
|
||||
if net_pl_balance:
|
||||
cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
||||
gl_entry = self.get_gl_dict({
|
||||
"account": self.closing_account_head,
|
||||
"debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
|
||||
"debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
|
||||
"credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
|
||||
"credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
|
||||
"cost_center": cost_center
|
||||
})
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
gl_entry.update({
|
||||
dimension: default_dimensions.get(self.company, {}).get(dimension)
|
||||
})
|
||||
|
||||
gl_entries.append(gl_entry)
|
||||
if self.cost_center_wise_pnl:
|
||||
costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts)
|
||||
gl_entries += costcenter_wise_gl_entries
|
||||
else:
|
||||
gl_entry = self.get_pnl_gl_entry(net_pl_balance)
|
||||
gl_entries.append(gl_entry)
|
||||
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
make_gl_entries(gl_entries)
|
||||
|
||||
def get_pnl_gl_entry(self, net_pl_balance):
|
||||
cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
||||
gl_entry = self.get_gl_dict({
|
||||
"account": self.closing_account_head,
|
||||
"debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
|
||||
"debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
|
||||
"credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
|
||||
"credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
|
||||
"cost_center": cost_center
|
||||
})
|
||||
|
||||
self.update_default_dimensions(gl_entry)
|
||||
|
||||
return gl_entry
|
||||
|
||||
def get_costcenter_wise_pnl_gl_entries(self, pl_accounts):
|
||||
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
||||
gl_entries = []
|
||||
|
||||
for acc in pl_accounts:
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entry = self.get_gl_dict({
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": acc.cost_center or company_cost_center,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
|
||||
}, item=acc)
|
||||
|
||||
self.update_default_dimensions(gl_entry)
|
||||
|
||||
gl_entries.append(gl_entry)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def update_default_dimensions(self, gl_entry):
|
||||
if not self.accounting_dimensions:
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
_, default_dimensions = get_dimensions()
|
||||
for dimension in self.accounting_dimensions:
|
||||
gl_entry.update({
|
||||
dimension: default_dimensions.get(self.company, {}).get(dimension)
|
||||
})
|
||||
|
||||
def get_pl_balances(self):
|
||||
"""Get balance for dimension-wise pl accounts"""
|
||||
|
||||
dimension_fields = ['t1.cost_center']
|
||||
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
for dimension in self.accounting_dimensions:
|
||||
dimension_fields.append('t1.{0}'.format(dimension))
|
||||
|
||||
def get_pl_balances(self, dimension_fields):
|
||||
"""Get balance for Profit and Loss accounts, only including valid transactions (not cancelled)"""
|
||||
return frappe.db.sql("""
|
||||
select
|
||||
t1.account, t2.account_currency, {dimension_fields},
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as balance_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as balance_in_company_currency
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2 and t2.company = %s
|
||||
|
@ -8,6 +8,7 @@ import frappe
|
||||
from frappe.utils import flt, today
|
||||
from erpnext.accounts.utils import get_fiscal_year, now
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
def test_closing_entry(self):
|
||||
@ -65,6 +66,58 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
self.assertEqual(gle_for_random_expense_account[0].amount_in_account_currency,
|
||||
-1*random_expense_account[0].balance_in_account_currency)
|
||||
|
||||
def test_cost_center_wise_posting(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
|
||||
cost_center1 = create_cost_center("Test Cost Center 1")
|
||||
cost_center2 = create_cost_center("Test Cost Center 2")
|
||||
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
cost_center=cost_center1,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
rate=400,
|
||||
debit_to="Debtors - TPC"
|
||||
)
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
cost_center=cost_center2,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
rate=200,
|
||||
debit_to="Debtors - TPC"
|
||||
)
|
||||
|
||||
pcv = frappe.get_doc({
|
||||
"transaction_date": today(),
|
||||
"posting_date": today(),
|
||||
"fiscal_year": get_fiscal_year(today())[0],
|
||||
"company": "Test PCV Company",
|
||||
"cost_center_wise_pnl": 1,
|
||||
"closing_account_head": surplus_account,
|
||||
"remarks": "Test",
|
||||
"doctype": "Period Closing Voucher"
|
||||
})
|
||||
pcv.insert()
|
||||
pcv.submit()
|
||||
|
||||
expected_gle = (
|
||||
('Sales - TPC', 200.0, 0.0, cost_center2),
|
||||
(surplus_account, 0.0, 200.0, cost_center2),
|
||||
('Sales - TPC', 400.0, 0.0, cost_center1),
|
||||
(surplus_account, 0.0, 400.0, cost_center1)
|
||||
)
|
||||
|
||||
pcv_gle = frappe.db.sql("""
|
||||
select account, debit, credit, cost_center from `tabGL Entry` where voucher_no=%s
|
||||
""", (pcv.name))
|
||||
|
||||
self.assertTrue(pcv_gle, expected_gle)
|
||||
|
||||
def make_period_closing_voucher(self):
|
||||
pcv = frappe.get_doc({
|
||||
"doctype": "Period Closing Voucher",
|
||||
@ -80,6 +133,38 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
return pcv
|
||||
|
||||
def create_company():
|
||||
company = frappe.get_doc({
|
||||
'doctype': 'Company',
|
||||
'company_name': "Test PCV Company",
|
||||
'country': 'United States',
|
||||
'default_currency': 'USD'
|
||||
})
|
||||
company.insert(ignore_if_duplicate = True)
|
||||
return company.name
|
||||
|
||||
def create_account():
|
||||
account = frappe.get_doc({
|
||||
"account_name": "Reserve and Surplus",
|
||||
"is_group": 0,
|
||||
"company": "Test PCV Company",
|
||||
"root_type": "Liability",
|
||||
"report_type": "Balance Sheet",
|
||||
"account_currency": "USD",
|
||||
"parent_account": "Current Liabilities - TPC",
|
||||
"doctype": "Account"
|
||||
}).insert(ignore_if_duplicate = True)
|
||||
return account.name
|
||||
|
||||
def create_cost_center(cc_name):
|
||||
costcenter = frappe.get_doc({
|
||||
"company": "Test PCV Company",
|
||||
"cost_center_name": cc_name,
|
||||
"doctype": "Cost Center",
|
||||
"parent_cost_center": "Test PCV Company - TPC"
|
||||
})
|
||||
costcenter.insert(ignore_if_duplicate = True)
|
||||
return costcenter.name
|
||||
|
||||
test_dependencies = ["Customer", "Cost Center"]
|
||||
test_records = frappe.get_test_records("Period Closing Voucher")
|
||||
|
@ -107,7 +107,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
frm.set_value("taxes", []);
|
||||
|
||||
for (let row of frm.doc.payment_reconciliation) {
|
||||
row.expected_amount = 0;
|
||||
row.expected_amount = row.opening_amount;
|
||||
}
|
||||
|
||||
for (let row of frm.doc.pos_transactions) {
|
||||
@ -154,6 +154,9 @@ function add_to_pos_transaction(d, frm) {
|
||||
function refresh_payments(d, frm) {
|
||||
d.payments.forEach(p => {
|
||||
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
|
||||
if (p.account == d.account_for_change_amount) {
|
||||
p.amount -= flt(d.change_amount);
|
||||
}
|
||||
if (payment) {
|
||||
payment.expected_amount += flt(p.amount);
|
||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||
|
@ -140,6 +140,7 @@ class POSInvoice(SalesInvoice):
|
||||
return
|
||||
|
||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
||||
|
||||
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||
if flt(available_stock) <= 0:
|
||||
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
|
||||
@ -213,8 +214,9 @@ class POSInvoice(SalesInvoice):
|
||||
for d in self.get("items"):
|
||||
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
|
||||
if not is_stock_item:
|
||||
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ")
|
||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||
if not frappe.db.exists('Product Bundle', d.item_code):
|
||||
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
|
||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||
|
||||
def validate_mode_of_payment(self):
|
||||
if len(self.payments) == 0:
|
||||
@ -455,15 +457,36 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
||||
bin_qty = get_bin_qty(item_code, warehouse)
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
return bin_qty - pos_sales_qty
|
||||
else:
|
||||
if frappe.db.exists('Product Bundle', item_code):
|
||||
return get_bundle_availability(item_code, warehouse)
|
||||
|
||||
def get_bundle_availability(bundle_item_code, warehouse):
|
||||
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
|
||||
|
||||
bundle_bin_qty = 1000000
|
||||
for item in product_bundle.items:
|
||||
item_bin_qty = get_bin_qty(item.item_code, warehouse)
|
||||
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles:
|
||||
bundle_bin_qty = max_available_bundles
|
||||
|
||||
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
|
||||
return bundle_bin_qty - pos_sales_qty
|
||||
|
||||
def get_bin_qty(item_code, warehouse):
|
||||
bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
|
||||
where item_code = %s and warehouse = %s
|
||||
limit 1""", (item_code, warehouse), as_dict=1)
|
||||
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
|
||||
bin_qty = bin_qty[0].actual_qty or 0 if bin_qty else 0
|
||||
|
||||
return bin_qty - pos_sales_qty
|
||||
return bin_qty[0].actual_qty or 0 if bin_qty else 0
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
|
||||
@ -522,4 +545,4 @@ def add_return_modes(doc, pos_profile):
|
||||
mode_of_payment = pos_payment_method.mode_of_payment
|
||||
if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]:
|
||||
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
|
||||
append_payment(payment_mode[0])
|
||||
append_payment(payment_mode[0])
|
||||
|
@ -20,9 +20,9 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money
|
||||
class MultiplePricingRuleConflict(frappe.ValidationError): pass
|
||||
|
||||
apply_on_table = {
|
||||
'Item Code': 'items',
|
||||
'Item Group': 'item_groups',
|
||||
'Brand': 'brands'
|
||||
'Item Code': 'items',
|
||||
'Item Group': 'item_groups',
|
||||
'Brand': 'brands'
|
||||
}
|
||||
|
||||
def get_pricing_rules(args, doc=None):
|
||||
@ -183,7 +183,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
condition = "ifnull({table}.{field}, '') in ({parent_groups})".format(
|
||||
table=table,
|
||||
field=field,
|
||||
parent_groups=", ".join([frappe.db.escape(d) for d in parent_groups])
|
||||
parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups)
|
||||
)
|
||||
|
||||
frappe.flags.tree_conditions[key] = condition
|
||||
@ -264,7 +264,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
|
||||
# find pricing rule with highest priority
|
||||
if pricing_rules:
|
||||
max_priority = max([cint(p.priority) for p in pricing_rules])
|
||||
max_priority = max(cint(p.priority) for p in pricing_rules)
|
||||
if max_priority:
|
||||
pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules))
|
||||
|
||||
@ -272,14 +272,14 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
pricing_rules = list(pricing_rules)
|
||||
|
||||
if len(pricing_rules) > 1:
|
||||
rate_or_discount = list(set([d.rate_or_discount for d in pricing_rules]))
|
||||
rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules))
|
||||
if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage":
|
||||
pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \
|
||||
or pricing_rules
|
||||
|
||||
if len(pricing_rules) > 1 and not args.for_shopping_cart:
|
||||
frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}")
|
||||
.format("\n".join([d.name for d in pricing_rules])), MultiplePricingRuleConflict)
|
||||
.format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict)
|
||||
elif pricing_rules:
|
||||
return pricing_rules[0]
|
||||
|
||||
@ -541,7 +541,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
|
||||
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
|
||||
if pricing_rule_args:
|
||||
items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item])
|
||||
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)
|
||||
|
||||
for args in pricing_rule_args:
|
||||
if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:
|
||||
@ -589,4 +589,4 @@ def update_coupon_code_count(coupon_name,transaction_type):
|
||||
elif transaction_type=='cancelled':
|
||||
if coupon.used>0:
|
||||
coupon.used=coupon.used-1
|
||||
coupon.save(ignore_permissions=True)
|
||||
coupon.save(ignore_permissions=True)
|
||||
|
@ -19,7 +19,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
||||
frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'});
|
||||
}
|
||||
else{
|
||||
frappe.msgprint('No Records for these settings.')
|
||||
frappe.msgprint(__('No Records for these settings.'))
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -33,7 +33,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
||||
type: 'GET',
|
||||
success: function(result) {
|
||||
if(jQuery.isEmptyObject(result)){
|
||||
frappe.msgprint('No Records for these settings.');
|
||||
frappe.msgprint(__('No Records for these settings.'));
|
||||
}
|
||||
else{
|
||||
window.location = url;
|
||||
@ -92,7 +92,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
||||
frm.refresh_field('customers');
|
||||
}
|
||||
else{
|
||||
frappe.throw('No Customers found with selected options.');
|
||||
frappe.throw(__('No Customers found with selected options.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -129,4 +129,4 @@ frappe.ui.form.on('Process Statement Of Accounts Customer', {
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
});
|
||||
}
|
||||
|
||||
company() {
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
}
|
||||
|
||||
onload() {
|
||||
super.onload();
|
||||
|
||||
@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
frm: frm,
|
||||
freeze_message: __("Creating Purchase Receipt ...")
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
})
|
||||
|
@ -592,11 +592,12 @@
|
||||
"label": "Raw Materials Supplied"
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"fieldname": "supplied_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Supplied Items",
|
||||
"options": "Purchase Receipt Item Supplied",
|
||||
"read_only": 1
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Receipt Item Supplied"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_26",
|
||||
@ -837,6 +838,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment (Company Currency)",
|
||||
@ -883,6 +885,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment",
|
||||
@ -1380,7 +1383,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-30 22:45:58.334107",
|
||||
"modified": "2021-06-15 18:20:56.806195",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
@ -68,9 +68,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
super(PurchaseInvoice, self).validate()
|
||||
|
||||
# apply tax withholding only if checked and applicable
|
||||
self.set_tax_withholding()
|
||||
|
||||
if not self.is_return:
|
||||
self.po_required()
|
||||
self.pr_required()
|
||||
@ -251,11 +248,9 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if self.update_stock and (not item.from_warehouse):
|
||||
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
|
||||
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
|
||||
msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
|
||||
msg += _("or it is not the default inventory account")
|
||||
msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format(
|
||||
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse))
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = warehouse_account[item.warehouse]["account"]
|
||||
else:
|
||||
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
|
||||
@ -266,8 +261,8 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if negative_expense_booked_in_pr:
|
||||
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
|
||||
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
|
||||
msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
|
||||
msg = _("Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}").format(
|
||||
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
@ -275,8 +270,9 @@ class PurchaseInvoice(BuyingController):
|
||||
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
|
||||
# This is done in cases when Purchase Invoice is created before Purchase Receipt
|
||||
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
|
||||
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
|
||||
msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
|
||||
msg = _("Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.").format(
|
||||
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))
|
||||
msg += "<br>"
|
||||
msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
@ -308,8 +304,8 @@ class PurchaseInvoice(BuyingController):
|
||||
if not d.purchase_order:
|
||||
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
|
||||
msg += "<br><br>"
|
||||
msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
|
||||
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
|
||||
msg += _("To submit the invoice without purchase order please set {0} as {1} in {2}").format(
|
||||
frappe.bold(_('Purchase Order Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
|
||||
def pr_required(self):
|
||||
@ -323,8 +319,8 @@ class PurchaseInvoice(BuyingController):
|
||||
if not d.purchase_receipt and d.item_code in stock_items:
|
||||
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
|
||||
msg += "<br><br>"
|
||||
msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
|
||||
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
|
||||
msg += _("To submit the invoice without purchase receipt please set {0} as {1} in {2}").format(
|
||||
frappe.bold(_('Purchase Receipt Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
|
||||
throw(msg, title=_("Mandatory Purchase Receipt"))
|
||||
|
||||
def validate_write_off_account(self):
|
||||
@ -404,6 +400,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.set_consumed_qty_in_po()
|
||||
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
|
||||
@ -456,6 +453,8 @@ class PurchaseInvoice(BuyingController):
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.allocate_advance_taxes(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
|
||||
gl_entries = merge_similar_entries(gl_entries)
|
||||
@ -1000,6 +999,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
@ -1090,6 +1090,7 @@ class PurchaseInvoice(BuyingController):
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_details.get("account_head"):
|
||||
d.update(tax_withholding_details)
|
||||
|
||||
accounts.append(d.account_head)
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
|
@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_tra
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
|
||||
test_ignore = ["Serial No"]
|
||||
@ -620,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
self.assertEqual(actual_qty_0, get_qty_after_transaction())
|
||||
|
||||
def test_subcontracting_via_purchase_invoice(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
update_backflush_based_on('BOM')
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
|
||||
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
|
||||
qty=100, basic_rate=100)
|
||||
@ -631,7 +634,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(pi.get("supplied_items")), 2)
|
||||
|
||||
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
|
||||
rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
|
||||
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
|
||||
|
||||
def test_rejected_serial_no(self):
|
||||
@ -950,6 +953,102 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
|
||||
# create a new supplier to test
|
||||
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
|
||||
tax_withholding_category = 'TDS - 194 - Dividends - Individual')
|
||||
|
||||
# Update tax withholding category with current fiscal year and rate details
|
||||
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
|
||||
|
||||
# Create Purchase Order with TDS applied
|
||||
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000)
|
||||
po.apply_tds = 1
|
||||
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
# Update Unrealized Profit / Loss Account which is used as default advance tax account
|
||||
frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC')
|
||||
|
||||
# Create Payment Entry Against the order
|
||||
payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
|
||||
payment_entry.paid_from = 'Cash - _TC'
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
# Check GLE for Payment Entry
|
||||
expected_gle = [
|
||||
['_Test Account Excise Duty - _TC', 3000, 0],
|
||||
['Cash - _TC', 0, 27000],
|
||||
['Creditors - _TC', 27000, 0],
|
||||
['TDS Payable - _TC', 0, 3000],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Payment Entry' and voucher_no=%s
|
||||
order by account asc""", (payment_entry.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.debit)
|
||||
self.assertEqual(expected_gle[i][2], gle.credit)
|
||||
|
||||
# Create Purchase Invoice against Purchase Order
|
||||
purchase_invoice = get_mapped_purchase_invoice(po.name)
|
||||
purchase_invoice.allocate_advances_automatically = 1
|
||||
purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
|
||||
purchase_invoice.save()
|
||||
purchase_invoice.submit()
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
# Zero net effect on final TDS Payable on invoice
|
||||
expected_gle = [
|
||||
['_Test Account Cost for Goods Sold - _TC', 30000, 0],
|
||||
['_Test Account Excise Duty - _TC', 0, 3000],
|
||||
['Creditors - _TC', 0, 27000],
|
||||
['TDS Payable - _TC', 3000, 3000]
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no=%s
|
||||
order by account asc""", (purchase_invoice.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.debit)
|
||||
self.assertEqual(expected_gle[i][2], gle.credit)
|
||||
|
||||
def update_tax_witholding_category(company, account, date):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
fiscal_year = get_fiscal_year(date=date, company=company)
|
||||
|
||||
if not frappe.db.get_value('Tax Withholding Rate',
|
||||
{'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}):
|
||||
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
|
||||
tds_category.append('rates', {
|
||||
'fiscal_year': fiscal_year[0],
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 2500,
|
||||
'cumulative_threshold': 0
|
||||
})
|
||||
tds_category.save()
|
||||
|
||||
if not frappe.db.get_value('Tax Withholding Account',
|
||||
{'parent': 'TDS - 194 - Dividends - Individual', 'account': account}):
|
||||
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
|
||||
tds_category.append('accounts', {
|
||||
'company': company,
|
||||
'account': account
|
||||
})
|
||||
tds_category.save()
|
||||
|
||||
def unlink_payment_on_cancel_of_invoice(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
|
@ -272,7 +272,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate ",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
@ -854,7 +854,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-30 09:02:39.256602",
|
||||
"modified": "2021-06-16 19:33:51.099386",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -12,6 +12,7 @@
|
||||
"charge_type",
|
||||
"row_id",
|
||||
"included_in_print_rate",
|
||||
"included_in_paid_amount",
|
||||
"col_break1",
|
||||
"account_head",
|
||||
"description",
|
||||
@ -21,6 +22,7 @@
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"section_break_9",
|
||||
"currency",
|
||||
"tax_amount",
|
||||
"tax_amount_after_discount_amount",
|
||||
"total",
|
||||
@ -205,12 +207,28 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "account_head.account_currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||
"description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",
|
||||
"fieldname": "included_in_paid_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Considered In Paid Amount"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-14 01:43:50.750455",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
|
@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
var me = this;
|
||||
super.onload();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
this.frm.set_df_property("debit_to", "print_hide", 0);
|
||||
|
@ -531,7 +531,7 @@ class SalesInvoice(SellingController):
|
||||
# set pos values in items
|
||||
for item in self.get("items"):
|
||||
if item.get('item_code'):
|
||||
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
|
||||
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True)
|
||||
for fname, val in iteritems(profile_details):
|
||||
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||
item.set(fname, val)
|
||||
@ -842,6 +842,8 @@ class SalesInvoice(SellingController):
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.allocate_advance_taxes(gl_entries)
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
@ -849,7 +851,6 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.make_loyalty_point_redemption_gle(gl_entries)
|
||||
self.make_pos_gl_entries(gl_entries)
|
||||
self.make_gle_for_change_amount(gl_entries)
|
||||
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
@ -983,7 +984,13 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def make_pos_gl_entries(self, gl_entries):
|
||||
if cint(self.is_pos):
|
||||
|
||||
skip_change_gl_entries = not cint(frappe.db.get_single_value('Accounts Settings', 'post_change_gl_entries'))
|
||||
|
||||
for payment_mode in self.payments:
|
||||
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
||||
payment_mode.base_amount -= flt(self.change_amount)
|
||||
|
||||
if payment_mode.amount:
|
||||
# POS, make payment entries
|
||||
gl_entries.append(
|
||||
@ -1015,8 +1022,11 @@ class SalesInvoice(SellingController):
|
||||
}, payment_mode_account_currency, item=self)
|
||||
)
|
||||
|
||||
if not skip_change_gl_entries:
|
||||
self.make_gle_for_change_amount(gl_entries)
|
||||
|
||||
def make_gle_for_change_amount(self, gl_entries):
|
||||
if cint(self.is_pos) and self.change_amount:
|
||||
if self.change_amount:
|
||||
if self.account_for_change_amount:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
|
@ -713,7 +713,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.submit()
|
||||
self.assertEqual(si.paid_amount, 100.0)
|
||||
|
||||
self.pos_gl_entry(si, pos, 50)
|
||||
self.validate_pos_gl_entry(si, pos, 50)
|
||||
|
||||
def test_pos_returns_with_repayment(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
@ -749,7 +749,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
|
||||
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
|
||||
|
||||
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
|
||||
make_purchase_receipt(company= "_Test Company with perpetual inventory",
|
||||
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
|
||||
|
||||
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
|
||||
@ -770,7 +770,45 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(pos.grand_total, 100.0)
|
||||
self.assertEqual(pos.write_off_amount, -5)
|
||||
|
||||
def pos_gl_entry(self, si, pos, cash_amount):
|
||||
def test_pos_with_no_gl_entry_for_change_amount(self):
|
||||
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)
|
||||
|
||||
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
|
||||
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
|
||||
|
||||
make_purchase_receipt(company= "_Test Company with perpetual inventory",
|
||||
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
|
||||
|
||||
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
|
||||
debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
|
||||
income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
|
||||
cost_center = "Main - TCP1", do_not_save=True)
|
||||
|
||||
pos.is_pos = 1
|
||||
pos.update_stock = 1
|
||||
|
||||
taxes = get_taxes_and_charges()
|
||||
pos.taxes = []
|
||||
for tax in taxes:
|
||||
pos.append("taxes", tax)
|
||||
|
||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
|
||||
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
self.assertEqual(pos.grand_total, 100.0)
|
||||
self.assertEqual(pos.change_amount, 10)
|
||||
|
||||
self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True)
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 1)
|
||||
|
||||
def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
|
||||
if validate_without_change_gle:
|
||||
cash_amount -= pos.change_amount
|
||||
|
||||
# check stock ledger entries
|
||||
sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
|
||||
where voucher_type = 'Sales Invoice' and voucher_no = %s""",
|
||||
@ -1899,69 +1937,80 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
frappe.flags.country = country
|
||||
|
||||
def test_einvoice_json(self):
|
||||
from erpnext.regional.india.e_invoice.utils import make_einvoice
|
||||
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
|
||||
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
si.items = []
|
||||
si.append("items", {
|
||||
"item_code": "_Test Item",
|
||||
"uom": "Nos",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 2000,
|
||||
"rate": 12,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
})
|
||||
si.append("items", {
|
||||
"item_code": "_Test Item 2",
|
||||
"uom": "Nos",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 420,
|
||||
"rate": 15,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
})
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.discount_amount = 100
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
|
||||
total_item_ass_value = 0
|
||||
total_item_cgst_value = 0
|
||||
total_item_sgst_value = 0
|
||||
total_item_igst_value = 0
|
||||
total_item_value = 0
|
||||
|
||||
for item in einvoice['ItemList']:
|
||||
total_item_ass_value += item['AssAmt']
|
||||
total_item_cgst_value += item['CgstAmt']
|
||||
total_item_sgst_value += item['SgstAmt']
|
||||
total_item_igst_value += item['IgstAmt']
|
||||
total_item_value += item['TotItemVal']
|
||||
|
||||
self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount'])
|
||||
self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt'])
|
||||
|
||||
value_details = einvoice['ValDtls']
|
||||
|
||||
self.assertEqual(einvoice['Version'], '1.1')
|
||||
self.assertEqual(value_details['AssVal'], total_item_ass_value)
|
||||
self.assertEqual(value_details['CgstVal'], total_item_cgst_value)
|
||||
self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
|
||||
self.assertEqual(value_details['IgstVal'], total_item_igst_value)
|
||||
|
||||
calculated_invoice_value = \
|
||||
value_details['AssVal'] + value_details['CgstVal'] \
|
||||
+ value_details['SgstVal'] + value_details['IgstVal'] \
|
||||
+ value_details['OthChrg'] - value_details['Discount']
|
||||
|
||||
self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
|
||||
|
||||
self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
|
||||
self.assertTrue(einvoice['EwbDtls'])
|
||||
validate_totals(einvoice)
|
||||
|
||||
si.apply_discount_on = 'Net Total'
|
||||
si.save()
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
[d.set('included_in_print_rate', 1) for d in si.taxes]
|
||||
si.save()
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
si.items = []
|
||||
si.append("items", {
|
||||
"item_code": "_Test Item",
|
||||
"uom": "Nos",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 2000,
|
||||
"rate": 12,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
})
|
||||
|
||||
si.append("items", {
|
||||
"item_code": "_Test Item 2",
|
||||
"uom": "Nos",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 420,
|
||||
"rate": 15,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
})
|
||||
|
||||
return si
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
item.set('taxes', [])
|
||||
item.append("taxes", {
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
"minimum_net_rate": 0,
|
||||
"maximum_net_rate": 500
|
||||
})
|
||||
|
||||
item.append("taxes", {
|
||||
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
|
||||
"minimum_net_rate": 501,
|
||||
"maximum_net_rate": 1000
|
||||
})
|
||||
|
||||
item.save()
|
||||
|
||||
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
|
||||
# Apply discount
|
||||
sales_invoice.apply_discount_on = 'Net Total'
|
||||
sales_invoice.discount_amount = 300
|
||||
sales_invoice.save()
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
|
||||
def make_test_address_for_ewaybill():
|
||||
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
|
||||
@ -2085,27 +2134,6 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
doc.assertEqual(expected_gle[i][2], gle.credit)
|
||||
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
def test_item_tax_validity(self):
|
||||
item = frappe.get_doc("Item", "_Test Item 2")
|
||||
|
||||
if item.taxes:
|
||||
item.taxes = []
|
||||
item.save()
|
||||
|
||||
item.append("taxes", {
|
||||
"item_tax_template": "_Test Item Tax Template 1 - _TC",
|
||||
"valid_from": add_days(nowdate(), 1)
|
||||
})
|
||||
|
||||
item.save()
|
||||
|
||||
sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
|
||||
sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1 - _TC"
|
||||
self.assertRaises(frappe.ValidationError, sales_invoice.save)
|
||||
|
||||
item.taxes = []
|
||||
item.save()
|
||||
|
||||
def create_sales_invoice(**args):
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
args = frappe._dict(args)
|
||||
|
@ -1,8 +1,10 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2013-04-24 11:39:32",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"charge_type",
|
||||
"row_id",
|
||||
@ -10,12 +12,14 @@
|
||||
"col_break_1",
|
||||
"description",
|
||||
"included_in_print_rate",
|
||||
"included_in_paid_amount",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
"currency",
|
||||
"tax_amount",
|
||||
"total",
|
||||
"tax_amount_after_discount_amount",
|
||||
@ -23,8 +27,7 @@
|
||||
"base_tax_amount",
|
||||
"base_total",
|
||||
"base_tax_amount_after_discount_amount",
|
||||
"item_wise_tax_detail",
|
||||
"parenttype"
|
||||
"item_wise_tax_detail"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -173,17 +176,6 @@
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "parenttype",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"in_filter": 1,
|
||||
"label": "Parenttype",
|
||||
"oldfieldname": "parenttype",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
@ -192,15 +184,34 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "account_head.account_currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||
"description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",
|
||||
"fieldname": "included_in_paid_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Considered In Paid Amount"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"modified": "2019-05-25 22:59:38.740883",
|
||||
"links": [],
|
||||
"modified": "2021-06-14 01:44:36.899147",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
}
|
@ -49,7 +49,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
if not parties:
|
||||
parties.append(party)
|
||||
|
||||
fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company)
|
||||
fiscal_year = get_fiscal_year(inv.get('posting_date') or inv.get('transaction_date'), company=inv.company)
|
||||
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
|
||||
|
||||
if not tax_details:
|
||||
@ -154,7 +154,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p
|
||||
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
|
||||
|
||||
tax_amount = 0
|
||||
posting_date = inv.posting_date
|
||||
posting_date = inv.get('posting_date') or inv.get('transaction_date')
|
||||
if party_type == 'Supplier':
|
||||
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
|
||||
if tax_deducted:
|
||||
@ -257,7 +257,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
||||
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
|
||||
if ldc and is_valid_certificate(
|
||||
ldc.valid_from, ldc.valid_upto,
|
||||
inv.posting_date, tax_deducted,
|
||||
inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
|
||||
inv.net_total, ldc.certificate_limit
|
||||
):
|
||||
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
|
||||
|
@ -112,7 +112,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
|
||||
si.submit()
|
||||
|
||||
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
|
||||
tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC')
|
||||
self.assertEqual(tcs_charged, 500)
|
||||
invoices.append(si)
|
||||
|
||||
|
@ -143,7 +143,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
validate_expense_against_budget(args)
|
||||
|
||||
def validate_cwip_accounts(gl_map):
|
||||
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
|
||||
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
|
||||
|
||||
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
|
||||
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
|
||||
@ -185,10 +185,10 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
for d in gl_map:
|
||||
if d.account == round_off_account:
|
||||
round_off_gle = d
|
||||
if d.debit_in_account_currency:
|
||||
debit_credit_diff -= flt(d.debit_in_account_currency)
|
||||
if d.debit:
|
||||
debit_credit_diff -= flt(d.debit)
|
||||
else:
|
||||
debit_credit_diff += flt(d.credit_in_account_currency)
|
||||
debit_credit_diff += flt(d.credit)
|
||||
round_off_account_exists = True
|
||||
|
||||
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
|
||||
|
@ -457,7 +457,7 @@ def validate_party_frozen_disabled(party_type, party_name):
|
||||
frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen)
|
||||
|
||||
elif party_type == "Employee":
|
||||
if frappe.db.get_value("Employee", party_name, "status") == "Left":
|
||||
if frappe.db.get_value("Employee", party_name, "status") != "Active":
|
||||
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
|
||||
|
||||
def get_timeline_data(doctype, name):
|
||||
|
@ -58,11 +58,9 @@ def get_conditions(filters):
|
||||
def get_data(filters):
|
||||
|
||||
data = []
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
|
||||
accounts = frappe.db.get_all("Account", fields=["name", "account_currency"],
|
||||
filters=conditions)
|
||||
filters=conditions, order_by='name')
|
||||
|
||||
for d in accounts:
|
||||
balance = get_balance_on(d.name, date=filters.report_date)
|
||||
|
@ -23,7 +23,7 @@ class TestAccountBalance(unittest.TestCase):
|
||||
|
||||
expected_data = [
|
||||
{
|
||||
"account": 'Sales - _TC2',
|
||||
"account": 'Direct Income - _TC2',
|
||||
"currency": 'EUR',
|
||||
"balance": -100.0,
|
||||
},
|
||||
@ -32,21 +32,21 @@ class TestAccountBalance(unittest.TestCase):
|
||||
"currency": 'EUR',
|
||||
"balance": -100.0,
|
||||
},
|
||||
{
|
||||
"account": 'Service - _TC2',
|
||||
"currency": 'EUR',
|
||||
"balance": 0.0,
|
||||
},
|
||||
{
|
||||
"account": 'Direct Income - _TC2',
|
||||
"currency": 'EUR',
|
||||
"balance": -100.0,
|
||||
},
|
||||
{
|
||||
"account": 'Indirect Income - _TC2',
|
||||
"currency": 'EUR',
|
||||
"balance": 0.0,
|
||||
},
|
||||
{
|
||||
"account": 'Sales - _TC2',
|
||||
"currency": 'EUR',
|
||||
"balance": -100.0,
|
||||
},
|
||||
{
|
||||
"account": 'Service - _TC2',
|
||||
"currency": 'EUR',
|
||||
"balance": 0.0,
|
||||
}
|
||||
]
|
||||
|
||||
self.assertEqual(expected_data, report[1])
|
||||
|
@ -584,6 +584,7 @@ class ReceivablePayableReport(object):
|
||||
`tabGL Entry`
|
||||
where
|
||||
docstatus < 2
|
||||
and is_cancelled = 0
|
||||
and party_type=%s
|
||||
and (party is not null and party != '')
|
||||
{1} {2} {3}"""
|
||||
|
@ -32,7 +32,7 @@ def get_accounts_in_mappers(mapping_names):
|
||||
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
|
||||
where cfma.parent in (%s)
|
||||
order by cfm.is_working_capital
|
||||
''', (', '.join(['"%s"' % d for d in mapping_names])))
|
||||
''', (', '.join('"%s"' % d for d in mapping_names)))
|
||||
|
||||
|
||||
def setup_mappers(mappers):
|
||||
@ -83,8 +83,8 @@ def setup_mappers(mappers):
|
||||
|
||||
account_types_labels = sorted(
|
||||
set(
|
||||
[(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
|
||||
for d in account_types]
|
||||
(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
|
||||
for d in account_types
|
||||
),
|
||||
key=lambda x: x[1]
|
||||
)
|
||||
@ -375,7 +375,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
|
||||
total = 0
|
||||
for period in period_list:
|
||||
start_date = get_start_date(period, accumulated_values, company)
|
||||
accounts = ', '.join(['"%s"' % d for d in account_names])
|
||||
accounts = ', '.join('"%s"' % d for d in account_names)
|
||||
|
||||
if opening_balances:
|
||||
date_info = dict(date=start_date)
|
||||
|
@ -145,7 +145,7 @@ class PartyLedgerSummaryReport(object):
|
||||
out = []
|
||||
for party, row in iteritems(self.party_data):
|
||||
if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount:
|
||||
total_party_adjustment = sum([amount for amount in itervalues(self.party_adjustment_details.get(party, {}))])
|
||||
total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {})))
|
||||
row.paid_amount -= total_party_adjustment
|
||||
|
||||
adjustments = self.party_adjustment_details.get(party, {})
|
||||
|
@ -369,7 +369,7 @@ def set_gl_entries_by_account(
|
||||
|
||||
if accounts:
|
||||
additional_conditions += " and account in ({})"\
|
||||
.format(", ".join([frappe.db.escape(d) for d in accounts]))
|
||||
.format(", ".join(frappe.db.escape(d) for d in accounts))
|
||||
|
||||
gl_filters = {
|
||||
"company": company,
|
||||
|
@ -334,7 +334,7 @@ def get_aii_accounts():
|
||||
|
||||
def get_purchase_receipts_against_purchase_order(item_list):
|
||||
po_pr_map = frappe._dict()
|
||||
po_item_rows = list(set([d.po_detail for d in item_list]))
|
||||
po_item_rows = list(set(d.po_detail for d in item_list))
|
||||
|
||||
if po_item_rows:
|
||||
purchase_receipts = frappe.db.sql("""
|
||||
|
@ -23,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
mode_of_payments = get_mode_of_payments(set([d.parent for d in item_list]))
|
||||
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
||||
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
||||
|
||||
data = []
|
||||
|
@ -77,14 +77,14 @@ def get_pos_entries(filters, group_by_field):
|
||||
), filters, as_dict=1)
|
||||
|
||||
def concat_mode_of_payments(pos_entries):
|
||||
mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries]))
|
||||
mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries))
|
||||
for entry in pos_entries:
|
||||
if mode_of_payments.get(entry.pos_invoice):
|
||||
entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, []))
|
||||
|
||||
def add_subtotal_row(data, group_invoices, group_by_field, group_by_value):
|
||||
grand_total = sum([d.grand_total for d in group_invoices])
|
||||
paid_amount = sum([d.paid_amount for d in group_invoices])
|
||||
grand_total = sum(d.grand_total for d in group_invoices)
|
||||
paid_amount = sum(d.paid_amount for d in group_invoices)
|
||||
data.append({
|
||||
group_by_field: group_by_value,
|
||||
"grand_total": grand_total,
|
||||
|
@ -168,21 +168,24 @@ def get_columns(filters):
|
||||
"label": _("Income"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 305
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname": "expense",
|
||||
"label": _("Expense"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 305
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname": "gross_profit_loss",
|
||||
"label": _("Gross Profit / Loss"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 307
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -26,7 +26,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
|
||||
invoice_expense_map, expense_accounts)
|
||||
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
|
||||
suppliers = list(set([d.supplier for d in invoice_list]))
|
||||
suppliers = list(set(d.supplier for d in invoice_list))
|
||||
supplier_details = get_supplier_details(suppliers)
|
||||
|
||||
company_currency = frappe.get_cached_value('Company', filters.company, "default_currency")
|
||||
@ -120,13 +120,13 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
and docstatus = 1 and (account_head is not null and account_head != '')
|
||||
and category in ('Total', 'Valuation and Total')
|
||||
and parent in (%s) order by account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
|
||||
unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts]
|
||||
@ -208,7 +208,7 @@ def get_invoice_expense_map(invoice_list):
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
group by parent, expense_account
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_expense_map = {}
|
||||
for d in expense_details:
|
||||
@ -221,7 +221,7 @@ def get_internal_invoice_map(invoice_list):
|
||||
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
|
||||
and is_internal_supplier = 1 and company = represents_company""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
internal_invoice_map = {}
|
||||
for d in unrealized_amount_details:
|
||||
@ -238,7 +238,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
|
||||
where parent in (%s) and category in ('Total', 'Valuation and Total')
|
||||
and base_tax_amount_after_discount_amount != 0
|
||||
group by parent, account_head, add_deduct_tax
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_tax_map = {}
|
||||
for d in tax_details:
|
||||
@ -258,7 +258,7 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_po_pr_map = {}
|
||||
for d in pi_items:
|
||||
|
@ -158,7 +158,7 @@ def get_sales_invoice_data(filters):
|
||||
def get_mode_of_payments(filters):
|
||||
mode_of_payments = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
|
||||
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
|
||||
from `tabSales Invoice` a, `tabSales Invoice Payment` b
|
||||
@ -197,7 +197,7 @@ def get_invoices(filters):
|
||||
def get_mode_of_payment_details(filters):
|
||||
mode_of_payment_details = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
|
||||
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
|
||||
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
|
||||
|
@ -248,19 +248,19 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
income_accounts = frappe.db.sql_list("""select distinct income_account
|
||||
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
|
||||
order by income_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
tax_accounts = frappe.db.sql_list("""select distinct account_head
|
||||
from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice'
|
||||
and docstatus = 1 and base_tax_amount_after_discount_amount != 0
|
||||
and parent in (%s) order by account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabSales Invoice` where docstatus = 1 and name in (%s)
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
for account in income_accounts:
|
||||
income_columns.append({
|
||||
@ -406,7 +406,7 @@ def get_invoices(filters, additional_query_columns):
|
||||
def get_invoice_income_map(invoice_list):
|
||||
income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount
|
||||
from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_income_map = {}
|
||||
for d in income_details:
|
||||
@ -419,7 +419,7 @@ def get_internal_invoice_map(invoice_list):
|
||||
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabSales Invoice` where name in (%s)
|
||||
and is_internal_customer = 1 and company = represents_company""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
internal_invoice_map = {}
|
||||
for d in unrealized_amount_details:
|
||||
@ -432,7 +432,7 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
|
||||
tax_details = frappe.db.sql("""select parent, account_head,
|
||||
sum(base_tax_amount_after_discount_amount) as tax_amount
|
||||
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_tax_map = {}
|
||||
for d in tax_details:
|
||||
@ -451,7 +451,7 @@ def get_invoice_so_dn_map(invoice_list):
|
||||
si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_so_dn_map = {}
|
||||
for d in si_items:
|
||||
@ -475,7 +475,7 @@ def get_invoice_cc_wh_map(invoice_list):
|
||||
si_items = frappe.db.sql("""select parent, cost_center, warehouse
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_cc_wh_map = {}
|
||||
for d in si_items:
|
||||
|
@ -78,7 +78,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
|
||||
and company=%s and posting_date between %s and %s
|
||||
""", (supplier, company, from_date, to_date), as_dict=1)
|
||||
|
||||
supplier_credit_amount = flt(sum([d.credit for d in entries]))
|
||||
supplier_credit_amount = flt(sum(d.credit for d in entries))
|
||||
|
||||
vouchers = [d.voucher_no for d in entries]
|
||||
vouchers += get_advance_vouchers([supplier], company=company,
|
||||
@ -91,7 +91,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
|
||||
from `tabGL Entry`
|
||||
where account=%s and posting_date between %s and %s
|
||||
and company=%s and credit > 0 and voucher_no in ({0})
|
||||
""".format(', '.join(["'%s'" % d for d in vouchers])),
|
||||
""".format(', '.join("'%s'" % d for d in vouchers)),
|
||||
(account, from_date, to_date, company))[0][0])
|
||||
|
||||
date_range_filter = [fiscal_year, from_date, to_date]
|
||||
|
@ -54,6 +54,32 @@ frappe.query_reports["TDS Payable Monthly"] = {
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"purchase_order",
|
||||
"label": __("Purchase Order"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Order",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["in", frappe.query_report.invoices]
|
||||
}
|
||||
}
|
||||
},
|
||||
on_change: function() {
|
||||
let supplier = frappe.query_report.get_filter_value('supplier');
|
||||
if(!supplier) return; // return if no supplier selected
|
||||
|
||||
// filter invoices based on selected supplier
|
||||
let invoices = [];
|
||||
frappe.query_report.invoice_data.map(d => {
|
||||
if(d.supplier==supplier)
|
||||
invoices.push(d.name)
|
||||
});
|
||||
frappe.query_report.invoices = invoices;
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"label": __("From Date"),
|
||||
@ -75,15 +101,17 @@ frappe.query_reports["TDS Payable Monthly"] = {
|
||||
onload: function(report) {
|
||||
// fetch all tds applied invoices
|
||||
frappe.call({
|
||||
"method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices",
|
||||
"method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices_and_orders",
|
||||
callback: function(r) {
|
||||
let invoices = [];
|
||||
|
||||
r.message.map(d => {
|
||||
invoices.push(d.name);
|
||||
});
|
||||
|
||||
report["invoice_data"] = r.message;
|
||||
report["invoice_data"] = r.message.invoices;
|
||||
report["invoices"] = invoices;
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -11,11 +11,14 @@ def execute(filters=None):
|
||||
validate_filters(filters)
|
||||
set_filters(filters)
|
||||
|
||||
# TDS payment entries
|
||||
payment_entries = get_payment_entires(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
if not filters["invoices"]:
|
||||
if not filters.get("invoices"):
|
||||
return columns, []
|
||||
|
||||
res = get_result(filters)
|
||||
res = get_result(filters, payment_entries)
|
||||
|
||||
return columns, res
|
||||
|
||||
@ -27,8 +30,9 @@ def validate_filters(filters):
|
||||
def set_filters(filters):
|
||||
invoices = []
|
||||
|
||||
if not filters["invoices"]:
|
||||
filters["invoices"] = get_tds_invoices()
|
||||
if not filters.get("invoices"):
|
||||
filters["invoices"] = get_tds_invoices_and_orders()
|
||||
|
||||
if filters.supplier and filters.purchase_invoice:
|
||||
for d in filters["invoices"]:
|
||||
if d.name == filters.purchase_invoice and d.supplier == filters.supplier:
|
||||
@ -41,13 +45,29 @@ def set_filters(filters):
|
||||
for d in filters["invoices"]:
|
||||
if d.name == filters.purchase_invoice:
|
||||
invoices.append(d)
|
||||
elif filters.supplier and filters.purchase_order:
|
||||
for d in filters.get("invoices"):
|
||||
if d.name == filters.purchase_order and d.supplier == filters.supplier:
|
||||
invoices.append(d)
|
||||
elif filters.supplier and not filters.purchase_order:
|
||||
for d in filters.get("invoices"):
|
||||
if d.supplier == filters.supplier:
|
||||
invoices.append(d)
|
||||
elif filters.purchase_order and not filters.supplier:
|
||||
for d in filters.get("invoices"):
|
||||
if d.name == filters.purchase_order:
|
||||
invoices.append(d)
|
||||
|
||||
filters["invoices"] = invoices if invoices else filters["invoices"]
|
||||
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
|
||||
|
||||
def get_result(filters):
|
||||
supplier_map, tds_docs = get_supplier_map(filters)
|
||||
gle_map = get_gle_map(filters)
|
||||
#print(filters.get('invoices'))
|
||||
|
||||
def get_result(filters, payment_entries):
|
||||
supplier_map, tds_docs = get_supplier_map(filters, payment_entries)
|
||||
documents = [d.get('name') for d in filters.get('invoices')] + [d.get('name') for d in payment_entries]
|
||||
|
||||
gle_map = get_gle_map(filters, documents)
|
||||
|
||||
out = []
|
||||
for d in gle_map:
|
||||
@ -62,10 +82,11 @@ def get_result(filters):
|
||||
|
||||
for k in gle_map[d]:
|
||||
if k.party == supplier_map[d] and k.credit > 0:
|
||||
total_amount_credited += k.credit
|
||||
elif account_list and k.account == account and k.credit > 0:
|
||||
tds_deducted = k.credit
|
||||
total_amount_credited += k.credit
|
||||
total_amount_credited += (k.credit - k.debit)
|
||||
elif account_list and k.account == account and (k.credit - k.debit) > 0:
|
||||
tds_deducted = (k.credit - k.debit)
|
||||
total_amount_credited += (k.credit - k.debit)
|
||||
voucher_type = k.voucher_type
|
||||
|
||||
rate = [i.tax_withholding_rate for i in tds_doc.rates
|
||||
if i.fiscal_year == gle_map[d][0].fiscal_year]
|
||||
@ -73,32 +94,36 @@ def get_result(filters):
|
||||
if rate and len(rate) > 0 and tds_deducted:
|
||||
rate = rate[0]
|
||||
|
||||
if getdate(filters.from_date) <= gle_map[d][0].posting_date \
|
||||
and getdate(filters.to_date) >= gle_map[d][0].posting_date:
|
||||
row = [supplier.pan, supplier.name]
|
||||
row = [supplier.pan, supplier.name]
|
||||
|
||||
if filters.naming_series == 'Naming Series':
|
||||
row.append(supplier.supplier_name)
|
||||
if filters.naming_series == 'Naming Series':
|
||||
row.append(supplier.supplier_name)
|
||||
|
||||
row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
|
||||
tds_deducted, gle_map[d][0].posting_date, "Purchase Invoice", d])
|
||||
out.append(row)
|
||||
row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
|
||||
tds_deducted, gle_map[d][0].posting_date, voucher_type, d])
|
||||
out.append(row)
|
||||
|
||||
return out
|
||||
|
||||
def get_supplier_map(filters):
|
||||
def get_supplier_map(filters, payment_entries):
|
||||
# create a supplier_map of the form {"purchase_invoice": {supplier_name, pan, tds_name}}
|
||||
# pre-fetch all distinct applicable tds docs
|
||||
supplier_map, tds_docs = {}, {}
|
||||
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
|
||||
supplier_list = [d.supplier for d in filters["invoices"]]
|
||||
|
||||
supplier_detail = frappe.db.get_all('Supplier',
|
||||
{"name": ["in", [d.supplier for d in filters["invoices"]]]},
|
||||
{"name": ["in", supplier_list]},
|
||||
["tax_withholding_category", "name", pan+" as pan", "supplier_type", "supplier_name"])
|
||||
|
||||
for d in filters["invoices"]:
|
||||
supplier_map[d.get("name")] = [k for k in supplier_detail
|
||||
if k.name == d.get("supplier")][0]
|
||||
|
||||
for d in payment_entries:
|
||||
supplier_map[d.get("name")] = [k for k in supplier_detail
|
||||
if k.name == d.get("supplier")][0]
|
||||
|
||||
for d in supplier_detail:
|
||||
if d.get("tax_withholding_category") not in tds_docs:
|
||||
tds_docs[d.get("tax_withholding_category")] = \
|
||||
@ -106,13 +131,19 @@ def get_supplier_map(filters):
|
||||
|
||||
return supplier_map, tds_docs
|
||||
|
||||
def get_gle_map(filters):
|
||||
def get_gle_map(filters, documents):
|
||||
# create gle_map of the form
|
||||
# {"purchase_invoice": list of dict of all gle created for this invoice}
|
||||
gle_map = {}
|
||||
gle = frappe.db.get_all('GL Entry',\
|
||||
{"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]], 'is_cancelled': 0},
|
||||
["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date"])
|
||||
|
||||
gle = frappe.db.get_all('GL Entry',
|
||||
{
|
||||
"voucher_no": ["in", documents],
|
||||
'is_cancelled': 0,
|
||||
'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
|
||||
},
|
||||
["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date", "voucher_type"],
|
||||
)
|
||||
|
||||
for d in gle:
|
||||
if not d.voucher_no in gle_map:
|
||||
@ -201,8 +232,26 @@ def get_columns(filters):
|
||||
|
||||
return columns
|
||||
|
||||
def get_payment_entires(filters):
|
||||
filter_dict = {
|
||||
'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
|
||||
'party_type': 'Supplier',
|
||||
'apply_tax_withholding_amount': 1
|
||||
}
|
||||
|
||||
if filters.get('purchase_invoice') or filters.get('purchase_order'):
|
||||
parent = frappe.db.get_all('Payment Entry Reference',
|
||||
{'reference_name': ('in', [d.get('name') for d in filters.get('invoices')])}, ['parent'])
|
||||
|
||||
filter_dict.update({'name': ('in', [d.get('parent') for d in parent])})
|
||||
|
||||
payment_entries = frappe.get_all('Payment Entry', fields=['name', 'party_name as supplier'],
|
||||
filters=filter_dict)
|
||||
|
||||
return payment_entries
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tds_invoices():
|
||||
def get_tds_invoices_and_orders():
|
||||
# fetch tds applicable supplier and fetch invoices for these suppliers
|
||||
suppliers = [d.name for d in frappe.db.get_list("Supplier",
|
||||
{"tax_withholding_category": ["!=", ""]}, ["name"])]
|
||||
@ -210,7 +259,12 @@ def get_tds_invoices():
|
||||
invoices = frappe.db.get_list("Purchase Invoice",
|
||||
{"supplier": ["in", suppliers]}, ["name", "supplier"])
|
||||
|
||||
orders = frappe.db.get_list("Purchase Order",
|
||||
{"supplier": ["in", suppliers]}, ["name", "supplier"])
|
||||
|
||||
invoices = invoices + orders
|
||||
invoices = [d for d in invoices if d.supplier]
|
||||
|
||||
frappe.cache().hset("invoices", frappe.session.user, invoices)
|
||||
|
||||
return invoices
|
||||
|
@ -139,6 +139,6 @@ def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=N
|
||||
gross_profit_data = GrossProfitGenerator(filters)
|
||||
result = gross_profit_data.grouped_data
|
||||
if not with_item_data:
|
||||
result = sum([d.gross_profit for d in result])
|
||||
result = sum(d.gross_profit for d in result)
|
||||
|
||||
return result
|
||||
|
@ -635,7 +635,7 @@ def get_held_invoices(party_type, party):
|
||||
'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
|
||||
as_dict=1
|
||||
)
|
||||
held_invoices = set([d['name'] for d in held_invoices])
|
||||
held_invoices = set(d['name'] for d in held_invoices)
|
||||
|
||||
return held_invoices
|
||||
|
||||
|
@ -707,6 +707,7 @@
|
||||
"link_to": "GST Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -717,6 +718,7 @@
|
||||
"link_to": "GST HSN Code",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -727,6 +729,7 @@
|
||||
"link_to": "GSTR-1",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -737,6 +740,7 @@
|
||||
"link_to": "GSTR-2",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -747,6 +751,7 @@
|
||||
"link_to": "GSTR 3B Report",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -757,6 +762,7 @@
|
||||
"link_to": "GST Sales Register",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -767,6 +773,7 @@
|
||||
"link_to": "GST Purchase Register",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -777,6 +784,7 @@
|
||||
"link_to": "GST Itemised Sales Register",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -787,6 +795,7 @@
|
||||
"link_to": "GST Itemised Purchase Register",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -797,6 +806,7 @@
|
||||
"link_to": "C-Form",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -807,6 +817,7 @@
|
||||
"link_to": "Lower Deduction Certificate",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
@ -1065,7 +1076,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-05-13 13:44:56.249888",
|
||||
"modified": "2021-06-10 03:17:31.427945",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
@ -1120,4 +1131,4 @@
|
||||
"type": "Dashboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -470,7 +470,7 @@ class TestAsset(unittest.TestCase):
|
||||
})
|
||||
asset.insert()
|
||||
accumulated_depreciation_after_full_schedule = \
|
||||
max([d.accumulated_depreciation_amount for d in asset.get("schedules")])
|
||||
max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
|
||||
|
||||
asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) -
|
||||
flt(accumulated_depreciation_after_full_schedule))
|
||||
|
@ -92,7 +92,7 @@ class AssetValueAdjustment(Document):
|
||||
d.value_after_depreciation = asset_value
|
||||
|
||||
if d.depreciation_method in ("Straight Line", "Manual"):
|
||||
end_date = max([s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx])
|
||||
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
|
||||
total_days = date_diff(end_date, self.date)
|
||||
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
|
||||
from_date = self.date
|
||||
|
@ -9,13 +9,14 @@
|
||||
"supp_master_name",
|
||||
"supplier_group",
|
||||
"buying_price_list",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"column_break_3",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"maintain_same_rate",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
@ -108,6 +109,13 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
|
||||
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bill for Rejected Quantity in Purchase Invoice"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@ -115,7 +123,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-04 20:01:44.087066",
|
||||
"modified": "2021-06-23 19:40:00.120822",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
@ -45,6 +45,47 @@ frappe.ui.form.on("Purchase Order", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
},
|
||||
|
||||
apply_tds: function(frm) {
|
||||
if (!frm.doc.apply_tds) {
|
||||
frm.set_value("tax_withholding_category", '');
|
||||
} else {
|
||||
frm.set_value("tax_withholding_category", frm.supplier_tds);
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.trigger('get_materials_from_supplier');
|
||||
},
|
||||
|
||||
get_materials_from_supplier: function(frm) {
|
||||
let po_details = [];
|
||||
|
||||
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
|
||||
frm.doc.supplied_items.forEach(d => {
|
||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||
po_details.push(d.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (po_details && po_details.length) {
|
||||
frm.add_custom_button(__('Return of Components'), () => {
|
||||
frm.call({
|
||||
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
|
||||
freeze: true,
|
||||
freeze_message: __('Creating Stock Entry'),
|
||||
args: { purchase_order: frm.doc.name, po_details: po_details },
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
const doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -209,7 +250,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
}
|
||||
|
||||
has_unsupplied_items() {
|
||||
return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty)
|
||||
return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
|
||||
}
|
||||
|
||||
make_stock_entry() {
|
||||
@ -313,7 +354,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
if(me.values) {
|
||||
me.values.sub_con_rm_items.map((row,i) => {
|
||||
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
|
||||
frappe.throw(__("Item Code, warehouse, quantity are required on row {0}", [i+1]));
|
||||
let row_id = i+1;
|
||||
frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id]));
|
||||
}
|
||||
})
|
||||
me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children())
|
||||
@ -504,12 +546,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
],
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.utils.add_comment",
|
||||
args: {
|
||||
reference_doctype: me.frm.doctype,
|
||||
reference_name: me.frm.docname,
|
||||
content: __('Reason for hold: ')+data.reason_for_hold,
|
||||
content: __(reason_for_hold),
|
||||
comment_email: frappe.session.user,
|
||||
comment_by: frappe.session.user_fullname
|
||||
},
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -14,11 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
|
||||
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
|
||||
from erpnext.stock.utils import get_bin
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
from six import string_types
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
|
||||
unlink_inter_company_doc
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
|
||||
update_linked_doc, unlink_inter_company_doc)
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@ -39,11 +39,18 @@ class PurchaseOrder(BuyingController):
|
||||
'percent_join_field': 'material_request'
|
||||
}]
|
||||
|
||||
def onload(self):
|
||||
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
self.set_onload("supplier_tds", supplier_tds)
|
||||
|
||||
def validate(self):
|
||||
super(PurchaseOrder, self).validate()
|
||||
|
||||
self.set_status()
|
||||
|
||||
# apply tax withholding only if checked and applicable
|
||||
self.set_tax_withholding()
|
||||
|
||||
self.validate_supplier()
|
||||
self.validate_schedule_date()
|
||||
validate_for_items(self)
|
||||
@ -87,6 +94,33 @@ class PurchaseOrder(BuyingController):
|
||||
if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')):
|
||||
self.validate_rate_with_reference_doc([["Supplier Quotation", "supplier_quotation", "supplier_quotation_item"]])
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_details.get("account_head"):
|
||||
d.update(tax_withholding_details)
|
||||
accounts.append(d.account_head)
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append("taxes", tax_withholding_details)
|
||||
|
||||
to_remove = [d for d in self.taxes
|
||||
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
# calculate totals again after applying TDS
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def validate_supplier(self):
|
||||
prevent_po = frappe.db.get_value("Supplier", self.supplier, 'prevent_pos')
|
||||
if prevent_po:
|
||||
@ -104,7 +138,7 @@ class PurchaseOrder(BuyingController):
|
||||
|
||||
def validate_minimum_order_qty(self):
|
||||
if not self.get("items"): return
|
||||
items = list(set([d.item_code for d in self.get("items")]))
|
||||
items = list(set(d.item_code for d in self.get("items")))
|
||||
|
||||
itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty
|
||||
from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items))
|
||||
@ -291,10 +325,10 @@ class PurchaseOrder(BuyingController):
|
||||
so.notify_update()
|
||||
|
||||
def has_drop_ship_item(self):
|
||||
return any([d.delivered_by_supplier for d in self.items])
|
||||
return any(d.delivered_by_supplier for d in self.items)
|
||||
|
||||
def is_against_so(self):
|
||||
return any([d.sales_order for d in self.items if d.sales_order])
|
||||
return any(d.sales_order for d in self.items if d.sales_order)
|
||||
|
||||
def set_received_qty_for_drop_ship_items(self):
|
||||
for item in self.items:
|
||||
@ -468,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(purchase_order, rm_items):
|
||||
if isinstance(rm_items, string_types):
|
||||
rm_items_list = rm_items
|
||||
|
||||
if isinstance(rm_items, str):
|
||||
rm_items_list = json.loads(rm_items)
|
||||
else:
|
||||
elif not rm_items:
|
||||
frappe.throw(_("No Items available for transfer"))
|
||||
|
||||
if rm_items_list:
|
||||
@ -508,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
|
||||
'qty': rm_item_data["qty"],
|
||||
'from_warehouse': rm_item_data["warehouse"],
|
||||
'stock_uom': rm_item_data["stock_uom"],
|
||||
'serial_no': rm_item_data.get('serial_no'),
|
||||
'batch_no': rm_item_data.get('batch_no'),
|
||||
'main_item_code': rm_item_data["item_code"],
|
||||
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
|
||||
}
|
||||
@ -547,3 +585,58 @@ def update_status(status, name):
|
||||
def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
|
||||
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_materials_from_supplier(purchase_order, po_details):
|
||||
if isinstance(po_details, str):
|
||||
po_details = json.loads(po_details)
|
||||
|
||||
doc = frappe.get_cached_doc('Purchase Order', purchase_order)
|
||||
doc.initialized_fields()
|
||||
doc.purchase_orders = [doc.name]
|
||||
doc.get_available_materials()
|
||||
|
||||
if not doc.available_materials:
|
||||
frappe.throw(_('Materials are already received against the purchase order {0}')
|
||||
.format(purchase_order))
|
||||
|
||||
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
|
||||
|
||||
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
|
||||
ste_doc = frappe.new_doc('Stock Entry')
|
||||
ste_doc.purpose = 'Material Transfer'
|
||||
ste_doc.purchase_order = po_doc.name
|
||||
ste_doc.company = po_doc.company
|
||||
ste_doc.is_return = 1
|
||||
|
||||
for key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
|
||||
else:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details)
|
||||
|
||||
ste_doc.set_stock_entry_type()
|
||||
ste_doc.calculate_rate_and_amount()
|
||||
|
||||
return ste_doc
|
||||
|
||||
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
|
||||
item = ste_doc.append('items', row.item_details)
|
||||
|
||||
po_detail = list(set(row.po_details).intersection(po_details))
|
||||
item.update({
|
||||
'qty': qty,
|
||||
'batch_no': batch_no,
|
||||
'basic_rate': row.item_details['rate'],
|
||||
'po_detail': po_detail[0] if po_detail else '',
|
||||
's_warehouse': row.item_details['t_warehouse'],
|
||||
't_warehouse': row.item_details['s_warehouse'],
|
||||
'item_code': row.item_details['rm_item_code'],
|
||||
'subcontracted_item': row.item_details['main_item_code'],
|
||||
'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
|
||||
})
|
@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
|
||||
from erpnext.stock.doctype.batch.test_batch import make_new_batch
|
||||
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
def test_make_purchase_receipt(self):
|
||||
@ -359,7 +358,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
update_child_qty_rate('Purchase Order', trans_item, po.name)
|
||||
po.reload()
|
||||
|
||||
total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
|
||||
total_reqd_qty_after_change = sum(d.get("required_qty") for d in po.as_dict().get("supplied_items"))
|
||||
|
||||
self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
|
||||
|
||||
@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||
|
||||
def test_exploded_items_in_subcontracted(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
item_code = "_Test Subcontracted FG Item 11"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=1,
|
||||
@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
for item in rm_items:
|
||||
transferred_rm_map[item.get('rm_item_code')] = item
|
||||
|
||||
for item in pr.get('supplied_items'):
|
||||
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_backflushed_based_on_for_multiple_batches(self):
|
||||
item_code = "_Test Subcontracted FG Item 2"
|
||||
make_item('Sub Contracted Raw Material 2', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
})
|
||||
|
||||
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
|
||||
raw_materials=["Sub Contracted Raw Material 2"])
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
order_qty = 500
|
||||
po = create_purchase_order(item_code=item_code, qty=order_qty,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
|
||||
|
||||
rm_items = [
|
||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
|
||||
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
|
||||
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.submit()
|
||||
|
||||
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
|
||||
make_new_batch(batch_id=batch, item_code=item_code)
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
|
||||
# partial receipt
|
||||
pr.get('items')[0].qty = 30
|
||||
pr.get('items')[0].batch_no = "ABCD1"
|
||||
|
||||
purchase_order = po.name
|
||||
purchase_order_item = po.items[0].name
|
||||
|
||||
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
|
||||
pr.append("items", {
|
||||
"item_code": pr.get('items')[0].item_code,
|
||||
"item_name": pr.get('items')[0].item_name,
|
||||
"uom": pr.get('items')[0].uom,
|
||||
"stock_uom": pr.get('items')[0].stock_uom,
|
||||
"warehouse": pr.get('items')[0].warehouse,
|
||||
"conversion_factor": pr.get('items')[0].conversion_factor,
|
||||
"cost_center": pr.get('items')[0].cost_center,
|
||||
"rate": pr.get('items')[0].rate,
|
||||
"qty": qty,
|
||||
"batch_no": batch_no,
|
||||
"purchase_order": purchase_order,
|
||||
"purchase_order_item": purchase_order_item
|
||||
})
|
||||
|
||||
pr.submit()
|
||||
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr1.get('items')[0].qty = 300
|
||||
pr1.get('items')[0].batch_no = "ABCD1"
|
||||
pr1.save()
|
||||
|
||||
pr_key = ("Sub Contracted Raw Material 2", po.name)
|
||||
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
|
||||
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_supplied_qty_against_subcontracted_po(self):
|
||||
@ -1111,28 +1037,35 @@ def create_purchase_order(**args):
|
||||
|
||||
po.schedule_date = add_days(nowdate(), 1)
|
||||
po.company = args.company or "_Test Company"
|
||||
po.supplier = args.customer or "_Test Supplier"
|
||||
po.supplier = args.supplier or "_Test Supplier"
|
||||
po.is_subcontracted = args.is_subcontracted or "No"
|
||||
po.currency = args.currency or frappe.get_cached_value('Company', po.company, "default_currency")
|
||||
po.conversion_factor = args.conversion_factor or 1
|
||||
po.supplier_warehouse = args.supplier_warehouse or None
|
||||
|
||||
po.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 10,
|
||||
"rate": args.rate or 500,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get('include_exploded_items', 1),
|
||||
"against_blanket_order": args.against_blanket_order
|
||||
})
|
||||
if args.rm_items:
|
||||
for row in args.rm_items:
|
||||
po.append("items", row)
|
||||
else:
|
||||
po.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 10,
|
||||
"rate": args.rate or 500,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get('include_exploded_items', 1),
|
||||
"against_blanket_order": args.against_blanket_order
|
||||
})
|
||||
|
||||
po.set_missing_values()
|
||||
if not args.do_not_save:
|
||||
po.insert()
|
||||
if not args.do_not_submit:
|
||||
if po.is_subcontracted == "Yes":
|
||||
supp_items = po.get("supplied_items")
|
||||
for d in supp_items:
|
||||
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
|
||||
if not d.reserve_warehouse:
|
||||
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
|
||||
po.submit()
|
||||
|
||||
return po
|
||||
|
@ -6,21 +6,25 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"bom_detail_no",
|
||||
"rm_item_code",
|
||||
"column_break_3",
|
||||
"stock_uom",
|
||||
"reserve_warehouse",
|
||||
"conversion_factor",
|
||||
"column_break_6",
|
||||
"rm_item_code",
|
||||
"bom_detail_no",
|
||||
"reference_name",
|
||||
"reserve_warehouse",
|
||||
"section_break2",
|
||||
"rate",
|
||||
"col_break2",
|
||||
"amount",
|
||||
"section_break1",
|
||||
"required_qty",
|
||||
"supplied_qty",
|
||||
"col_break1",
|
||||
"supplied_qty"
|
||||
"consumed_qty",
|
||||
"returned_qty",
|
||||
"total_supplied_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -125,6 +129,8 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Supplied Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -142,13 +148,42 @@
|
||||
{
|
||||
"fieldname": "col_break2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Returned Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_supplied_qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Total Supplied Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-09 15:17:58.128242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item Supplied",
|
||||
|
@ -6,10 +6,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"description",
|
||||
"rm_item_code",
|
||||
"item_name",
|
||||
"bom_detail_no",
|
||||
"col_break1",
|
||||
"rm_item_code",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"conversion_factor",
|
||||
"reference_name",
|
||||
@ -25,7 +26,8 @@
|
||||
"secbreak_3",
|
||||
"batch_no",
|
||||
"col_break4",
|
||||
"serial_no"
|
||||
"serial_no",
|
||||
"purchase_order"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -52,7 +54,6 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Data",
|
||||
@ -81,18 +82,20 @@
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Required Qty",
|
||||
"label": "Available Qty For Consumption",
|
||||
"oldfieldname": "required_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty to Be Consumed",
|
||||
"oldfieldname": "consumed_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -183,12 +186,28 @@
|
||||
{
|
||||
"fieldname": "col_break4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_order",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Order",
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-19 19:33:04.431213",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Receipt Item Supplied",
|
||||
|
@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
|
||||
def create_rfq_items(sq_doc, supplier, data):
|
||||
sq_doc.append('items', {
|
||||
"item_code": data.item_code,
|
||||
"item_name": data.item_name,
|
||||
"description": data.description,
|
||||
"qty": data.qty,
|
||||
"rate": data.rate,
|
||||
"conversion_factor": data.conversion_factor if data.conversion_factor else None,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
|
||||
"warehouse": data.warehouse or '',
|
||||
args = {}
|
||||
|
||||
for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
|
||||
'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
|
||||
args[field] = data.get(field)
|
||||
|
||||
args.update({
|
||||
"request_for_quotation_item": data.name,
|
||||
"request_for_quotation": data.parent
|
||||
"request_for_quotation": data.parent,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier",
|
||||
{'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
|
||||
})
|
||||
|
||||
sq_doc.append('items', args)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pdf(doctype, name, supplier):
|
||||
doc = get_rfq_doc(doctype, name, supplier)
|
||||
@ -391,7 +393,7 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
|
||||
def get_supplier_tag():
|
||||
if not frappe.cache().hget("Supplier", "Tags"):
|
||||
filters = {"document_type": "Supplier"}
|
||||
tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag]))
|
||||
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
|
||||
frappe.cache().hset("Supplier", "Tags", tags)
|
||||
|
||||
return frappe.cache().hget("Supplier", "Tags")
|
||||
|
@ -576,6 +576,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment (Company Currency",
|
||||
@ -620,6 +621,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment",
|
||||
@ -802,7 +804,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-03 15:18:29.073368",
|
||||
"modified": "2021-04-19 00:58:20.995491",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
|
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Subcontract Order Summary"] = {
|
||||
"filters": [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("From Date"),
|
||||
fieldname:"from_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("To Date"),
|
||||
fieldname:"to_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Purchase Order"),
|
||||
fieldname: "name",
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Order",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
is_subcontracted: 'Yes',
|
||||
company: frappe.query_report.get_filter_value('company')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-05-31 14:43:32.417694",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-05-31 14:43:32.417694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Subcontract Order Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Subcontract Order Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(report_filters):
|
||||
data = []
|
||||
orders = get_subcontracted_orders(report_filters)
|
||||
|
||||
if orders:
|
||||
supplied_items = get_supplied_items(orders, report_filters)
|
||||
po_details = prepare_subcontracted_data(orders, supplied_items)
|
||||
get_subcontracted_data(po_details, data)
|
||||
|
||||
return data
|
||||
|
||||
def get_subcontracted_orders(report_filters):
|
||||
fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
|
||||
'`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
|
||||
'`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
|
||||
|
||||
filters = get_filters(report_filters)
|
||||
|
||||
return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
|
||||
|
||||
def get_filters(report_filters):
|
||||
filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
|
||||
['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
|
||||
|
||||
for field in ['name', 'company']:
|
||||
if report_filters.get(field):
|
||||
filters.append(['Purchase Order', field, '=', report_filters.get(field)])
|
||||
|
||||
return filters
|
||||
|
||||
def get_supplied_items(orders, report_filters):
|
||||
if not orders:
|
||||
return []
|
||||
|
||||
fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
|
||||
'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
|
||||
|
||||
filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
|
||||
|
||||
supplied_items = {}
|
||||
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
|
||||
new_key = (row.parent, row.reference_name, row.main_item_code)
|
||||
|
||||
supplied_items.setdefault(new_key, []).append(row)
|
||||
|
||||
return supplied_items
|
||||
|
||||
def prepare_subcontracted_data(orders, supplied_items):
|
||||
po_details = {}
|
||||
for row in orders:
|
||||
key = (row.po_id, row.name, row.item_code)
|
||||
if key not in po_details:
|
||||
po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
|
||||
|
||||
details = po_details[key]
|
||||
|
||||
if supplied_items.get(key):
|
||||
for supplied_item in supplied_items[key]:
|
||||
details['supplied_items'].append(supplied_item)
|
||||
|
||||
return po_details
|
||||
|
||||
def get_subcontracted_data(po_details, data):
|
||||
for key, details in po_details.items():
|
||||
res = details.po_item
|
||||
for index, row in enumerate(details.supplied_items):
|
||||
if index != 0:
|
||||
res = {}
|
||||
|
||||
res.update(row)
|
||||
data.append(res)
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"label": _("Purchase Order"),
|
||||
"fieldname": "po_id",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Order",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": _("Status"),
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"width": 80
|
||||
},
|
||||
{
|
||||
"label": _("Subcontracted Item"),
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label": _("Order Qty"),
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 90
|
||||
},
|
||||
{
|
||||
"label": _("Received Qty"),
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Supplied Item"),
|
||||
"fieldname": "rm_item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label": _("Required Qty"),
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Supplied Qty"),
|
||||
"fieldname": "supplied_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Consumed Qty"),
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"label": _("Returned Qty"),
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
}
|
||||
]
|
@ -9,10 +9,10 @@ def execute(filters=None):
|
||||
if filters.from_date >= filters.to_date:
|
||||
frappe.msgprint(_("To Date must be greater than From Date"))
|
||||
|
||||
data = []
|
||||
columns = get_columns()
|
||||
get_data(data , filters)
|
||||
return columns, data
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data or []
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
@ -21,13 +21,12 @@ def get_columns():
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "purchase_order",
|
||||
"options": "Purchase Order",
|
||||
"width": 150
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"label": _("Date"),
|
||||
"fieldtype": "Date",
|
||||
"fieldname": "date",
|
||||
"hidden": 1,
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
@ -41,97 +40,58 @@ def get_columns():
|
||||
"label": _("Item Code"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "rm_item_code",
|
||||
"width": 100
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": _("Required Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "r_qty",
|
||||
"width": 100
|
||||
"fieldname": "reqd_qty",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": _("Transferred Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "t_qty",
|
||||
"width": 100
|
||||
"fieldname": "transferred_qty",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"label": _("Pending Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "p_qty",
|
||||
"width": 100
|
||||
"width": 150
|
||||
}
|
||||
]
|
||||
|
||||
def get_data(data, filters):
|
||||
po = get_po(filters)
|
||||
po_transferred_qty_map = frappe._dict(get_transferred_quantity([v.name for v in po]))
|
||||
def get_data(filters):
|
||||
po_rm_item_details = get_po_items_to_supply(filters)
|
||||
|
||||
sub_items = get_purchase_order_item_supplied([v.name for v in po])
|
||||
data = []
|
||||
for row in po_rm_item_details:
|
||||
transferred_qty = row.get("transferred_qty") or 0
|
||||
if transferred_qty < row.get("reqd_qty", 0):
|
||||
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
|
||||
row.p_qty = pending_qty if pending_qty > 0 else 0
|
||||
data.append(row)
|
||||
|
||||
for order in po:
|
||||
for item in sub_items:
|
||||
if order.name == item.parent and order.name in po_transferred_qty_map and \
|
||||
item.required_qty != po_transferred_qty_map.get(order.name).get(item.rm_item_code):
|
||||
transferred_qty = po_transferred_qty_map.get(order.name).get(item.rm_item_code) \
|
||||
if po_transferred_qty_map.get(order.name).get(item.rm_item_code) else 0
|
||||
row ={
|
||||
'purchase_order': item.parent,
|
||||
'date': order.transaction_date,
|
||||
'supplier': order.supplier,
|
||||
'rm_item_code': item.rm_item_code,
|
||||
'r_qty': item.required_qty,
|
||||
't_qty':transferred_qty,
|
||||
'p_qty':item.required_qty - transferred_qty
|
||||
}
|
||||
return data
|
||||
|
||||
data.append(row)
|
||||
|
||||
return(data)
|
||||
|
||||
def get_po(filters):
|
||||
record_filters = [
|
||||
["is_subcontracted", "=", "Yes"],
|
||||
["supplier", "=", filters.supplier],
|
||||
["transaction_date", "<=", filters.to_date],
|
||||
["transaction_date", ">=", filters.from_date],
|
||||
["docstatus", "=", 1]
|
||||
]
|
||||
return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"])
|
||||
|
||||
def get_transferred_quantity(po_name):
|
||||
stock_entries = get_stock_entry(po_name)
|
||||
stock_entries_detail = get_stock_entry_detail([v.name for v in stock_entries])
|
||||
po_transferred_qty_map = {}
|
||||
|
||||
|
||||
for entry in stock_entries:
|
||||
for details in stock_entries_detail:
|
||||
if details.parent == entry.name:
|
||||
details["Purchase_order"] = entry.purchase_order
|
||||
if entry.purchase_order not in po_transferred_qty_map:
|
||||
po_transferred_qty_map[entry.purchase_order] = {}
|
||||
po_transferred_qty_map[entry.purchase_order][details.item_code] = details.qty
|
||||
else:
|
||||
po_transferred_qty_map[entry.purchase_order][details.item_code] = po_transferred_qty_map[entry.purchase_order].get(details.item_code, 0) + details.qty
|
||||
|
||||
return po_transferred_qty_map
|
||||
|
||||
|
||||
def get_stock_entry(po):
|
||||
return frappe.get_all("Stock Entry", filters=[
|
||||
('purchase_order', 'IN', po),
|
||||
('stock_entry_type', '=', 'Send to Subcontractor'),
|
||||
('docstatus', '=', 1)
|
||||
], fields=["name", "purchase_order"])
|
||||
|
||||
def get_stock_entry_detail(se):
|
||||
return frappe.get_all("Stock Entry Detail", filters=[
|
||||
["parent", "in", se]
|
||||
def get_po_items_to_supply(filters):
|
||||
return frappe.db.get_all(
|
||||
"Purchase Order",
|
||||
fields=[
|
||||
"name as purchase_order",
|
||||
"transaction_date as date",
|
||||
"supplier as supplier",
|
||||
"`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
|
||||
"`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
|
||||
"`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty"
|
||||
],
|
||||
fields=["parent", "item_code", "qty"])
|
||||
|
||||
def get_purchase_order_item_supplied(po):
|
||||
return frappe.get_all("Purchase Order Item Supplied", filters=[
|
||||
('parent', 'IN', po)
|
||||
], fields=['parent', 'rm_item_code', 'required_qty'])
|
||||
filters = [
|
||||
["Purchase Order", "per_received", "<", "100"],
|
||||
["Purchase Order", "is_subcontracted", "=", "Yes"],
|
||||
["Purchase Order", "supplier", "=", filters.supplier],
|
||||
["Purchase Order", "transaction_date", "<=", filters.to_date],
|
||||
["Purchase Order", "transaction_date", ">=", filters.from_date],
|
||||
["Purchase Order", "docstatus", "=", 1]
|
||||
]
|
||||
)
|
@ -12,34 +12,80 @@ import json, frappe, unittest
|
||||
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
|
||||
|
||||
def test_pending_and_transferred_qty(self):
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
# Material Receipt of RMs
|
||||
make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
|
||||
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
|
||||
transfer_subcontracted_raw_materials(po.name)
|
||||
col, data = execute(filters=frappe._dict({'supplier': po.supplier,
|
||||
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
|
||||
'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))}))
|
||||
self.assertEqual(data[0]['purchase_order'], po.name)
|
||||
self.assertIn(data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
|
||||
self.assertIn(data[0]['p_qty'], [9, 18])
|
||||
self.assertIn(data[0]['t_qty'], [1, 2])
|
||||
|
||||
self.assertEqual(data[1]['purchase_order'], po.name)
|
||||
self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
|
||||
self.assertIn(data[1]['p_qty'], [9, 18])
|
||||
self.assertIn(data[1]['t_qty'], [1, 2])
|
||||
se = transfer_subcontracted_raw_materials(po)
|
||||
|
||||
col, data = execute(filters=frappe._dict(
|
||||
{
|
||||
'supplier': po.supplier,
|
||||
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
|
||||
'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))
|
||||
}
|
||||
))
|
||||
po.reload()
|
||||
|
||||
po_data = [row for row in data if row.get('purchase_order') == po.name]
|
||||
# Alphabetically sort to be certain of order
|
||||
po_data = sorted(po_data, key = lambda i: i['rm_item_code'])
|
||||
|
||||
self.assertEqual(len(po_data), 2)
|
||||
self.assertEqual(po_data[0]['purchase_order'], po.name)
|
||||
|
||||
self.assertEqual(po_data[0]['rm_item_code'], '_Test Item')
|
||||
self.assertEqual(po_data[0]['p_qty'], 8)
|
||||
self.assertEqual(po_data[0]['transferred_qty'], 2)
|
||||
|
||||
self.assertEqual(po_data[1]['rm_item_code'], '_Test Item Home Desktop 100')
|
||||
self.assertEqual(po_data[1]['p_qty'], 19)
|
||||
self.assertEqual(po_data[1]['transferred_qty'], 1)
|
||||
|
||||
se.cancel()
|
||||
po.cancel()
|
||||
|
||||
def transfer_subcontracted_raw_materials(po):
|
||||
# Order of supplied items fetched in PO is flaky
|
||||
transfer_qty_map = {
|
||||
'_Test Item': 2,
|
||||
'_Test Item Home Desktop 100': 1
|
||||
}
|
||||
|
||||
item_1 = po.supplied_items[0].rm_item_code
|
||||
item_2 = po.supplied_items[1].rm_item_code
|
||||
|
||||
rm_item = [
|
||||
{'item_code': '_Test Item', 'rm_item_code': '_Test Item', 'item_name': '_Test Item', 'qty': 1,
|
||||
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100, 'stock_uom': 'Nos'},
|
||||
{'item_code': '_Test Item Home Desktop 100', 'rm_item_code': '_Test Item Home Desktop 100', 'item_name': '_Test Item Home Desktop 100', 'qty': 2,
|
||||
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
|
||||
{
|
||||
'name': po.supplied_items[0].name,
|
||||
'item_code': item_1,
|
||||
'rm_item_code': item_1,
|
||||
'item_name': item_1,
|
||||
'qty': transfer_qty_map[item_1],
|
||||
'warehouse': '_Test Warehouse - _TC',
|
||||
'rate': 100,
|
||||
'amount': 100 * transfer_qty_map[item_1],
|
||||
'stock_uom': 'Nos'
|
||||
},
|
||||
{
|
||||
'name': po.supplied_items[1].name,
|
||||
'item_code': item_2,
|
||||
'rm_item_code': item_2,
|
||||
'item_name': item_2,
|
||||
'qty': transfer_qty_map[item_2],
|
||||
'warehouse': '_Test Warehouse - _TC',
|
||||
'rate': 100,
|
||||
'amount': 100 * transfer_qty_map[item_2],
|
||||
'stock_uom': 'Nos'
|
||||
}
|
||||
]
|
||||
rm_item_string = json.dumps(rm_item)
|
||||
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
|
||||
se.from_warehouse = '_Test Warehouse 1 - _TC'
|
||||
se.to_warehouse = '_Test Warehouse 1 - _TC'
|
||||
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
|
||||
se.from_warehouse = '_Test Warehouse - _TC'
|
||||
se.to_warehouse = '_Test Warehouse - _TC'
|
||||
se.stock_entry_type = 'Send to Subcontractor'
|
||||
se.save()
|
||||
se.submit()
|
||||
return se
|
||||
|
54
erpnext/change_log/v13/v13_5_0.md
Normal file
54
erpnext/change_log/v13/v13_5_0.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Version 13.5.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Tax deduction against advance payments ([#25831](https://github.com/frappe/erpnext/pull/25831))
|
||||
- Cost-center wise period closing entry ([#25766](https://github.com/frappe/erpnext/pull/25766))
|
||||
- Create Quality Inspections from account and stock documents ([#25221](https://github.com/frappe/erpnext/pull/25221))
|
||||
- Item Taxes based on net rate ([#25961](https://github.com/frappe/erpnext/pull/25961))
|
||||
- Enable/disable gl entry posting for change given in pos ([#25822](https://github.com/frappe/erpnext/pull/25822))
|
||||
- Add Inactive status to Employee ([#26029](https://github.com/frappe/erpnext/pull/26029))
|
||||
- Added check box to combine items with same BOM ([#25478](https://github.com/frappe/erpnext/pull/25478))
|
||||
- Item Tax Templates for Germany ([#25858](https://github.com/frappe/erpnext/pull/25858))
|
||||
- Refactored leave balance report ([#25771](https://github.com/frappe/erpnext/pull/25771))
|
||||
- Refactored Vehicle Expenses Report ([#25727](https://github.com/frappe/erpnext/pull/25727))
|
||||
- Refactored maintenance schedule and visit document ([#25358](https://github.com/frappe/erpnext/pull/25358))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Cannot add same item with different rates ([#25849](https://github.com/frappe/erpnext/pull/25849))
|
||||
- Show only company addresses for ITC reversal entry ([#25866](https://github.com/frappe/erpnext/pull/25866))
|
||||
- Hiding Rounding Adjustment field ([#25380](https://github.com/frappe/erpnext/pull/25380))
|
||||
- Auto tax calculations in Payment Entry ([#26055](https://github.com/frappe/erpnext/pull/26055))
|
||||
- Not able to select the item code in work order ([#25915](https://github.com/frappe/erpnext/pull/25915))
|
||||
- Cannot reset plaid link for a bank account ([#25869](https://github.com/frappe/erpnext/pull/25869))
|
||||
- Student invalid password reset link ([#25826](https://github.com/frappe/erpnext/pull/25826))
|
||||
- Multiple pos issues ([#25928](https://github.com/frappe/erpnext/pull/25928))
|
||||
- Add Product Bundles to POS ([#25860](https://github.com/frappe/erpnext/pull/25860))
|
||||
- Enable Parallel tests ([#25862](https://github.com/frappe/erpnext/pull/25862))
|
||||
- Service item check on e-Invoicing ([#25986](https://github.com/frappe/erpnext/pull/25986))
|
||||
- Choose correct Salary Structure Assignment when getting data for formula eval ([#25981](https://github.com/frappe/erpnext/pull/25981))
|
||||
- Ignore internal transfer invoices from GST Reports ([#25969](https://github.com/frappe/erpnext/pull/25969))
|
||||
- Taxable value for invoices with additional discount ([#26056](https://github.com/frappe/erpnext/pull/26056))
|
||||
- Validate negative allocated amount in Payment Entry ([#25799](https://github.com/frappe/erpnext/pull/25799))
|
||||
- Allow all System Managers to delete company transactions ([#25834](https://github.com/frappe/erpnext/pull/25834))
|
||||
- Wrong round off gl entry posted in case of purchase invoice ([#25775](https://github.com/frappe/erpnext/pull/25775))
|
||||
- Use dictionary filter instead of list ([#25874](https://github.com/frappe/erpnext/pull/25874))
|
||||
- Ageing error in PSOA ([#25855](https://github.com/frappe/erpnext/pull/25855))
|
||||
- On click of duplicate button system has not copied the difference account ([#25988](https://github.com/frappe/erpnext/pull/25988))
|
||||
- Assign Product Bundle's conversion_factor to Pack… ([#25840](https://github.com/frappe/erpnext/pull/25840))
|
||||
- Rename Loan Management workspace to Loans ([#25856](https://github.com/frappe/erpnext/pull/25856))
|
||||
- Fix stock quantity calculation when negative_stock_allowe… ([#25859](https://github.com/frappe/erpnext/pull/25859))
|
||||
- Update cost center from pos profile ([#25971](https://github.com/frappe/erpnext/pull/25971))
|
||||
- Ensure website theme is applied correctly ([#25863](https://github.com/frappe/erpnext/pull/25863))
|
||||
- Only display GST card in Accounting Workspace if it's in India ([#26000](https://github.com/frappe/erpnext/pull/26000))
|
||||
- Incorrect gstin fetched incase of branch company address ([#25841](https://github.com/frappe/erpnext/pull/25841))
|
||||
- Sort account balances by account name ([#26009](https://github.com/frappe/erpnext/pull/26009))
|
||||
- Custom conversion factor field not mapped from job card to stock entry ([#25956](https://github.com/frappe/erpnext/pull/25956))
|
||||
- Chart of accounts importer always error ([#25882](https://github.com/frappe/erpnext/pull/25882))
|
||||
- Create POS Invoice for Product Bundles ([#25847](https://github.com/frappe/erpnext/pull/25847))
|
||||
- Wrap dates in getdate for leave application ([#25899](https://github.com/frappe/erpnext/pull/25899))
|
||||
- Closing entry shows incorrect expected amount ([#25868](https://github.com/frappe/erpnext/pull/25868))
|
||||
- Add Hold status column in the Issue Summary Report ([#25828](https://github.com/frappe/erpnext/pull/25828))
|
||||
- Rendering of broken image on pos ([#25872](https://github.com/frappe/erpnext/pull/25872))
|
||||
- Timeout error in the repost item valuation ([#25854](https://github.com/frappe/erpnext/pull/25854))
|
@ -116,6 +116,8 @@ class AccountsController(TransactionBase):
|
||||
|
||||
if self.doctype == 'Purchase Invoice':
|
||||
self.calculate_paid_amount()
|
||||
# apply tax withholding only if checked and applicable
|
||||
self.set_tax_withholding()
|
||||
|
||||
if self.doctype in ['Purchase Invoice', 'Sales Invoice']:
|
||||
pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid"
|
||||
@ -225,7 +227,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def validate_date_with_fiscal_year(self):
|
||||
if self.meta.get_field("fiscal_year"):
|
||||
date_field = ""
|
||||
date_field = None
|
||||
if self.meta.get_field("posting_date"):
|
||||
date_field = "posting_date"
|
||||
elif self.meta.get_field("transaction_date"):
|
||||
@ -608,8 +610,8 @@ class AccountsController(TransactionBase):
|
||||
order_field = "purchase_order"
|
||||
order_doctype = "Purchase Order"
|
||||
|
||||
order_list = list(set([d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)]))
|
||||
order_list = list(set(d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
journal_entries = get_advance_journal_entries(party_type, party, party_account,
|
||||
amount_field, order_doctype, order_list, include_unallocated)
|
||||
@ -633,8 +635,8 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def validate_advance_entries(self):
|
||||
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
|
||||
order_list = list(set([d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)]))
|
||||
order_list = list(set(d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
if not order_list: return
|
||||
|
||||
@ -700,6 +702,7 @@ class AccountsController(TransactionBase):
|
||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
self.update_allocated_advance_taxes_on_cancel()
|
||||
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
|
||||
@ -707,6 +710,87 @@ class AccountsController(TransactionBase):
|
||||
if frappe.db.get_single_value('Accounts Settings', 'unlink_advance_payment_on_cancelation_of_order'):
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
|
||||
def get_tax_map(self):
|
||||
tax_map = {}
|
||||
for tax in self.get('taxes'):
|
||||
tax_map.setdefault(tax.account_head, 0.0)
|
||||
tax_map[tax.account_head] += tax.tax_amount
|
||||
|
||||
return tax_map
|
||||
|
||||
def update_allocated_advance_taxes_on_cancel(self):
|
||||
if self.get('advances'):
|
||||
tax_accounts = [d.account_head for d in self.get('taxes')]
|
||||
allocated_tax_map = frappe._dict(frappe.get_all('GL Entry', fields=['account', 'sum(credit - debit)'],
|
||||
filters={'voucher_no': self.name, 'account': ('in', tax_accounts)},
|
||||
group_by='account', as_list=1))
|
||||
|
||||
tax_map = self.get_tax_map()
|
||||
|
||||
for pe in self.get('advances'):
|
||||
if pe.reference_type == 'Payment Entry':
|
||||
pe = frappe.get_doc('Payment Entry', pe.reference_name)
|
||||
for tax in pe.get('taxes'):
|
||||
allocated_amount = tax_map.get(tax.account_head) - allocated_tax_map.get(tax.account_head)
|
||||
if allocated_amount > tax.tax_amount:
|
||||
allocated_amount = tax.tax_amount
|
||||
|
||||
if allocated_amount:
|
||||
frappe.db.set_value('Advance Taxes and Charges', tax.name, 'allocated_amount',
|
||||
tax.allocated_amount - allocated_amount)
|
||||
tax_map[tax.account_head] -= allocated_amount
|
||||
allocated_tax_map[tax.account_head] -= allocated_amount
|
||||
|
||||
def allocate_advance_taxes(self, gl_entries):
|
||||
tax_map = self.get_tax_map()
|
||||
for pe in self.get("advances"):
|
||||
if pe.reference_type == "Payment Entry" and \
|
||||
frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'):
|
||||
pe = frappe.get_doc("Payment Entry", pe.reference_name)
|
||||
for tax in pe.get("taxes"):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
else:
|
||||
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
|
||||
party = self.supplier if self.doctype == "Purchase Invoice" else self.customer
|
||||
unallocated_amount = tax.tax_amount - tax.allocated_amount
|
||||
if tax_map.get(tax.account_head):
|
||||
amount = tax_map.get(tax.account_head)
|
||||
if amount < unallocated_amount:
|
||||
unallocated_amount = amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": tax.account_head,
|
||||
"against": party,
|
||||
dr_or_cr: unallocated_amount,
|
||||
dr_or_cr + "_in_account_currency": unallocated_amount
|
||||
if account_currency==self.company_currency
|
||||
else unallocated_amount,
|
||||
"cost_center": tax.cost_center
|
||||
}, account_currency, item=tax))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": pe.advance_tax_account,
|
||||
"against": party,
|
||||
rev_dr_cr: unallocated_amount,
|
||||
rev_dr_cr + "_in_account_currency": unallocated_amount
|
||||
if account_currency==self.company_currency
|
||||
else unallocated_amount,
|
||||
"cost_center": tax.cost_center
|
||||
}, account_currency, item=tax))
|
||||
|
||||
frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount",
|
||||
tax.allocated_amount + unallocated_amount)
|
||||
|
||||
tax_map[tax.account_head] -= unallocated_amount
|
||||
|
||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
item_allowance = {}
|
||||
@ -744,8 +828,14 @@ class AccountsController(TransactionBase):
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||
|
||||
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
|
||||
def get_company_default(self, fieldname):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
@ -1108,7 +1198,7 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c
|
||||
|
||||
|
||||
def validate_taxes_and_charges(tax):
|
||||
if tax.charge_type in ['Actual', 'On Net Total'] and tax.row_id:
|
||||
if tax.charge_type in ['Actual', 'On Net Total', 'On Paid Amount'] and tax.row_id:
|
||||
frappe.throw(_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"))
|
||||
elif tax.charge_type in ['On Previous Row Amount', 'On Previous Row Total']:
|
||||
if cint(tax.idx) == 1:
|
||||
@ -1125,20 +1215,19 @@ def validate_taxes_and_charges(tax):
|
||||
|
||||
def validate_inclusive_tax(tax, doc):
|
||||
def _on_previous_row_error(row_range):
|
||||
throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx,
|
||||
row_range))
|
||||
throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
|
||||
|
||||
if cint(getattr(tax, "included_in_print_rate", None)):
|
||||
if tax.charge_type == "Actual":
|
||||
# inclusive tax cannot be of type Actual
|
||||
throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate").format(tax.idx))
|
||||
throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx))
|
||||
elif tax.charge_type == "On Previous Row Amount" and \
|
||||
not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate):
|
||||
# referred row should also be inclusive
|
||||
_on_previous_row_error(tax.row_id)
|
||||
elif tax.charge_type == "On Previous Row Total" and \
|
||||
not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]):
|
||||
# all rows about the reffered tax should be inclusive
|
||||
# all rows about the referred tax should be inclusive
|
||||
_on_previous_row_error("1 - %d" % (tax.row_id,))
|
||||
elif tax.get("category") == "Valuation":
|
||||
frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
|
||||
@ -1240,7 +1329,6 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
||||
|
||||
return list(payment_entries_against_order) + list(unallocated_payment_entries)
|
||||
|
||||
|
||||
def update_invoice_status():
|
||||
# Daily update the status of the invoices
|
||||
|
||||
@ -1449,6 +1537,7 @@ def validate_and_delete_children(parent, data):
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
def check_doc_permissions(doc, perm_type='create'):
|
||||
|
@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
class BuyingController(StockController):
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.subcontracting import Subcontracting
|
||||
|
||||
class BuyingController(StockController, Subcontracting):
|
||||
|
||||
def get_feed(self):
|
||||
if self.get("supplier_name"):
|
||||
@ -57,6 +58,11 @@ class BuyingController(StockController):
|
||||
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
self.update_valuation_rate()
|
||||
|
||||
def onload(self):
|
||||
super(BuyingController, self).onload()
|
||||
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
|
||||
'backflush_raw_materials_of_subcontract_based_on'))
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super(BuyingController, self).set_missing_values(for_validate)
|
||||
|
||||
@ -171,18 +177,19 @@ class BuyingController(StockController):
|
||||
|
||||
TODO: rename item_tax_amount to valuation_tax_amount
|
||||
"""
|
||||
stock_and_asset_items = []
|
||||
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
|
||||
|
||||
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
|
||||
last_item_idx = 1
|
||||
for d in self.get("items"):
|
||||
if d.item_code and d.item_code in stock_and_asset_items:
|
||||
if (d.item_code and d.item_code in stock_and_asset_items):
|
||||
stock_and_asset_items_qty += flt(d.qty)
|
||||
stock_and_asset_items_amount += flt(d.base_net_amount)
|
||||
last_item_idx = d.idx
|
||||
|
||||
total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
|
||||
if d.category in ["Valuation", "Valuation and Total"]])
|
||||
total_valuation_amount = sum(flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
|
||||
if d.category in ["Valuation", "Valuation and Total"])
|
||||
|
||||
valuation_amount_adjustment = total_valuation_amount
|
||||
for i, item in enumerate(self.get("items")):
|
||||
@ -255,7 +262,7 @@ class BuyingController(StockController):
|
||||
supplied_items_cost = 0.0
|
||||
for d in self.get("supplied_items"):
|
||||
if d.reference_name == item_row_id:
|
||||
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
|
||||
if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
|
||||
rate = get_incoming_rate({
|
||||
"item_code": d.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
@ -285,11 +292,13 @@ class BuyingController(StockController):
|
||||
if item in self.sub_contracted_items and not item.bom:
|
||||
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
|
||||
|
||||
if self.doctype == "Purchase Order":
|
||||
for supplied_item in self.get("supplied_items"):
|
||||
if not supplied_item.reserve_warehouse:
|
||||
frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
|
||||
if self.doctype != "Purchase Order":
|
||||
return
|
||||
|
||||
for row in self.get("supplied_items"):
|
||||
if not row.reserve_warehouse:
|
||||
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
|
||||
frappe.throw(_(msg))
|
||||
else:
|
||||
for item in self.get("items"):
|
||||
if item.bom:
|
||||
@ -297,23 +306,7 @@ class BuyingController(StockController):
|
||||
|
||||
def create_raw_materials_supplied(self, raw_material_table):
|
||||
if self.is_subcontracted=="Yes":
|
||||
parent_items = []
|
||||
backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
|
||||
"backflush_raw_materials_of_subcontract_based_on")
|
||||
if (self.doctype == 'Purchase Receipt' and
|
||||
backflush_raw_materials_based_on != 'BOM'):
|
||||
self.update_raw_materials_supplied_based_on_stock_entries()
|
||||
else:
|
||||
for item in self.get("items"):
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
item.rm_supp_cost = 0.0
|
||||
if item.bom and item.item_code in self.sub_contracted_items:
|
||||
self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
|
||||
|
||||
if [item.item_code, item.name] not in parent_items:
|
||||
parent_items.append([item.item_code, item.name])
|
||||
|
||||
self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
|
||||
self.set_materials_for_subcontracted_items(raw_material_table)
|
||||
|
||||
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
for item in self.get("items"):
|
||||
@ -322,176 +315,6 @@ class BuyingController(StockController):
|
||||
if self.is_subcontracted == "No" and self.get("supplied_items"):
|
||||
self.set('supplied_items', [])
|
||||
|
||||
def update_raw_materials_supplied_based_on_stock_entries(self):
|
||||
self.set('supplied_items', [])
|
||||
|
||||
purchase_orders = set([d.purchase_order for d in self.items])
|
||||
|
||||
# qty of raw materials backflushed (for each item per purchase order)
|
||||
backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
|
||||
|
||||
# qty of "finished good" item yet to be received
|
||||
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
|
||||
|
||||
for item in self.get('items'):
|
||||
if not item.purchase_order:
|
||||
continue
|
||||
|
||||
# reset raw_material cost
|
||||
item.rm_supp_cost = 0
|
||||
|
||||
# qty of raw materials transferred to the supplier
|
||||
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
|
||||
|
||||
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
|
||||
|
||||
item_key = '{}{}'.format(item.item_code, item.purchase_order)
|
||||
|
||||
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
|
||||
|
||||
if not fg_yet_to_be_received:
|
||||
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
|
||||
.format(item.idx, frappe.bold(item.item_code),
|
||||
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
|
||||
title=_("Limit Crossed"))
|
||||
|
||||
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
|
||||
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
|
||||
|
||||
for raw_material in transferred_raw_materials + non_stock_items:
|
||||
rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
|
||||
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
||||
|
||||
consumed_qty = raw_material_data.get('qty', 0)
|
||||
consumed_serial_nos = raw_material_data.get('serial_no', '')
|
||||
consumed_batch_nos = raw_material_data.get('batch_nos', '')
|
||||
|
||||
transferred_qty = raw_material.qty
|
||||
|
||||
rm_qty_to_be_consumed = transferred_qty - consumed_qty
|
||||
|
||||
# backflush all remaining transferred qty in the last Purchase Receipt
|
||||
if fg_yet_to_be_received == item.qty:
|
||||
qty = rm_qty_to_be_consumed
|
||||
else:
|
||||
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
|
||||
|
||||
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
|
||||
qty = frappe.utils.ceil(qty)
|
||||
|
||||
if qty > rm_qty_to_be_consumed:
|
||||
qty = rm_qty_to_be_consumed
|
||||
|
||||
if not qty: continue
|
||||
|
||||
if raw_material.serial_nos:
|
||||
set_serial_nos(raw_material, consumed_serial_nos, qty)
|
||||
|
||||
if raw_material.batch_nos:
|
||||
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
|
||||
|
||||
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
|
||||
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
|
||||
|
||||
for batch_data in batches_qty:
|
||||
qty = batch_data['qty']
|
||||
raw_material.batch_no = batch_data['batch']
|
||||
if qty > 0:
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
else:
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
|
||||
def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
|
||||
rm = self.append('supplied_items', {})
|
||||
rm.update(raw_material_data)
|
||||
|
||||
if not rm.main_item_code:
|
||||
rm.main_item_code = fg_item_row.item_code
|
||||
|
||||
rm.reference_name = fg_item_row.name
|
||||
rm.required_qty = qty
|
||||
rm.consumed_qty = qty
|
||||
|
||||
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
|
||||
exploded_item = 1
|
||||
if hasattr(item, 'include_exploded_items'):
|
||||
exploded_item = item.get('include_exploded_items')
|
||||
|
||||
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
|
||||
|
||||
used_alternative_items = []
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
|
||||
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
|
||||
|
||||
raw_materials_cost = 0
|
||||
items = list(set([d.item_code for d in bom_items]))
|
||||
item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
|
||||
from `tabItem` i, `tabItem Default` id
|
||||
where id.parent=i.name and id.company=%s and i.name in ({0})"""
|
||||
.format(", ".join(["%s"] * len(items))), [self.company] + items))
|
||||
|
||||
for bom_item in bom_items:
|
||||
if self.doctype == "Purchase Order":
|
||||
reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
|
||||
if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
|
||||
reserve_warehouse = None
|
||||
|
||||
conversion_factor = item.conversion_factor
|
||||
if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
|
||||
bom_item.item_code in used_alternative_items):
|
||||
alternative_item_data = used_alternative_items.get(bom_item.item_code)
|
||||
bom_item.item_code = alternative_item_data.item_code
|
||||
bom_item.item_name = alternative_item_data.item_name
|
||||
bom_item.stock_uom = alternative_item_data.stock_uom
|
||||
conversion_factor = alternative_item_data.conversion_factor
|
||||
bom_item.description = alternative_item_data.description
|
||||
|
||||
# check if exists
|
||||
exists = 0
|
||||
for d in self.get(raw_material_table):
|
||||
if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
|
||||
and d.reference_name == item.name:
|
||||
rm, exists = d, 1
|
||||
break
|
||||
|
||||
if not exists:
|
||||
rm = self.append(raw_material_table, {})
|
||||
|
||||
required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
|
||||
flt(conversion_factor), rm.precision("required_qty"))
|
||||
rm.reference_name = item.name
|
||||
rm.bom_detail_no = bom_item.name
|
||||
rm.main_item_code = item.item_code
|
||||
rm.rm_item_code = bom_item.item_code
|
||||
rm.stock_uom = bom_item.stock_uom
|
||||
rm.required_qty = required_qty
|
||||
rm.rate = bom_item.rate
|
||||
rm.conversion_factor = conversion_factor
|
||||
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
rm.consumed_qty = required_qty
|
||||
rm.description = bom_item.description
|
||||
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
|
||||
rm.batch_no = item.batch_no
|
||||
elif not rm.reserve_warehouse:
|
||||
rm.reserve_warehouse = reserve_warehouse
|
||||
|
||||
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
|
||||
"""Remove all those child items which are no longer present in main item table"""
|
||||
delete_list = []
|
||||
for d in self.get(raw_material_table):
|
||||
if [d.main_item_code, d.reference_name] not in parent_items:
|
||||
# mark for deletion from doclist
|
||||
delete_list.append(d)
|
||||
|
||||
# delete from doclist
|
||||
if delete_list:
|
||||
rm_supplied_details = self.get(raw_material_table)
|
||||
self.set(raw_material_table, [])
|
||||
for d in rm_supplied_details:
|
||||
if d not in delete_list:
|
||||
self.append(raw_material_table, d)
|
||||
|
||||
@property
|
||||
def sub_contracted_items(self):
|
||||
if not hasattr(self, "_sub_contracted_items"):
|
||||
@ -683,7 +506,8 @@ class BuyingController(StockController):
|
||||
self.process_fixed_asset()
|
||||
self.update_fixed_asset(field)
|
||||
|
||||
update_last_purchase_rate(self, is_submit = 1)
|
||||
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
|
||||
update_last_purchase_rate(self, is_submit = 1)
|
||||
|
||||
def on_cancel(self):
|
||||
super(BuyingController, self).on_cancel()
|
||||
@ -691,7 +515,9 @@ class BuyingController(StockController):
|
||||
if self.get('is_return'):
|
||||
return
|
||||
|
||||
update_last_purchase_rate(self, is_submit = 0)
|
||||
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
|
||||
update_last_purchase_rate(self, is_submit = 0)
|
||||
|
||||
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
|
||||
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
|
||||
|
||||
@ -863,104 +689,6 @@ class BuyingController(StockController):
|
||||
else:
|
||||
validate_item_type(self, "is_purchase_item", "purchase")
|
||||
|
||||
|
||||
def get_items_from_bom(item_code, bom, exploded_item=1):
|
||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||
|
||||
bom_items = frappe.db.sql("""select t2.item_code, t2.name,
|
||||
t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
|
||||
t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
|
||||
from
|
||||
`tabBOM` t1, `tab{0}` t2, tabItem t3
|
||||
where
|
||||
t2.parent = t1.name and t1.item = %s
|
||||
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
|
||||
and t2.sourced_by_supplier = 0
|
||||
and t2.item_code = t3.name""".format(doctype),
|
||||
(item_code, bom), as_dict=1)
|
||||
|
||||
if not bom_items:
|
||||
msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
|
||||
|
||||
return bom_items
|
||||
|
||||
def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
|
||||
common_query = """
|
||||
SELECT
|
||||
sed.item_code AS rm_item_code,
|
||||
SUM(sed.qty) AS qty,
|
||||
sed.description,
|
||||
sed.stock_uom,
|
||||
sed.subcontracted_item AS main_item_code,
|
||||
{serial_no_concat_syntax} AS serial_nos,
|
||||
{batch_no_concat_syntax} AS batch_nos
|
||||
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
|
||||
WHERE
|
||||
se.name = sed.parent
|
||||
AND se.docstatus=1
|
||||
AND se.purpose='Send to Subcontractor'
|
||||
AND se.purchase_order = %s
|
||||
AND IFNULL(sed.t_warehouse, '') != ''
|
||||
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
|
||||
GROUP BY sed.item_code, sed.subcontracted_item
|
||||
"""
|
||||
raw_materials = frappe.db.multisql({
|
||||
'mariadb': common_query.format(
|
||||
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
|
||||
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
|
||||
),
|
||||
'postgres': common_query.format(
|
||||
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
|
||||
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
|
||||
)
|
||||
}, (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
return raw_materials
|
||||
|
||||
def get_backflushed_subcontracted_raw_materials(purchase_orders):
|
||||
purchase_receipts = frappe.get_all("Purchase Receipt Item",
|
||||
fields = ["purchase_order", "item_code", "name", "parent"],
|
||||
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
|
||||
|
||||
distinct_purchase_receipts = {}
|
||||
for pr in purchase_receipts:
|
||||
key = (pr.purchase_order, pr.item_code, pr.parent)
|
||||
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
|
||||
|
||||
backflushed_raw_materials_map = frappe._dict()
|
||||
for args, references in iteritems(distinct_purchase_receipts):
|
||||
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
|
||||
|
||||
for data in purchase_receipt_supplied_items:
|
||||
pr_key = (data.rm_item_code, data.main_item_code, args[0])
|
||||
if pr_key not in backflushed_raw_materials_map:
|
||||
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
|
||||
"qty": 0.0,
|
||||
"serial_no": [],
|
||||
"batch_no": [],
|
||||
"consumed_batch": {}
|
||||
}))
|
||||
|
||||
row = backflushed_raw_materials_map.get(pr_key)
|
||||
row.qty += data.consumed_qty
|
||||
|
||||
for field in ["serial_no", "batch_no"]:
|
||||
if data.get(field):
|
||||
row[field].append(data.get(field))
|
||||
|
||||
if data.get("batch_no"):
|
||||
if data.get("batch_no") in row.consumed_batch:
|
||||
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
|
||||
else:
|
||||
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
|
||||
|
||||
return backflushed_raw_materials_map
|
||||
|
||||
def get_supplied_items(item_code, purchase_receipt, references):
|
||||
return frappe.get_all("Purchase Receipt Item Supplied",
|
||||
fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
|
||||
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
|
||||
|
||||
def get_asset_item_details(asset_items):
|
||||
asset_items_data = {}
|
||||
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
|
||||
@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
|
||||
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
def get_qty_to_be_received(purchase_orders):
|
||||
return frappe._dict(frappe.db.sql("""
|
||||
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
|
||||
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
|
||||
FROM `tabPurchase Order Item` poi
|
||||
WHERE
|
||||
poi.`parent` in %s
|
||||
GROUP BY poi.`item_code`, poi.`parent`
|
||||
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
|
||||
""", (purchase_orders)))
|
||||
|
||||
def get_non_stock_items(purchase_order, fg_item_code):
|
||||
return frappe.db.sql("""
|
||||
SELECT
|
||||
pois.main_item_code,
|
||||
pois.rm_item_code,
|
||||
item.description,
|
||||
pois.required_qty AS qty,
|
||||
pois.rate,
|
||||
1 as non_stock_item,
|
||||
pois.stock_uom
|
||||
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
|
||||
WHERE
|
||||
pois.`rm_item_code` = item.`name`
|
||||
AND item.is_stock_item = 0
|
||||
AND pois.`parent` = %s
|
||||
AND pois.`main_item_code` = %s
|
||||
""", (purchase_order, fg_item_code), as_dict=1)
|
||||
|
||||
|
||||
def set_serial_nos(raw_material, consumed_serial_nos, qty):
|
||||
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
|
||||
set(get_serial_nos(consumed_serial_nos))
|
||||
if serial_nos and qty <= len(serial_nos):
|
||||
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
|
||||
|
||||
def get_transferred_batch_qty_map(purchase_order, fg_item):
|
||||
# returns
|
||||
# {
|
||||
# (item_code, fg_code): {
|
||||
# batch1: 10, # qty
|
||||
# batch2: 16
|
||||
# },
|
||||
# }
|
||||
transferred_batch_qty_map = {}
|
||||
transferred_batches = frappe.db.sql("""
|
||||
SELECT
|
||||
sed.batch_no,
|
||||
SUM(sed.qty) AS qty,
|
||||
sed.item_code,
|
||||
sed.subcontracted_item
|
||||
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
|
||||
WHERE
|
||||
se.name = sed.parent
|
||||
AND se.docstatus=1
|
||||
AND se.purpose='Send to Subcontractor'
|
||||
AND se.purchase_order = %s
|
||||
AND ifnull(sed.subcontracted_item, '') in ('', %s)
|
||||
AND sed.batch_no IS NOT NULL
|
||||
GROUP BY
|
||||
sed.batch_no,
|
||||
sed.item_code
|
||||
""", (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
for batch_data in transferred_batches:
|
||||
key = ((batch_data.item_code, fg_item)
|
||||
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
|
||||
transferred_batch_qty_map.setdefault(key, OrderedDict())
|
||||
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return transferred_batch_qty_map
|
||||
|
||||
def get_backflushed_batch_qty_map(purchase_order, fg_item):
|
||||
# returns
|
||||
# {
|
||||
# (item_code, fg_code): {
|
||||
# batch1: 10, # qty
|
||||
# batch2: 16
|
||||
# },
|
||||
# }
|
||||
backflushed_batch_qty_map = {}
|
||||
backflushed_batches = frappe.db.sql("""
|
||||
SELECT
|
||||
pris.batch_no,
|
||||
SUM(pris.consumed_qty) AS qty,
|
||||
pris.rm_item_code AS item_code
|
||||
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
|
||||
WHERE
|
||||
pr.name = pri.parent
|
||||
AND pri.parent = pris.parent
|
||||
AND pri.purchase_order = %s
|
||||
AND pri.item_code = pris.main_item_code
|
||||
AND pr.docstatus = 1
|
||||
AND pris.main_item_code = %s
|
||||
AND pris.batch_no IS NOT NULL
|
||||
GROUP BY
|
||||
pris.rm_item_code, pris.batch_no
|
||||
""", (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
for batch_data in backflushed_batches:
|
||||
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
|
||||
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return backflushed_batch_qty_map
|
||||
|
||||
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
|
||||
# Returns available batches to be backflushed based on requirements
|
||||
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
|
||||
if not transferred_batches:
|
||||
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
|
||||
|
||||
available_batches = []
|
||||
|
||||
for (batch, transferred_qty) in transferred_batches.items():
|
||||
backflushed_qty = backflushed_batches.get(batch, 0)
|
||||
available_qty = transferred_qty - backflushed_qty
|
||||
|
||||
if available_qty >= required_qty:
|
||||
available_batches.append({'batch': batch, 'qty': required_qty})
|
||||
break
|
||||
elif available_qty != 0:
|
||||
available_batches.append({'batch': batch, 'qty': available_qty})
|
||||
required_qty -= available_qty
|
||||
|
||||
for row in available_batches:
|
||||
if backflushed_batches.get(row.get('batch'), 0) > 0:
|
||||
backflushed_batches[row.get('batch')] += row.get('qty')
|
||||
else:
|
||||
backflushed_batches[row.get('batch')] = row.get('qty')
|
||||
|
||||
return available_batches
|
||||
|
@ -4,6 +4,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import erpnext
|
||||
import json
|
||||
from frappe.desk.reportview import get_match_cond, get_filters_cond
|
||||
from frappe.utils import nowdate, getdate
|
||||
from collections import defaultdict
|
||||
@ -18,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabEmployee`
|
||||
where status = 'Active'
|
||||
where status in ('Active', 'Suspended')
|
||||
and docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or employee_name like %(txt)s)
|
||||
@ -87,7 +88,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Customer", fields)
|
||||
|
||||
searchfields = frappe.get_meta("Customer").get_search_fields()
|
||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabCustomer`
|
||||
where docstatus < 2
|
||||
@ -198,6 +199,9 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
conditions = []
|
||||
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
|
||||
#Get searchfields from meta and use in Item Link field query
|
||||
meta = frappe.get_meta("Item", cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
@ -311,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql("""select {fields} from `tabProject`
|
||||
where
|
||||
`tabProject`.status not in ("Completed", "Cancelled")
|
||||
and {cond} {match_cond} {scond}
|
||||
and {cond} {scond} {match_cond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
idx desc,
|
||||
|
@ -428,7 +428,7 @@ class SellingController(StockController):
|
||||
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
|
||||
|
||||
def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
|
||||
doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
|
||||
doc_list = list(set(d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)))
|
||||
if doc_list:
|
||||
po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]
|
||||
|
||||
|
@ -299,8 +299,8 @@ class StatusUpdater(Document):
|
||||
args['name'] = self.get(args['percent_join_field_parent'])
|
||||
self._update_percent_field(args, update_modified)
|
||||
else:
|
||||
distinct_transactions = set([d.get(args['percent_join_field'])
|
||||
for d in self.get_all_children(args['source_dt'])])
|
||||
distinct_transactions = set(d.get(args['percent_join_field'])
|
||||
for d in self.get_all_children(args['source_dt']))
|
||||
|
||||
for name in distinct_transactions:
|
||||
if name:
|
||||
|
@ -1,17 +1,21 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, erpnext
|
||||
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
|
||||
from frappe import _
|
||||
import frappe.defaults
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
||||
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
|
||||
|
||||
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
||||
class QualityInspectionRejectedError(frappe.ValidationError): pass
|
||||
@ -189,7 +193,6 @@ class StockController(AccountsController):
|
||||
if hasattr(self, "items"):
|
||||
item_doclist = self.get("items")
|
||||
elif self.doctype == "Stock Reconciliation":
|
||||
import json
|
||||
item_doclist = []
|
||||
data = json.loads(self.reconciliation_json)
|
||||
for row in data[data.index(self.head_row)+1:]:
|
||||
@ -310,7 +313,7 @@ class StockController(AccountsController):
|
||||
|
||||
def get_serialized_items(self):
|
||||
serialized_items = []
|
||||
item_codes = list(set([d.item_code for d in self.get("items")]))
|
||||
item_codes = list(set(d.item_code for d in self.get("items")))
|
||||
if item_codes:
|
||||
serialized_items = frappe.db.sql_list("""select name from `tabItem`
|
||||
where has_serial_no=1 and name in ({})""".format(", ".join(["%s"]*len(item_codes))),
|
||||
@ -319,10 +322,10 @@ class StockController(AccountsController):
|
||||
return serialized_items
|
||||
|
||||
def validate_warehouse(self):
|
||||
from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
|
||||
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
|
||||
|
||||
warehouses = list(set([d.warehouse for d in
|
||||
self.get("items") if getattr(d, "warehouse", None)]))
|
||||
warehouses = list(set(d.warehouse for d in
|
||||
self.get("items") if getattr(d, "warehouse", None)))
|
||||
|
||||
target_warehouses = list(set([d.target_warehouse for d in
|
||||
self.get("items") if getattr(d, "target_warehouse", None)]))
|
||||
@ -498,20 +501,106 @@ class StockController(AccountsController):
|
||||
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||
self.company, self.doctype, self.name)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
inspections = []
|
||||
for item in items:
|
||||
if flt(item.get("sample_size")) > flt(item.get("qty")):
|
||||
frappe.throw(_("{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})").format(
|
||||
item_name=item.get("item_name"),
|
||||
sample_size=item.get("sample_size"),
|
||||
accepted_quantity=item.get("qty")
|
||||
))
|
||||
|
||||
quality_inspection = frappe.get_doc({
|
||||
"doctype": "Quality Inspection",
|
||||
"inspection_type": "Incoming",
|
||||
"inspected_by": frappe.session.user,
|
||||
"reference_type": doctype,
|
||||
"reference_name": docname,
|
||||
"item_code": item.get("item_code"),
|
||||
"description": item.get("description"),
|
||||
"sample_size": flt(item.get("sample_size")),
|
||||
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
|
||||
"batch_no": item.get("batch_no")
|
||||
}).insert()
|
||||
quality_inspection.save()
|
||||
inspections.append(quality_inspection.name)
|
||||
|
||||
return inspections
|
||||
|
||||
def is_reposting_pending():
|
||||
return frappe.db.exists("Repost Item Valuation",
|
||||
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
||||
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
|
||||
def future_sle_exists(args):
|
||||
sl_entries = frappe.get_all("Stock Ledger Entry",
|
||||
if validate_future_sle_not_exists(args, key, sl_entries):
|
||||
return False
|
||||
elif get_cached_data(args, key):
|
||||
return True
|
||||
|
||||
if not sl_entries:
|
||||
sl_entries = get_sle_entries_against_voucher(args)
|
||||
if not sl_entries:
|
||||
return
|
||||
|
||||
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
|
||||
|
||||
data = frappe.db.sql("""
|
||||
select item_code, warehouse, count(name) as total_row
|
||||
from `tabStock Ledger Entry`
|
||||
where
|
||||
({})
|
||||
and timestamp(posting_date, posting_time)
|
||||
>= timestamp(%(posting_date)s, %(posting_time)s)
|
||||
and voucher_no != %(voucher_no)s
|
||||
and is_cancelled = 0
|
||||
GROUP BY
|
||||
item_code, warehouse
|
||||
""".format(" or ".join(or_conditions)), args, as_dict=1)
|
||||
|
||||
for d in data:
|
||||
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
|
||||
|
||||
return len(data)
|
||||
|
||||
def validate_future_sle_not_exists(args, key, sl_entries=None):
|
||||
item_key = ''
|
||||
if args.get('item_code'):
|
||||
item_key = (args.get('item_code'), args.get('warehouse'))
|
||||
|
||||
if not sl_entries and hasattr(frappe.local, 'future_sle'):
|
||||
if (not frappe.local.future_sle.get(key) or
|
||||
(item_key and item_key not in frappe.local.future_sle.get(key))):
|
||||
return True
|
||||
|
||||
def get_cached_data(args, key):
|
||||
if not hasattr(frappe.local, 'future_sle'):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
if key not in frappe.local.future_sle:
|
||||
frappe.local.future_sle[key] = frappe._dict({})
|
||||
|
||||
if args.get('item_code'):
|
||||
item_key = (args.get('item_code'), args.get('warehouse'))
|
||||
count = frappe.local.future_sle[key].get(item_key)
|
||||
|
||||
return True if (count or count == 0) else False
|
||||
else:
|
||||
return frappe.local.future_sle[key]
|
||||
|
||||
def get_sle_entries_against_voucher(args):
|
||||
return frappe.get_all("Stock Ledger Entry",
|
||||
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
|
||||
fields=["item_code", "warehouse"],
|
||||
order_by="creation asc")
|
||||
|
||||
if not sl_entries:
|
||||
return
|
||||
|
||||
def get_conditions_to_validate_future_sle(sl_entries):
|
||||
warehouse_items_map = {}
|
||||
for entry in sl_entries:
|
||||
if entry.warehouse not in warehouse_items_map:
|
||||
@ -522,23 +611,10 @@ def future_sle_exists(args):
|
||||
or_conditions = []
|
||||
for warehouse, items in warehouse_items_map.items():
|
||||
or_conditions.append(
|
||||
"warehouse = '{}' and item_code in ({})".format(
|
||||
warehouse,
|
||||
", ".join(frappe.db.escape(item) for item in items)
|
||||
)
|
||||
)
|
||||
f"""warehouse = {frappe.db.escape(warehouse)}
|
||||
and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
|
||||
|
||||
return frappe.db.sql("""
|
||||
select name
|
||||
from `tabStock Ledger Entry`
|
||||
where
|
||||
({})
|
||||
and timestamp(posting_date, posting_time)
|
||||
>= timestamp(%(posting_date)s, %(posting_time)s)
|
||||
and voucher_no != %(voucher_no)s
|
||||
and is_cancelled = 0
|
||||
limit 1
|
||||
""".format(" or ".join(or_conditions)), args)
|
||||
return or_conditions
|
||||
|
||||
def create_repost_item_valuation_entry(args):
|
||||
args = frappe._dict(args)
|
||||
|
393
erpnext/controllers/subcontracting.py
Normal file
393
erpnext/controllers/subcontracting.py
Normal file
@ -0,0 +1,393 @@
|
||||
import frappe
|
||||
import copy
|
||||
from frappe import _
|
||||
from frappe.utils import flt, cint, get_link_to_form
|
||||
from collections import defaultdict
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
class Subcontracting():
|
||||
def set_materials_for_subcontracted_items(self, raw_material_table):
|
||||
if self.doctype == 'Purchase Invoice' and not self.update_stock:
|
||||
return
|
||||
|
||||
self.raw_material_table = raw_material_table
|
||||
self.__identify_change_in_item_table()
|
||||
self.__prepare_supplied_items()
|
||||
self.__validate_supplied_items()
|
||||
|
||||
def __prepare_supplied_items(self):
|
||||
self.initialized_fields()
|
||||
self.__get_purchase_orders()
|
||||
self.__get_pending_qty_to_receive()
|
||||
self.get_available_materials()
|
||||
self.__remove_changed_rows()
|
||||
self.__set_supplied_items()
|
||||
|
||||
def initialized_fields(self):
|
||||
self.available_materials = frappe._dict()
|
||||
self.__transferred_items = frappe._dict()
|
||||
self.alternative_item_details = frappe._dict()
|
||||
self.__get_backflush_based_on()
|
||||
|
||||
def __get_backflush_based_on(self):
|
||||
self.backflush_based_on = frappe.db.get_single_value("Buying Settings",
|
||||
"backflush_raw_materials_of_subcontract_based_on")
|
||||
|
||||
def __get_purchase_orders(self):
|
||||
self.purchase_orders = []
|
||||
|
||||
if self.doctype == 'Purchase Order':
|
||||
return
|
||||
|
||||
self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
|
||||
|
||||
def __identify_change_in_item_table(self):
|
||||
self.__changed_name = []
|
||||
self.__reference_name = []
|
||||
|
||||
if self.doctype == 'Purchase Order' or self.is_new():
|
||||
self.set(self.raw_material_table, [])
|
||||
return
|
||||
|
||||
item_dict = self.__get_data_before_save()
|
||||
if not item_dict:
|
||||
return True
|
||||
|
||||
for n_row in self.items:
|
||||
self.__reference_name.append(n_row.name)
|
||||
if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
|
||||
self.__changed_name.append(n_row.name)
|
||||
|
||||
if item_dict.get(n_row.name):
|
||||
del item_dict[n_row.name]
|
||||
|
||||
self.__changed_name.extend(item_dict.keys())
|
||||
|
||||
def __get_data_before_save(self):
|
||||
item_dict = {}
|
||||
if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save:
|
||||
for row in self._doc_before_save.get('items'):
|
||||
item_dict[row.name] = (row.item_code, row.qty)
|
||||
|
||||
return item_dict
|
||||
|
||||
def get_available_materials(self):
|
||||
''' Get the available raw materials which has been transferred to the supplier.
|
||||
available_materials = {
|
||||
(item_code, subcontracted_item, purchase_order): {
|
||||
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
|
||||
}
|
||||
}
|
||||
'''
|
||||
if not self.purchase_orders:
|
||||
return
|
||||
|
||||
for row in self.__get_transferred_items():
|
||||
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
|
||||
|
||||
if key not in self.available_materials:
|
||||
self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [],
|
||||
'batch_no': defaultdict(float), 'item_details': row, 'po_details': []})
|
||||
)
|
||||
|
||||
details = self.available_materials[key]
|
||||
details.qty += row.qty
|
||||
details.po_details.append(row.po_detail)
|
||||
|
||||
if row.serial_no:
|
||||
details.serial_no.extend(get_serial_nos(row.serial_no))
|
||||
|
||||
if row.batch_no:
|
||||
details.batch_no[row.batch_no] += row.qty
|
||||
|
||||
self.__set_alternative_item_details(row)
|
||||
|
||||
self.__transferred_items = copy.deepcopy(self.available_materials)
|
||||
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
|
||||
self.__update_consumed_materials(doctype)
|
||||
|
||||
def __update_consumed_materials(self, doctype, return_consumed_items=False):
|
||||
'''Deduct the consumed materials from the available materials.'''
|
||||
|
||||
pr_items = self.__get_received_items(doctype)
|
||||
if not pr_items:
|
||||
return ([], {}) if return_consumed_items else None
|
||||
|
||||
pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items}
|
||||
consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
|
||||
|
||||
if return_consumed_items:
|
||||
return (consumed_materials, pr_items)
|
||||
|
||||
for row in consumed_materials:
|
||||
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
|
||||
if not self.available_materials.get(key):
|
||||
continue
|
||||
|
||||
self.available_materials[key]['qty'] -= row.consumed_qty
|
||||
if row.serial_no:
|
||||
self.available_materials[key]['serial_no'] = list(
|
||||
set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no))
|
||||
)
|
||||
|
||||
if row.batch_no:
|
||||
self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty
|
||||
|
||||
def __get_transferred_items(self):
|
||||
fields = ['`tabStock Entry`.`purchase_order`']
|
||||
alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'}
|
||||
|
||||
child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount',
|
||||
'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor',
|
||||
's_warehouse', 't_warehouse', 'item_group', 'po_detail']
|
||||
|
||||
if self.backflush_based_on == 'BOM':
|
||||
child_table_fields.append('original_item')
|
||||
|
||||
for field in child_table_fields:
|
||||
fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}')
|
||||
|
||||
filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'],
|
||||
['Stock Entry', 'purchase_order', 'in', self.purchase_orders]]
|
||||
|
||||
return frappe.get_all('Stock Entry', fields = fields, filters=filters)
|
||||
|
||||
def __get_received_items(self, doctype):
|
||||
fields = []
|
||||
self.po_field = 'purchase_order'
|
||||
|
||||
for field in ['name', self.po_field, 'parent']:
|
||||
fields.append(f'`tab{doctype} Item`.`{field}`')
|
||||
|
||||
filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]]
|
||||
if doctype == 'Purchase Invoice':
|
||||
filters.append(['Purchase Invoice', 'update_stock', "=", 1])
|
||||
|
||||
return frappe.get_all(f'{doctype}', fields = fields, filters = filters)
|
||||
|
||||
def __get_consumed_items(self, doctype, pr_items):
|
||||
return frappe.get_all('Purchase Receipt Item Supplied',
|
||||
fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'],
|
||||
filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype})
|
||||
|
||||
def __set_alternative_item_details(self, row):
|
||||
if row.get('original_item'):
|
||||
self.alternative_item_details[row.get('original_item')] = row
|
||||
|
||||
def __get_pending_qty_to_receive(self):
|
||||
'''Get qty to be received against the purchase order.'''
|
||||
|
||||
self.qty_to_be_received = defaultdict(float)
|
||||
|
||||
if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders:
|
||||
for row in frappe.get_all('Purchase Order Item',
|
||||
fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'],
|
||||
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}):
|
||||
|
||||
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item'
|
||||
fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit']
|
||||
|
||||
alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'}
|
||||
for field in ['item_code', 'name', 'rate', 'stock_uom',
|
||||
'source_warehouse', 'description', 'item_name', 'stock_uom']:
|
||||
fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}')
|
||||
|
||||
filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1],
|
||||
['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]]
|
||||
|
||||
return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or []
|
||||
|
||||
def __remove_changed_rows(self):
|
||||
if not self.__changed_name:
|
||||
return
|
||||
|
||||
i=1
|
||||
self.set(self.raw_material_table, [])
|
||||
for d in self._doc_before_save.supplied_items:
|
||||
if d.reference_name in self.__changed_name:
|
||||
continue
|
||||
|
||||
if (d.reference_name not in self.__reference_name):
|
||||
continue
|
||||
|
||||
d.idx = i
|
||||
self.append('supplied_items', d)
|
||||
|
||||
i += 1
|
||||
|
||||
def __set_supplied_items(self):
|
||||
self.bom_items = {}
|
||||
|
||||
has_supplied_items = True if self.get(self.raw_material_table) else False
|
||||
for row in self.items:
|
||||
if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name)
|
||||
or (has_supplied_items and not self.__changed_name))):
|
||||
continue
|
||||
|
||||
if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM':
|
||||
for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')):
|
||||
qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor)
|
||||
bom_item.main_item_code = row.item_code
|
||||
self.__update_reserve_warehouse(bom_item, row)
|
||||
self.__set_alternative_item(bom_item)
|
||||
self.__add_supplied_item(row, bom_item, qty)
|
||||
|
||||
elif self.backflush_based_on != 'BOM':
|
||||
for key, transfer_item in self.available_materials.items():
|
||||
if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
|
||||
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
|
||||
transfer_item.qty -= qty
|
||||
self.__add_supplied_item(row, transfer_item.get('item_details'), qty)
|
||||
|
||||
if self.qty_to_be_received:
|
||||
self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
|
||||
|
||||
def __update_reserve_warehouse(self, row, item):
|
||||
if self.doctype == 'Purchase Order':
|
||||
row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse)
|
||||
|
||||
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
||||
key = (item_row.item_code, item_row.purchase_order)
|
||||
|
||||
if self.qty_to_be_received == item_row.qty:
|
||||
return transfer_item.qty
|
||||
|
||||
if self.qty_to_be_received:
|
||||
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
|
||||
transfer_item.item_details.required_qty = transfer_item.qty
|
||||
|
||||
if (transfer_item.serial_no or frappe.get_cached_value('UOM',
|
||||
transfer_item.item_details.stock_uom, 'must_be_whole_number')):
|
||||
return frappe.utils.ceil(qty)
|
||||
|
||||
return qty
|
||||
|
||||
def __set_alternative_item(self, bom_item):
|
||||
if self.alternative_item_details.get(bom_item.rm_item_code):
|
||||
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
|
||||
|
||||
def __add_supplied_item(self, item_row, bom_item, qty):
|
||||
bom_item.conversion_factor = item_row.conversion_factor
|
||||
rm_obj = self.append(self.raw_material_table, bom_item)
|
||||
rm_obj.reference_name = item_row.name
|
||||
|
||||
if self.doctype == 'Purchase Order':
|
||||
rm_obj.required_qty = qty
|
||||
else:
|
||||
rm_obj.consumed_qty = 0
|
||||
rm_obj.purchase_order = item_row.purchase_order
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
|
||||
|
||||
if (self.available_materials.get(key) and self.available_materials[key]['batch_no']):
|
||||
new_rm_obj = None
|
||||
for batch_no, batch_qty in self.available_materials[key]['batch_no'].items():
|
||||
if batch_qty >= qty:
|
||||
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
|
||||
self.available_materials[key]['batch_no'][batch_no] -= qty
|
||||
return
|
||||
|
||||
elif qty > 0 and batch_qty > 0:
|
||||
qty -= batch_qty
|
||||
new_rm_obj = self.append(self.raw_material_table, bom_item)
|
||||
new_rm_obj.reference_name = item_row.name
|
||||
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
|
||||
self.available_materials[key]['batch_no'][batch_no] = 0
|
||||
|
||||
if abs(qty) > 0 and not new_rm_obj:
|
||||
self.__set_consumed_qty(rm_obj, qty)
|
||||
else:
|
||||
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
|
||||
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
|
||||
rm_obj.required_qty = required_qty
|
||||
rm_obj.consumed_qty = consumed_qty
|
||||
|
||||
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
|
||||
rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no,
|
||||
'required_qty': qty, 'purchase_order': item_row.purchase_order})
|
||||
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
|
||||
def __set_serial_nos(self, item_row, rm_obj):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
|
||||
if (self.available_materials.get(key) and self.available_materials[key]['serial_no']):
|
||||
used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)]
|
||||
rm_obj.serial_no = '\n'.join(used_serial_nos)
|
||||
|
||||
# Removed the used serial nos from the list
|
||||
for sn in used_serial_nos:
|
||||
self.available_materials[key]['serial_no'].remove(sn)
|
||||
|
||||
def set_consumed_qty_in_po(self):
|
||||
# Update consumed qty back in the purchase order
|
||||
if self.is_subcontracted != 'Yes':
|
||||
return
|
||||
|
||||
self.__get_purchase_orders()
|
||||
itemwise_consumed_qty = defaultdict(float)
|
||||
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
|
||||
consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
|
||||
|
||||
for row in consumed_items:
|
||||
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
|
||||
itemwise_consumed_qty[key] += row.consumed_qty
|
||||
|
||||
self.__update_consumed_qty_in_po(itemwise_consumed_qty)
|
||||
|
||||
def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
|
||||
fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name']
|
||||
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}
|
||||
|
||||
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'):
|
||||
key = (row.rm_item_code, row.main_item_code, row.parent)
|
||||
consumed_qty = itemwise_consumed_qty.get(key, 0)
|
||||
|
||||
if row.supplied_qty < consumed_qty:
|
||||
consumed_qty = row.supplied_qty
|
||||
|
||||
itemwise_consumed_qty[key] -= consumed_qty
|
||||
frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty)
|
||||
|
||||
def __validate_supplied_items(self):
|
||||
if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']:
|
||||
return
|
||||
|
||||
for row in self.get(self.raw_material_table):
|
||||
self.__validate_consumed_qty(row)
|
||||
|
||||
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
|
||||
if not self.__transferred_items or not self.__transferred_items.get(key):
|
||||
return
|
||||
|
||||
self.__validate_batch_no(row, key)
|
||||
self.__validate_serial_no(row, key)
|
||||
|
||||
def __validate_consumed_qty(self, row):
|
||||
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
|
||||
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
|
||||
|
||||
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
|
||||
|
||||
def __validate_batch_no(self, row, key):
|
||||
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
|
||||
link = get_link_to_form('Purchase Order', row.purchase_order)
|
||||
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
|
||||
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
|
||||
|
||||
def __validate_serial_no(self, row, key):
|
||||
if row.get('serial_no'):
|
||||
serial_nos = get_serial_nos(row.get('serial_no'))
|
||||
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no'))
|
||||
|
||||
if incorrect_sn:
|
||||
incorrect_sn = "\n".join(incorrect_sn)
|
||||
link = get_link_to_form('Purchase Order', row.purchase_order)
|
||||
msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}'
|
||||
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
|
@ -54,6 +54,7 @@ class calculate_taxes_and_totals(object):
|
||||
if item.item_code and item.get('item_tax_template'):
|
||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||
args = {
|
||||
'net_rate': item.net_rate or item.rate,
|
||||
'tax_category': self.doc.get('tax_category'),
|
||||
'posting_date': self.doc.get('posting_date'),
|
||||
'bill_date': self.doc.get('bill_date'),
|
||||
@ -77,10 +78,12 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
taxes = _get_item_tax_template(args, item_taxes + item_group_taxes, for_validate=True)
|
||||
|
||||
if item.item_tax_template not in taxes:
|
||||
frappe.throw(_("Row {0}: Invalid Item Tax Template for item {1}").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
))
|
||||
if taxes:
|
||||
if item.item_tax_template not in taxes:
|
||||
item.item_tax_template = taxes[0]
|
||||
frappe.msgprint(_("Row {0}: Item Tax template updated as per validity and rate applied").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
))
|
||||
|
||||
def validate_conversion_rate(self):
|
||||
# validate conversion rate
|
||||
@ -375,10 +378,10 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
def manipulate_grand_total_for_inclusive_tax(self):
|
||||
# if fully inclusive taxes and diff
|
||||
if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]):
|
||||
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
||||
last_tax = self.doc.get("taxes")[-1]
|
||||
non_inclusive_tax_amount = sum([flt(d.tax_amount_after_discount_amount)
|
||||
for d in self.doc.get("taxes") if not d.included_in_print_rate])
|
||||
non_inclusive_tax_amount = sum(flt(d.tax_amount_after_discount_amount)
|
||||
for d in self.doc.get("taxes") if not d.included_in_print_rate)
|
||||
|
||||
diff = self.doc.total + non_inclusive_tax_amount \
|
||||
- flt(last_tax.total, last_tax.precision("total"))
|
||||
@ -518,8 +521,8 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
def calculate_total_advance(self):
|
||||
if self.doc.docstatus < 2:
|
||||
total_allocated_amount = sum([flt(adv.allocated_amount, adv.precision("allocated_amount"))
|
||||
for adv in self.doc.get("advances")])
|
||||
total_allocated_amount = sum(flt(adv.allocated_amount, adv.precision("allocated_amount"))
|
||||
for adv in self.doc.get("advances"))
|
||||
|
||||
self.doc.total_advance = flt(total_allocated_amount, self.doc.precision("total_advance"))
|
||||
|
||||
@ -619,7 +622,7 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
if self.doc.doctype == "Sales Invoice" \
|
||||
and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \
|
||||
and any([d.type == "Cash" for d in self.doc.payments]):
|
||||
and any(d.type == "Cash" for d in self.doc.payments):
|
||||
grand_total = self.doc.rounded_total or self.doc.grand_total
|
||||
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
|
||||
|
||||
@ -655,7 +658,13 @@ class calculate_taxes_and_totals(object):
|
||||
item.margin_type = None
|
||||
item.margin_rate_or_amount = 0.0
|
||||
|
||||
if item.margin_type and item.margin_rate_or_amount:
|
||||
if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
|
||||
item.margin_type = "Amount"
|
||||
item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
|
||||
item.precision("margin_rate_or_amount"))
|
||||
item.rate_with_margin = item.rate
|
||||
|
||||
elif item.margin_type and item.margin_rate_or_amount:
|
||||
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
|
||||
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
|
||||
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
|
||||
@ -683,7 +692,6 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
self.calculate_paid_amount()
|
||||
|
||||
|
||||
def get_itemised_tax_breakup_html(doc):
|
||||
if not doc.taxes:
|
||||
return
|
||||
|
@ -26,8 +26,8 @@ class TestMapper(unittest.TestCase):
|
||||
|
||||
# Assert that all inserted items are present in updated sales order
|
||||
src_items = item_list_1 + item_list_2 + item_list_3
|
||||
self.assertEqual(set([d for d in src_items]),
|
||||
set([d.item_code for d in updated_so.items]))
|
||||
self.assertEqual(set(d for d in src_items),
|
||||
set(d.item_code for d in updated_so.items))
|
||||
|
||||
|
||||
def make_quotation(self, item_list, customer):
|
||||
|
@ -113,7 +113,7 @@ def post_process(doctype, data):
|
||||
doc.set_indicator()
|
||||
|
||||
doc.status_display = ", ".join(doc.status_display)
|
||||
doc.items_preview = ", ".join([d.item_name for d in doc.items if d.item_name])
|
||||
doc.items_preview = ", ".join(d.item_name for d in doc.items if d.item_name)
|
||||
result.append(doc)
|
||||
|
||||
return result
|
||||
|
@ -38,7 +38,7 @@ class Appointment(Document):
|
||||
number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents')
|
||||
if not number_of_agents == 0:
|
||||
if (number_of_appointments_in_same_slot >= number_of_agents):
|
||||
frappe.throw('Time slot is not available')
|
||||
frappe.throw(_('Time slot is not available'))
|
||||
# Link lead
|
||||
if not self.party:
|
||||
lead = self.find_lead_by_email()
|
||||
@ -75,10 +75,10 @@ class Appointment(Document):
|
||||
subject=_('Appointment Confirmation'))
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.msgprint(
|
||||
'Please check your email to confirm the appointment')
|
||||
_('Please check your email to confirm the appointment'))
|
||||
else :
|
||||
frappe.msgprint(
|
||||
'Appointment was created. But no lead was found. Please check the email to confirm')
|
||||
_('Appointment was created. But no lead was found. Please check the email to confirm'))
|
||||
|
||||
def on_change(self):
|
||||
# Sync Calendar
|
||||
@ -91,7 +91,7 @@ class Appointment(Document):
|
||||
|
||||
def set_verified(self, email):
|
||||
if not email == self.customer_email:
|
||||
frappe.throw('Email verification failed.')
|
||||
frappe.throw(_('Email verification failed.'))
|
||||
# Create new lead
|
||||
self.create_lead_and_link()
|
||||
# Remove unverified status
|
||||
@ -184,7 +184,7 @@ class Appointment(Document):
|
||||
appointment_event.insert(ignore_permissions=True)
|
||||
self.calendar_event = appointment_event.name
|
||||
self.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def _get_verify_url(self):
|
||||
verify_route = '/book_appointment/verify'
|
||||
params = {
|
||||
|
@ -280,7 +280,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:",
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"label": "Territory",
|
||||
@ -431,7 +430,7 @@
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2021-01-06 19:42:46.190051",
|
||||
"modified": "2021-06-04 10:11:22.831139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
||||
get_chart_data: function (_columns, result) {
|
||||
return {
|
||||
data: {
|
||||
labels: result.map(d => d[0]),
|
||||
labels: result.map(d => d.creation_date),
|
||||
datasets: [{
|
||||
name: "First Response Time",
|
||||
values: result.map(d => d[1])
|
||||
values: result.map(d => d.first_response_time)
|
||||
}]
|
||||
},
|
||||
type: "line",
|
||||
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
||||
hide_days: 0,
|
||||
hide_seconds: 0
|
||||
};
|
||||
value = frappe.utils.get_formatted_duration(d, duration_options);
|
||||
return value;
|
||||
return frappe.utils.get_formatted_duration(d, duration_options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ frappe.ui.form.on('Assessment Result', {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.trigger('setup_chart');
|
||||
}
|
||||
frm.set_df_property('details', 'read_only', 1);
|
||||
|
||||
frm.get_field('details').grid.cannot_add_rows = true;
|
||||
|
||||
frm.set_query('course', function() {
|
||||
return {
|
||||
|
@ -91,4 +91,4 @@ def check_activity_exists(enrollment, content_type, content):
|
||||
if activity:
|
||||
return activity[0].name
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
@ -75,7 +75,7 @@ class CourseSchedulingTool(Document):
|
||||
"""Validates if Course Start Date is greater than Course End Date"""
|
||||
if self.course_start_date > self.course_end_date:
|
||||
frappe.throw(
|
||||
"Course Start Date cannot be greater than Course End Date.")
|
||||
_("Course Start Date cannot be greater than Course End Date."))
|
||||
|
||||
def delete_course_schedule(self, rescheduled, reschedule_errors):
|
||||
"""Delete all course schedule within the Date range and specified filters"""
|
||||
|
@ -219,7 +219,6 @@ def get_quiz(quiz_name, course):
|
||||
try:
|
||||
quiz = frappe.get_doc("Quiz", quiz_name)
|
||||
questions = quiz.get_questions()
|
||||
duration = quiz.duration
|
||||
except:
|
||||
frappe.throw(_("Quiz {0} does not exist").format(quiz_name), frappe.DoesNotExistError)
|
||||
return None
|
||||
@ -236,15 +235,17 @@ def get_quiz(quiz_name, course):
|
||||
return {
|
||||
'questions': questions,
|
||||
'activity': None,
|
||||
'duration':duration
|
||||
'is_time_bound': quiz.is_time_bound,
|
||||
'duration': quiz.duration
|
||||
}
|
||||
|
||||
student = get_current_student()
|
||||
course_enrollment = get_enrollment("course", course, student.name)
|
||||
status, score, result, time_taken = check_quiz_completion(quiz, course_enrollment)
|
||||
return {
|
||||
'questions': questions,
|
||||
'questions': questions,
|
||||
'activity': {'is_complete': status, 'score': score, 'result': result, 'time_taken': time_taken},
|
||||
'is_time_bound': quiz.is_time_bound,
|
||||
'duration': quiz.duration
|
||||
}
|
||||
|
||||
@ -372,9 +373,9 @@ def check_content_completion(content_name, content_type, enrollment_name):
|
||||
def check_quiz_completion(quiz, enrollment_name):
|
||||
attempts = frappe.get_all("Quiz Activity",
|
||||
filters={
|
||||
'enrollment': enrollment_name,
|
||||
'enrollment': enrollment_name,
|
||||
'quiz': quiz.name
|
||||
},
|
||||
},
|
||||
fields=["name", "activity_date", "score", "status", "time_taken"]
|
||||
)
|
||||
status = False if quiz.max_attempts == 0 else bool(len(attempts) >= quiz.max_attempts)
|
||||
@ -389,4 +390,4 @@ def check_quiz_completion(quiz, enrollment_name):
|
||||
time_taken = attempts[0]['time_taken']
|
||||
if result == 'Pass':
|
||||
status = True
|
||||
return status, score, result, time_taken
|
||||
return status, score, result, time_taken
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user