Merge branch 'develop' into item-variants-perf

This commit is contained in:
Marica 2022-02-16 13:03:32 +05:30 committed by GitHub
commit 201a38bfbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
139 changed files with 3187 additions and 2309 deletions

1
.github/stale.yml vendored
View File

@ -30,6 +30,7 @@ issues:
exemptLabels: exemptLabels:
- valid - valid
- to-validate - to-validate
- QA
markComment: > markComment: >
This issue has been automatically marked as inactive because it has not had This issue has been automatically marked as inactive because it has not had
recent activity and it wasn't validated by maintainer team. It will be recent activity and it wasn't validated by maintainer team. It will be

View File

@ -0,0 +1,44 @@
describe("Bulk Transaction Processing", () => {
before(() => {
cy.login();
cy.visit("/app/website");
});
it("Creates To Sales Order", () => {
cy.visit("/app/sales-order");
cy.url().should("include", "/sales-order");
cy.window()
.its("frappe.csrf_token")
.then((csrf_token) => {
return cy
.request({
url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Frappe-CSRF-Token": csrf_token,
},
timeout: 60000,
})
.then((res) => {
expect(res.status).eq(200);
});
});
cy.wait(5000);
cy.get(
".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
).check({ force: true });
cy.wait(3000);
cy.get(".actions-btn-group > .btn-primary").click({ force: true });
cy.wait(3000);
cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
.contains("Sales Invoice")
.click({ force: true });
cy.wait(3000);
cy.get(".modal-content > .modal-footer > .standard-actions")
.contains("Yes")
.click({ force: true });
cy.contains("Creation of Sales Invoice successful");
});
});

View File

@ -2,8 +2,6 @@ import inspect
import frappe import frappe
from erpnext.hooks import regional_overrides
__version__ = '14.0.0-dev' __version__ = '14.0.0-dev'
def get_default_company(user=None): def get_default_company(user=None):
@ -121,14 +119,17 @@ def allow_regional(fn):
@erpnext.allow_regional @erpnext.allow_regional
def myfunction(): def myfunction():
pass''' pass'''
def caller(*args, **kwargs): def caller(*args, **kwargs):
region = get_region() overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__ function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
if region in regional_overrides and fn_name in regional_overrides[region]:
return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs) if not overrides or function_path not in overrides:
else:
return fn(*args, **kwargs) return fn(*args, **kwargs)
# Priority given to last installed app
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
return caller return caller
def get_last_membership(member): def get_last_membership(member):

View File

@ -7,35 +7,30 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"accounts_transactions_settings_section", "invoice_and_billing_tab",
"over_billing_allowance", "enable_features_section",
"role_allowed_to_over_bill",
"credit_controller",
"make_payment_via_journal_entry",
"column_break_11",
"check_supplier_invoice_uniqueness",
"unlink_payment_on_cancellation_of_invoice", "unlink_payment_on_cancellation_of_invoice",
"automatically_fetch_payment_terms",
"delete_linked_ledger_entries",
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order", "unlink_advance_payment_on_cancelation_of_order",
"column_break_13",
"delete_linked_ledger_entries",
"invoicing_features_section",
"check_supplier_invoice_uniqueness",
"automatically_fetch_payment_terms",
"column_break_17",
"enable_common_party_accounting", "enable_common_party_accounting",
"post_change_gl_entries",
"enable_discount_accounting", "enable_discount_accounting",
"tax_settings_section", "report_setting_section",
"determine_address_tax_category_from", "use_custom_cash_flow",
"column_break_19",
"add_taxes_from_item_tax_template",
"period_closing_settings_section",
"acc_frozen_upto",
"frozen_accounts_modifier",
"column_break_4",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
"book_deferred_entries_based_on", "book_deferred_entries_based_on",
"column_break_18", "column_break_18",
"automatically_process_deferred_accounting_entry", "automatically_process_deferred_accounting_entry",
"book_deferred_entries_via_journal_entry", "book_deferred_entries_via_journal_entry",
"submit_journal_entries", "submit_journal_entries",
"tax_settings_section",
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
"print_settings", "print_settings",
"show_inclusive_tax_in_print", "show_inclusive_tax_in_print",
"column_break_12", "column_break_12",
@ -43,8 +38,25 @@
"currency_exchange_section", "currency_exchange_section",
"allow_stale", "allow_stale",
"stale_days", "stale_days",
"report_settings_sb", "invoicing_settings_tab",
"use_custom_cash_flow" "accounts_transactions_settings_section",
"over_billing_allowance",
"column_break_11",
"role_allowed_to_over_bill",
"credit_controller",
"make_payment_via_journal_entry",
"pos_tab",
"pos_setting_section",
"post_change_gl_entries",
"assets_tab",
"asset_settings_section",
"book_asset_depreciation_entry_automatically",
"closing_settings_tab",
"period_closing_settings_section",
"acc_frozen_upto",
"column_break_25",
"frozen_accounts_modifier",
"report_settings_sb"
], ],
"fields": [ "fields": [
{ {
@ -70,10 +82,6 @@
"label": "Determine Address Tax Category From", "label": "Determine Address Tax Category From",
"options": "Billing Address\nShipping Address" "options": "Billing Address\nShipping Address"
}, },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{ {
"fieldname": "credit_controller", "fieldname": "credit_controller",
"fieldtype": "Link", "fieldtype": "Link",
@ -83,6 +91,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field",
"fieldname": "check_supplier_invoice_uniqueness", "fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness" "label": "Check Supplier Invoice Number Uniqueness"
@ -168,7 +177,7 @@
"description": "Only select this if you have set up the Cash Flow Mapper documents", "description": "Only select this if you have set up the Cash Flow Mapper documents",
"fieldname": "use_custom_cash_flow", "fieldname": "use_custom_cash_flow",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Use Custom Cash Flow Format" "label": "Enable Custom Cash Flow Format"
}, },
{ {
"default": "0", "default": "0",
@ -241,7 +250,7 @@
{ {
"fieldname": "accounts_transactions_settings_section", "fieldname": "accounts_transactions_settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Transactions Settings" "label": "Credit Limit Settings"
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
@ -272,9 +281,72 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"fieldname": "enable_common_party_accounting", "fieldname": "enable_common_party_accounting",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Common Party Accounting" "label": "Enable Common Party Accounting"
},
{
"fieldname": "enable_features_section",
"fieldtype": "Section Break",
"label": "Invoice Cancellation"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"fieldname": "asset_settings_section",
"fieldtype": "Section Break",
"label": "Asset Settings"
},
{
"fieldname": "invoicing_settings_tab",
"fieldtype": "Tab Break",
"label": "Credit Limits"
},
{
"fieldname": "assets_tab",
"fieldtype": "Tab Break",
"label": "Assets"
},
{
"fieldname": "closing_settings_tab",
"fieldtype": "Tab Break",
"label": "Accounts Closing"
},
{
"fieldname": "pos_setting_section",
"fieldtype": "Section Break",
"label": "POS Setting"
},
{
"fieldname": "invoice_and_billing_tab",
"fieldtype": "Tab Break",
"label": "Invoice and Billing"
},
{
"fieldname": "invoicing_features_section",
"fieldtype": "Section Break",
"label": "Invoicing Features"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_tab",
"fieldtype": "Tab Break",
"label": "POS"
},
{
"fieldname": "report_setting_section",
"fieldtype": "Section Break",
"label": "Report Setting"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -282,7 +354,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-10-11 17:42:36.427699", "modified": "2022-02-04 12:32:36.805652",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",
@ -309,5 +381,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -14,6 +14,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}); });
}, },
onload: function (frm) {
frm.trigger('bank_account');
},
refresh: function (frm) { refresh: function (frm) {
frappe.require("bank-reconciliation-tool.bundle.js", () => frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool") frm.trigger("make_reconciliation_tool")
@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
bank_account: function (frm) { bank_account: function (frm) {
frappe.db.get_value( frappe.db.get_value(
"Bank Account", "Bank Account",
frm.bank_account, frm.doc.bank_account,
"account", "account",
(r) => { (r) => {
frappe.db.get_value( frappe.db.get_value(

View File

@ -2,7 +2,7 @@
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"creation": "2018-11-22 22:45:00.370913", "creation": "2022-01-19 01:09:13.297137",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 1, "editable_grid": 1,
@ -10,6 +10,9 @@
"field_order": [ "field_order": [
"title", "title",
"company", "company",
"column_break_3",
"disabled",
"section_break_5",
"taxes" "taxes"
], ],
"fields": [ "fields": [
@ -36,10 +39,24 @@
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
} }
], ],
"links": [], "links": [],
"modified": "2021-03-08 19:50:21.416513", "modified": "2022-01-18 21:11:23.105589",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Item Tax Template", "name": "Item Tax Template",
@ -82,6 +99,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@ -8,6 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", { frappe.ui.form.on("Journal Entry", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("bank_account", "account", "account"); frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -167,7 +167,8 @@ class OpeningInvoiceCreationTool(Document):
"is_pos": 0, "is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0, "update_stock": 0,
"invoice_number": row.invoice_number "invoice_number": row.invoice_number,
"disable_rounded_total": 1
}) })
accounting_dimension = get_accounting_dimensions() accounting_dimension = get_accounting_dimensions()

View File

@ -172,9 +172,10 @@ class POSInvoice(SalesInvoice):
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_stock_availablility(self): def validate_stock_availablility(self):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
if self.is_return or self.docstatus != 1: if self.is_return or self.docstatus != 1:
return return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'): for d in self.get('items'):
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item')) is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
if is_service_item: if is_service_item:
@ -186,7 +187,7 @@ class POSInvoice(SalesInvoice):
elif d.batch_no: elif d.batch_no:
self.validate_pos_reserved_batch_qty(d) self.validate_pos_reserved_batch_qty(d)
else: else:
if allow_negative_stock: if is_negative_stock_allowed(item_code=d.item_code):
return return
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse) available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)

View File

@ -586,23 +586,29 @@ class TestPOSInvoice(unittest.TestCase):
item_price.insert() item_price.insert()
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10) pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
pr.save() pr.save()
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
# rate shouldn't change
self.assertEquals(pos_inv.items[0].rate, 405)
pos_inv.ignore_pricing_rule = 1 try:
pos_inv.items[0].rate = 300 pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
pos_inv.save() pos_inv.items[0].rate = 300
self.assertEquals(pos_inv.ignore_pricing_rule, 1) pos_inv.save()
# rate should change since pricing rules are ignored self.assertEquals(pos_inv.items[0].discount_percentage, 10)
self.assertEquals(pos_inv.items[0].rate, 300) # rate shouldn't change
self.assertEquals(pos_inv.items[0].rate, 405)
item_price.delete() pos_inv.ignore_pricing_rule = 1
pos_inv.delete() pos_inv.save()
pr.delete() self.assertEquals(pos_inv.ignore_pricing_rule, 1)
# rate should reset since pricing rules are ignored
self.assertEquals(pos_inv.items[0].rate, 450)
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.items[0].rate, 300)
finally:
item_price.delete()
pos_inv.delete()
pr.delete()
def create_pos_invoice(**args): def create_pos_invoice(**args):

View File

@ -84,12 +84,20 @@ class POSInvoiceMergeLog(Document):
sales_invoice.set_posting_time = 1 sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save() sales_invoice.save()
self.write_off_fractional_amount(sales_invoice, data)
sales_invoice.submit() sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name self.consolidated_invoice = sales_invoice.name
return sales_invoice.name return sales_invoice.name
def write_off_fractional_amount(self, invoice, data):
pos_invoice_grand_total = sum(d.grand_total for d in data)
if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
invoice.save()
def process_merging_into_credit_note(self, data): def process_merging_into_credit_note(self, data):
credit_note = self.get_new_sales_invoice() credit_note = self.get_new_sales_invoice()
credit_note.is_return = 1 credit_note.is_return = 1
@ -102,6 +110,7 @@ class POSInvoiceMergeLog(Document):
# TODO: return could be against multiple sales invoice which could also have been consolidated? # TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice # credit_note.return_against = self.consolidated_invoice
credit_note.save() credit_note.save()
self.write_off_fractional_amount(credit_note, data)
credit_note.submit() credit_note.submit()
self.consolidated_credit_note = credit_note.name self.consolidated_credit_note = credit_note.name
@ -135,9 +144,15 @@ class POSInvoiceMergeLog(Document):
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse): i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
found = True found = True
i.qty = i.qty + item.qty i.qty = i.qty + item.qty
i.amount = i.amount + item.net_amount
i.net_amount = i.amount
i.base_amount = i.base_amount + item.base_net_amount
i.base_net_amount = i.base_amount
if not found: if not found:
item.rate = item.net_rate item.rate = item.net_rate
item.amount = item.net_amount
item.base_amount = item.base_net_amount
item.price_list_rate = 0 item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item) items.append(si_item)
@ -169,6 +184,7 @@ class POSInvoiceMergeLog(Document):
found = True found = True
if not found: if not found:
payments.append(payment) payments.append(payment)
rounding_adjustment += doc.rounding_adjustment rounding_adjustment += doc.rounding_adjustment
rounded_total += doc.rounded_total rounded_total += doc.rounded_total
base_rounding_adjustment += doc.base_rounding_adjustment base_rounding_adjustment += doc.base_rounding_adjustment

View File

@ -12,6 +12,7 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices, consolidate_pos_invoices,
) )
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestPOSInvoiceMergeLog(unittest.TestCase): class TestPOSInvoiceMergeLog(unittest.TestCase):
@ -150,3 +151,132 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidation_round_off_error_1(self):
'''
Test round off error in consolidated invoice creation if POS Invoice has inclusive tax
'''
frappe.db.sql("delete from `tabPOS Invoice`")
try:
make_stock_entry(
to_warehouse="_Test Warehouse - _TC",
item_code="_Test Item",
rate=8000,
qty=10,
)
init_user_and_profile()
inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 0)
self.assertEqual(consolidated_invoice.status, 'Paid')
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidation_round_off_error_2(self):
'''
Test the same case as above but with an Unpaid POS Invoice
'''
frappe.db.sql("delete from `tabPOS Invoice`")
try:
make_stock_entry(
to_warehouse="_Test Warehouse - _TC",
item_code="_Test Item",
rate=8000,
qty=10,
)
init_user_and_profile()
inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
})
inv2.insert()
inv2.submit()
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
inv3.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000
})
inv3.insert()
inv3.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
self.assertNotEqual(consolidated_invoice.status, 'Paid')
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@ -249,13 +249,17 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
"free_item_data": [], "free_item_data": [],
"parent": args.parent, "parent": args.parent,
"parenttype": args.parenttype, "parenttype": args.parenttype,
"child_docname": args.get('child_docname') "child_docname": args.get('child_docname'),
}) })
if args.ignore_pricing_rule or not args.item_code: if args.ignore_pricing_rule or not args.item_code:
if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"): if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), item_details = remove_pricing_rule_for_item(
item_details, args.get('item_code')) args.get("pricing_rules"),
item_details,
item_code=args.get("item_code"),
rate=args.get("price_list_rate"),
)
return item_details return item_details
update_args_for_pricing_rule(args) update_args_for_pricing_rule(args)
@ -308,8 +312,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if not doc: return item_details if not doc: return item_details
elif args.get("pricing_rules"): elif args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), item_details = remove_pricing_rule_for_item(
item_details, args.get('item_code')) args.get("pricing_rules"),
item_details,
item_code=args.get("item_code"),
rate=args.get("price_list_rate"),
)
return item_details return item_details
@ -390,7 +398,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += (pricing_rule.get(field, 0) item_details[field] += (pricing_rule.get(field, 0)
if pricing_rule else args.get(field, 0)) if pricing_rule else args.get(field, 0))
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
from erpnext.accounts.doctype.pricing_rule.utils import ( from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules, get_applied_pricing_rules,
get_pricing_rule_items, get_pricing_rule_items,
@ -403,6 +411,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
if pricing_rule.rate_or_discount == 'Discount Percentage': if pricing_rule.rate_or_discount == 'Discount Percentage':
item_details.discount_percentage = 0.0 item_details.discount_percentage = 0.0
item_details.discount_amount = 0.0 item_details.discount_amount = 0.0
item_details.rate = rate or 0.0
if pricing_rule.rate_or_discount == 'Discount Amount': if pricing_rule.rate_or_discount == 'Discount Amount':
item_details.discount_amount = 0.0 item_details.discount_amount = 0.0
@ -421,6 +430,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
item_details.applied_on_items = ','.join(items) item_details.applied_on_items = ','.join(items)
item_details.pricing_rules = '' item_details.pricing_rules = ''
item_details.pricing_rule_removed = True
return item_details return item_details
@ -432,9 +442,12 @@ def remove_pricing_rules(item_list):
out = [] out = []
for item in item_list: for item in item_list:
item = frappe._dict(item) item = frappe._dict(item)
if item.get('pricing_rules'): if item.get("pricing_rules"):
out.append(remove_pricing_rule_for_item(item.get("pricing_rules"), out.append(
item, item.item_code)) remove_pricing_rule_for_item(
item.get("pricing_rules"), item, item.item_code, item.get("price_list_rate")
)
)
return out return out

View File

@ -628,6 +628,46 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]: for doc in [si, si1]:
doc.delete() doc.delete()
def test_remove_pricing_rule(self):
item = make_item("Water Flask")
make_item_price("Water Flask", "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
"price_or_product_discount": "Price",
"items": [{
"item_code": "Water Flask",
}],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Discount Percentage",
"discount_percentage": 20,
"company": "_Test Company"
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
si.selling_price_list = "_Test Price List"
si.save()
self.assertEqual(si.items[0].price_list_rate, 100)
self.assertEqual(si.items[0].discount_percentage, 20)
self.assertEqual(si.items[0].rate, 80)
si.ignore_pricing_rule = 1
si.save()
self.assertEqual(si.items[0].discount_percentage, 0)
self.assertEqual(si.items[0].rate, 100)
si.delete()
rule.delete()
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
def test_multiple_pricing_rules_with_min_qty(self): def test_multiple_pricing_rules_with_min_qty(self):
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
@ -648,6 +688,7 @@ class TestPricingRule(unittest.TestCase):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
test_dependencies = ["Campaign"] test_dependencies = ["Campaign"]
def make_pricing_rule(**args): def make_pricing_rule(**args):

View File

@ -178,8 +178,8 @@ class PurchaseInvoice(BuyingController):
if self.supplier and account.account_type != "Payable": if self.supplier and account.account_type != "Payable":
frappe.throw( frappe.throw(
_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.") _("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account") .format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account")
) )
self.party_account_currency = account.account_currency self.party_account_currency = account.account_currency
@ -537,8 +537,11 @@ class PurchaseInvoice(BuyingController):
voucher_wise_stock_value = {} voucher_wise_stock_value = {}
if self.update_stock: if self.update_stock:
for d in frappe.get_all('Stock Ledger Entry', stock_ledger_entries = frappe.get_all("Stock Ledger Entry",
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): fields = ["voucher_detail_no", "stock_value_difference", "warehouse"],
filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}
)
for d in stock_ledger_entries:
voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes") valuation_tax_accounts = [d.account_head for d in self.get("taxes")

View File

@ -56,4 +56,14 @@ frappe.listview_settings["Purchase Invoice"] = {
]; ];
} }
}, },
onload: function(listview) {
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
});
listview.page.add_action_item(__("Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment");
});
}
}; };

View File

@ -285,7 +285,7 @@ class SalesInvoice(SellingController):
filters={ invoice_or_credit_note: self.name }, filters={ invoice_or_credit_note: self.name },
pluck="pos_closing_entry" pluck="pos_closing_entry"
) )
if pos_closing_entry: if pos_closing_entry and pos_closing_entry[0]:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
frappe.bold("Consolidated Sales Invoice"), frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0]) get_link_to_form("POS Closing Entry", pos_closing_entry[0])
@ -572,7 +572,10 @@ class SalesInvoice(SellingController):
frappe.throw(msg, title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable": if self.customer and account.account_type != "Receivable":
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " " msg = _("Please ensure {} account {} is a Receivable account.").format(
frappe.bold("Debit To"),
frappe.bold(self.debit_to)
) + " "
msg += _("Change the account type to Receivable or select a different account.") msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
@ -1249,14 +1252,14 @@ class SalesInvoice(SellingController):
def update_billing_status_in_dn(self, update_modified=True): def update_billing_status_in_dn(self, update_modified=True):
updated_delivery_notes = [] updated_delivery_notes = []
for d in self.get("items"): for d in self.get("items"):
if d.dn_detail: if d.so_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
elif d.dn_detail:
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", d.dn_detail) where dn_detail=%s and docstatus=1""", d.dn_detail)
billed_amt = billed_amt and billed_amt[0][0] or 0 billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
updated_delivery_notes.append(d.delivery_note) updated_delivery_notes.append(d.delivery_note)
elif d.so_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
for dn in set(updated_delivery_notes): for dn in set(updated_delivery_notes):
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)

View File

@ -21,5 +21,15 @@ frappe.listview_settings['Sales Invoice'] = {
}; };
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status]; return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
}, },
right_column: "grand_total" right_column: "grand_total",
onload: function(listview) {
listview.page.add_action_item(__("Delivery Note"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
});
listview.page.add_action_item(__("Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment");
});
}
}; };

View File

@ -2,12 +2,13 @@
"actions": [], "actions": [],
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:title", "autoname": "field:title",
"creation": "2018-11-22 23:38:39.668804", "creation": "2022-01-19 01:09:28.920486",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title" "title",
"disabled"
], ],
"fields": [ "fields": [
{ {
@ -18,14 +19,21 @@
"label": "Title", "label": "Title",
"reqd": 1, "reqd": 1,
"unique": 1 "unique": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-03-03 11:50:38.748872", "modified": "2022-01-18 21:13:41.161017",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Tax Category", "name": "Tax Category",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -65,5 +73,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -319,13 +319,18 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
""" """
if not gl_entries: if not gl_entries:
gl_entries = frappe.get_all("GL Entry", gl_entry = frappe.qb.DocType("GL Entry")
fields = ["*"], gl_entries = (frappe.qb.from_(
filters = { gl_entry
"voucher_type": voucher_type, ).select(
"voucher_no": voucher_no, '*'
"is_cancelled": 0 ).where(
}) gl_entry.voucher_type == voucher_type
).where(
gl_entry.voucher_no == voucher_no
).where(
gl_entry.is_cancelled == 0
).for_update()).run(as_dict=1)
if gl_entries: if gl_entries:
validate_accounting_period(gl_entries) validate_accounting_period(gl_entries)
@ -333,23 +338,24 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
for entry in gl_entries: for entry in gl_entries:
entry['name'] = None new_gle = copy.deepcopy(entry)
debit = entry.get('debit', 0) new_gle['name'] = None
credit = entry.get('credit', 0) debit = new_gle.get('debit', 0)
credit = new_gle.get('credit', 0)
debit_in_account_currency = entry.get('debit_in_account_currency', 0) debit_in_account_currency = new_gle.get('debit_in_account_currency', 0)
credit_in_account_currency = entry.get('credit_in_account_currency', 0) credit_in_account_currency = new_gle.get('credit_in_account_currency', 0)
entry['debit'] = credit new_gle['debit'] = credit
entry['credit'] = debit new_gle['credit'] = debit
entry['debit_in_account_currency'] = credit_in_account_currency new_gle['debit_in_account_currency'] = credit_in_account_currency
entry['credit_in_account_currency'] = debit_in_account_currency new_gle['credit_in_account_currency'] = debit_in_account_currency
entry['remarks'] = "On cancellation of " + entry['voucher_no'] new_gle['remarks'] = "On cancellation of " + new_gle['voucher_no']
entry['is_cancelled'] = 1 new_gle['is_cancelled'] = 1
if entry['debit'] or entry['credit']: if new_gle['debit'] or new_gle['credit']:
make_entry(entry, adv_adj, "Yes") make_entry(new_gle, adv_adj, "Yes")
def check_freezing_date(posting_date, adv_adj=False): def check_freezing_date(posting_date, adv_adj=False):

View File

@ -1,23 +0,0 @@
{
"align_labels_right": 0,
"creation": "2017-08-08 12:33:04.773099",
"custom_format": 1,
"disabled": 0,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"Serial No\") }}:</b> {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"modified": "2020-04-29 16:39:12.936215",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST POS Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@ -120,11 +120,11 @@ def check_opening_balance(asset, liability, equity):
opening_balance = 0 opening_balance = 0
float_precision = cint(frappe.db.get_default("float_precision")) or 2 float_precision = cint(frappe.db.get_default("float_precision")) or 2
if asset: if asset:
opening_balance = flt(asset[0].get("opening_balance", 0), float_precision) opening_balance = flt(asset[-1].get("opening_balance", 0), float_precision)
if liability: if liability:
opening_balance -= flt(liability[0].get("opening_balance", 0), float_precision) opening_balance -= flt(liability[-1].get("opening_balance", 0), float_precision)
if equity: if equity:
opening_balance -= flt(equity[0].get("opening_balance", 0), float_precision) opening_balance -= flt(equity[-1].get("opening_balance", 0), float_precision)
opening_balance = flt(opening_balance, float_precision) opening_balance = flt(opening_balance, float_precision)
if opening_balance: if opening_balance:

View File

@ -282,7 +282,8 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
total_row = { total_row = {
"account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
"account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
"currency": company_currency "currency": company_currency,
"opening_balance": 0.0
} }
for row in out: for row in out:
@ -294,6 +295,7 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
total_row.setdefault("total", 0.0) total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"]) total_row["total"] += flt(row["total"])
total_row["opening_balance"] += row["opening_balance"]
row["total"] = "" row["total"] = ""
if "total" in total_row: if "total" in total_row:

View File

@ -42,6 +42,11 @@ frappe.query_reports["Gross Profit"] = {
"parent_field": "parent_invoice", "parent_field": "parent_invoice",
"initial_depth": 3, "initial_depth": 3,
"formatter": function(value, row, column, data, default_formatter) { "formatter": function(value, row, column, data, default_formatter) {
if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
column._options = "Sales Invoice";
} else {
column._options = "Item";
}
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);
if (data && (data.indent == 0.0 || row[1].content == "Total")) { if (data && (data.indent == 0.0 || row[1].content == "Total")) {

View File

@ -23,7 +23,7 @@ def validate_filters(filters):
def get_result(filters, tds_docs, tds_accounts, tax_category_map): def get_result(filters, tds_docs, tds_accounts, tax_category_map):
supplier_map = get_supplier_pan_map() supplier_map = get_supplier_pan_map()
tax_rate_map = get_tax_rate_map(filters) tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(filters, tds_docs) gle_map = get_gle_map(tds_docs)
out = [] out = []
for name, details in gle_map.items(): for name, details in gle_map.items():
@ -78,7 +78,7 @@ def get_supplier_pan_map():
return supplier_map return supplier_map
def get_gle_map(filters, documents): def get_gle_map(documents):
# create gle_map of the form # create gle_map of the form
# {"purchase_invoice": list of dict of all gle created for this invoice} # {"purchase_invoice": list of dict of all gle created for this invoice}
gle_map = {} gle_map = {}
@ -86,7 +86,7 @@ def get_gle_map(filters, documents):
gle = frappe.db.get_all('GL Entry', gle = frappe.db.get_all('GL Entry',
{ {
"voucher_no": ["in", documents], "voucher_no": ["in", documents],
"credit": (">", 0) "is_cancelled": 0
}, },
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"], ["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
) )
@ -184,21 +184,28 @@ def get_tds_docs(filters):
payment_entries = [] payment_entries = []
journal_entries = [] journal_entries = []
tax_category_map = {} tax_category_map = {}
or_filters = {}
bank_accounts = frappe.get_all('Account', {'is_group': 0, 'account_type': 'Bank'}, pluck="name")
tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')}, tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')},
pluck="account") pluck="account")
query_filters = { query_filters = {
"credit": ('>', 0),
"account": ("in", tds_accounts), "account": ("in", tds_accounts),
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
"is_cancelled": 0 "is_cancelled": 0,
"against": ("not in", bank_accounts)
} }
if filters.get('supplier'): if filters.get("supplier"):
query_filters.update({'against': filters.get('supplier')}) del query_filters["account"]
del query_filters["against"]
or_filters = {
"against": filters.get('supplier'),
"party": filters.get('supplier')
}
tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"]) tds_docs = frappe.get_all("GL Entry", filters=query_filters, or_filters=or_filters, fields=["voucher_no", "voucher_type", "against", "party"])
for d in tds_docs: for d in tds_docs:
if d.voucher_type == "Purchase Invoice": if d.voucher_type == "Purchase Invoice":

View File

@ -0,0 +1,34 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Bulk Transaction Log', {
before_load: function(frm) {
query(frm);
},
refresh: function(frm) {
frm.disable_save();
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
query(frm);
}
);
});
}
});
function query(frm) {
frappe.call({
method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
args: {
log_date: frm.doc.log_date
}
}).then((r) => {
if (r.message) {
frm.remove_custom_button("Retry Failed Transactions");
} else {
frappe.show_alert(__("Retrying Failed Transactions"), 5);
}
});
}

View File

@ -0,0 +1,51 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-30 13:41:16.343827",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"log_date",
"logger_data"
],
"fields": [
{
"fieldname": "log_date",
"fieldtype": "Date",
"label": "Log Date",
"read_only": 1
},
{
"fieldname": "logger_data",
"fieldtype": "Table",
"label": "Logger Data",
"options": "Bulk Transaction Log Detail"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-02-03 17:23:02.935325",
"modified_by": "Administrator",
"module": "Bulk Transaction",
"name": "Bulk Transaction Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,66 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from datetime import date
import frappe
from frappe.model.document import Document
from erpnext.utilities.bulk_transaction import task, update_logger
class BulkTransactionLog(Document):
pass
@frappe.whitelist()
def retry_failing_transaction(log_date=None):
btp = frappe.qb.DocType("Bulk Transaction Log Detail")
data = (
frappe.qb.from_(btp)
.select(btp.transaction_name, btp.from_doctype, btp.to_doctype)
.distinct()
.where(btp.retried != 1)
.where(btp.transaction_status == "Failed")
.where(btp.date == log_date)
).run(as_dict=True)
if data:
if not log_date:
log_date = str(date.today())
if len(data) > 10:
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
else:
job(data, log_date)
else:
return "No Failed Records"
def job(data, log_date):
for d in data:
failed = []
try:
frappe.db.savepoint("before_creation_of_record")
task(d.transaction_name, d.from_doctype, d.to_doctype)
except Exception as e:
frappe.db.rollback(save_point="before_creation_of_record")
failed.append(e)
update_logger(
d.transaction_name,
e,
d.from_doctype,
d.to_doctype,
status="Failed",
log_date=log_date,
restarted=1
)
if not failed:
update_logger(
d.transaction_name,
None,
d.from_doctype,
d.to_doctype,
status="Success",
log_date=log_date,
restarted=1,
)

View File

@ -0,0 +1,81 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from datetime import date
import frappe
from erpnext.utilities.bulk_transaction import transaction_processing
class TestBulkTransactionLog(unittest.TestCase):
def setUp(self):
create_company()
create_customer()
create_item()
def test_for_single_record(self):
so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
data = frappe.db.get_list("Sales Invoice", filters = {"posting_date": date.today(), "customer": "Bulk Customer"}, fields=["*"])
if not data:
self.fail("No Sales Invoice Created !")
def test_entry_in_log(self):
so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
doc = frappe.get_doc("Bulk Transaction Log", str(date.today()))
for d in doc.get("logger_data"):
if d.transaction_name == so_name:
self.assertEqual(d.transaction_name, so_name)
self.assertEqual(d.transaction_status, "Success")
self.assertEqual(d.from_doctype, "Sales Order")
self.assertEqual(d.to_doctype, "Sales Invoice")
self.assertEqual(d.retried, 0)
def create_company():
if not frappe.db.exists('Company', '_Test Company'):
frappe.get_doc({
'doctype': 'Company',
'company_name': '_Test Company',
'country': 'India',
'default_currency': 'INR'
}).insert()
def create_customer():
if not frappe.db.exists('Customer', 'Bulk Customer'):
frappe.get_doc({
'doctype': 'Customer',
'customer_name': 'Bulk Customer'
}).insert()
def create_item():
if not frappe.db.exists("Item", "MK"):
frappe.get_doc({
"doctype": "Item",
"item_code": "MK",
"item_name": "Milk",
"description": "Milk",
"item_group": "Products"
}).insert()
def create_so(intent=None):
so = frappe.new_doc("Sales Order")
so.customer = "Bulk Customer"
so.company = "_Test Company"
so.transaction_date = date.today()
so.set_warehouse = "Finished Goods - _TC"
so.append("items", {
"item_code": "MK",
"delivery_date": date.today(),
"qty": 10,
"rate": 80,
})
so.insert()
so.submit()
return so.name

View File

@ -0,0 +1,86 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-30 13:38:30.926047",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"transaction_name",
"date",
"time",
"transaction_status",
"error_description",
"from_doctype",
"to_doctype",
"retried"
],
"fields": [
{
"fieldname": "transaction_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Name",
"options": "from_doctype"
},
{
"fieldname": "transaction_status",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Status",
"read_only": 1
},
{
"fieldname": "error_description",
"fieldtype": "Long Text",
"label": "Error Description",
"read_only": 1
},
{
"fieldname": "from_doctype",
"fieldtype": "Link",
"label": "From Doctype",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "to_doctype",
"fieldtype": "Link",
"label": "To Doctype",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date ",
"read_only": 1
},
{
"fieldname": "time",
"fieldtype": "Time",
"label": "Time",
"read_only": 1
},
{
"fieldname": "retried",
"fieldtype": "Int",
"label": "Retried",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-02-03 19:57:31.650359",
"modified_by": "Administrator",
"module": "Bulk Transaction",
"name": "Bulk Transaction Log Detail",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

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

View File

@ -6,14 +6,17 @@
"document_type": "Other", "document_type": "Other",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"supplier_and_price_defaults_section",
"supp_master_name", "supp_master_name",
"supplier_group", "supplier_group",
"column_break_4",
"buying_price_list", "buying_price_list",
"maintain_same_rate_action", "maintain_same_rate_action",
"role_to_override_stop_action", "role_to_override_stop_action",
"column_break_3", "transaction_settings_section",
"po_required", "po_required",
"pr_required", "pr_required",
"column_break_12",
"maintain_same_rate", "maintain_same_rate",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice", "bill_for_rejected_quantity_in_purchase_invoice",
@ -42,10 +45,6 @@
"label": "Default Buying Price List", "label": "Default Buying Price List",
"options": "Price List" "options": "Price List"
}, },
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{ {
"fieldname": "po_required", "fieldname": "po_required",
"fieldtype": "Select", "fieldtype": "Select",
@ -73,7 +72,7 @@
{ {
"fieldname": "subcontract", "fieldname": "subcontract",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Subcontract" "label": "Subcontracting Settings"
}, },
{ {
"default": "Material Transferred for Subcontract", "default": "Material Transferred for Subcontract",
@ -116,6 +115,24 @@
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice", "fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice" "label": "Bill for Rejected Quantity in Purchase Invoice"
},
{
"fieldname": "supplier_and_price_defaults_section",
"fieldtype": "Section Break",
"label": "Supplier and Price Defaults"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "transaction_settings_section",
"fieldtype": "Section Break",
"label": "Transaction Settings"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -123,7 +140,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-09-08 19:26:23.548837", "modified": "2022-01-27 17:57:58.367048",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",
@ -141,5 +158,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -29,8 +29,22 @@ frappe.listview_settings['Purchase Order'] = {
listview.call_for_selected_items(method, { "status": "Closed" }); listview.call_for_selected_items(method, { "status": "Closed" });
}); });
listview.page.add_menu_item(__("Re-open"), function () { listview.page.add_menu_item(__("Reopen"), function () {
listview.call_for_selected_items(method, { "status": "Submitted" }); listview.call_for_selected_items(method, { "status": "Submitted" });
}); });
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
});
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
});
listview.page.add_action_item(__("Advance Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
});
} }
}; };

View File

@ -682,17 +682,18 @@ class TestPurchaseOrder(unittest.TestCase):
bin1 = frappe.db.get_value("Bin", bin1 = frappe.db.get_value("Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
# Submit PO # Submit PO
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
bin2 = frappe.db.get_value("Bin", bin2 = frappe.db.get_value("Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
self.assertNotEqual(bin1.modified, bin2.modified)
# Create stock transfer # Create stock transfer
rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item", rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item",

View File

@ -142,6 +142,26 @@ def make_purchase_order(source_name, target_doc=None):
return doclist return doclist
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
doc = get_mapped_doc("Supplier Quotation", source_name, {
"Supplier Quotation": {
"doctype": "Purchase Invoice",
"validation": {
"docstatus": ["=", 1],
}
},
"Supplier Quotation Item": {
"doctype": "Purchase Invoice Item"
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges"
}
}, target_doc)
return doc
@frappe.whitelist() @frappe.whitelist()
def make_quotation(source_name, target_doc=None): def make_quotation(source_name, target_doc=None):
doclist = get_mapped_doc("Supplier Quotation", source_name, { doclist = get_mapped_doc("Supplier Quotation", source_name, {

View File

@ -8,5 +8,15 @@ frappe.listview_settings['Supplier Quotation'] = {
} else if(doc.status==="Expired") { } else if(doc.status==="Expired") {
return [__("Expired"), "gray", "status,=,Expired"]; return [__("Expired"), "gray", "status,=,Expired"];
} }
},
onload: function(listview) {
listview.page.add_action_item(__("Purchase Order"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
});
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice");
});
} }
}; };

View File

@ -49,7 +49,7 @@ valid_scorecard = [
"min_grade":0.0,"name":"Very Poor", "min_grade":0.0,"name":"Very Poor",
"prevent_rfqs":1, "prevent_rfqs":1,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":30.0, "max_grade":30.0,
"prevent_pos":1, "prevent_pos":1,
"warn_pos":0, "warn_pos":0,
@ -65,7 +65,7 @@ valid_scorecard = [
"name":"Poor", "name":"Poor",
"prevent_rfqs":1, "prevent_rfqs":1,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":50.0, "max_grade":50.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,
@ -81,7 +81,7 @@ valid_scorecard = [
"name":"Average", "name":"Average",
"prevent_rfqs":0, "prevent_rfqs":0,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":80.0, "max_grade":80.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,
@ -97,7 +97,7 @@ valid_scorecard = [
"name":"Excellent", "name":"Excellent",
"prevent_rfqs":0, "prevent_rfqs":0,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":100.0, "max_grade":100.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,

View File

@ -407,6 +407,22 @@ class AccountsController(TransactionBase):
if item_qty != len(get_serial_nos(item.get('serial_no'))): if item_qty != len(get_serial_nos(item.get('serial_no'))):
item.set(fieldname, value) item.set(fieldname, value)
elif (
ret.get("pricing_rule_removed")
and value is not None
and fieldname
in [
"discount_percentage",
"discount_amount",
"rate",
"margin_rate_or_amount",
"margin_type",
"remove_free_item",
]
):
# reset pricing rule fields if pricing_rule_removed
item.set(fieldname, value)
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
@ -1318,6 +1334,9 @@ class AccountsController(TransactionBase):
payment_schedule['discount_type'] = schedule.discount_type payment_schedule['discount_type'] = schedule.discount_type
payment_schedule['discount'] = schedule.discount payment_schedule['discount'] = schedule.discount
if not schedule.invoice_portion:
payment_schedule['payment_amount'] = schedule.payment_amount
self.append("payment_schedule", payment_schedule) self.append("payment_schedule", payment_schedule)
def set_due_date(self): def set_due_date(self):

View File

@ -710,6 +710,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
item_doc = frappe.get_cached_doc('Item', filters.get('item_code')) item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
item_group = filters.get('item_group') item_group = filters.get('item_group')
company = filters.get('company')
taxes = item_doc.taxes or [] taxes = item_doc.taxes or []
while item_group: while item_group:
@ -718,7 +719,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
item_group = item_group_doc.parent_item_group item_group = item_group_doc.parent_item_group
if not taxes: if not taxes:
return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """) return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True)
else: else:
valid_from = filters.get('valid_from') valid_from = filters.get('valid_from')
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
@ -727,7 +728,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
'item_code': filters.get('item_code'), 'item_code': filters.get('item_code'),
'posting_date': valid_from, 'posting_date': valid_from,
'tax_category': filters.get('tax_category'), 'tax_category': filters.get('tax_category'),
'company': filters.get('company') 'company': company
} }
taxes = _get_item_tax_template(args, taxes, for_validate=True) taxes = _get_item_tax_template(args, taxes, for_validate=True)

View File

@ -74,7 +74,8 @@ class SellingController(StockController):
doctype=self.doctype, company=self.company, doctype=self.doctype, company=self.company,
posting_date=self.get('posting_date'), posting_date=self.get('posting_date'),
fetch_payment_terms_template=fetch_payment_terms_template, fetch_payment_terms_template=fetch_payment_terms_template,
party_address=self.customer_address, shipping_address=self.shipping_address_name) party_address=self.customer_address, shipping_address=self.shipping_address_name,
company_address=self.get('company_address'))
if not self.meta.get_field("sales_team"): if not self.meta.get_field("sales_team"):
party_details.pop("sales_team") party_details.pop("sales_team")
self.update_if_missing(party_details) self.update_if_missing(party_details)

View File

@ -400,6 +400,16 @@ class StatusUpdater(Document):
ref_doc = frappe.get_doc(ref_dt, ref_dn) ref_doc = frappe.get_doc(ref_dt, ref_dn)
ref_doc.db_set("per_billed", per_billed) ref_doc.db_set("per_billed", per_billed)
# set billling status
if hasattr(ref_doc, 'billing_status'):
if ref_doc.per_billed < 0.001:
ref_doc.db_set("billing_status", "Not Billed")
elif ref_doc.per_billed > 99.999999:
ref_doc.db_set("billing_status", "Fully Billed")
else:
ref_doc.db_set("billing_status", "Partly Billed")
ref_doc.set_status(update=True) ref_doc.set_status(update=True)
def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"): def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"):

View File

@ -3,6 +3,7 @@
import json import json
from collections import defaultdict from collections import defaultdict
from typing import List, Tuple
import frappe import frappe
from frappe import _ from frappe import _
@ -181,33 +182,28 @@ class StockController(AccountsController):
return details return details
def get_items_and_warehouses(self): def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]:
items, warehouses = [], [] """Get list of items and warehouses affected by a transaction"""
if hasattr(self, "items"): if not (hasattr(self, "items") or hasattr(self, "packed_items")):
item_doclist = self.get("items") return [], []
elif self.doctype == "Stock Reconciliation":
item_doclist = []
data = json.loads(self.reconciliation_json)
for row in data[data.index(self.head_row)+1:]:
d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row))
item_doclist.append(d)
if item_doclist: item_rows = (self.get("items") or []) + (self.get("packed_items") or [])
for d in item_doclist:
if d.item_code and d.item_code not in items:
items.append(d.item_code)
if d.get("warehouse") and d.warehouse not in warehouses: items = {d.item_code for d in item_rows if d.item_code}
warehouses.append(d.warehouse)
if self.doctype == "Stock Entry": warehouses = set()
if d.get("s_warehouse") and d.s_warehouse not in warehouses: for d in item_rows:
warehouses.append(d.s_warehouse) if d.get("warehouse"):
if d.get("t_warehouse") and d.t_warehouse not in warehouses: warehouses.add(d.warehouse)
warehouses.append(d.t_warehouse)
return items, warehouses if self.doctype == "Stock Entry":
if d.get("s_warehouse"):
warehouses.add(d.s_warehouse)
if d.get("t_warehouse"):
warehouses.add(d.t_warehouse)
return list(items), list(warehouses)
def get_stock_ledger_details(self): def get_stock_ledger_details(self):
stock_ledger = {} stock_ledger = {}
@ -219,7 +215,7 @@ class StockController(AccountsController):
from from
`tabStock Ledger Entry` `tabStock Ledger Entry`
where where
voucher_type=%s and voucher_no=%s voucher_type=%s and voucher_no=%s and is_cancelled = 0
""", (self.doctype, self.name), as_dict=True) """, (self.doctype, self.name), as_dict=True)
for sle in stock_ledger_entries: for sle in stock_ledger_entries:

View File

@ -106,6 +106,9 @@ class calculate_taxes_and_totals(object):
self.doc.conversion_rate = flt(self.doc.conversion_rate) self.doc.conversion_rate = flt(self.doc.conversion_rate)
def calculate_item_values(self): def calculate_item_values(self):
if self.doc.get('is_consolidated'):
return
if not self.discount_amount_applied: if not self.discount_amount_applied:
for item in self.doc.get("items"): for item in self.doc.get("items"):
self.doc.round_floats_in(item) self.doc.round_floats_in(item)
@ -647,12 +650,12 @@ class calculate_taxes_and_totals(object):
def calculate_change_amount(self): def calculate_change_amount(self):
self.doc.change_amount = 0.0 self.doc.change_amount = 0.0
self.doc.base_change_amount = 0.0 self.doc.base_change_amount = 0.0
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
if self.doc.doctype == "Sales Invoice" \ if self.doc.doctype == "Sales Invoice" \
and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \ and self.doc.paid_amount > 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
self.doc.change_amount = flt(self.doc.paid_amount - grand_total + self.doc.change_amount = flt(self.doc.paid_amount - grand_total +
self.doc.write_off_amount, self.doc.precision("change_amount")) self.doc.write_off_amount, self.doc.precision("change_amount"))

View File

@ -1,524 +0,0 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
import csv
import math
import time
from io import StringIO
import dateutil
import frappe
from frappe import _
import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws
#Get and Create Products
def get_products_details():
products = get_products_instance()
reports = get_reports_instance()
mws_settings = frappe.get_doc("Amazon MWS Settings")
market_place_list = return_as_list(mws_settings.market_place_id)
for marketplace in market_place_list:
report_id = request_and_fetch_report_id("_GET_FLAT_FILE_OPEN_LISTINGS_DATA_", None, None, market_place_list)
if report_id:
listings_response = reports.get_report(report_id=report_id)
#Get ASIN Codes
string_io = StringIO(frappe.safe_decode(listings_response.original))
csv_rows = list(csv.reader(string_io, delimiter='\t'))
asin_list = list(set([row[1] for row in csv_rows[1:]]))
#break into chunks of 10
asin_chunked_list = list(chunks(asin_list, 10))
#Map ASIN Codes to SKUs
sku_asin = [{"asin":row[1],"sku":row[0]} for row in csv_rows[1:]]
#Fetch Products List from ASIN
for asin_list in asin_chunked_list:
products_response = call_mws_method(products.get_matching_product,marketplaceid=marketplace,
asins=asin_list)
matching_products_list = products_response.parsed
for product in matching_products_list:
skus = [row["sku"] for row in sku_asin if row["asin"]==product.ASIN]
for sku in skus:
create_item_code(product, sku)
def get_products_instance():
mws_settings = frappe.get_doc("Amazon MWS Settings")
products = mws.Products(
account_id = mws_settings.seller_id,
access_key = mws_settings.aws_access_key_id,
secret_key = mws_settings.secret_key,
region = mws_settings.region,
domain = mws_settings.domain
)
return products
def get_reports_instance():
mws_settings = frappe.get_doc("Amazon MWS Settings")
reports = mws.Reports(
account_id = mws_settings.seller_id,
access_key = mws_settings.aws_access_key_id,
secret_key = mws_settings.secret_key,
region = mws_settings.region,
domain = mws_settings.domain
)
return reports
#returns list as expected by amazon API
def return_as_list(input_value):
if isinstance(input_value, list):
return input_value
else:
return [input_value]
#function to chunk product data
def chunks(l, n):
for i in range(0, len(l), n):
yield l[i:i+n]
def request_and_fetch_report_id(report_type, start_date=None, end_date=None, marketplaceids=None):
reports = get_reports_instance()
report_response = reports.request_report(report_type=report_type,
start_date=start_date,
end_date=end_date,
marketplaceids=marketplaceids)
report_request_id = report_response.parsed["ReportRequestInfo"]["ReportRequestId"]["value"]
generated_report_id = None
#poll to get generated report
for x in range(1,10):
report_request_list_response = reports.get_report_request_list(requestids=[report_request_id])
report_status = report_request_list_response.parsed["ReportRequestInfo"]["ReportProcessingStatus"]["value"]
if report_status == "_SUBMITTED_" or report_status == "_IN_PROGRESS_":
#add time delay to wait for amazon to generate report
time.sleep(15)
continue
elif report_status == "_CANCELLED_":
break
elif report_status == "_DONE_NO_DATA_":
break
elif report_status == "_DONE_":
generated_report_id = report_request_list_response.parsed["ReportRequestInfo"]["GeneratedReportId"]["value"]
break
return generated_report_id
def call_mws_method(mws_method, *args, **kwargs):
mws_settings = frappe.get_doc("Amazon MWS Settings")
max_retries = mws_settings.max_retry_limit
for x in range(0, max_retries):
try:
response = mws_method(*args, **kwargs)
return response
except Exception as e:
delay = math.pow(4, x) * 125
frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed')
time.sleep(delay)
continue
mws_settings.enable_sync = 0
mws_settings.save()
frappe.throw(_("Sync has been temporarily disabled because maximum retries have been exceeded"))
def create_item_code(amazon_item_json, sku):
if frappe.db.get_value("Item", sku):
return
item = frappe.new_doc("Item")
new_manufacturer = create_manufacturer(amazon_item_json)
new_brand = create_brand(amazon_item_json)
mws_settings = frappe.get_doc("Amazon MWS Settings")
item.item_code = sku
item.amazon_item_code = amazon_item_json.ASIN
item.item_group = mws_settings.item_group
item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
item.brand = new_brand
item.manufacturer = new_manufacturer
item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL
temp_item_group = amazon_item_json.Product.AttributeSets.ItemAttributes.ProductGroup
item_group = frappe.db.get_value("Item Group",filters={"item_group_name": temp_item_group})
if not item_group:
igroup = frappe.new_doc("Item Group")
igroup.item_group_name = temp_item_group
igroup.parent_item_group = mws_settings.item_group
igroup.insert()
item.append("item_defaults", {'company':mws_settings.company})
item.insert(ignore_permissions=True)
create_item_price(amazon_item_json, item.item_code)
return item.name
def create_manufacturer(amazon_item_json):
if not amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer:
return None
existing_manufacturer = frappe.db.get_value("Manufacturer",
filters={"short_name":amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer})
if not existing_manufacturer:
manufacturer = frappe.new_doc("Manufacturer")
manufacturer.short_name = amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer
manufacturer.insert()
return manufacturer.short_name
else:
return existing_manufacturer
def create_brand(amazon_item_json):
if not amazon_item_json.Product.AttributeSets.ItemAttributes.Brand:
return None
existing_brand = frappe.db.get_value("Brand",
filters={"brand":amazon_item_json.Product.AttributeSets.ItemAttributes.Brand})
if not existing_brand:
brand = frappe.new_doc("Brand")
brand.brand = amazon_item_json.Product.AttributeSets.ItemAttributes.Brand
brand.insert()
return brand.brand
else:
return existing_brand
def create_item_price(amazon_item_json, item_code):
item_price = frappe.new_doc("Item Price")
item_price.price_list = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "price_list")
if not("ListPrice" in amazon_item_json.Product.AttributeSets.ItemAttributes):
item_price.price_list_rate = 0
else:
item_price.price_list_rate = amazon_item_json.Product.AttributeSets.ItemAttributes.ListPrice.Amount
item_price.item_code = item_code
item_price.insert()
#Get and create Orders
def get_orders(after_date):
try:
orders = get_orders_instance()
statuses = ["PartiallyShipped", "Unshipped", "Shipped", "Canceled"]
mws_settings = frappe.get_doc("Amazon MWS Settings")
market_place_list = return_as_list(mws_settings.market_place_id)
orders_response = call_mws_method(orders.list_orders, marketplaceids=market_place_list,
fulfillment_channels=["MFN", "AFN"],
lastupdatedafter=after_date,
orderstatus=statuses,
max_results='50')
while True:
orders_list = []
if "Order" in orders_response.parsed.Orders:
orders_list = return_as_list(orders_response.parsed.Orders.Order)
if len(orders_list) == 0:
break
for order in orders_list:
create_sales_order(order, after_date)
if not "NextToken" in orders_response.parsed:
break
next_token = orders_response.parsed.NextToken
orders_response = call_mws_method(orders.list_orders_by_next_token, next_token)
except Exception as e:
frappe.log_error(title="get_orders", message=e)
def get_orders_instance():
mws_settings = frappe.get_doc("Amazon MWS Settings")
orders = mws.Orders(
account_id = mws_settings.seller_id,
access_key = mws_settings.aws_access_key_id,
secret_key = mws_settings.secret_key,
region= mws_settings.region,
domain= mws_settings.domain,
version="2013-09-01"
)
return orders
def create_sales_order(order_json,after_date):
customer_name = create_customer(order_json)
create_address(order_json, customer_name)
market_place_order_id = order_json.AmazonOrderId
so = frappe.db.get_value("Sales Order",
filters={"amazon_order_id": market_place_order_id},
fieldname="name")
taxes_and_charges = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "taxes_charges")
if so:
return
if not so:
items = get_order_items(market_place_order_id)
delivery_date = dateutil.parser.parse(order_json.LatestShipDate).strftime("%Y-%m-%d")
transaction_date = dateutil.parser.parse(order_json.PurchaseDate).strftime("%Y-%m-%d")
so = frappe.get_doc({
"doctype": "Sales Order",
"naming_series": "SO-",
"amazon_order_id": market_place_order_id,
"marketplace_id": order_json.MarketplaceId,
"customer": customer_name,
"delivery_date": delivery_date,
"transaction_date": transaction_date,
"items": items,
"company": frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "company")
})
try:
if taxes_and_charges:
charges_and_fees = get_charges_and_fees(market_place_order_id)
for charge in charges_and_fees.get("charges"):
so.append('taxes', charge)
for fee in charges_and_fees.get("fees"):
so.append('taxes', fee)
so.insert(ignore_permissions=True)
so.submit()
except Exception as e:
import traceback
frappe.log_error(message=traceback.format_exc(), title="Create Sales Order")
def create_customer(order_json):
order_customer_name = ""
if not("BuyerName" in order_json):
order_customer_name = "Buyer - " + order_json.AmazonOrderId
else:
order_customer_name = order_json.BuyerName
existing_customer_name = frappe.db.get_value("Customer",
filters={"name": order_customer_name}, fieldname="name")
if existing_customer_name:
filters = [
["Dynamic Link", "link_doctype", "=", "Customer"],
["Dynamic Link", "link_name", "=", existing_customer_name],
["Dynamic Link", "parenttype", "=", "Contact"]
]
existing_contacts = frappe.get_list("Contact", filters)
if existing_contacts:
pass
else:
new_contact = frappe.new_doc("Contact")
new_contact.first_name = order_customer_name
new_contact.append('links', {
"link_doctype": "Customer",
"link_name": existing_customer_name
})
new_contact.insert()
return existing_customer_name
else:
mws_customer_settings = frappe.get_doc("Amazon MWS Settings")
new_customer = frappe.new_doc("Customer")
new_customer.customer_name = order_customer_name
new_customer.customer_group = mws_customer_settings.customer_group
new_customer.territory = mws_customer_settings.territory
new_customer.customer_type = mws_customer_settings.customer_type
new_customer.save()
new_contact = frappe.new_doc("Contact")
new_contact.first_name = order_customer_name
new_contact.append('links', {
"link_doctype": "Customer",
"link_name": new_customer.name
})
new_contact.insert()
return new_customer.name
def create_address(amazon_order_item_json, customer_name):
filters = [
["Dynamic Link", "link_doctype", "=", "Customer"],
["Dynamic Link", "link_name", "=", customer_name],
["Dynamic Link", "parenttype", "=", "Address"]
]
existing_address = frappe.get_list("Address", filters)
if not("ShippingAddress" in amazon_order_item_json):
return None
else:
make_address = frappe.new_doc("Address")
if "AddressLine1" in amazon_order_item_json.ShippingAddress:
make_address.address_line1 = amazon_order_item_json.ShippingAddress.AddressLine1
else:
make_address.address_line1 = "Not Provided"
if "City" in amazon_order_item_json.ShippingAddress:
make_address.city = amazon_order_item_json.ShippingAddress.City
else:
make_address.city = "Not Provided"
if "StateOrRegion" in amazon_order_item_json.ShippingAddress:
make_address.state = amazon_order_item_json.ShippingAddress.StateOrRegion
if "PostalCode" in amazon_order_item_json.ShippingAddress:
make_address.pincode = amazon_order_item_json.ShippingAddress.PostalCode
for address in existing_address:
address_doc = frappe.get_doc("Address", address["name"])
if (address_doc.address_line1 == make_address.address_line1 and
address_doc.pincode == make_address.pincode):
return address
make_address.append("links", {
"link_doctype": "Customer",
"link_name": customer_name
})
make_address.address_type = "Shipping"
make_address.insert()
def get_order_items(market_place_order_id):
mws_orders = get_orders_instance()
order_items_response = call_mws_method(mws_orders.list_order_items, amazon_order_id=market_place_order_id)
final_order_items = []
order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem)
warehouse = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "warehouse")
while True:
for order_item in order_items_list:
if not "ItemPrice" in order_item:
price = 0
else:
price = order_item.ItemPrice.Amount
final_order_items.append({
"item_code": get_item_code(order_item),
"item_name": order_item.SellerSKU,
"description": order_item.Title,
"rate": price,
"qty": order_item.QuantityOrdered,
"stock_uom": "Nos",
"warehouse": warehouse,
"conversion_factor": "1.0"
})
if not "NextToken" in order_items_response.parsed:
break
next_token = order_items_response.parsed.NextToken
order_items_response = call_mws_method(mws_orders.list_order_items_by_next_token, next_token)
order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem)
return final_order_items
def get_item_code(order_item):
sku = order_item.SellerSKU
item_code = frappe.db.get_value("Item", {"item_code": sku}, "item_code")
if item_code:
return item_code
def get_charges_and_fees(market_place_order_id):
finances = get_finances_instance()
charges_fees = {"charges":[], "fees":[]}
response = call_mws_method(finances.list_financial_events, amazon_order_id=market_place_order_id)
shipment_event_list = return_as_list(response.parsed.FinancialEvents.ShipmentEventList)
for shipment_event in shipment_event_list:
if shipment_event:
shipment_item_list = return_as_list(shipment_event.ShipmentEvent.ShipmentItemList.ShipmentItem)
for shipment_item in shipment_item_list:
charges, fees = [], []
if 'ItemChargeList' in shipment_item.keys():
charges = return_as_list(shipment_item.ItemChargeList.ChargeComponent)
if 'ItemFeeList' in shipment_item.keys():
fees = return_as_list(shipment_item.ItemFeeList.FeeComponent)
for charge in charges:
if(charge.ChargeType != "Principal") and float(charge.ChargeAmount.CurrencyAmount) != 0:
charge_account = get_account(charge.ChargeType)
charges_fees.get("charges").append({
"charge_type":"Actual",
"account_head": charge_account,
"tax_amount": charge.ChargeAmount.CurrencyAmount,
"description": charge.ChargeType + " for " + shipment_item.SellerSKU
})
for fee in fees:
if float(fee.FeeAmount.CurrencyAmount) != 0:
fee_account = get_account(fee.FeeType)
charges_fees.get("fees").append({
"charge_type":"Actual",
"account_head": fee_account,
"tax_amount": fee.FeeAmount.CurrencyAmount,
"description": fee.FeeType + " for " + shipment_item.SellerSKU
})
return charges_fees
def get_finances_instance():
mws_settings = frappe.get_doc("Amazon MWS Settings")
finances = mws.Finances(
account_id = mws_settings.seller_id,
access_key = mws_settings.aws_access_key_id,
secret_key = mws_settings.secret_key,
region= mws_settings.region,
domain= mws_settings.domain,
version="2015-05-01"
)
return finances
def get_account(name):
existing_account = frappe.db.get_value("Account", {"account_name": "Amazon {0}".format(name)})
account_name = existing_account
mws_settings = frappe.get_doc("Amazon MWS Settings")
if not existing_account:
try:
new_account = frappe.new_doc("Account")
new_account.account_name = "Amazon {0}".format(name)
new_account.company = mws_settings.company
new_account.parent_account = mws_settings.market_place_account_group
new_account.insert(ignore_permissions=True)
account_name = new_account.name
except Exception as e:
frappe.log_error(message=e, title="Create Account")
return account_name

View File

@ -1,651 +0,0 @@
#!/usr/bin/env python
#
# Basic interface to Amazon MWS
# Based on http://code.google.com/p/amazon-mws-python
# Extended to include finances object
import base64
import hashlib
import hmac
import re
from urllib.parse import quote
from erpnext.erpnext_integrations.doctype.amazon_mws_settings import xml_utils
try:
from xml.etree.ElementTree import ParseError as XMLError
except ImportError:
from xml.parsers.expat import ExpatError as XMLError
from time import gmtime, strftime
from requests import request
from requests.exceptions import HTTPError
__all__ = [
'Feeds',
'Inventory',
'MWSError',
'Reports',
'Orders',
'Products',
'Recommendations',
'Sellers',
'Finances'
]
# See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf page 8
# for a list of the end points and marketplace IDs
MARKETPLACES = {
"CA": "https://mws.amazonservices.ca", #A2EUQ1WTGCTBG2
"US": "https://mws.amazonservices.com", #ATVPDKIKX0DER",
"DE": "https://mws-eu.amazonservices.com", #A1PA6795UKMFR9
"ES": "https://mws-eu.amazonservices.com", #A1RKKUPIHCS9HS
"FR": "https://mws-eu.amazonservices.com", #A13V1IB3VIYZZH
"IN": "https://mws.amazonservices.in", #A21TJRUUN4KGV
"IT": "https://mws-eu.amazonservices.com", #APJ6JRA9NG5V4
"UK": "https://mws-eu.amazonservices.com", #A1F83G8C2ARO7P
"JP": "https://mws.amazonservices.jp", #A1VC38T7YXB528
"CN": "https://mws.amazonservices.com.cn", #AAHKV2X7AFYLW
"AE": " https://mws.amazonservices.ae", #A2VIGQ35RCS4UG
"MX": "https://mws.amazonservices.com.mx", #A1AM78C64UM0Y8
"BR": "https://mws.amazonservices.com", #A2Q3Y263D00KWC
}
class MWSError(Exception):
"""
Main MWS Exception class
"""
# Allows quick access to the response object.
# Do not rely on this attribute, always check if its not None.
response = None
def calc_md5(string):
"""Calculates the MD5 encryption for the given string
"""
md = hashlib.md5()
md.update(string)
return base64.encodebytes(md.digest()).decode().strip()
def remove_empty(d):
"""
Helper function that removes all keys from a dictionary (d),
that have an empty value.
"""
for key in list(d):
if not d[key]:
del d[key]
return d
def remove_namespace(xml):
xml = xml.decode('utf-8')
regex = re.compile(' xmlns(:ns2)?="[^"]+"|(ns2:)|(xml:)')
return regex.sub('', xml)
class DictWrapper(object):
def __init__(self, xml, rootkey=None):
self.original = xml
self._rootkey = rootkey
self._mydict = xml_utils.xml2dict().fromstring(remove_namespace(xml))
self._response_dict = self._mydict.get(list(self._mydict)[0], self._mydict)
@property
def parsed(self):
if self._rootkey:
return self._response_dict.get(self._rootkey)
else:
return self._response_dict
class DataWrapper(object):
"""
Text wrapper in charge of validating the hash sent by Amazon.
"""
def __init__(self, data, header):
self.original = data
if 'content-md5' in header:
hash_ = calc_md5(self.original)
if header['content-md5'] != hash_:
raise MWSError("Wrong Contentlength, maybe amazon error...")
@property
def parsed(self):
return self.original
class MWS(object):
""" Base Amazon API class """
# This is used to post/get to the different uris used by amazon per api
# ie. /Orders/2011-01-01
# All subclasses must define their own URI only if needed
URI = "/"
# The API version varies in most amazon APIs
VERSION = "2009-01-01"
# There seem to be some xml namespace issues. therefore every api subclass
# is recommended to define its namespace, so that it can be referenced
# like so AmazonAPISubclass.NS.
# For more information see http://stackoverflow.com/a/8719461/389453
NS = ''
# Some APIs are available only to either a "Merchant" or "Seller"
# the type of account needs to be sent in every call to the amazon MWS.
# This constant defines the exact name of the parameter Amazon expects
# for the specific API being used.
# All subclasses need to define this if they require another account type
# like "Merchant" in which case you define it like so.
# ACCOUNT_TYPE = "Merchant"
# Which is the name of the parameter for that specific account type.
ACCOUNT_TYPE = "SellerId"
def __init__(self, access_key, secret_key, account_id, region='US', domain='', uri="", version=""):
self.access_key = access_key
self.secret_key = secret_key
self.account_id = account_id
self.version = version or self.VERSION
self.uri = uri or self.URI
if domain:
self.domain = domain
elif region in MARKETPLACES:
self.domain = MARKETPLACES[region]
else:
error_msg = "Incorrect region supplied ('%(region)s'). Must be one of the following: %(marketplaces)s" % {
"marketplaces" : ', '.join(MARKETPLACES.keys()),
"region" : region,
}
raise MWSError(error_msg)
def make_request(self, extra_data, method="GET", **kwargs):
"""Make request to Amazon MWS API with these parameters
"""
# Remove all keys with an empty value because
# Amazon's MWS does not allow such a thing.
extra_data = remove_empty(extra_data)
params = {
'AWSAccessKeyId': self.access_key,
self.ACCOUNT_TYPE: self.account_id,
'SignatureVersion': '2',
'Timestamp': self.get_timestamp(),
'Version': self.version,
'SignatureMethod': 'HmacSHA256',
}
params.update(extra_data)
request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)])
signature = self.calc_signature(method, request_description)
url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature))
headers = {'User-Agent': 'python-amazon-mws/0.0.1 (Language=Python)'}
headers.update(kwargs.get('extra_headers', {}))
try:
# Some might wonder as to why i don't pass the params dict as the params argument to request.
# My answer is, here i have to get the url parsed string of params in order to sign it, so
# if i pass the params dict as params to request, request will repeat that step because it will need
# to convert the dict to a url parsed string, so why do it twice if i can just pass the full url :).
response = request(method, url, data=kwargs.get('body', ''), headers=headers)
response.raise_for_status()
# When retrieving data from the response object,
# be aware that response.content returns the content in bytes while response.text calls
# response.content and converts it to unicode.
data = response.content
# I do not check the headers to decide which content structure to server simply because sometimes
# Amazon's MWS API returns XML error responses with "text/plain" as the Content-Type.
try:
parsed_response = DictWrapper(data, extra_data.get("Action") + "Result")
except XMLError:
parsed_response = DataWrapper(data, response.headers)
except HTTPError as e:
error = MWSError(str(e))
error.response = e.response
raise error
# Store the response object in the parsed_response for quick access
parsed_response.response = response
return parsed_response
def get_service_status(self):
"""
Returns a GREEN, GREEN_I, YELLOW or RED status.
Depending on the status/availability of the API its being called from.
"""
return self.make_request(extra_data=dict(Action='GetServiceStatus'))
def calc_signature(self, method, request_description):
"""Calculate MWS signature to interface with Amazon
"""
sig_data = method + '\n' + self.domain.replace('https://', '').lower() + '\n' + self.uri + '\n' + request_description
sig_data = sig_data.encode('utf-8')
secret_key = self.secret_key.encode('utf-8')
digest = hmac.new(secret_key, sig_data, hashlib.sha256).digest()
return base64.b64encode(digest).decode('utf-8')
def get_timestamp(self):
"""
Returns the current timestamp in proper format.
"""
return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
def enumerate_param(self, param, values):
"""
Builds a dictionary of an enumerated parameter.
Takes any iterable and returns a dictionary.
ie.
enumerate_param('MarketplaceIdList.Id', (123, 345, 4343))
returns
{
MarketplaceIdList.Id.1: 123,
MarketplaceIdList.Id.2: 345,
MarketplaceIdList.Id.3: 4343
}
"""
params = {}
if values is not None:
if not param.endswith('.'):
param = "%s." % param
for num, value in enumerate(values):
params['%s%d' % (param, (num + 1))] = value
return params
class Feeds(MWS):
""" Amazon MWS Feeds API """
ACCOUNT_TYPE = "Merchant"
def submit_feed(self, feed, feed_type, marketplaceids=None,
content_type="text/xml", purge='false'):
"""
Uploads a feed ( xml or .tsv ) to the seller's inventory.
Can be used for creating/updating products on Amazon.
"""
data = dict(Action='SubmitFeed',
FeedType=feed_type,
PurgeAndReplace=purge)
data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids))
md = calc_md5(feed)
return self.make_request(data, method="POST", body=feed,
extra_headers={'Content-MD5': md, 'Content-Type': content_type})
def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None,
processingstatuses=None, fromdate=None, todate=None):
"""
Returns a list of all feed submissions submitted in the previous 90 days.
That match the query parameters.
"""
data = dict(Action='GetFeedSubmissionList',
MaxCount=max_count,
SubmittedFromDate=fromdate,
SubmittedToDate=todate,)
data.update(self.enumerate_param('FeedSubmissionIdList.Id', feedids))
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses))
return self.make_request(data)
def get_submission_list_by_next_token(self, token):
data = dict(Action='GetFeedSubmissionListByNextToken', NextToken=token)
return self.make_request(data)
def get_feed_submission_count(self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None):
data = dict(Action='GetFeedSubmissionCount',
SubmittedFromDate=fromdate,
SubmittedToDate=todate)
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses))
return self.make_request(data)
def cancel_feed_submissions(self, feedids=None, feedtypes=None, fromdate=None, todate=None):
data = dict(Action='CancelFeedSubmissions',
SubmittedFromDate=fromdate,
SubmittedToDate=todate)
data.update(self.enumerate_param('FeedSubmissionIdList.Id.', feedids))
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
return self.make_request(data)
def get_feed_submission_result(self, feedid):
data = dict(Action='GetFeedSubmissionResult', FeedSubmissionId=feedid)
return self.make_request(data)
class Reports(MWS):
""" Amazon MWS Reports API """
ACCOUNT_TYPE = "Merchant"
## REPORTS ###
def get_report(self, report_id):
data = dict(Action='GetReport', ReportId=report_id)
return self.make_request(data)
def get_report_count(self, report_types=(), acknowledged=None, fromdate=None, todate=None):
data = dict(Action='GetReportCount',
Acknowledged=acknowledged,
AvailableFromDate=fromdate,
AvailableToDate=todate)
data.update(self.enumerate_param('ReportTypeList.Type.', report_types))
return self.make_request(data)
def get_report_list(self, requestids=(), max_count=None, types=(), acknowledged=None,
fromdate=None, todate=None):
data = dict(Action='GetReportList',
Acknowledged=acknowledged,
AvailableFromDate=fromdate,
AvailableToDate=todate,
MaxCount=max_count)
data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids))
data.update(self.enumerate_param('ReportTypeList.Type.', types))
return self.make_request(data)
def get_report_list_by_next_token(self, token):
data = dict(Action='GetReportListByNextToken', NextToken=token)
return self.make_request(data)
def get_report_request_count(self, report_types=(), processingstatuses=(), fromdate=None, todate=None):
data = dict(Action='GetReportRequestCount',
RequestedFromDate=fromdate,
RequestedToDate=todate)
data.update(self.enumerate_param('ReportTypeList.Type.', report_types))
data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses))
return self.make_request(data)
def get_report_request_list(self, requestids=(), types=(), processingstatuses=(),
max_count=None, fromdate=None, todate=None):
data = dict(Action='GetReportRequestList',
MaxCount=max_count,
RequestedFromDate=fromdate,
RequestedToDate=todate)
data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids))
data.update(self.enumerate_param('ReportTypeList.Type.', types))
data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses))
return self.make_request(data)
def get_report_request_list_by_next_token(self, token):
data = dict(Action='GetReportRequestListByNextToken', NextToken=token)
return self.make_request(data)
def request_report(self, report_type, start_date=None, end_date=None, marketplaceids=()):
data = dict(Action='RequestReport',
ReportType=report_type,
StartDate=start_date,
EndDate=end_date)
data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids))
return self.make_request(data)
### ReportSchedule ###
def get_report_schedule_list(self, types=()):
data = dict(Action='GetReportScheduleList')
data.update(self.enumerate_param('ReportTypeList.Type.', types))
return self.make_request(data)
def get_report_schedule_count(self, types=()):
data = dict(Action='GetReportScheduleCount')
data.update(self.enumerate_param('ReportTypeList.Type.', types))
return self.make_request(data)
class Orders(MWS):
""" Amazon Orders API """
URI = "/Orders/2013-09-01"
VERSION = "2013-09-01"
NS = '{https://mws.amazonservices.com/Orders/2011-01-01}'
def list_orders(self, marketplaceids, created_after=None, created_before=None, lastupdatedafter=None,
lastupdatedbefore=None, orderstatus=(), fulfillment_channels=(),
payment_methods=(), buyer_email=None, seller_orderid=None, max_results='100'):
data = dict(Action='ListOrders',
CreatedAfter=created_after,
CreatedBefore=created_before,
LastUpdatedAfter=lastupdatedafter,
LastUpdatedBefore=lastupdatedbefore,
BuyerEmail=buyer_email,
SellerOrderId=seller_orderid,
MaxResultsPerPage=max_results,
)
data.update(self.enumerate_param('OrderStatus.Status.', orderstatus))
data.update(self.enumerate_param('MarketplaceId.Id.', marketplaceids))
data.update(self.enumerate_param('FulfillmentChannel.Channel.', fulfillment_channels))
data.update(self.enumerate_param('PaymentMethod.Method.', payment_methods))
return self.make_request(data)
def list_orders_by_next_token(self, token):
data = dict(Action='ListOrdersByNextToken', NextToken=token)
return self.make_request(data)
def get_order(self, amazon_order_ids):
data = dict(Action='GetOrder')
data.update(self.enumerate_param('AmazonOrderId.Id.', amazon_order_ids))
return self.make_request(data)
def list_order_items(self, amazon_order_id):
data = dict(Action='ListOrderItems', AmazonOrderId=amazon_order_id)
return self.make_request(data)
def list_order_items_by_next_token(self, token):
data = dict(Action='ListOrderItemsByNextToken', NextToken=token)
return self.make_request(data)
class Products(MWS):
""" Amazon MWS Products API """
URI = '/Products/2011-10-01'
VERSION = '2011-10-01'
NS = '{http://mws.amazonservices.com/schema/Products/2011-10-01}'
def list_matching_products(self, marketplaceid, query, contextid=None):
""" Returns a list of products and their attributes, ordered by
relevancy, based on a search query that you specify.
Your search query can be a phrase that describes the product
or it can be a product identifier such as a UPC, EAN, ISBN, or JAN.
"""
data = dict(Action='ListMatchingProducts',
MarketplaceId=marketplaceid,
Query=query,
QueryContextId=contextid)
return self.make_request(data)
def get_matching_product(self, marketplaceid, asins):
""" Returns a list of products and their attributes, based on a list of
ASIN values that you specify.
"""
data = dict(Action='GetMatchingProduct', MarketplaceId=marketplaceid)
data.update(self.enumerate_param('ASINList.ASIN.', asins))
return self.make_request(data)
def get_matching_product_for_id(self, marketplaceid, type, id):
""" Returns a list of products and their attributes, based on a list of
product identifier values (asin, sellersku, upc, ean, isbn and JAN)
Added in Fourth Release, API version 2011-10-01
"""
data = dict(Action='GetMatchingProductForId',
MarketplaceId=marketplaceid,
IdType=type)
data.update(self.enumerate_param('IdList.Id', id))
return self.make_request(data)
def get_competitive_pricing_for_sku(self, marketplaceid, skus):
""" Returns the current competitive pricing of a product,
based on the SellerSKU and MarketplaceId that you specify.
"""
data = dict(Action='GetCompetitivePricingForSKU', MarketplaceId=marketplaceid)
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
return self.make_request(data)
def get_competitive_pricing_for_asin(self, marketplaceid, asins):
""" Returns the current competitive pricing of a product,
based on the ASIN and MarketplaceId that you specify.
"""
data = dict(Action='GetCompetitivePricingForASIN', MarketplaceId=marketplaceid)
data.update(self.enumerate_param('ASINList.ASIN.', asins))
return self.make_request(data)
def get_lowest_offer_listings_for_sku(self, marketplaceid, skus, condition="Any", excludeme="False"):
data = dict(Action='GetLowestOfferListingsForSKU',
MarketplaceId=marketplaceid,
ItemCondition=condition,
ExcludeMe=excludeme)
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
return self.make_request(data)
def get_lowest_offer_listings_for_asin(self, marketplaceid, asins, condition="Any", excludeme="False"):
data = dict(Action='GetLowestOfferListingsForASIN',
MarketplaceId=marketplaceid,
ItemCondition=condition,
ExcludeMe=excludeme)
data.update(self.enumerate_param('ASINList.ASIN.', asins))
return self.make_request(data)
def get_product_categories_for_sku(self, marketplaceid, sku):
data = dict(Action='GetProductCategoriesForSKU',
MarketplaceId=marketplaceid,
SellerSKU=sku)
return self.make_request(data)
def get_product_categories_for_asin(self, marketplaceid, asin):
data = dict(Action='GetProductCategoriesForASIN',
MarketplaceId=marketplaceid,
ASIN=asin)
return self.make_request(data)
def get_my_price_for_sku(self, marketplaceid, skus, condition=None):
data = dict(Action='GetMyPriceForSKU',
MarketplaceId=marketplaceid,
ItemCondition=condition)
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
return self.make_request(data)
def get_my_price_for_asin(self, marketplaceid, asins, condition=None):
data = dict(Action='GetMyPriceForASIN',
MarketplaceId=marketplaceid,
ItemCondition=condition)
data.update(self.enumerate_param('ASINList.ASIN.', asins))
return self.make_request(data)
class Sellers(MWS):
""" Amazon MWS Sellers API """
URI = '/Sellers/2011-07-01'
VERSION = '2011-07-01'
NS = '{http://mws.amazonservices.com/schema/Sellers/2011-07-01}'
def list_marketplace_participations(self):
"""
Returns a list of marketplaces a seller can participate in and
a list of participations that include seller-specific information in that marketplace.
The operation returns only those marketplaces where the seller's account is in an active state.
"""
data = dict(Action='ListMarketplaceParticipations')
return self.make_request(data)
def list_marketplace_participations_by_next_token(self, token):
"""
Takes a "NextToken" and returns the same information as "list_marketplace_participations".
Based on the "NextToken".
"""
data = dict(Action='ListMarketplaceParticipations', NextToken=token)
return self.make_request(data)
#### Fulfillment APIs ####
class InboundShipments(MWS):
URI = "/FulfillmentInboundShipment/2010-10-01"
VERSION = '2010-10-01'
# To be completed
class Inventory(MWS):
""" Amazon MWS Inventory Fulfillment API """
URI = '/FulfillmentInventory/2010-10-01'
VERSION = '2010-10-01'
NS = "{http://mws.amazonaws.com/FulfillmentInventory/2010-10-01}"
def list_inventory_supply(self, skus=(), datetime=None, response_group='Basic'):
""" Returns information on available inventory """
data = dict(Action='ListInventorySupply',
QueryStartDateTime=datetime,
ResponseGroup=response_group,
)
data.update(self.enumerate_param('SellerSkus.member.', skus))
return self.make_request(data, "POST")
def list_inventory_supply_by_next_token(self, token):
data = dict(Action='ListInventorySupplyByNextToken', NextToken=token)
return self.make_request(data, "POST")
class OutboundShipments(MWS):
URI = "/FulfillmentOutboundShipment/2010-10-01"
VERSION = "2010-10-01"
# To be completed
class Recommendations(MWS):
""" Amazon MWS Recommendations API """
URI = '/Recommendations/2013-04-01'
VERSION = '2013-04-01'
NS = "{https://mws.amazonservices.com/Recommendations/2013-04-01}"
def get_last_updated_time_for_recommendations(self, marketplaceid):
"""
Checks whether there are active recommendations for each category for the given marketplace, and if there are,
returns the time when recommendations were last updated for each category.
"""
data = dict(Action='GetLastUpdatedTimeForRecommendations',
MarketplaceId=marketplaceid)
return self.make_request(data, "POST")
def list_recommendations(self, marketplaceid, recommendationcategory=None):
"""
Returns your active recommendations for a specific category or for all categories for a specific marketplace.
"""
data = dict(Action="ListRecommendations",
MarketplaceId=marketplaceid,
RecommendationCategory=recommendationcategory)
return self.make_request(data, "POST")
def list_recommendations_by_next_token(self, token):
"""
Returns the next page of recommendations using the NextToken parameter.
"""
data = dict(Action="ListRecommendationsByNextToken",
NextToken=token)
return self.make_request(data, "POST")
class Finances(MWS):
""" Amazon Finances API"""
URI = '/Finances/2015-05-01'
VERSION = '2015-05-01'
NS = "{https://mws.amazonservices.com/Finances/2015-05-01}"
def list_financial_events(self , posted_after=None, posted_before=None,
amazon_order_id=None, max_results='100'):
data = dict(Action='ListFinancialEvents',
PostedAfter=posted_after,
PostedBefore=posted_before,
AmazonOrderId=amazon_order_id,
MaxResultsPerPage=max_results,
)
return self.make_request(data)

View File

@ -1,2 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt

View File

@ -1,237 +0,0 @@
{
"actions": [],
"creation": "2018-07-31 05:51:41.357047",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable_amazon",
"mws_credentials",
"seller_id",
"aws_access_key_id",
"mws_auth_token",
"secret_key",
"column_break_4",
"market_place_id",
"region",
"domain",
"section_break_13",
"company",
"warehouse",
"item_group",
"price_list",
"column_break_17",
"customer_group",
"territory",
"customer_type",
"market_place_account_group",
"section_break_12",
"after_date",
"taxes_charges",
"sync_products",
"sync_orders",
"column_break_10",
"enable_sync",
"max_retry_limit"
],
"fields": [
{
"default": "0",
"fieldname": "enable_amazon",
"fieldtype": "Check",
"label": "Enable Amazon"
},
{
"fieldname": "mws_credentials",
"fieldtype": "Section Break",
"label": "MWS Credentials"
},
{
"fieldname": "seller_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Seller ID",
"reqd": 1
},
{
"fieldname": "aws_access_key_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "AWS Access Key ID",
"reqd": 1
},
{
"fieldname": "mws_auth_token",
"fieldtype": "Data",
"in_list_view": 1,
"label": "MWS Auth Token",
"reqd": 1
},
{
"fieldname": "secret_key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Secret Key",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "market_place_id",
"fieldtype": "Data",
"label": "Market Place ID",
"reqd": 1
},
{
"fieldname": "region",
"fieldtype": "Select",
"label": "Region",
"options": "\nAE\nAU\nBR\nCA\nCN\nDE\nES\nFR\nIN\nJP\nIT\nMX\nUK\nUS",
"reqd": 1
},
{
"fieldname": "domain",
"fieldtype": "Data",
"label": "Domain",
"reqd": 1
},
{
"fieldname": "section_break_13",
"fieldtype": "Section Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
"reqd": 1
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group",
"reqd": 1
},
{
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List",
"reqd": 1
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "customer_group",
"fieldtype": "Link",
"label": "Customer Group",
"options": "Customer Group",
"reqd": 1
},
{
"fieldname": "territory",
"fieldtype": "Link",
"label": "Territory",
"options": "Territory",
"reqd": 1
},
{
"fieldname": "customer_type",
"fieldtype": "Select",
"label": "Customer Type",
"options": "Individual\nCompany",
"reqd": 1
},
{
"fieldname": "market_place_account_group",
"fieldtype": "Link",
"label": "Market Place Account Group",
"options": "Account",
"reqd": 1
},
{
"fieldname": "section_break_12",
"fieldtype": "Section Break"
},
{
"description": "Amazon will synch data updated after this date",
"fieldname": "after_date",
"fieldtype": "Datetime",
"label": "After Date",
"reqd": 1
},
{
"default": "0",
"description": "Get financial breakup of Taxes and charges data by Amazon ",
"fieldname": "taxes_charges",
"fieldtype": "Check",
"label": "Sync Taxes and Charges"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"default": "3",
"fieldname": "max_retry_limit",
"fieldtype": "Int",
"label": "Max Retry Limit"
},
{
"description": "Always sync your products from Amazon MWS before synching the Orders details",
"fieldname": "sync_products",
"fieldtype": "Button",
"label": "Sync Products",
"options": "get_products_details"
},
{
"description": "Click this button to pull your Sales Order data from Amazon MWS.",
"fieldname": "sync_orders",
"fieldtype": "Button",
"label": "Sync Orders",
"options": "get_order_details"
},
{
"default": "0",
"description": "Check this to enable a scheduled Daily synchronization routine via scheduler",
"fieldname": "enable_sync",
"fieldtype": "Check",
"label": "Enable Scheduled Sync"
}
],
"issingle": 1,
"links": [],
"modified": "2020-04-07 14:26:20.174848",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Amazon MWS Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,46 +0,0 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
import dateutil
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document
from erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods import get_orders
class AmazonMWSSettings(Document):
def validate(self):
if self.enable_amazon == 1:
self.enable_sync = 1
setup_custom_fields()
else:
self.enable_sync = 0
@frappe.whitelist()
def get_products_details(self):
if self.enable_amazon == 1:
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details')
@frappe.whitelist()
def get_order_details(self):
if self.enable_amazon == 1:
after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d")
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_orders', after_date=after_date)
def schedule_get_order_details():
mws_settings = frappe.get_doc("Amazon MWS Settings")
if mws_settings.enable_sync and mws_settings.enable_amazon:
after_date = dateutil.parser.parse(mws_settings.after_date).strftime("%Y-%m-%d")
get_orders(after_date = after_date)
def setup_custom_fields():
custom_fields = {
"Item": [dict(fieldname='amazon_item_code', label='Amazon Item Code',
fieldtype='Data', insert_after='series', read_only=1, print_hide=1)],
"Sales Order": [dict(fieldname='amazon_order_id', label='Amazon Order ID',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)]
}
create_custom_fields(custom_fields)

View File

@ -1,8 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestAmazonMWSSettings(unittest.TestCase):
pass

View File

@ -1,104 +0,0 @@
"""
Created on Tue Jun 26 15:42:07 2012
Borrowed from https://github.com/timotheus/ebaysdk-python
@author: pierre
"""
import re
import xml.etree.ElementTree as ET
class object_dict(dict):
"""object view of dict, you can
>>> a = object_dict()
>>> a.fish = 'fish'
>>> a['fish']
'fish'
>>> a['water'] = 'water'
>>> a.water
'water'
>>> a.test = {'value': 1}
>>> a.test2 = object_dict({'name': 'test2', 'value': 2})
>>> a.test, a.test2.name, a.test2.value
(1, 'test2', 2)
"""
def __init__(self, initd=None):
if initd is None:
initd = {}
dict.__init__(self, initd)
def __getattr__(self, item):
try:
d = self.__getitem__(item)
except KeyError:
return None
if isinstance(d, dict) and 'value' in d and len(d) == 1:
return d['value']
else:
return d
# if value is the only key in object, you can omit it
def __setstate__(self, item):
return False
def __setattr__(self, item, value):
self.__setitem__(item, value)
def getvalue(self, item, value=None):
return self.get(item, {}).get('value', value)
class xml2dict(object):
def __init__(self):
pass
def _parse_node(self, node):
node_tree = object_dict()
# Save attrs and text, hope there will not be a child with same name
if node.text:
node_tree.value = node.text
for (k, v) in node.attrib.items():
k, v = self._namespace_split(k, object_dict({'value':v}))
node_tree[k] = v
#Save childrens
for child in node.getchildren():
tag, tree = self._namespace_split(child.tag,
self._parse_node(child))
if tag not in node_tree: # the first time, so store it in dict
node_tree[tag] = tree
continue
old = node_tree[tag]
if not isinstance(old, list):
node_tree.pop(tag)
node_tree[tag] = [old] # multi times, so change old dict to a list
node_tree[tag].append(tree) # add the new one
return node_tree
def _namespace_split(self, tag, value):
"""
Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
ns = http://cs.sfsu.edu/csc867/myscheduler
name = patients
"""
result = re.compile(r"\{(.*)\}(.*)").search(tag)
if result:
value.namespace, tag = result.groups()
return (tag, value)
def parse(self, file):
"""parse a xml file to a dict"""
f = open(file, 'r')
return self.fromstring(f.read())
def fromstring(self, s):
"""parse a string"""
t = ET.fromstring(s)
root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
return object_dict({root_tag: root_tree})

View File

@ -13,7 +13,7 @@ from frappe.utils import call_hook_method, cint, flt, get_url
class GoCardlessSettings(Document): class GoCardlessSettings(Document):
supported_currencies = ["EUR", "DKK", "GBP", "SEK"] supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
def validate(self): def validate(self):
self.initialize_client() self.initialize_client()
@ -80,7 +80,7 @@ class GoCardlessSettings(Document):
def validate_transaction_currency(self, currency): def validate_transaction_currency(self, currency):
if currency not in self.supported_currencies: if currency not in self.supported_currencies:
frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency)) frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency))
def get_payment_url(self, **kwargs): def get_payment_url(self, **kwargs):
return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs))) return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))

View File

@ -29,17 +29,6 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Amazon MWS Settings",
"link_count": 0,
"link_to": "Amazon MWS Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,

View File

@ -333,7 +333,6 @@ scheduler_events = {
"hourly": [ "hourly": [
'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
"erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.hourly_reminder",
@ -341,7 +340,8 @@ scheduler_events = {
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts" "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
], ],
"hourly_long": [ "hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction"
], ],
"daily": [ "daily": [
"erpnext.stock.reorder_item.reorder_item", "erpnext.stock.reorder_item.reorder_item",

View File

@ -27,12 +27,13 @@
"fetch_from": "employee.user_id", "fetch_from": "employee.user_id",
"fieldname": "user_id", "fieldname": "user_id",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "ERPNext User ID", "label": "ERPNext User ID",
"read_only": 1 "read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-06-06 10:41:20.313756", "modified": "2022-02-13 19:44:21.302938",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Group Table", "name": "Employee Group Table",

View File

@ -546,7 +546,7 @@ class TestLeaveApplication(unittest.TestCase):
from erpnext.hr.utils import allocate_earned_leaves from erpnext.hr.utils import allocate_earned_leaves
i = 0 i = 0
while(i<14): while(i<14):
allocate_earned_leaves() allocate_earned_leaves(ignore_duplicates=True)
i += 1 i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
@ -554,7 +554,7 @@ class TestLeaveApplication(unittest.TestCase):
frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0) frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0)
i = 0 i = 0
while(i<6): while(i<6):
allocate_earned_leaves() allocate_earned_leaves(ignore_duplicates=True)
i += 1 i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)

View File

@ -8,11 +8,10 @@ from math import ceil
import frappe import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate
class LeavePolicyAssignment(Document): class LeavePolicyAssignment(Document):
def validate(self): def validate(self):
self.validate_policy_assignment_overlap() self.validate_policy_assignment_overlap()
self.set_dates() self.set_dates()
@ -94,10 +93,12 @@ class LeavePolicyAssignment(Document):
new_leaves_allocated = 0 new_leaves_allocated = 0
elif leave_type_details.get(leave_type).is_earned_leave == 1: elif leave_type_details.get(leave_type).is_earned_leave == 1:
if self.assignment_based_on == "Leave Period": if not self.assignment_based_on:
new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
else:
new_leaves_allocated = 0 new_leaves_allocated = 0
else:
# get leaves for past months if assignment is based on Leave Period / Joining Date
new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
elif getdate(date_of_joining) > getdate(self.effective_from): elif getdate(date_of_joining) > getdate(self.effective_from):
remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1)) remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
@ -108,21 +109,24 @@ class LeavePolicyAssignment(Document):
def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
from erpnext.hr.utils import get_monthly_earned_leave from erpnext.hr.utils import get_monthly_earned_leave
current_month = get_datetime().month current_date = frappe.flags.current_date or getdate()
current_year = get_datetime().year if current_date > getdate(self.effective_to):
current_date = getdate(self.effective_to)
from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date") from_date = getdate(self.effective_from)
if getdate(date_of_joining) > getdate(from_date): if getdate(date_of_joining) > from_date:
from_date = date_of_joining from_date = getdate(date_of_joining)
from_date_month = get_datetime(from_date).month
from_date_year = get_datetime(from_date).year
months_passed = 0 months_passed = 0
if current_year == from_date_year and current_month > from_date_month: based_on_doj = leave_type_details.get(leave_type).based_on_date_of_joining
months_passed = current_month - from_date_month
elif current_year > from_date_year: if current_date.year == from_date.year and current_date.month >= from_date.month:
months_passed = (12 - from_date_month) + current_month months_passed = current_date.month - from_date.month
months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj)
elif current_date.year > from_date.year:
months_passed = (12 - from_date.month) + current_date.month
months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj)
if months_passed > 0: if months_passed > 0:
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
@ -134,6 +138,23 @@ class LeavePolicyAssignment(Document):
return new_leaves_allocated return new_leaves_allocated
def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj):
date = getdate(frappe.flags.current_date) or getdate()
if based_on_doj:
# if leave type allocation is based on DOJ, and the date of assignment creation is same as DOJ,
# then the month should be considered
if date.day == date_of_joining.day:
months_passed += 1
else:
last_day_of_month = get_last_day(date)
# if its the last day of the month, then that month should be considered
if last_day_of_month == date:
months_passed += 1
return months_passed
@frappe.whitelist() @frappe.whitelist()
def create_assignment_for_multiple_employees(employees, data): def create_assignment_for_multiple_employees(employees, data):
@ -168,7 +189,7 @@ def create_assignment_for_multiple_employees(employees, data):
def get_leave_type_details(): def get_leave_type_details():
leave_type_details = frappe._dict() leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type", leave_types = frappe.get_all("Leave Type",
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "based_on_date_of_joining",
"is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"]) "is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"])
for d in leave_types: for d in leave_types:
leave_type_details.setdefault(d.name, d) leave_type_details.setdefault(d.name, d)

View File

@ -4,7 +4,7 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import add_months, get_first_day, getdate from frappe.utils import add_months, get_first_day, get_last_day, getdate
from erpnext.hr.doctype.leave_application.test_leave_application import ( from erpnext.hr.doctype.leave_application.test_leave_application import (
get_employee, get_employee,
@ -20,36 +20,31 @@ test_dependencies = ["Employee"]
class TestLeavePolicyAssignment(unittest.TestCase): class TestLeavePolicyAssignment(unittest.TestCase):
def setUp(self): def setUp(self):
for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec frappe.db.delete(doctype)
employee = get_employee()
self.original_doj = employee.date_of_joining
self.employee = employee
def test_grant_leaves(self): def test_grant_leaves(self):
leave_period = get_leave_period() leave_period = get_leave_period()
employee = get_employee() # allocation = 10
# create the leave policy with leave type "_Test Leave Type", allocation = 10
leave_policy = create_leave_policy() leave_policy = create_leave_policy()
leave_policy.submit() leave_policy.submit()
data = { data = {
"assignment_based_on": "Leave Period", "assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name, "leave_policy": leave_policy.name,
"leave_period": leave_period.name "leave_period": leave_period.name
} }
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1)
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
leave_policy_assignment_doc.reload()
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
leave_allocation = frappe.get_list("Leave Allocation", filters={ leave_allocation = frappe.get_list("Leave Allocation", filters={
"employee": employee.name, "employee": self.employee.name,
"leave_policy":leave_policy.name, "leave_policy":leave_policy.name,
"leave_policy_assignment": leave_policy_assignments[0], "leave_policy_assignment": leave_policy_assignments[0],
"docstatus": 1})[0] "docstatus": 1})[0]
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
@ -61,63 +56,46 @@ class TestLeavePolicyAssignment(unittest.TestCase):
def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self): def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self):
leave_period = get_leave_period() leave_period = get_leave_period()
employee = get_employee()
# create the leave policy with leave type "_Test Leave Type", allocation = 10 # create the leave policy with leave type "_Test Leave Type", allocation = 10
leave_policy = create_leave_policy() leave_policy = create_leave_policy()
leave_policy.submit() leave_policy.submit()
data = { data = {
"assignment_based_on": "Leave Period", "assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name, "leave_policy": leave_policy.name,
"leave_period": leave_period.name "leave_period": leave_period.name
} }
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
leave_policy_assignment_doc.reload()
# every leave is allocated no more leave can be granted now # every leave is allocated no more leave can be granted now
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1)
leave_allocation = frappe.get_list("Leave Allocation", filters={ leave_allocation = frappe.get_list("Leave Allocation", filters={
"employee": employee.name, "employee": self.employee.name,
"leave_policy":leave_policy.name, "leave_policy":leave_policy.name,
"leave_policy_assignment": leave_policy_assignments[0], "leave_policy_assignment": leave_policy_assignments[0],
"docstatus": 1})[0] "docstatus": 1})[0]
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
# User all allowed to grant leave when there is no allocation against assignment
leave_alloc_doc.cancel() leave_alloc_doc.cancel()
leave_alloc_doc.delete() leave_alloc_doc.delete()
self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 0)
leave_policy_assignment_doc.reload()
# User are now allowed to grant leave
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
def test_earned_leave_allocation(self): def test_earned_leave_allocation(self):
leave_period = create_leave_period("Test Earned Leave Period") leave_period = create_leave_period("Test Earned Leave Period")
employee = get_employee()
leave_type = create_earned_leave_type("Test Earned Leave") leave_type = create_earned_leave_type("Test Earned Leave")
leave_policy = frappe.get_doc({ leave_policy = frappe.get_doc({
"doctype": "Leave Policy", "doctype": "Leave Policy",
"title": "Test Leave Policy", "title": "Test Leave Policy",
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}] "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
}).insert() }).submit()
data = { data = {
"assignment_based_on": "Leave Period", "assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name, "leave_policy": leave_policy.name,
"leave_period": leave_period.name "leave_period": leave_period.name
} }
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
leaves_allocated = frappe.db.get_value("Leave Allocation", { leaves_allocated = frappe.db.get_value("Leave Allocation", {
@ -125,11 +103,200 @@ class TestLeavePolicyAssignment(unittest.TestCase):
}, "total_leaves_allocated") }, "total_leaves_allocated")
self.assertEqual(leaves_allocated, 0) self.assertEqual(leaves_allocated, 0)
def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self):
leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1)))
# Case 1: assignment created one month after the leave period, should allocate 1 leave
frappe.flags.current_date = get_first_day(getdate())
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")
self.assertEqual(leaves_allocated, 1)
def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self):
leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)))
# Case 2: assignment created on the last day of the leave period's latter month
# should allocate 1 leave for current month even though the month has not ended
# since the daily job might have already executed
frappe.flags.current_date = get_last_day(getdate())
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")
self.assertEqual(leaves_allocated, 3)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")
self.assertEqual(leaves_allocated, 3)
def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)))
# initial leave allocation = 5
leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave",
from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0)
leave_allocation.submit()
# Case 3: assignment created on the last day of the leave period's latter month with carry forwarding
frappe.flags.current_date = get_last_day(add_months(getdate(), -1))
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name,
"carry_forward": 1
}
# carry forwarded leaves = 5, 3 leaves allocated for passed months
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
details = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True)
self.assertEqual(details.new_leaves_allocated, 2)
self.assertEqual(details.unused_leaves, 5)
self.assertEqual(details.total_leaves_allocated, 7)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import is_earned_leave_already_allocated
frappe.flags.current_date = get_last_day(getdate())
allocation = frappe.get_doc("Leave Allocation", details.name)
# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation))
def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
leave_type = create_earned_leave_type("Test Earned Leave")
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"title": "Test Leave Policy",
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
}).submit()
# joining date set to 2 months back
self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
self.employee.save()
# assignment created on the last day of the current month
frappe.flags.current_date = get_last_day(getdate())
data = {
"assignment_based_on": "Joining Date",
"leave_policy": leave_policy.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated")
effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from")
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_last_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated")
self.assertEqual(leaves_allocated, 3)
def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True)
# joining date set to 2 months back
self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
self.employee.save()
# assignment created on the same day of the current month, should allocate leaves including the current month
frappe.flags.current_date = get_first_day(getdate())
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")
self.assertEqual(leaves_allocated, 3)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_first_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")
self.assertEqual(leaves_allocated, 3)
def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"title": "Test Leave Policy",
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
}).submit()
# joining date set to 2 months back
# leave should be allocated for current month too since this day is same as the joining day
self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
self.employee.save()
# assignment created on the first day of the current month
frappe.flags.current_date = get_first_day(getdate())
data = {
"assignment_based_on": "Joining Date",
"leave_policy": leave_policy.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated")
effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from")
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
frappe.flags.current_date = get_first_day(getdate())
allocate_earned_leaves()
leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
"total_leaves_allocated")
self.assertEqual(leaves_allocated, 3)
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
frappe.flags.current_date = None
def create_earned_leave_type(leave_type): def create_earned_leave_type(leave_type, based_on_doj=False):
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
return frappe.get_doc(dict( return frappe.get_doc(dict(
@ -138,13 +305,15 @@ def create_earned_leave_type(leave_type):
is_earned_leave=1, is_earned_leave=1,
earned_leave_frequency="Monthly", earned_leave_frequency="Monthly",
rounding=0.5, rounding=0.5,
max_leaves_allowed=6 is_carry_forward=1,
based_on_date_of_joining=based_on_doj
)).insert() )).insert()
def create_leave_period(name): def create_leave_period(name, start_date=None):
frappe.delete_doc_if_exists("Leave Period", name, force=1) frappe.delete_doc_if_exists("Leave Period", name, force=1)
start_date = get_first_day(getdate()) if not start_date:
start_date = get_first_day(getdate())
return frappe.get_doc(dict( return frappe.get_doc(dict(
name=name, name=name,
@ -154,3 +323,16 @@ def create_leave_period(name):
company="_Test Company", company="_Test Company",
is_active=1 is_active=1
)).insert() )).insert()
def setup_leave_period_and_policy(start_date, based_on_doj=False):
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj)
leave_period = create_leave_period("Test Earned Leave Period",
start_date=start_date)
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"title": "Test Leave Policy",
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
}).insert()
return leave_period, leave_policy

View File

@ -237,7 +237,7 @@ def generate_leave_encashment():
create_leave_encashment(leave_allocation=leave_allocation) create_leave_encashment(leave_allocation=leave_allocation)
def allocate_earned_leaves(): def allocate_earned_leaves(ignore_duplicates=False):
'''Allocate earned leaves to Employees''' '''Allocate earned leaves to Employees'''
e_leave_types = get_earned_leaves() e_leave_types = get_earned_leaves()
today = getdate() today = getdate()
@ -261,13 +261,13 @@ def allocate_earned_leaves():
from_date=allocation.from_date from_date=allocation.from_date
if e_leave_type.based_on_date_of_joining_date: if e_leave_type.based_on_date_of_joining:
from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date): if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining):
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates)
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False):
earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding) earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
allocation = frappe.get_doc('Leave Allocation', allocation.name) allocation = frappe.get_doc('Leave Allocation', allocation.name)
@ -277,9 +277,12 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type
new_allocation = e_leave_type.max_leaves_allowed new_allocation = e_leave_type.max_leaves_allowed
if new_allocation != allocation.total_leaves_allocated: if new_allocation != allocation.total_leaves_allocated:
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
today_date = today() today_date = today()
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
def get_monthly_earned_leave(annual_leaves, frequency, rounding): def get_monthly_earned_leave(annual_leaves, frequency, rounding):
earned_leaves = 0.0 earned_leaves = 0.0
@ -297,6 +300,28 @@ def get_monthly_earned_leave(annual_leaves, frequency, rounding):
return earned_leaves return earned_leaves
def is_earned_leave_already_allocated(allocation, annual_allocation):
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
get_leave_type_details,
)
leave_type_details = get_leave_type_details()
date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment)
leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type,
annual_allocation, leave_type_details, date_of_joining)
# exclude carry-forwarded leaves while checking for leave allocation for passed months
num_allocations = allocation.total_leaves_allocated
if allocation.unused_leaves:
num_allocations -= allocation.unused_leaves
if num_allocations >= leaves_for_passed_months:
return True
return False
def get_leave_allocations(date, leave_type): def get_leave_allocations(date, leave_type):
return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
from `tabLeave Allocation` from `tabLeave Allocation`
@ -318,7 +343,7 @@ def create_additional_leave_ledger_entry(allocation, leaves, date):
allocation.unused_leaves = 0 allocation.unused_leaves = 0
allocation.create_leave_ledger_entry() allocation.create_leave_ledger_entry()
def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date): def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining):
import calendar import calendar
from dateutil import relativedelta from dateutil import relativedelta
@ -329,7 +354,7 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining
#last day of month #last day of month
last_day = calendar.monthrange(to_date.year, to_date.month)[1] last_day = calendar.monthrange(to_date.year, to_date.month)[1]
if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day): if (from_date.day == to_date.day and based_on_date_of_joining) or (not based_on_date_of_joining and to_date.day == last_day):
if frequency == "Monthly": if frequency == "Monthly":
return True return True
elif frequency == "Quarterly" and rd.months % 3: elif frequency == "Quarterly" and rd.months % 3:

View File

@ -46,7 +46,7 @@ frappe.ui.form.on('Loan', {
}); });
}); });
$.each(["payment_account", "loan_account"], function (i, field) { $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
frm.set_query(field, function () { frm.set_query(field, function () {
return { return {
"filters": { "filters": {
@ -88,6 +88,10 @@ frappe.ui.form.on('Loan', {
frm.add_custom_button(__('Loan Write Off'), function() { frm.add_custom_button(__('Loan Write Off'), function() {
frm.trigger("make_loan_write_off_entry"); frm.trigger("make_loan_write_off_entry");
},__('Create')); },__('Create'));
frm.add_custom_button(__('Loan Refund'), function() {
frm.trigger("make_loan_refund");
},__('Create'));
} }
} }
frm.trigger("toggle_fields"); frm.trigger("toggle_fields");
@ -155,6 +159,21 @@ frappe.ui.form.on('Loan', {
}) })
}, },
make_loan_refund: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name
},
method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv",
callback: function (r) {
if (r.message) {
let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
})
},
request_loan_closure: function(frm) { request_loan_closure: function(frm) {
frappe.confirm(__("Do you really want to close this loan"), frappe.confirm(__("Do you really want to close this loan"),
function() { function() {

View File

@ -2,7 +2,7 @@
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "ACC-LOAN-.YYYY.-.#####", "autoname": "ACC-LOAN-.YYYY.-.#####",
"creation": "2019-08-29 17:29:18.176786", "creation": "2022-01-25 10:30:02.294967",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 1, "editable_grid": 1,
@ -34,6 +34,7 @@
"is_term_loan", "is_term_loan",
"account_info", "account_info",
"mode_of_payment", "mode_of_payment",
"disbursement_account",
"payment_account", "payment_account",
"column_break_9", "column_break_9",
"loan_account", "loan_account",
@ -356,12 +357,21 @@
"fieldtype": "Date", "fieldtype": "Date",
"label": "Closure Date", "label": "Closure Date",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "loan_type.disbursement_account",
"fieldname": "disbursement_account",
"fieldtype": "Link",
"label": "Disbursement Account",
"options": "Account",
"read_only": 1,
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-10-12 18:10:32.360818", "modified": "2022-01-25 16:29:16.325501",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan", "name": "Loan",
@ -391,5 +401,6 @@
"search_fields": "posting_date", "search_fields": "posting_date",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -10,6 +10,7 @@ from frappe import _
from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
import erpnext import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import ( from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import (
@ -233,17 +234,15 @@ def request_loan_closure(loan, posting_date=None):
loan_type = frappe.get_value('Loan', loan, 'loan_type') loan_type = frappe.get_value('Loan', loan, 'loan_type')
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount') write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
# checking greater than 0 as there may be some minor precision error if pending_amount and abs(pending_amount) < write_off_limit:
if not pending_amount:
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
elif pending_amount < write_off_limit:
# Auto create loan write off and update status as loan closure requested # Auto create loan write off and update status as loan closure requested
write_off = make_loan_write_off(loan) write_off = make_loan_write_off(loan)
write_off.submit() write_off.submit()
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') elif pending_amount > 0:
else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
@frappe.whitelist() @frappe.whitelist()
def get_loan_application(loan_application): def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application) loan = frappe.get_doc("Loan Application", loan_application)
@ -401,3 +400,38 @@ def add_single_month(date):
return get_last_day(add_months(date, 1)) return get_last_day(add_months(date, 1))
else: else:
return add_months(date, 1) return add_months(date, 1)
@frappe.whitelist()
def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0):
loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant',
'loan_account', 'payment_account', 'posting_date', 'company', 'name',
'total_payment', 'total_principal_paid'], as_dict=1)
loan_details.doctype = 'Loan'
loan_details[loan_details.applicant_type.lower()] = loan_details.applicant
if not amount:
amount = flt(loan_details.total_principal_paid - loan_details.total_payment)
if amount < 0:
frappe.throw(_('No excess amount pending for refund'))
refund_jv = get_payment_entry(loan_details, {
"party_type": loan_details.applicant_type,
"party_account": loan_details.loan_account,
"amount_field_party": 'debit_in_account_currency',
"amount_field_bank": 'credit_in_account_currency',
"amount": amount,
"bank_account": loan_details.payment_account
})
if reference_number:
refund_jv.cheque_no = reference_number
if reference_date:
refund_jv.cheque_date = reference_date
if submit:
refund_jv.submit()
return refund_jv

View File

@ -42,16 +42,17 @@ class TestLoan(unittest.TestCase):
create_loan_type("Personal Loan", 500000, 8.4, create_loan_type("Personal Loan", 500000, 8.4,
is_term_loan=1, is_term_loan=1,
mode_of_payment='Cash', mode_of_payment='Cash',
disbursement_account='Disbursement Account - _TC',
payment_account='Payment Account - _TC', payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC', loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC', interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC') penalty_income_account='Penalty Income Account - _TC')
create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC') 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC') 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type() create_loan_security_type()
create_loan_security() create_loan_security()
@ -679,6 +680,29 @@ class TestLoan(unittest.TestCase):
loan.load_from_db() loan.load_from_db()
self.assertEqual(loan.status, "Loan Closure Requested") self.assertEqual(loan.status, "Loan Closure Requested")
def test_loan_repayment_against_partially_disbursed_loan(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
first_date = '2019-10-01'
last_date = '2019-10-30'
make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date)
loan.load_from_db()
self.assertEqual(loan.status, "Partially Disbursed")
create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
flt(loan.loan_amount/3))
def test_loan_amount_write_off(self): def test_loan_amount_write_off(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
@ -790,6 +814,18 @@ def create_loan_accounts():
"account_type": "Bank", "account_type": "Bank",
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Disbursement Account - _TC"):
frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Disbursement Account",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Bank Accounts - _TC",
"account_type": "Bank",
}).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Interest Income Account - _TC"): if not frappe.db.exists("Account", "Interest Income Account - _TC"):
frappe.get_doc({ frappe.get_doc({
"doctype": "Account", "doctype": "Account",
@ -815,7 +851,7 @@ def create_loan_accounts():
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)
def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None, def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None,
mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
repayment_method=None, repayment_periods=None): repayment_method=None, repayment_periods=None):
if not frappe.db.exists("Loan Type", loan_name): if not frappe.db.exists("Loan Type", loan_name):
@ -829,6 +865,7 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i
"penalty_interest_rate": penalty_interest_rate, "penalty_interest_rate": penalty_interest_rate,
"grace_period_in_days": grace_period_in_days, "grace_period_in_days": grace_period_in_days,
"mode_of_payment": mode_of_payment, "mode_of_payment": mode_of_payment,
"disbursement_account": disbursement_account,
"payment_account": payment_account, "payment_account": payment_account,
"loan_account": loan_account, "loan_account": loan_account,
"interest_income_account": interest_income_account, "interest_income_account": interest_income_account,

View File

@ -15,7 +15,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
class TestLoanApplication(unittest.TestCase): class TestLoanApplication(unittest.TestCase):
def setUp(self): def setUp(self):
create_loan_accounts() create_loan_accounts()
create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18)
self.applicant = make_employee("kate_loan@loan.com", "_Test Company") self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR') make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR')

View File

@ -122,7 +122,7 @@ class LoanDisbursement(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": loan_details.loan_account,
"against": loan_details.payment_account, "against": loan_details.disbursement_account,
"debit": self.disbursed_amount, "debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@ -137,7 +137,7 @@ class LoanDisbursement(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.payment_account, "account": loan_details.disbursement_account,
"against": loan_details.loan_account, "against": loan_details.loan_account,
"credit": self.disbursed_amount, "credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount,

View File

@ -44,8 +44,8 @@ class TestLoanDisbursement(unittest.TestCase):
def setUp(self): def setUp(self):
create_loan_accounts() create_loan_accounts()
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC') 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type() create_loan_security_type()
create_loan_security() create_loan_security()

View File

@ -30,8 +30,8 @@ class TestLoanInterestAccrual(unittest.TestCase):
def setUp(self): def setUp(self):
create_loan_accounts() create_loan_accounts()
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC') 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type() create_loan_security_type()
create_loan_security() create_loan_security()

View File

@ -125,7 +125,7 @@ class LoanRepayment(AccountsController):
def update_paid_amount(self): def update_paid_amount(self):
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable', 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
'written_off_amount'], as_dict=1) 'written_off_amount'], as_dict=1)
loan.update({ loan.update({
@ -153,7 +153,7 @@ class LoanRepayment(AccountsController):
def mark_as_unpaid(self): def mark_as_unpaid(self):
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable', 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
'written_off_amount'], as_dict=1) 'written_off_amount'], as_dict=1)
no_of_repayments = len(self.repayment_details) no_of_repayments = len(self.repayment_details)
@ -345,7 +345,7 @@ class LoanRepayment(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.penalty_income_account, "account": loan_details.penalty_income_account,
"against": payment_account, "against": loan_details.loan_account,
"credit": self.total_penalty_paid, "credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid, "credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@ -367,7 +367,9 @@ class LoanRepayment(AccountsController):
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": remarks, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date),
"party_type": loan_details.applicant_type if self.repay_from_salary else '',
"party": loan_details.applicant if self.repay_from_salary else ''
}) })
) )

View File

@ -15,7 +15,7 @@ frappe.ui.form.on('Loan Type', {
}); });
}); });
$.each(["payment_account", "loan_account"], function (i, field) { $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
frm.set_query(field, function () { frm.set_query(field, function () {
return { return {
"filters": { "filters": {

View File

@ -19,9 +19,10 @@
"description", "description",
"account_details_section", "account_details_section",
"mode_of_payment", "mode_of_payment",
"disbursement_account",
"payment_account", "payment_account",
"loan_account",
"column_break_12", "column_break_12",
"loan_account",
"interest_income_account", "interest_income_account",
"penalty_income_account", "penalty_income_account",
"amended_from" "amended_from"
@ -79,7 +80,7 @@
{ {
"fieldname": "payment_account", "fieldname": "payment_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payment Account", "label": "Repayment Account",
"options": "Account", "options": "Account",
"reqd": 1 "reqd": 1
}, },
@ -149,15 +150,23 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Auto Write Off Amount ", "label": "Auto Write Off Amount ",
"options": "Company:company:default_currency" "options": "Company:company:default_currency"
},
{
"fieldname": "disbursement_account",
"fieldtype": "Link",
"label": "Disbursement Account",
"options": "Account",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 18:10:57.368490", "modified": "2022-01-25 16:23:57.009349",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Type", "name": "Loan Type",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -181,5 +190,6 @@
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -28,9 +28,24 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
class ProductionPlan(Document): class ProductionPlan(Document):
def validate(self): def validate(self):
self.set_pending_qty_in_row_without_reference()
self.calculate_total_planned_qty() self.calculate_total_planned_qty()
self.set_status() self.set_status()
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
if self.docstatus > 0: # set only to initialise value before submit
return
for item in self.po_items:
if not item.get("sales_order") or not item.get("material_request"):
item.pending_qty = item.planned_qty
def calculate_total_planned_qty(self):
self.total_planned_qty = 0
for d in self.po_items:
self.total_planned_qty += flt(d.planned_qty)
def validate_data(self): def validate_data(self):
for d in self.get('po_items'): for d in self.get('po_items'):
if not d.bom_no: if not d.bom_no:
@ -263,11 +278,6 @@ class ProductionPlan(Document):
'qty': so_detail['qty'] 'qty': so_detail['qty']
}) })
def calculate_total_planned_qty(self):
self.total_planned_qty = 0
for d in self.po_items:
self.total_planned_qty += flt(d.planned_qty)
def calculate_total_produced_qty(self): def calculate_total_produced_qty(self):
self.total_produced_qty = 0 self.total_produced_qty = 0
for d in self.po_items: for d in self.po_items:
@ -275,10 +285,11 @@ class ProductionPlan(Document):
self.db_set("total_produced_qty", self.total_produced_qty, update_modified=False) self.db_set("total_produced_qty", self.total_produced_qty, update_modified=False)
def update_produced_qty(self, produced_qty, production_plan_item): def update_produced_pending_qty(self, produced_qty, production_plan_item):
for data in self.po_items: for data in self.po_items:
if data.name == production_plan_item: if data.name == production_plan_item:
data.produced_qty = produced_qty data.produced_qty = produced_qty
data.pending_qty = flt(data.planned_qty - produced_qty)
data.db_update() data.db_update()
self.calculate_total_produced_qty() self.calculate_total_produced_qty()
@ -341,6 +352,7 @@ class ProductionPlan(Document):
def get_production_items(self): def get_production_items(self):
item_dict = {} item_dict = {}
for d in self.po_items: for d in self.po_items:
item_details = { item_details = {
"production_item" : d.item_code, "production_item" : d.item_code,
@ -357,12 +369,12 @@ class ProductionPlan(Document):
"production_plan" : self.name, "production_plan" : self.name,
"production_plan_item" : d.name, "production_plan_item" : d.name,
"product_bundle_item" : d.product_bundle_item, "product_bundle_item" : d.product_bundle_item,
"planned_start_date" : d.planned_start_date "planned_start_date" : d.planned_start_date,
"project" : self.project
} }
item_details.update({ if not item_details['project'] and d.sales_order:
"project": self.project or frappe.db.get_value("Sales Order", d.sales_order, "project") item_details['project'] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
})
if self.get_items_from == "Material Request": if self.get_items_from == "Material Request":
item_details.update({ item_details.update({
@ -380,39 +392,59 @@ class ProductionPlan(Document):
@frappe.whitelist() @frappe.whitelist()
def make_work_order(self): def make_work_order(self):
from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse
wo_list, po_list = [], [] wo_list, po_list = [], []
subcontracted_po = {} subcontracted_po = {}
default_warehouses = get_default_warehouse()
self.validate_data() self.make_work_order_for_finished_goods(wo_list, default_warehouses)
self.make_work_order_for_finished_goods(wo_list) self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses)
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
self.make_subcontracted_purchase_order(subcontracted_po, po_list) self.make_subcontracted_purchase_order(subcontracted_po, po_list)
self.show_list_created_message('Work Order', wo_list) self.show_list_created_message('Work Order', wo_list)
self.show_list_created_message('Purchase Order', po_list) self.show_list_created_message('Purchase Order', po_list)
def make_work_order_for_finished_goods(self, wo_list): def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
items_data = self.get_production_items() items_data = self.get_production_items()
for key, item in items_data.items(): for key, item in items_data.items():
if self.sub_assembly_items: if self.sub_assembly_items:
item['use_multi_level_bom'] = 0 item['use_multi_level_bom'] = 0
set_default_warehouses(item, default_warehouses)
work_order = self.create_work_order(item) work_order = self.create_work_order(item)
if work_order: if work_order:
wo_list.append(work_order) wo_list.append(work_order)
def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po): def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses):
for row in self.sub_assembly_items: for row in self.sub_assembly_items:
if row.type_of_manufacturing == 'Subcontract': if row.type_of_manufacturing == 'Subcontract':
subcontracted_po.setdefault(row.supplier, []).append(row) subcontracted_po.setdefault(row.supplier, []).append(row)
continue continue
args = {} work_order_data = {
self.prepare_args_for_sub_assembly_items(row, args) 'wip_warehouse': default_warehouses.get('wip_warehouse'),
work_order = self.create_work_order(args) 'fg_warehouse': default_warehouses.get('fg_warehouse')
}
self.prepare_data_for_sub_assembly_items(row, work_order_data)
work_order = self.create_work_order(work_order_data)
if work_order: if work_order:
wo_list.append(work_order) wo_list.append(work_order)
def prepare_data_for_sub_assembly_items(self, row, wo_data):
for field in ["production_item", "item_name", "qty", "fg_warehouse",
"description", "bom_no", "stock_uom", "bom_level",
"production_plan_item", "schedule_date"]:
if row.get(field):
wo_data[field] = row.get(field)
wo_data.update({
"use_multi_level_bom": 0,
"production_plan": self.name,
"production_plan_sub_assembly_item": row.name
})
def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders): def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
if not subcontracted_po: if not subcontracted_po:
return return
@ -423,7 +455,7 @@ class ProductionPlan(Document):
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
po.is_subcontracted = 'Yes' po.is_subcontracted = 'Yes'
for row in po_list: for row in po_list:
args = { po_data = {
'item_code': row.production_item, 'item_code': row.production_item,
'warehouse': row.fg_warehouse, 'warehouse': row.fg_warehouse,
'production_plan_sub_assembly_item': row.name, 'production_plan_sub_assembly_item': row.name,
@ -433,9 +465,9 @@ class ProductionPlan(Document):
for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name', for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
'description', 'production_plan_item']: 'description', 'production_plan_item']:
args[field] = row.get(field) po_data[field] = row.get(field)
po.append('items', args) po.append('items', po_data)
po.set_missing_values() po.set_missing_values()
po.flags.ignore_mandatory = True po.flags.ignore_mandatory = True
@ -452,24 +484,9 @@ class ProductionPlan(Document):
doc_list = [get_link_to_form(doctype, p) for p in doc_list] doc_list = [get_link_to_form(doctype, p) for p in doc_list]
msgprint(_("{0} created").format(comma_and(doc_list))) msgprint(_("{0} created").format(comma_and(doc_list)))
def prepare_args_for_sub_assembly_items(self, row, args):
for field in ["production_item", "item_name", "qty", "fg_warehouse",
"description", "bom_no", "stock_uom", "bom_level",
"production_plan_item", "schedule_date"]:
args[field] = row.get(field)
args.update({
"use_multi_level_bom": 0,
"production_plan": self.name,
"production_plan_sub_assembly_item": row.name
})
def create_work_order(self, item): def create_work_order(self, item):
from erpnext.manufacturing.doctype.work_order.work_order import ( from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
OverProductionError,
get_default_warehouse,
)
warehouse = get_default_warehouse()
wo = frappe.new_doc("Work Order") wo = frappe.new_doc("Work Order")
wo.update(item) wo.update(item)
wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date') wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date')
@ -478,11 +495,11 @@ class ProductionPlan(Document):
wo.fg_warehouse = item.get("warehouse") wo.fg_warehouse = item.get("warehouse")
wo.set_work_order_operations() wo.set_work_order_operations()
wo.set_required_items()
if not wo.fg_warehouse:
wo.fg_warehouse = warehouse.get('fg_warehouse')
try: try:
wo.flags.ignore_mandatory = True wo.flags.ignore_mandatory = True
wo.flags.ignore_validate = True
wo.insert() wo.insert()
return wo.name return wo.name
except OverProductionError: except OverProductionError:
@ -1023,3 +1040,8 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
if d.value: if d.value:
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1) get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)
def set_default_warehouses(row, default_warehouses):
for field in ['wip_warehouse', 'fg_warehouse']:
if not row.get(field):
row[field] = default_warehouses.get(field)

View File

@ -11,6 +11,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
) )
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
) )
@ -36,15 +37,21 @@ class TestProductionPlan(ERPNextTestCase):
if not frappe.db.get_value('BOM', {'item': item}): if not frappe.db.get_value('BOM', {'item': item}):
make_bom(item = item, raw_materials = raw_materials) make_bom(item = item, raw_materials = raw_materials)
def test_production_plan(self): def test_production_plan_mr_creation(self):
"Test if MRs are created for unavailable raw materials."
pln = create_production_plan(item_code='Test Production Item 1') pln = create_production_plan(item_code='Test Production Item 1')
self.assertTrue(len(pln.mr_items), 2) self.assertTrue(len(pln.mr_items), 2)
pln.make_material_request()
pln = frappe.get_doc('Production Plan', pln.name) pln.make_material_request()
pln.reload()
self.assertTrue(pln.status, 'Material Requested') self.assertTrue(pln.status, 'Material Requested')
material_requests = frappe.get_all('Material Request Item', fields = ['distinct parent'],
filters = {'production_plan': pln.name}, as_list=1) material_requests = frappe.get_all(
'Material Request Item',
fields = ['distinct parent'],
filters = {'production_plan': pln.name},
as_list=1
)
self.assertTrue(len(material_requests), 2) self.assertTrue(len(material_requests), 2)
@ -66,27 +73,42 @@ class TestProductionPlan(ERPNextTestCase):
pln.cancel() pln.cancel()
def test_production_plan_start_date(self): def test_production_plan_start_date(self):
"Test if Work Order has same Planned Start Date as Prod Plan."
planned_date = add_to_date(date=None, days=3) planned_date = add_to_date(date=None, days=3)
plan = create_production_plan(item_code='Test Production Item 1', planned_start_date=planned_date) plan = create_production_plan(
item_code='Test Production Item 1',
planned_start_date=planned_date
)
plan.make_work_order() plan.make_work_order()
work_orders = frappe.get_all('Work Order', fields = ['name', 'planned_start_date'], work_orders = frappe.get_all(
filters = {'production_plan': plan.name}) 'Work Order',
fields = ['name', 'planned_start_date'],
filters = {'production_plan': plan.name}
)
self.assertEqual(work_orders[0].planned_start_date, planned_date) self.assertEqual(work_orders[0].planned_start_date, planned_date)
for wo in work_orders: for wo in work_orders:
frappe.delete_doc('Work Order', wo.name) frappe.delete_doc('Work Order', wo.name)
frappe.get_doc('Production Plan', plan.name).cancel() plan.reload()
plan.cancel()
def test_production_plan_for_existing_ordered_qty(self): def test_production_plan_for_existing_ordered_qty(self):
"""
- Enable 'ignore_existing_ordered_qty'.
- Test if MR Planning table pulls Raw Material Qty even if it is in stock.
"""
sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", sr1 = create_stock_reconciliation(item_code="Raw Material Item 1",
target="_Test Warehouse - _TC", qty=1, rate=110) target="_Test Warehouse - _TC", qty=1, rate=110)
sr2 = create_stock_reconciliation(item_code="Raw Material Item 2", sr2 = create_stock_reconciliation(item_code="Raw Material Item 2",
target="_Test Warehouse - _TC", qty=1, rate=120) target="_Test Warehouse - _TC", qty=1, rate=120)
pln = create_production_plan(item_code='Test Production Item 1', ignore_existing_ordered_qty=0) pln = create_production_plan(
item_code='Test Production Item 1',
ignore_existing_ordered_qty=1
)
self.assertTrue(len(pln.mr_items), 1) self.assertTrue(len(pln.mr_items), 1)
self.assertTrue(flt(pln.mr_items[0].quantity), 1.0) self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
@ -95,23 +117,39 @@ class TestProductionPlan(ERPNextTestCase):
pln.cancel() pln.cancel()
def test_production_plan_with_non_stock_item(self): def test_production_plan_with_non_stock_item(self):
pln = create_production_plan(item_code='Test Production Item 1', include_non_stock_items=0) "Test if MR Planning table includes Non Stock RM."
pln = create_production_plan(
item_code='Test Production Item 1',
include_non_stock_items=1
)
self.assertTrue(len(pln.mr_items), 3) self.assertTrue(len(pln.mr_items), 3)
pln.cancel() pln.cancel()
def test_production_plan_without_multi_level(self): def test_production_plan_without_multi_level(self):
pln = create_production_plan(item_code='Test Production Item 1', use_multi_level_bom=0) "Test MR Planning table for non exploded BOM."
pln = create_production_plan(
item_code='Test Production Item 1',
use_multi_level_bom=0
)
self.assertTrue(len(pln.mr_items), 2) self.assertTrue(len(pln.mr_items), 2)
pln.cancel() pln.cancel()
def test_production_plan_without_multi_level_for_existing_ordered_qty(self): def test_production_plan_without_multi_level_for_existing_ordered_qty(self):
"""
- Disable 'ignore_existing_ordered_qty'.
- Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for
non exploded BOM.
"""
sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", sr1 = create_stock_reconciliation(item_code="Raw Material Item 1",
target="_Test Warehouse - _TC", qty=1, rate=130) target="_Test Warehouse - _TC", qty=1, rate=130)
sr2 = create_stock_reconciliation(item_code="Subassembly Item 1", sr2 = create_stock_reconciliation(item_code="Subassembly Item 1",
target="_Test Warehouse - _TC", qty=1, rate=140) target="_Test Warehouse - _TC", qty=1, rate=140)
pln = create_production_plan(item_code='Test Production Item 1', pln = create_production_plan(
use_multi_level_bom=0, ignore_existing_ordered_qty=0) item_code='Test Production Item 1',
use_multi_level_bom=0,
ignore_existing_ordered_qty=0
)
self.assertTrue(len(pln.mr_items), 0) self.assertTrue(len(pln.mr_items), 0)
sr1.cancel() sr1.cancel()
@ -119,6 +157,7 @@ class TestProductionPlan(ERPNextTestCase):
pln.cancel() pln.cancel()
def test_production_plan_sales_orders(self): def test_production_plan_sales_orders(self):
"Test if previously fulfilled SO (with WO) is pulled into Prod Plan."
item = 'Test Production Item 1' item = 'Test Production Item 1'
so = make_sales_order(item_code=item, qty=1) so = make_sales_order(item_code=item, qty=1)
sales_order = so.name sales_order = so.name
@ -166,24 +205,25 @@ class TestProductionPlan(ERPNextTestCase):
self.assertEqual(sales_orders, []) self.assertEqual(sales_orders, [])
def test_production_plan_combine_items(self): def test_production_plan_combine_items(self):
"Test combining FG items in Production Plan."
item = 'Test Production Item 1' item = 'Test Production Item 1'
so = make_sales_order(item_code=item, qty=1) so1 = make_sales_order(item_code=item, qty=1)
pln = frappe.new_doc('Production Plan') pln = frappe.new_doc('Production Plan')
pln.company = so.company pln.company = so1.company
pln.get_items_from = 'Sales Order' pln.get_items_from = 'Sales Order'
pln.append('sales_orders', { pln.append('sales_orders', {
'sales_order': so.name, 'sales_order': so1.name,
'sales_order_date': so.transaction_date, 'sales_order_date': so1.transaction_date,
'customer': so.customer, 'customer': so1.customer,
'grand_total': so.grand_total 'grand_total': so1.grand_total
}) })
so = make_sales_order(item_code=item, qty=2) so2 = make_sales_order(item_code=item, qty=2)
pln.append('sales_orders', { pln.append('sales_orders', {
'sales_order': so.name, 'sales_order': so2.name,
'sales_order_date': so.transaction_date, 'sales_order_date': so2.transaction_date,
'customer': so.customer, 'customer': so2.customer,
'grand_total': so.grand_total 'grand_total': so2.grand_total
}) })
pln.combine_items = 1 pln.combine_items = 1
pln.get_items() pln.get_items()
@ -214,28 +254,37 @@ class TestProductionPlan(ERPNextTestCase):
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
self.assertEqual(so_wo_qty, 0.0) self.assertEqual(so_wo_qty, 0.0)
latest_plan = frappe.get_doc('Production Plan', pln.name) pln.reload()
latest_plan.cancel() pln.cancel()
def test_pp_to_mr_customer_provided(self): def test_pp_to_mr_customer_provided(self):
#Material Request from Production Plan for Customer Provided " Test Material Request from Production Plan for Customer Provided Item."
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
create_item('Production Item CUST') create_item('Production Item CUST')
for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items(): for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items():
if not frappe.db.get_value('BOM', {'item': item}): if not frappe.db.get_value('BOM', {'item': item}):
make_bom(item = item, raw_materials = raw_materials) make_bom(item = item, raw_materials = raw_materials)
production_plan = create_production_plan(item_code = 'Production Item CUST') production_plan = create_production_plan(item_code = 'Production Item CUST')
production_plan.make_material_request() production_plan.make_material_request()
material_request = frappe.db.get_value('Material Request Item', {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, 'parent')
material_request = frappe.db.get_value(
'Material Request Item',
{'production_plan': production_plan.name, 'item_code': 'CUST-0987'},
'parent'
)
mr = frappe.get_doc('Material Request', material_request) mr = frappe.get_doc('Material Request', material_request)
self.assertTrue(mr.material_request_type, 'Customer Provided') self.assertTrue(mr.material_request_type, 'Customer Provided')
self.assertTrue(mr.customer, '_Test Customer') self.assertTrue(mr.customer, '_Test Customer')
def test_production_plan_with_multi_level_bom(self): def test_production_plan_with_multi_level_bom(self):
#|Item Code | Qty | """
#|Test BOM 1 | 1 | Item Code | Qty |
#| Test BOM 2 | 2 | |Test BOM 1 | 1 |
#| Test BOM 3 | 3 | |Test BOM 2 | 2 |
|Test BOM 3 | 3 |
"""
for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]: for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]:
create_item(item_code, is_stock_item=1) create_item(item_code, is_stock_item=1)
@ -264,15 +313,18 @@ class TestProductionPlan(ERPNextTestCase):
pln.make_work_order() pln.make_work_order()
#last level sub-assembly work order produce qty #last level sub-assembly work order produce qty
to_produce_qty = frappe.db.get_value("Work Order", to_produce_qty = frappe.db.get_value(
{"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty") "Work Order",
{"production_plan": pln.name, "production_item": "Test BOM 3"},
"qty"
)
self.assertEqual(to_produce_qty, 18.0) self.assertEqual(to_produce_qty, 18.0)
pln.cancel() pln.cancel()
frappe.delete_doc("Production Plan", pln.name) frappe.delete_doc("Production Plan", pln.name)
def test_get_warehouse_list_group(self): def test_get_warehouse_list_group(self):
"""Check if required warehouses are returned""" "Check if required child warehouses are returned."
warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]' warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]'
warehouses = set(get_warehouse_list(warehouse_json)) warehouses = set(get_warehouse_list(warehouse_json))
@ -284,6 +336,7 @@ class TestProductionPlan(ERPNextTestCase):
msg=f"Following warehouses were expected {', '.join(missing_warehouse)}") msg=f"Following warehouses were expected {', '.join(missing_warehouse)}")
def test_get_warehouse_list_single(self): def test_get_warehouse_list_single(self):
"Check if same warehouse is returned in absence of child warehouses."
warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]' warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]'
warehouses = set(get_warehouse_list(warehouse_json)) warehouses = set(get_warehouse_list(warehouse_json))
@ -292,6 +345,7 @@ class TestProductionPlan(ERPNextTestCase):
self.assertEqual(warehouses, expected_warehouses) self.assertEqual(warehouses, expected_warehouses)
def test_get_sales_order_with_variant(self): def test_get_sales_order_with_variant(self):
"Check if Template BOM is fetched in absence of Variant BOM."
rm_item = create_item('PIV_RM', valuation_rate = 100) rm_item = create_item('PIV_RM', valuation_rate = 100)
if not frappe.db.exists('Item', {"item_code": 'PIV'}): if not frappe.db.exists('Item', {"item_code": 'PIV'}):
item = create_item('PIV', valuation_rate = 100) item = create_item('PIV', valuation_rate = 100)
@ -348,7 +402,7 @@ class TestProductionPlan(ERPNextTestCase):
frappe.db.rollback() frappe.db.rollback()
def test_subassmebly_sorting(self): def test_subassmebly_sorting(self):
""" Test subassembly sorting in case of multiple items with nested BOMs""" "Test subassembly sorting in case of multiple items with nested BOMs."
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
prefix = "_TestLevel_" prefix = "_TestLevel_"
@ -386,6 +440,7 @@ class TestProductionPlan(ERPNextTestCase):
self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item) self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item)
def test_multiple_work_order_for_production_plan_item(self): def test_multiple_work_order_for_production_plan_item(self):
"Test producing Prod Plan (making WO) in parts."
def create_work_order(item, pln, qty): def create_work_order(item, pln, qty):
# Get Production Items # Get Production Items
items_data = pln.get_production_items() items_data = pln.get_production_items()
@ -441,7 +496,107 @@ class TestProductionPlan(ERPNextTestCase):
pln.reload() pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 0) self.assertEqual(pln.po_items[0].ordered_qty, 0)
def test_production_plan_pending_qty_with_sales_order(self):
"""
Test Prod Plan impact via: SO -> Prod Plan -> WO -> SE -> SE (cancel)
"""
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_se_from_wo,
)
make_stock_entry(item_code="Raw Material Item 1",
target="Work In Progress - _TC",
qty=2, basic_rate=100
)
make_stock_entry(item_code="Raw Material Item 2",
target="Work In Progress - _TC",
qty=2, basic_rate=100
)
item = 'Test Production Item 1'
so = make_sales_order(item_code=item, qty=1)
pln = create_production_plan(
company=so.company,
get_items_from="Sales Order",
sales_order=so,
skip_getting_mr_items=True
)
self.assertEqual(pln.po_items[0].pending_qty, 1)
wo = make_wo_order_test_record(
item_code=item, qty=1,
company=so.company,
wip_warehouse='Work In Progress - _TC',
fg_warehouse='Finished Goods - _TC',
skip_transfer=1,
do_not_submit=True
)
wo.production_plan = pln.name
wo.production_plan_item = pln.po_items[0].name
wo.submit()
se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1))
se.submit()
pln.reload()
self.assertEqual(pln.po_items[0].pending_qty, 0)
se.cancel()
pln.reload()
self.assertEqual(pln.po_items[0].pending_qty, 1)
def test_production_plan_pending_qty_independent_items(self):
"Test Prod Plan impact if items are added independently (no from SO or MR)."
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_se_from_wo,
)
make_stock_entry(item_code="Raw Material Item 1",
target="Work In Progress - _TC",
qty=2, basic_rate=100
)
make_stock_entry(item_code="Raw Material Item 2",
target="Work In Progress - _TC",
qty=2, basic_rate=100
)
pln = create_production_plan(
item_code='Test Production Item 1',
skip_getting_mr_items=True
)
self.assertEqual(pln.po_items[0].pending_qty, 1)
wo = make_wo_order_test_record(
item_code='Test Production Item 1', qty=1,
company=pln.company,
wip_warehouse='Work In Progress - _TC',
fg_warehouse='Finished Goods - _TC',
skip_transfer=1,
do_not_submit=True
)
wo.production_plan = pln.name
wo.production_plan_item = pln.po_items[0].name
wo.submit()
se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1))
se.submit()
pln.reload()
self.assertEqual(pln.po_items[0].pending_qty, 0)
se.cancel()
pln.reload()
self.assertEqual(pln.po_items[0].pending_qty, 1)
def create_production_plan(**args): def create_production_plan(**args):
"""
sales_order (obj): Sales Order Doc Object
get_items_from (str): Sales Order/Material Request
skip_getting_mr_items (bool): Whether or not to plan for new MRs
"""
args = frappe._dict(args) args = frappe._dict(args)
pln = frappe.get_doc({ pln = frappe.get_doc({
@ -449,20 +604,35 @@ def create_production_plan(**args):
'company': args.company or '_Test Company', 'company': args.company or '_Test Company',
'customer': args.customer or '_Test Customer', 'customer': args.customer or '_Test Customer',
'posting_date': nowdate(), 'posting_date': nowdate(),
'include_non_stock_items': args.include_non_stock_items or 1, 'include_non_stock_items': args.include_non_stock_items or 0,
'include_subcontracted_items': args.include_subcontracted_items or 1, 'include_subcontracted_items': args.include_subcontracted_items or 0,
'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 1, 'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 0,
'po_items': [{ 'get_items_from': 'Sales Order'
})
if not args.get("sales_order"):
pln.append('po_items', {
'use_multi_level_bom': args.use_multi_level_bom or 1, 'use_multi_level_bom': args.use_multi_level_bom or 1,
'item_code': args.item_code, 'item_code': args.item_code,
'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'), 'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'),
'planned_qty': args.planned_qty or 1, 'planned_qty': args.planned_qty or 1,
'planned_start_date': args.planned_start_date or now_datetime() 'planned_start_date': args.planned_start_date or now_datetime()
}] })
})
mr_items = get_items_for_material_requests(pln.as_dict()) if args.get("get_items_from") == "Sales Order" and args.get("sales_order"):
for d in mr_items: so = args.get("sales_order")
pln.append('mr_items', d) pln.append('sales_orders', {
'sales_order': so.name,
'sales_order_date': so.transaction_date,
'customer': so.customer,
'grand_total': so.grand_total
})
pln.get_items()
if not args.get("skip_getting_mr_items"):
mr_items = get_items_for_material_requests(pln.as_dict())
for d in mr_items:
pln.append('mr_items', d)
if not args.do_not_save: if not args.do_not_save:
pln.insert() pln.insert()

View File

@ -201,6 +201,21 @@ class TestWorkOrder(ERPNextTestCase):
self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production),
cint(bin1_on_start_production.reserved_qty_for_production)) cint(bin1_on_start_production.reserved_qty_for_production))
def test_reserved_qty_for_production_closed(self):
wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=self.warehouse)
item = wo1.required_items[0].item_code
bin_before = get_bin(item, self.warehouse)
bin_before.update_reserved_qty_for_production()
make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=self.warehouse)
close_work_order(wo1.name, "Closed")
bin_after = get_bin(item, self.warehouse)
self.assertEqual(bin_before.reserved_qty_for_production, bin_after.reserved_qty_for_production)
def test_backflush_qty_for_overpduction_manufacture(self): def test_backflush_qty_for_overpduction_manufacture(self):
cancel_stock_entry = [] cancel_stock_entry = []
allow_overproduction("overproduction_percentage_for_work_order", 30) allow_overproduction("overproduction_percentage_for_work_order", 30)
@ -703,7 +718,8 @@ class TestWorkOrder(ERPNextTestCase):
wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse, wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse,
company=company) company=company)
self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture'))
self.assertRaises(frappe.ValidationError, stock_entry.save)
def test_wo_completion_with_pl_bom(self): def test_wo_completion_with_pl_bom(self):
from erpnext.manufacturing.doctype.bom.test_bom import ( from erpnext.manufacturing.doctype.bom.test_bom import (

View File

@ -8,6 +8,8 @@ from dateutil.relativedelta import relativedelta
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Case
from frappe.query_builder.functions import Sum
from frappe.utils import ( from frappe.utils import (
cint, cint,
date_diff, date_diff,
@ -74,7 +76,6 @@ class WorkOrder(Document):
self.set_required_items(reset_only_qty = len(self.get("required_items"))) self.set_required_items(reset_only_qty = len(self.get("required_items")))
def validate_sales_order(self): def validate_sales_order(self):
if self.sales_order: if self.sales_order:
self.check_sales_order_on_hold_or_close() self.check_sales_order_on_hold_or_close()
@ -271,7 +272,7 @@ class WorkOrder(Document):
produced_qty = total_qty[0][0] if total_qty else 0 produced_qty = total_qty[0][0] if total_qty else 0
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item)
def before_submit(self): def before_submit(self):
self.create_serial_no_batch_no() self.create_serial_no_batch_no()
@ -544,7 +545,7 @@ class WorkOrder(Document):
if node.is_bom: if node.is_bom:
operations.extend(_get_operations(node.name, qty=node.exploded_qty)) operations.extend(_get_operations(node.name, qty=node.exploded_qty))
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
for correct_index, operation in enumerate(operations, start=1): for correct_index, operation in enumerate(operations, start=1):
@ -625,7 +626,7 @@ class WorkOrder(Document):
frappe.delete_doc("Job Card", d.name) frappe.delete_doc("Job Card", d.name)
def validate_production_item(self): def validate_production_item(self):
if frappe.db.get_value("Item", self.production_item, "has_variants"): if frappe.get_cached_value("Item", self.production_item, "has_variants"):
frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError) frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
if self.production_item: if self.production_item:
@ -1175,3 +1176,27 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
doc.set_item_locations() doc.set_item_locations()
return doc return doc
def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
"""Get total reserved quantity for any item in specified warehouse"""
wo = frappe.qb.DocType("Work Order")
wo_item = frappe.qb.DocType("Work Order Item")
return (
frappe.qb
.from_(wo)
.from_(wo_item)
.select(Sum(Case()
.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
.else_(wo_item.required_qty - wo_item.consumed_qty))
)
.where(
(wo_item.item_code == item_code)
& (wo_item.parent == wo.name)
& (wo.docstatus == 1)
& (wo_item.source_warehouse == warehouse)
& (wo.status.notin(["Stopped", "Completed", "Closed"]))
& ((wo_item.required_qty > wo_item.transferred_qty)
| (wo_item.required_qty > wo_item.consumed_qty))
)
).run()[0][0] or 0.0

View File

@ -89,10 +89,10 @@ def get_bom_stock(filters):
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records(): def get_manufacturer_records():
details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"])
manufacture_details = frappe._dict() manufacture_details = frappe._dict()
for detail in details: for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {}) dic = manufacture_details.setdefault(detail.get('item_code'), {})
dic.setdefault('manufacturer', []).append(detail.get('manufacturer')) dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no')) dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))

View File

@ -172,10 +172,15 @@ class ProductionPlanReport(object):
self.purchase_details = {} self.purchase_details = {}
for d in frappe.get_all("Purchase Order Item", purchased_items = frappe.get_all("Purchase Order Item",
fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"], fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"],
filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)}, filters={
group_by = "item_code, warehouse"): "item_code": ("in", self.item_codes),
"warehouse": ("in", self.warehouses),
"docstatus": 1,
},
group_by = "item_code, warehouse")
for d in purchased_items:
key = (d.item_code, d.warehouse) key = (d.item_code, d.warehouse)
if key not in self.purchase_details: if key not in self.purchase_details:
self.purchase_details.setdefault(key, d) self.purchase_details.setdefault(key, d)

View File

@ -21,4 +21,5 @@ Communication
Loan Management Loan Management
Payroll Payroll
Telephony Telephony
Bulk Transaction
E-commerce E-commerce

View File

@ -349,3 +349,6 @@ erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v14_0.migrate_cost_center_allocations erpnext.patches.v14_0.migrate_cost_center_allocations
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.shopping_cart_to_ecommerce erpnext.patches.v13_0.shopping_cart_to_ecommerce
erpnext.patches.v13_0.update_disbursement_account
erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v14_0.delete_amazon_mws_doctype

View File

@ -1,12 +0,0 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
count = frappe.db.sql("SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';")[0][0]
if count == 0:
frappe.db.sql("UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';")
frappe.reload_doc("ERPNext Integrations", "doctype", "Amazon MWS Settings")

View File

@ -0,0 +1,22 @@
import frappe
def execute():
frappe.reload_doc("loan_management", "doctype", "loan_type")
frappe.reload_doc("loan_management", "doctype", "loan")
loan_type = frappe.qb.DocType("Loan Type")
loan = frappe.qb.DocType("Loan")
frappe.qb.update(
loan_type
).set(
loan_type.disbursement_account, loan_type.payment_account
).run()
frappe.qb.update(
loan
).set(
loan.disbursement_account, loan.payment_account
).run()

View File

@ -0,0 +1,28 @@
import frappe
from erpnext.stock.utils import get_bin
def execute():
wo = frappe.qb.DocType("Work Order")
wo_item = frappe.qb.DocType("Work Order Item")
incorrect_item_wh = (
frappe.qb
.from_(wo)
.join(wo_item).on(wo.name == wo_item.parent)
.select(wo_item.item_code, wo.source_warehouse).distinct()
.where(
(wo.status == "Closed")
& (wo.docstatus == 1)
& (wo.source_warehouse.notnull())
)
).run()
for item_code, warehouse in incorrect_item_wh:
if not (item_code and warehouse):
continue
bin = get_bin(item_code, warehouse)
bin.update_reserved_qty_for_production()

View File

@ -0,0 +1,5 @@
import frappe
def execute():
frappe.delete_doc("DocType", "Amazon MWS Settings", ignore_missing=True)

View File

@ -3,6 +3,14 @@
frappe.ui.form.on('Gratuity', { frappe.ui.form.on('Gratuity', {
setup: function (frm) { setup: function (frm) {
frm.set_query("salary_component", function () {
return {
filters: {
type: "Earning"
}
};
});
frm.set_query("expense_account", function () { frm.set_query("expense_account", function () {
return { return {
filters: { filters: {
@ -24,7 +32,7 @@ frappe.ui.form.on('Gratuity', {
}); });
}, },
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.docstatus == 1 && frm.doc.status == "Unpaid") { if (frm.doc.docstatus == 1 && !frm.doc.pay_via_salary_slip && frm.doc.status == "Unpaid") {
frm.add_custom_button(__("Create Payment Entry"), function () { frm.add_custom_button(__("Create Payment Entry"), function () {
return frappe.call({ return frappe.call({
method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry', method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',

View File

@ -1,7 +1,7 @@
{ {
"actions": [], "actions": [],
"autoname": "HR-GRA-PAY-.#####", "autoname": "HR-GRA-PAY-.#####",
"creation": "2020-08-05 20:52:13.024683", "creation": "2022-01-27 16:24:28.200061",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
@ -16,6 +16,9 @@
"company", "company",
"gratuity_rule", "gratuity_rule",
"section_break_5", "section_break_5",
"pay_via_salary_slip",
"payroll_date",
"salary_component",
"payable_account", "payable_account",
"expense_account", "expense_account",
"mode_of_payment", "mode_of_payment",
@ -78,18 +81,20 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval: !doc.pay_via_salary_slip",
"fieldname": "expense_account", "fieldname": "expense_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Expense Account", "label": "Expense Account",
"options": "Account", "mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
"reqd": 1 "options": "Account"
}, },
{ {
"depends_on": "eval: !doc.pay_via_salary_slip",
"fieldname": "mode_of_payment", "fieldname": "mode_of_payment",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Mode of Payment", "label": "Mode of Payment",
"options": "Mode of Payment", "mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
"reqd": 1 "options": "Mode of Payment"
}, },
{ {
"fieldname": "gratuity_rule", "fieldname": "gratuity_rule",
@ -151,23 +156,45 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: !doc.pay_via_salary_slip",
"fieldname": "payable_account", "fieldname": "payable_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payable Account", "label": "Payable Account",
"options": "Account", "mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
"reqd": 1 "options": "Account"
}, },
{ {
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center" "options": "Cost Center"
},
{
"default": "1",
"fieldname": "pay_via_salary_slip",
"fieldtype": "Check",
"label": "Pay via Salary Slip"
},
{
"depends_on": "pay_via_salary_slip",
"fieldname": "payroll_date",
"fieldtype": "Date",
"label": "Payroll Date",
"mandatory_depends_on": "pay_via_salary_slip"
},
{
"depends_on": "pay_via_salary_slip",
"fieldname": "salary_component",
"fieldtype": "Link",
"label": "Salary Component",
"mandatory_depends_on": "pay_via_salary_slip",
"options": "Salary Component"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-01-19 12:54:37.306145", "modified": "2022-02-02 14:00:45.536152",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Gratuity", "name": "Gratuity",

View File

@ -21,7 +21,10 @@ class Gratuity(AccountsController):
self.status = "Unpaid" self.status = "Unpaid"
def on_submit(self): def on_submit(self):
self.create_gl_entries() if self.pay_via_salary_slip:
self.create_additional_salary()
else:
self.create_gl_entries()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ['GL Entry'] self.ignore_linked_doctypes = ['GL Entry']
@ -64,6 +67,19 @@ class Gratuity(AccountsController):
return gl_entry return gl_entry
def create_additional_salary(self):
if self.pay_via_salary_slip:
additional_salary = frappe.new_doc('Additional Salary')
additional_salary.employee = self.employee
additional_salary.salary_component = self.salary_component
additional_salary.overwrite_salary_structure_amount = 0
additional_salary.amount = self.amount
additional_salary.payroll_date = self.payroll_date
additional_salary.company = self.company
additional_salary.ref_doctype = self.doctype
additional_salary.ref_docname = self.name
additional_salary.submit()
def set_total_advance_paid(self): def set_total_advance_paid(self):
paid_amount = frappe.db.sql(""" paid_amount = frappe.db.sql("""
select ifnull(sum(debit_in_account_currency), 0) as paid_amount select ifnull(sum(debit_in_account_currency), 0) as paid_amount

View File

@ -10,7 +10,7 @@ def get_data():
'transactions': [ 'transactions': [
{ {
'label': _('Payment'), 'label': _('Payment'),
'items': ['Payment Entry'] 'items': ['Payment Entry', 'Additional Salary']
} }
] ]
} }

View File

@ -18,27 +18,25 @@ from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
test_dependencies = ["Salary Component", "Salary Slip", "Account"] test_dependencies = ["Salary Component", "Salary Slip", "Account"]
class TestGratuity(unittest.TestCase): class TestGratuity(unittest.TestCase):
@classmethod def setUp(self):
def setUpClass(cls): frappe.db.delete("Gratuity")
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
def setUp(self):
frappe.db.sql("DELETE FROM `tabGratuity`")
def test_get_last_salary_slip_should_return_none_for_new_employee(self): def test_get_last_salary_slip_should_return_none_for_new_employee(self):
new_employee = make_employee("new_employee@salary.com", company='_Test Company') new_employee = make_employee("new_employee@salary.com", company='_Test Company')
salary_slip = get_last_salary_slip(new_employee) salary_slip = get_last_salary_slip(new_employee)
assert salary_slip is None assert salary_slip is None
def test_check_gratuity_amount_based_on_current_slab(self): def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
employee, sal_slip = create_employee_and_get_last_salary_slip() employee, sal_slip = create_employee_and_get_last_salary_slip()
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)") rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
gratuity = create_gratuity(employee=employee, rule=rule.name) # work experience calculation
#work experience calculation
date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
@ -64,6 +62,9 @@ class TestGratuity(unittest.TestCase):
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
# additional salary creation (Pay via salary slip)
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
def test_check_gratuity_amount_based_on_all_previous_slabs(self): def test_check_gratuity_amount_based_on_all_previous_slabs(self):
employee, sal_slip = create_employee_and_get_last_salary_slip() employee, sal_slip = create_employee_and_get_last_salary_slip()
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
@ -117,8 +118,8 @@ class TestGratuity(unittest.TestCase):
self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2)) self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2))
def tearDown(self): def tearDown(self):
frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.rollback()
frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
def get_gratuity_rule(name): def get_gratuity_rule(name):
rule = frappe.db.exists("Gratuity Rule", name) rule = frappe.db.exists("Gratuity Rule", name)
@ -141,9 +142,14 @@ def create_gratuity(**args):
gratuity.employee = args.employee gratuity.employee = args.employee
gratuity.posting_date = getdate() gratuity.posting_date = getdate()
gratuity.gratuity_rule = args.rule or "Rule Under Limited Contract (UAE)" gratuity.gratuity_rule = args.rule or "Rule Under Limited Contract (UAE)"
gratuity.expense_account = args.expense_account or 'Payment Account - _TC' gratuity.pay_via_salary_slip = args.pay_via_salary_slip or 0
gratuity.payable_account = args.payable_account or get_payable_account("_Test Company") if gratuity.pay_via_salary_slip:
gratuity.mode_of_payment = args.mode_of_payment or 'Cash' gratuity.payroll_date = getdate()
gratuity.salary_component = "Performance Bonus"
else:
gratuity.expense_account = args.expense_account or 'Payment Account - _TC'
gratuity.payable_account = args.payable_account or get_payable_account("_Test Company")
gratuity.mode_of_payment = args.mode_of_payment or 'Cash'
gratuity.save() gratuity.save()
gratuity.submit() gratuity.submit()

View File

@ -527,11 +527,12 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account):
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
def remove_payrolled_employees(emp_list, start_date, end_date): def remove_payrolled_employees(emp_list, start_date, end_date):
new_emp_list = []
for employee_details in emp_list: for employee_details in emp_list:
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}): if not frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}):
emp_list.remove(employee_details) new_emp_list.append(employee_details)
return emp_list return new_emp_list
@frappe.whitelist() @frappe.whitelist()
def get_start_end_dates(payroll_frequency, start_date=None, company=None): def get_start_end_dates(payroll_frequency, start_date=None, company=None):

View File

@ -124,7 +124,7 @@ class TestPayrollEntry(unittest.TestCase):
if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
create_account(account_name="_Test Payroll Payable", create_account(account_name="_Test Payroll Payable",
company="_Test Company", parent_account="Current Liabilities - _TC") company="_Test Company", parent_account="Current Liabilities - _TC", account_type="Payable")
if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \
frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
@ -214,6 +214,7 @@ class TestPayrollEntry(unittest.TestCase):
create_loan_type("Car Loan", 500000, 8.4, create_loan_type("Car Loan", 500000, 8.4,
is_term_loan=1, is_term_loan=1,
mode_of_payment='Cash', mode_of_payment='Cash',
disbursement_account='Disbursement Account - _TC',
payment_account='Payment Account - _TC', payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC', loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC', interest_income_account='Interest Income Account - _TC',

View File

@ -6,6 +6,7 @@ import random
import unittest import unittest
import frappe import frappe
from frappe.model.document import Document
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
add_months, add_months,
@ -370,6 +371,7 @@ class TestSalarySlip(unittest.TestCase):
create_loan_type("Car Loan", 500000, 8.4, create_loan_type("Car Loan", 500000, 8.4,
is_term_loan=1, is_term_loan=1,
mode_of_payment='Cash', mode_of_payment='Cash',
disbursement_account='Disbursement Account - _TC',
payment_account='Payment Account - _TC', payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC', loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC', interest_income_account='Interest Income Account - _TC',
@ -686,20 +688,25 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
def make_salary_component(salary_components, test_tax, company_list=None): def make_salary_component(salary_components, test_tax, company_list=None):
for salary_component in salary_components: for salary_component in salary_components:
if not frappe.db.exists('Salary Component', salary_component["salary_component"]): if frappe.db.exists('Salary Component', salary_component["salary_component"]):
if test_tax: continue
if salary_component["type"] == "Earning":
salary_component["is_tax_applicable"] = 1 if test_tax:
elif salary_component["salary_component"] == "TDS": if salary_component["type"] == "Earning":
salary_component["variable_based_on_taxable_salary"] = 1 salary_component["is_tax_applicable"] = 1
salary_component["amount_based_on_formula"] = 0 elif salary_component["salary_component"] == "TDS":
salary_component["amount"] = 0 salary_component["variable_based_on_taxable_salary"] = 1
salary_component["formula"] = "" salary_component["amount_based_on_formula"] = 0
salary_component["condition"] = "" salary_component["amount"] = 0
salary_component["doctype"] = "Salary Component" salary_component["formula"] = ""
salary_component["salary_component_abbr"] = salary_component["abbr"] salary_component["condition"] = ""
frappe.get_doc(salary_component).insert()
get_salary_component_account(salary_component["salary_component"], company_list) salary_component["salary_component_abbr"] = salary_component["abbr"]
doc = frappe.new_doc("Salary Component")
doc.update(salary_component)
doc.insert()
get_salary_component_account(doc, company_list)
def get_salary_component_account(sal_comp, company_list=None): def get_salary_component_account(sal_comp, company_list=None):
company = erpnext.get_default_company() company = erpnext.get_default_company()
@ -707,7 +714,9 @@ def get_salary_component_account(sal_comp, company_list=None):
if company_list and company not in company_list: if company_list and company not in company_list:
company_list.append(company) company_list.append(company)
sal_comp = frappe.get_doc("Salary Component", sal_comp) if not isinstance(sal_comp, Document):
sal_comp = frappe.get_doc("Salary Component", sal_comp)
if not sal_comp.get("accounts"): if not sal_comp.get("accounts"):
for d in company_list: for d in company_list:
company_abbr = frappe.get_cached_value('Company', d, 'abbr') company_abbr = frappe.get_cached_value('Company', d, 'abbr')
@ -725,7 +734,7 @@ def get_salary_component_account(sal_comp, company_list=None):
}) })
sal_comp.save() sal_comp.save()
def create_account(account_name, company, parent_account): def create_account(account_name, company, parent_account, account_type=None):
company_abbr = frappe.get_cached_value('Company', company, 'abbr') company_abbr = frappe.get_cached_value('Company', company, 'abbr')
account = frappe.db.get_value("Account", account_name + " - " + company_abbr) account = frappe.db.get_value("Account", account_name + " - " + company_abbr)
if not account: if not account:

View File

@ -58,6 +58,7 @@
"width": "50%" "width": "50%"
}, },
{ {
"allow_on_submit": 1,
"default": "Yes", "default": "Yes",
"fieldname": "is_active", "fieldname": "is_active",
"fieldtype": "Select", "fieldtype": "Select",
@ -232,10 +233,11 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 15:41:12.342380", "modified": "2022-02-03 23:50:10.205676",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Structure", "name": "Salary Structure",
"naming_rule": "Set by user",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -271,5 +273,6 @@
], ],
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -39,7 +39,8 @@
"public/js/utils/dimension_tree_filter.js", "public/js/utils/dimension_tree_filter.js",
"public/js/telephony.js", "public/js/telephony.js",
"public/js/templates/call_link.html", "public/js/templates/call_link.html",
"public/js/templates/node_card.html" "public/js/templates/node_card.html",
"public/js/bulk_transaction_processing.js"
], ],
"js/item-dashboard.min.js": [ "js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard.html",

View File

@ -0,0 +1,30 @@
frappe.provide("erpnext.bulk_transaction_processing");
$.extend(erpnext.bulk_transaction_processing, {
create: function(listview, from_doctype, to_doctype) {
let checked_items = listview.get_checked_items();
const doc_name = [];
checked_items.forEach((Item)=> {
if (Item.docstatus == 0) {
doc_name.push(Item.name);
}
});
let count_of_rows = checked_items.length;
frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), ()=>{
if (doc_name.length == 0) {
frappe.call({
method: "erpnext.utilities.bulk_transaction.transaction_processing",
args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype}
}).then(()=> {
});
if (count_of_rows > 10) {
frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, to_doctype]);
}
} else {
frappe.msgprint(__("Selected document must be in submitted state"));
}
});
}
});

View File

@ -1463,7 +1463,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"item_code": d.item_code, "item_code": d.item_code,
"pricing_rules": d.pricing_rules, "pricing_rules": d.pricing_rules,
"parenttype": d.parenttype, "parenttype": d.parenttype,
"parent": d.parent "parent": d.parent,
"price_list_rate": d.price_list_rate
}) })
} }
}); });
@ -2288,7 +2289,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
() => this.frm.doc.ignore_pricing_rule=1, () => this.frm.doc.ignore_pricing_rule=1,
() => me.ignore_pricing_rule(), () => me.ignore_pricing_rule(),
() => this.frm.doc.ignore_pricing_rule=0, () => this.frm.doc.ignore_pricing_rule=0,
() => me.apply_pricing_rule() () => me.apply_pricing_rule(),
() => this.frm.save()
]); ]);
} else { } else {
frappe.run_serially([ frappe.run_serially([

View File

@ -22,5 +22,6 @@ import "./call_popup/call_popup";
import "./utils/dimension_tree_filter"; import "./utils/dimension_tree_filter";
import "./telephony"; import "./telephony";
import "./templates/call_link.html"; import "./templates/call_link.html";
import "./bulk_transaction_processing";
// import { sum } from 'frappe/public/utils/util.js' // import { sum } from 'frappe/public/utils/util.js'

View File

@ -295,6 +295,10 @@ class GSTR3BReport(Document):
inter_state_supply_details = {} inter_state_supply_details = {}
for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
export_type = self.invoice_detail_map.get(inv, {}).get('export_type')
for rate, items in items_based_on_rate.items(): for rate, items in items_based_on_rate.items():
for item_code, taxable_value in self.invoice_items.get(inv).items(): for item_code, taxable_value in self.invoice_items.get(inv).items():
if item_code in items: if item_code in items:
@ -302,9 +306,8 @@ class GSTR3BReport(Document):
self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value
elif item_code in self.is_non_gst: elif item_code in self.is_non_gst:
self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value
elif rate == 0: elif rate == 0 or (gst_category == 'Overseas' and export_type == 'Without Payment of Tax'):
self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value
#self.report_dict['sup_details']['osup_zero'][key] += tax_amount
else: else:
if inv in self.cgst_sgst_invoices: if inv in self.cgst_sgst_invoices:
tax_rate = rate/2 tax_rate = rate/2
@ -315,9 +318,6 @@ class GSTR3BReport(Document):
self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100) self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100)
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \ if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
self.gst_details.get("gst_state") != place_of_supply.split("-")[1]: self.gst_details.get("gst_state") != place_of_supply.split("-")[1]:
inter_state_supply_details.setdefault((gst_category, place_of_supply), { inter_state_supply_details.setdefault((gst_category, place_of_supply), {

View File

@ -219,7 +219,6 @@ def get_regional_address_details(party_details, doctype, company):
if not party_details.place_of_supply: return party_details if not party_details.place_of_supply: return party_details
if not party_details.company_gstin: return party_details if not party_details.company_gstin: return party_details
if not party_details.supplier_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",

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